We know that the Koa class library has the following key features:

  • Middleware mechanisms that support the Onion circle model
  • Encapsulate request and Response to provide context objects, facilitating HTTP operations
  • Asynchronous functions, middleware error handling mechanisms

This article will take you through the steps of using TS to complete a simple framework that implements the core functions of Koa

Step 1: Basic Server running

Objective: To complete a new Koa Server that is fundamentally feasible

  • Support app. Listen listening port to start the Server
  • App. use adds middleware like handlers

The core code is as follows:

class Koa {
  private middleware: middlewareFn = (a)= > {};
  constructor() {}
  listen(port: number, cb: noop) {
    const server = http.createServer((req, res) = > {
      this.middleware(req, res);
    });
    return server.listen(port, cb);
  }
  use(middlewareFn: middlewareFn) {
    this.middleware = middlewareFn;
    return this; }}const app = new Koa();
app.use((req, res) = > {
  res.writeHead(200);
  res.end("A request come in");
});
app.listen(3000, () = > {console.log("Server listen on port 3000");
});
Copy the code

Step 2: Implementation of onion ring middleware mechanism

Goal: Next we need to refine the Listen and Use methods to implement the Onion ring middleware model

As shown in the code below, in this step we want app.use to support the addition of multiple middleware, and the middleware to be executed in the order of onion rings (like deep recursive calls)

app.use(async (req, res, next) => {
  console.log("middleware 1 start");
  // There are two things to note here:
  The next() function must be called only once
  // we must use await when calling next
  // The specific reason will be explained in detail in the following code implementation
  await next();
  console.log("middleware 1 end");
});
app.use(async (req, res, next) => {
  console.log("middleware 2 start");
  await next();
  console.log("middleware 2 end");
});
app.use(async (req, res, next) => {
  res.writeHead(200);
  res.end("An request come in");
  await next();
});
app.listen(3000, () = > {console.log("Server listen on port 3000");
});
Copy the code

Let’s take a look at how the onion ring mechanism works:

class Koa {... use(middlewareFn: middlewareFn) {Use uses an array to store all middleware
    this.middlewares.push(middlewareFn);
    return this;
  }
  listen(port: number, cb: noop) {
    // 2. A function that converts the middleware array into serial calls through composeMiddleware, called in the callback function in createServer
    // So the real focus is composeMiddleware. If so, let's look at the implementation of this function
    // BTW: Fn is generated when listen is called, which means we can't dynamically add middleware at runtime
    const fn = composeMiddleware(this.middlewares);
    const server = http.createServer(async (req, res) => {
      await fn(req, res);
    });
    returnserver.listen(port, cb); }}// 3. Core of onion Ring model:
// Input parameter: all collected middleware
// Returns a function that calls the middleware array serially
function composeMiddleware(middlewares: middlewareFn[]) {
  return (req: IncomingMessage, res: ServerResponse) = > {
    let start = - 1;
    // dispatch: triggers the ith middleware execution
    function dispatch(i: number) {
      // At first, you may not understand why there is such a judgment, but you can look at the whole function to consider the problem
      // Start < I, next() should start === I
      // If next() is called multiple times, the second and subsequent calls will result in start >= I because start === I was assigned before
      if (i <= start) {
        return Promise.reject(new Error("next() call more than once!"));
      }
      if (i >= middlewares.length) {
        return Promise.resolve();
      }
      start = i;
      const middleware = middlewares[i];
      // Here's the thing!!
      // Fetch the i-th middleware execution and pass dispatch(I +1) as next to each of the next middleware
      // Now let's review the two questions we raised earlier:
      // 1. Why must the next function be called only once in KOA middleware
      // You can see that if you do not call next, the next middleware will not be able to trigger, resulting in suspended animation and a timeout
      // Calling next multiple times will be executed multiple times by the current middleware
      // 2. Why should I await next()
      // This is also the core of the onion ring call mechanism, when executing to await next(), next() is executed to wait for the result to be returned, and then proceed further
      return middleware(req, res, () => {
        return dispatch(i + 1);
      });
    }
    return dispatch(0);
  };
}
Copy the code

Step 3: Context provides

Target: Encapsulates the Context and provides convenient operations for request and response

// 1. Define KoaRequest, KoaResponse, KoaContextinterface KoaContext { request? : KoaRequest; response? : KoaResponse; body:String | null;
}
const context: KoaContext = {
  get body() {
    return this.response! .body; }, set body(body) {this.response!.body = body;
  }
};

function composeMiddleware(middlewares: middlewareFn[]) {
  return (context: KoaContext) = > {
    let start = - 1;
    function dispatch(i: number) {
      / /.. Omit other code..
      // All middleware accept the context parameter
      middleware(context, () => {
        return dispatch(i + 1);
      });
    }
    return dispatch(0);
  };
}

class Koa {
  private context: KoaContext = Object.create(context);
  listen(port: number, cb: noop) {
    const fn = composeMiddleware(this.middlewares);
    const server = http.createServer(async (req, res) => {
      // create context object with req and res
      Context creates a new object, not assigning it directly to this.context
      Since the context fits the request associated with it, it also ensures that each request is a new context object
      const context = this.createContext(req, res);
      await fn(context);
      if (context.response && context.response.res) {
        context.response.res.writeHead(200); context.response.res.end(context.body); }});return server.listen(port, cb);
  }
  // create a context object
  createContext(req: IncomingMessage, res: ServerResponse): KoaContext {
    // Why use object.create instead of assigning directly?
    // Make sure that each request request, response, and context are all new
    const request = Object.create(this.request);
    const response = Object.create(this.response);
    const context = Object.create(this.context);
    request.req = req;
    response.res = res;
    context.request = request;
    context.response = response;
    returncontext; }}Copy the code

Step 4: Asynchronous function error handling mechanism

Target: Support app.on(“error”) to listen for error events to handle exceptions

Let’s recall how exceptions are handled in Koa. The code might look something like this:

app.use(async (context, next) => {
  console.log("middleware 2 start");
  // throw new Error(" Error ");
  await next();
  console.log("middleware 2 end");
});

// KOA unified error handling: listen for error events
app.on("error", (error, context) => {
  console.error(` request${context.url}An error has occurred);
});
Copy the code

As you can see from the code above, the core lies in:

  • Koa instance APP needs to support event triggering and event listening capabilities
  • We need to catch the asynchronous function exception and raise the error event

Let’s see how the code works:

// 1, inherit EventEmitter, increase the ability to trigger events and monitor
class Koa extends EventEmitter {
  listen(port: number, cb: noop) {
    const fn = composeMiddleware(this.middlewares);
    const server = http.createServer(async (req, res) => {
      const context = this.createContext(req, res);
      // 2. Call fn to await an exception with a try catch
      try {
        await fn(context);
        if (context.response && context.response.res) {
          context.response.res.writeHead(200); context.response.res.end(context.body); }}catch (error) {
        console.error("Server Error");
        // 3, provide more information about context when raising error, aspect log, locate the problem
        this.emit("error", error, context); }});returnserver.listen(port, cb); }}Copy the code

conclusion

So far we have TypeScript for the simplified VERSION of the Koa library

  • Onion ring middleware mechanism
  • Context encapsulates request and Response
  • Asynchronous exception error handling mechanism

The complete Demo code can be found in KOA2-Reference

For more exciting articles, welcome to Star in our warehouse and pay attention to our Nuggets. We publish several high quality articles about the big front end every week.

The resources

  • In-depth understanding of Koa2 middleware mechanisms
  • Probably the most complete koA source code parsing guide to date