As I said before Koa2 from zero to scaffold , and From shallow to deep understanding of Koa2 source code
This article explains how to write a Koa2
Step 1: encapsulate HTTP service and create Koa constructor
After reading the source code of Koa2, we learned that Koa's service application is based on the Node's native HTTP module, which is encapsulated. First, we use the native Node to implement the HTTP service
const http = require('http') const server = http.createServer((req, res) => { res.writeHead(200) res.end('hello world') }) server.listen(3000, () => { console.log('Monitor 3000 port') })
Let's see how to implement HTTP service with Koa2
const Koa = require('Koa') const app = new Koa() app.use((ctx, next) => { ctx.body = 'hello world' }) app.listen(3000, () => { console.log('3000 Request succeeded') })
The first step to implement Koa is to encapsulate the native HTTP service. We create a new lib / application. Exe according to the structure of Koa source code JS file, the code is as follows:
const http = require('http') class Application { constructor() { this.callbackFunc } listen(port) { const server = http.createServer(this.callback()) server.listen(port) } use(fn) { this.callbackFunc = fn } callback() { return (req, res) => this.callbackFunc(req, res) } } module.exports = Application
We introduce handwritten Koa and write a demo
const Koa = require('./lib/application') const app = new Koa() app.use((req, res) => { res.writeHead(200) res.end('hello world') }) app.listen(3000, () => { console.log('3000 Request succeeded') })
After starting the service, enter in the browser http://localhost:3000 , the content displays "Hello, World"“
Then we have two directions: one is to simplify res.writeHead(200) and res.end('Hello world '); The second is to plug in multiple middleware. To make the first point, you need to write the context, response and request files first. In the second point, we need to rely on context later, so we first simplify the native response and request and integrate them into the context (ctx) object
Step 2: build request, response and context objects
Request, response and context objects correspond to request js,response.js,context.js,request.js processing request body, response JS handles the response body, and context integrates request and response
// request let url = require('url') module.exports = { get query() { return url.parse(this.req.url, true).query }, }
// response module.exporrs = { get body() { return this._body }, set body(data) { this._body = data }, get status() { return this.res.statusCode }, set status(statusCode) { if (typeof statusCode !== 'number') { throw new Error('statusCode must be a number') } this.res.statusCode = statusCode }, }
Here, we only deal with query in request and body and status in response. Whether it is a request or a response, we use the get and set of ES6. In short, get/set is able to value and assign a key
Now that we have implemented request and response and obtained the request and response objects and their encapsulation methods, let's write context. We once said in the source code analysis that context inherits the parameters of request and response objects, including both the methods in the request body and the methods in the response body, such as CTX Query queries the parameters on the url in the request body through CTX Body returns data.
module.exports = { get query() { return this.request.query }, get body() { return this.response.body }, set body(data) { this.response.body = data }, get status() { return this.response.status }, set status(statusCode) { this.response.status = statusCode }, }
delegate is used in the source code, and the context in the context request,context. The method on response is proxied to the context, that is, context request. query === context. query; context.response. body === context. body. And context request,context. The response is mounted in the application
To sum up: request JS is responsible for simplifying the code of the request body, response JS is responsible for simplifying the code of the response body, context JS integrates the request body and response body into one object, generates them on the application, and modifies the application JS file, add the following code:
const http = require('http'); const context = require('context') const request = require('request') const response = require('response') class Application { constructor() { this.callbackFunc this.context = context this.request = request this.response = response } ... createConext(req, res) { const ctx = Object.create(this.context) ctx.request = Object.create(this.request) ctx.response = Object.create(this.response) ctx.req = ctx.request.req = req ctx.res = ctx.response.res = res return ctx } ... }
Because context, request and response are used in other methods, we assign them to this in the constructor context,this.request,this.response . We have implemented the context ctx. Now let's return to the previous problem, which is abbreviated as res.writeHead(200), res.end('Hello world ')
We want to simplify res.writeHead(200) and res.end('Hello world ') to CTX Body = 'hello world', how to change it?
res.writeHead(200) and res.end('Hello world ') are native, ctx Body = 'hello world' is how Koa is used. We need to use ctx Body = 'hello world' is parsed and converted into res.writeHead(200) and res.end('Hello world '). Fortunately, ctx has been obtained through createContext, so create another method to encapsulate res.end with ctx Body
responseBody(ctx) { let context = ctx.body if (typeof context === 'string') { ctx.res.end(context) } else if (typeof context === 'object') { ctx.res.end(JSON.stringify(context)) } }
Finally, we modify the callback method
// callback() { // return (req, res) => this.callbackFunc(req, res) // } callback() { return (req, res) => { // Encapsulate the native req and res as ctx const ctx = this.createContext(req, res) // Execute the function in use, CTX Body assignment this.callbackFunc(ctx) // Encapsulate res.end with CTX Body representation return this.responseBody(ctx) } }
PS: specific code: see Step 2 in the warehouse
Step 3: middleware mechanism and onion model
As we know, the most important function of Koa2 is middleware, which can be expressed in multiple uses. The function in each use method is a middleware, which is expressed and passed to the next intermediate by the second parameter next, for example
app.use(async (ctx, next) => { console.log(1) await next() console.log(6) }) app.use(async (ctx, next) => { console.log(2) await next() console.log(5) }) app.use(async (ctx, next) => { console.log(3) ctx.body = 'hello world' console.log(4) }) // Result 123456
Therefore, our middleware is an array. Secondly, execute and pause execution through next. As soon as next, suspend the execution of this middleware and execute the next middleware.
Koa's onion model is implemented in Koa1 with generator + co.js, while Koa2 uses async/await + Promise. This time, we also use async/await + Promise to implement it
In the source code analysis, we said that the middleware synthesis of Koa2 is an independent library, namely koa compose. Its core code is as follows:
function compose(middleware) { return function (context, next) { let index = -1 return dispatch(0) function dispatch(i) { if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i let fn = middleware[i] if (i === middleware.length) fn = next if (!fn) return Promise.resolve() try { return Promise.resolve(fn(context, dispatch.bind(null, i + 1))) } catch (err) { return Promise.reject(err) } } } }
The specific interpretation can be viewed from the source code analysis. We don't explore it here
The two solutions posted here are actually recursive
componse() { return async (ctx) => { function createNext(middleware, oldNext) { return async () => { await middleware(ctx, oldNext) } } let len = this.middlewares.length let next = async () => { return Promise.resolve() } for (let i = len - 1; i >= 0; i--) { let currentMiddleware = this.middlewares[i] next = createNext(currentMiddleware, next) } await next() } }
There is also the source code. The author can't write a good description of the compose function. However, readers should understand it by themselves
Step 4: error capture and monitoring mechanism
How to capture the error code in the middleware? Because the middleware returns the Promise instance, we only need to handle the catch error and add the onerror method
onerror(err, ctx) { if (err.code === 'ENOENT') { ctx.status = 404 } else { ctx.status = 500 } let msg = ctx.message || 'Internal error' ctx.res.end(msg) this.emit('error', err) } callback() { return (req, res) => { const ctx = this.createContext(req, res) const respond = () => this.responseBody(ctx) + const onerror = (err) => this.onerror(err, ctx) let fn = this.componse() + return fn(ctx).then(respond).catch(onerror) } }
We have only captured the errors in the middleware, but how can we know and notify the developers if the wrong code is written in other places? Node provides a native module - events, and our Application class can inherit it to obtain the monitoring function, so that we can catch all the errors on the server
summary
We first read the source code of Koa2, knew its data structure and usage, and then gradually handwritten one. Thank you very much here First little tadpole Koa2 framework principle analysis and implementation, his article is the basis for me to write koa2 article. Back to koa2, its function is particularly simple, that is, it processes the native req and res, so that developers can write code more easily; In addition, the concept of middleware is introduced, which is like a plug-in. It can be used when it is introduced. It can reduce code when it is not needed. Lightweight is probably the keyword of koa2
GitHub address: https://github.com/johanazhu/...