In the previous article, we introduced what is the basis of Koa2
briefly review
what is koa2
- NodeJS web development framework
- Koa can be seen as an abstraction of the HTTP module of nodejs
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
It can be seen that there are only four files: application.js
, context.js
, request.js
, response.js
application
As the entry file, it inherits the Emitter module. The Emitter module is a native module of NodeJS. In short, the Emitter module can implement event monitoring and event triggering capabilities.
Delete the comments, from the point of view of finishing Application
constructor
Application provides eight methods on its prototype, including listen, toJSON, inspect, use, callback, handleRequest, createContext, and onerror, among which
- listen: Provide HTTP service
- use: middleware mount
- callback: Get the callback function required by the http server
- handleRequest: handle the request body
- createContext: Construct ctx, merge node's req, res, and construct Koa's parameters - ctx
- onerror: error handling
Don't care about the rest, let's take a look at the constructor constructor
Halo, this is all and what, let's start the simplest service and see the example
const Koa = require('Koa')
const app = new Koa()
app.use((ctx) => {
ctx.body = 'hello world'
})
app.listen(3000, () => {
console.log('3000请求成功')
})
console.dir(app)
It can be seen that our instances correspond to the constructors one by one,
Break point to see prototype
Oh, remove the non-key fields, we only focus on the key points
this.middleware, this.context, this.request, this.response on Koa's constructor
The prototypes are: listen, use, callback, handleRequest, createContext, onerror
Note: The following code is to delete exceptions and non-critical code
watch 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 go to 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 handling of context, and the special handling of res
Consolidation of middleware
Used koa-compose
to merge middleware, which is also the key to the onion model, the source address of koa-compose: https://github.com/koajs/compose . This code has not been moved for three years, and it is stable.
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)
}
}
}
}
It's hard to understand in a blink of an eye. We need to understand what middleware is, that is, an array of middleware. So how did it come from? There is this.middleware in the constructor, who used it -- use method
Let's jump out and look at the use method first
use
use(fn) {
this.middleware.push(fn)
return this
}
Except for exception handling, the key is these two steps, this.middleware
is an array, the first step is to push the middleware in this.middleware
; the second step returns this so that it can be called in a chain. I was interviewed on how to make a chain call of promises, and I was confused. I didn't expect to see it here.
Looking back at the koa-compose source code, 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 it's running 123456
The composition of its this.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, and you can pass values if you are an object.
const fn = compose(this.middleware)
We JavaScriptize it, nothing else needs to be changed, just change the last function to
async (ctx, next) => {
console.log(3);
-ctx.body = 'hello world';
+console.log('hello world');
console.log(4);
}
Parse koa-compose line by line
This paragraph is very important. It is often tested during the interview. It asks you to write a compose by hand. Look at it.
//1. async (ctx, next) => { console.log(1); await next(); console.log(6); } 中间件
//2. const fn = compose(this.middleware) 合并中间件
//3. fn() 执行中间件
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)
, that is, 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()
, that is, 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 为第一个中间件
if (i === middleware.length) fn = next // 当弄到最后一个中间件时,最后一个中间件赋值为 fn
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
// 返回一个 Promise 实例,执行 递归执行 dispatch(1)
} catch (err) {
return Promise.reject(err)
}
}
}
}
That is, the first middleware must wait for the second middleware to finish executing before returning, and the second must wait for the third one to finish executing before returning until the middleware is finished executing.
Promise.resolve
is a Promise instance, the reason why Promise.resolve
is used is to solve asynchrony, and the reason why Promise.resolve
is used to solve asynchronous
Throw away Promise.resolve
, let's look at the use of recursion first, 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
Going back and looking at compose again, the code is something like
// 假设 this.middleware = [fn1, fn2, fn3]
function fn(context, next) {
if (i === middleware.length) fn = next // fn3 没有 next
if (!fn) return Promise.resolve() // 因为 fn 为空,执行这一行
function dispatch (0) {
return Promise.resolve(fn(context, function dispatch(1) {
return Promise.resolve(fn(context, function dispatch(2) {
return Promise.resolve()
}))
}))
}
}
}
This recursive way is similar to the execution stack, first in first out
Think more about the use of recursion, don't care too much about Promise.resolve
handling of context
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, return a context - context, the code is very clear, not explained
Special handling of res
In the callback, this.createContext is executed first. After getting the context, handleRequest is executed. 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请求成功');
});
After such a piece of code is instantiated, the four generals of this.middleware, this.context, this.request, and this.response are obtained. When you use app.use(), push the functions in it to this.middleware. When using app.listen() again, it is equivalent to starting an HTTP service, which combines middleware, obtains context, and handles res specially
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 caught my eye
// 1.
const proto = module.exports = {
inspect(){...},
toJSON(){...},
...
}
// 2.
delegate(proto, 'response')
.method('attachment')
.access('status')
...
The first one can be understood as, const proto = { inspect() {...} ...}, and module.exports exports this object
The second can be seen this way, delegate is a proxy, which is designed for the convenience of developers
// 将内部对象 response 的属性,委托至暴露在外的 proto 上
delegate(proto, 'response')
.method('redirect')
.method('vary')
.access('status')
.access('body')
.getter('headerSent')
.getter('writable');
...
And use delegate(proto, 'response').access('status')...
, which is the file exported in context.js, to proxy all parameters on proto.response to proto, what is proto.response? It is context.response, where does context.response come from?
To recap, 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))
...
}
With context.response, it is clear, context.response = this.response, because of the delegate, the parameters on context.response are delegated to the context, for example
- ctx.header is proxied on ctx.request.header
- ctx.body is proxied on ctx.response.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
This is probably the case. After knowing so much, how to write a Koa2 by hand? Please see the next article - handwriting Koa2
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。