In the previous article, we introduced what is Basis of Koa2
Briefly review
What is koa2
- web development framework of NodeJS
- Koa can be seen as an abstraction of nodejs' HTTP module
Source code focus
Middleware mechanism
Onion model
compose
Source code structure
Source address of Koa2: https://github.com/koajs/koa
Among them, lib is its source code
As you can see, there are only four files: application js,context.js,request.js,response.js
application
As an entry file, it inherits the Emitter module, which is the native module of NodeJS. In short, the Emitter module can realize the ability of event listening and event triggering
Delete the comments and look at the Application constructor from the perspective of sorting
Application provides eight methods on its prototype, including listen, toJSON, inspect, use, callback, handleRequest, createContext and oneror
- listen: provides HTTP services
- use: middleware mount
- Callback: get the callback function required by http server
- handleRequest: handle the request body
- createContext: construct ctx, combine req and res of node, and construct the parameter of Koa - ctx
- onerror: error handling
Don't care about the others. Let's take a look at the constructor
Halo, what and what are these? Let's start one of the simplest services and take a look at the examples
const Koa = require('Koa') const app = new Koa() app.use((ctx) => { ctx.body = 'hello world' }) app.listen(3000, () => { console.log('3000 Request succeeded') }) console.dir(app)
It can be seen that our instance corresponds to the constructor one by one,
Interrupt and look at the prototype
Oh, except for non critical fields, we only focus on the key points
This on Koa's constructor middleware, this.context, this.request,this.response
The prototype includes: listen, use, callback, handleRequest, createContext and oneror
Note: the following codes delete exceptions and non critical codes
listen first
... listen(...args) { const server = http.createServer(this.callback()) return server.listen(...args) } ...
It can be seen that listen encapsulates an http service with the http module, focusing on the incoming this callback(). OK, let's look at the callback method now
callback
callback() { const fn = compose(this.middleware) const handleRequest = (req, res) => { const ctx = this.createContext(req, res) return this.handleRequest(ctx, fn) } return handleRequest }
It includes the merging of middleware, the processing of context, and the special processing of res
Merging of Middleware
Koa compose is used to merge middleware, which is also the key of onion model. The source address of KOA compose is: https://github.com/koajs/compose . The code hasn't moved for three years. It's steady
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) } } } }
We don't know what the constructor of this.middleware needs first Middleware, who used the use method
Let's jump out and look at the method first
use
use(fn) { this.middleware.push(fn) return this }
The key to removing exception handling is these two steps, this Middleware is an array. The first step is this push Middleware in middleware; The second step is to return to this so that it can be called in a chain. At the beginning, I was interviewed how to do the chain call of promise. I was stunned. I didn't expect to see it here
Looking back at the source code of KOA compose, imagine this scenario
... 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); }); ...
We know that its operation is 123456
It's this The composition of middleware is
this.middleware = [ async (ctx, next) => { console.log(1) await next() console.log(6) }, async (ctx, next) => { console.log(2) await next() console.log(5) }, async (ctx, next) => { console.log(3) ctx.body = 'hello world' console.log(4) }, ]
Don't be surprised. Functions are also one of the objects. If they are objects, they can pass values
const fn = compose(this.middleware)
We will JavaScript it. We don't need to change anything else. We just need to change the last function to
async (ctx, next) => { console.log(3); -ctx.body = 'hello world'; +console.log('hello world'); console.log(4); }
Parsing koa compose line by line
This paragraph is very important. You often take it in the interview. Let you write a composition and write it
//1. async (ctx, next) => { console.log(1); await next(); console.log(6); } middleware //2. const fn = compose(this.middleware) merge Middleware //3. fn() execution Middleware 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); } } }; }
Execute const fn = compose(this.middleware), i.e. the following code
const fn = 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) } } } }
Execute fn(), i.e. the following code:
const fn = 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 // index = 0 let fn = middleware[i] // fn is the first middleware if (i === middleware.length) fn = next // When the last middleware is obtained, the value of the last middleware is fn if (!fn) return Promise.resolve() try { return Promise.resolve(fn(context, dispatch.bind(null, i + 1))) // Return a Promise instance and execute recursive dispatch(1) } catch (err) { return Promise.reject(err) } } } }
That is, the first middleware will not return until the second middleware is executed, and the second middleware will not return until the third middleware is executed
Promise.resolve is a promise instance, so promise is used Resolve is to solve asynchrony, so promise is used Resolve is to solve asynchronous
Throw promise Resolve, let's take a look at the use of recursion and execute the following code
const fn = function () { return dispatch(0); function dispatch(i) { if (i > 3) return; i++; console.log(i); return dispatch(i++); } }; fn(); // 1,2,3,4
Looking back at compose again, the code is similar to
// Suppose this middleware = [fn1, fn2, fn3] function fn(context, next) { if (i === middleware.length) fn = next // fn3 no next if (!fn) return Promise.resolve() // Execute this line because fn is empty function dispatch (0) { return Promise.resolve(fn(context, function dispatch(1) { return Promise.resolve(fn(context, function dispatch(2) { return Promise.resolve() })) })) } } }
This recursive method is similar to the execution stack, first in first out
Here we need to think more about the use of recursion Resolve don't care too much
Context processing
The processing of the context calls createContext
createContext(req, res) { const context = Object.create(this.context) const request = (context.request = Object.create(this.request)) const response = (context.response = Object.create(this.response)) context.app = request.app = response.app = this context.req = request.req = response.req = req context.res = request.res = response.res = res request.ctx = response.ctx = context request.response = response response.request = request context.originalUrl = request.originalUrl = req.url context.state = {} return context }
Pass in the native request and response and return a context - context. The code is very clear and does not explain
Special treatment of res
In the callback, execute this first Createcontext. After getting the context, execute handleRequest. Look at the code first:
handleRequest(ctx, fnMiddleware) { const res = ctx.res res.statusCode = 404 const onerror = (err) => ctx.onerror(err) const handleResponse = () => respond(ctx) onFinished(res, onerror) return fnMiddleware(ctx).then(handleResponse).catch(onerror) }
Everything is clear
const Koa = require('Koa'); const app = new Koa(); console.log('app', app); app.use((ctx, next) => { ctx.body = 'hello world'; }); app.listen(3000, () => { console.log('3000 Request succeeded'); });
After instantiating such a piece of code, you get this middleware,this.context,this.request,this.response four generals, you use app When using (), push the function to this middleware. Then use app Listen () is equivalent to an HTTP service. It combines middleware, obtains context, and performs special processing on res
error handling
onerror(err) { if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err)) if (404 == err.status || err.expose) return if (this.silent) return const msg = err.stack || err.toString() console.error() console.error(msg.replace(/^/gm, ' ')) console.error() }
context.js
Two things introduced me
// 1. const proto = module.exports = { inspect(){...}, toJSON(){...}, ... } // 2. delegate(proto, 'response') .method('attachment') .access('status') ...
The first can be understood as const proto = {inspect() {...}...}, And module Exports exports this object
Second, delegate is an agent, which is designed for the convenience of developers
// Delegate the attribute of the internal object response to the exposed proto delegate(proto, 'response') .method('redirect') .method('vary') .access('status') .access('body') .getter('headerSent') .getter('writable'); ...
Instead, use delegate (proto, 'response') access('status')..., It's in context JS exported file, put proto All parameters on the response are proxied to proto, which is proto What is response? It's context response,context. Where did the response come from?
To review, in createContext
createContext(req, res) { const context = Object.create(this.context) const request = (context.request = Object.create(this.request)) const response = (context.response = Object.create(this.response)) ... }
context. If you have a response, you will understand it. Context response = this. Response, because delegate, so context The parameters on the response are proxied to the context, for example
- ctx. The header is CTX request. Proxy on header
- ctx. Yes. CTX response. Agent on body
request.js and response js
One handles the request object and the other handles the return object, which is basically a simplified processing of the native req and res, using a lot of get and post syntax in ES6
That's about it. After knowing so much, how to write a Koa2? Please see the next article - handwritten Koa2