[implement TypeScript version Koa from zero]

What will this article say?

How to complete a coverage from scratch Koa Core function Node.js Class library
 Explain from the code level Koa Some reasons for code writing: for example, why must middleware call next Functions ctx What does it have to do with a request

We know that the Koa class library mainly has the following important features:

Middleware mechanism supporting onion ring model
 encapsulation request,response provide context Object, convenient http operation
 Error handling mechanism of asynchronous function and Middleware

Step 1: basic Server running

Objective: to complete the basic feasibility of the new Koa Server

support app.listen Listening port start Server
 support app.use Add class middleware Processing function

The core code is as follows:

class Koa {
  private middleware: middlewareFn = () => {};
  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");
});

Step 2: implementation of onion ring middleware mechanism

Objective: next, we will improve the listen and use methods and implement the onion ring middleware model

As shown in the following code, in this step, we hope that app.use can support adding multiple middleware, and the middleware is executed in the order of onion rings (similar to deep recursive calls)

app.use(async (req, res, next) => {
  console.log("middleware 1 start");
  // The specific reasons 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");
});

The above Demo has three points that we should pay attention to:

In middleware next()Function must and can only be called once
 call next Function must be used await

We will analyze the reasons for using these methods one by one in the following code. Let's take a look at how to implement this onion ring mechanism:

class Koa {
  ...
  use(middlewareFn: middlewareFn) {
    // 1. When use is called, an array is used to store all middleware
    this.middlewares.push(middlewareFn);
    return this;
  }
  listen(port: number, cb: noop) {
    // 2, transform the middleware array into serial function called onion ring through composeMiddleware, and call it in the callback function in createServer.
    // Therefore, the real focus is composeMiddleware. If it does, let's look at the implementation of this function
    // BTW: it can be seen from here that fn is generated after the listen function is called, which means that we cannot dynamically add middleware at runtime
    const fn = composeMiddleware(this.middlewares);
    const server = http.createServer(async (req, res) => {
      await fn(req, res);
    });
    return server.listen(port, cb);
  }
}

// 3. The core of the onion ring model:
// Input: all collected Middleware
// Return: call the function of middleware array serially
function composeMiddleware(middlewares: middlewareFn[]) {
  return (req: IncomingMessage, res: ServerResponse) => {
    let start = -1;
    // dispatch: trigger the ith middleware execution
    function dispatch(i: number) {
      // At first, you may not understand why you judge here. You can look at the whole function to think about this problem
      // Normally, start < I before each call, and start === i after calling next()
      // If multiple next() is invoked, the second and subsequent calls will result in start > = I, since the start === i assignment has been completed 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 comes the point!!!
      // Take out the ith middleware for execution, and pass the dispatch(i+1) to the next middleware as the next function
      return middleware(req, res, () => {
        return dispatch(i + 1);
      });
    }
    return dispatch(0);
  };
}

Promise mainly involves several knowledge points:

async Function returns a Promise All middleware objects will return one promise [object]
async An error was encountered inside the function await Execution is suspended when called await Function, wait for the result to be returned and continue to execute downward
async An error inside the function will cause the returned Promise Become reject state

Now let's review the previous questions:

Why must and can only call the next function once in koa middleware

 You can see that if you don't call next,It won't trigger dispatch(i+1),The next middleware cannot be triggered, resulting in a fake dead state and a final request timeout
 
 Call multiple times next It will cause the next middleware to execute multiple times

Why should await be added to the next() call

 This is also the core of the onion ring call mechanism, when executed to await next(),Will execute next()[Call the next middleware] wait for the returned result, and then execute downward

Step 3: provide Context

Objective: encapsulate Context and provide a convenient operation mode of request and response

// 1. Define KoaRequest, KoaResponse and KoaContext
interface 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 codes
      // 2. 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) => {
      // 3. Using req and res to create context objects
      // It should be noted here: context is to create a new object, rather than directly assign a value to this.context
      // Because context is suitable for request Association, 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);
  }
  // 4. Create context object
  createContext(req: IncomingMessage, res: ServerResponse): KoaContext {
    // Why use Object.create instead of direct assignment?
    // The reason is the same as above. It is necessary to ensure that every request, response and context are brand 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;
    return context;
  }
}

Step 4: asynchronous function error handling mechanism

Objective: support listening for error events and handling exceptions through app.on("error")

Let's recall how to handle exceptions in Koa. The code may be similar to the following:

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

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

As can be seen from the above code, the core lies in:

Koa example app It needs to support event triggering and event listening capabilities
 We need to catch asynchronous function exceptions and trigger them error event

Let's see how to implement the specific code:

// 1. Inherit EventEmitter to increase event triggering and listening capabilities
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. When await calls fn, you can use try catch to catch exceptions and trigger exception events
      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. When an error is triggered, provide more information about the context, log records, and locate the problem
        this.emit("error", error, context);
      }
    });
    return server.listen(port, cb);
  }
}

summary

So far, we have used TypeScript to complete the short version of Koa class library, which supports

Onion ring middleware mechanism
Context encapsulation request,response
 Asynchronous exception error handling mechanism

The complete Demo code can be referenced koa2-reference

For more wonderful articles, welcome to Star our warehouse. We will launch several high-quality articles related to the large front-end field every week.

Tags: node.js TypeScript koa

Posted on Mon, 29 Nov 2021 20:37:05 -0500 by merebel