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.