NodeNote, continuously updated in the React related library source analysis, react TS3 project
[TOC]
Koa class structure
Module. Exports = class Application extends Emitter {constructor() {
super();
this.proxy = false; this.middleware = []; this.subdomainOffset = 2; this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); } listen(... args) {... }toJSON() {... }inspect() {... } use(fn) {... }callback() {... } handleRequest(ctx, fnMiddleware) {... } createContext(req, res) {... } onerror(err) {... }}Copy the code
Simple Example start
The following is an official example of koA in cascading form,
const Koa = require('koa');
const app = new Koa();
// logger
app.use(async (ctx, next) => {
await next();
const rt = ctx.response.get('X-Response-Time');
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});
// x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
// response
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
Copy the code
The working process of the above example can be divided into the preparation phase and the processing of RequES events.
Preparation stage
In the preparation stage, koA instance will be created, middleware will be registered, and the specified port will be monitored. The processing mode of middleware is onion model design mode:
- To create a
koa
For instanceapp
; - registered
logger
Middleware, that is, adding middleware functions to a namedmiddleware
In the array of. - registered
x-response-time
Middleware. - registered
response
Middleware. - Listen on port 3000.
1. Create koA instances, call Constructor, define instance properties middleware, and so on
The use method simply adds middleware to the instance’s property Middleware array and returns the instance’s own this.
use(fn) {
if(typeof fn ! = ='function') throw new TypeError('middleware must be a function! ');
debug('use %s', fn._name || fn.name || The '-');
this.middleware.push(fn);
return this;
}
Copy the code
3. Listen to port 3000 and call the prototype object method listen, which internally calls the Node HTTP module and the instance method this.callback. The result is then returned as an incoming parameter to http.createserver as a callback function to the request event that the port fires. When http.createServer runs, it returns an HTTP server that listens on port 3000. When a request is made on port 3000, the Node mechanism’s Request event is raised and the function returned by this.callback() is called. The this.callback function organizes the middleware functions into an onion model and returns an execution entry function that executes the middleware in a particular order.
During the execution of this.callback(), the compose function is first called to handle the middleware, and the handleRequest function is returned, which is a callback to the Request event and is used by the middleware to handle the REq and RES.
listen(... args) { debug('listen');
const server = http.createServer(this.callback());
returnserver.listen(... args); }callbackConst fn = compose(this.middleware); const fn = compose(this.middleware);if(! this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
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);
}
Copy the code
How does Compose organize the middleware array
Compose is a compose function that executes middleware in order:
- Assume that the middleware functions are all executed in one step before the next middleware is executed
- The middleware functions first execute one part and then execute the other middleware, and then come back and execute the rest.
- Middleware functions are asynchronous cases
- Restrict one middleware execution to one time
- Allows middleware to intercept information and handle errors
Middleware functions are all executed in one step before the next middleware is executed:
function myCompose(ctx = [],middleware){
return dispatch(0)
function dispatch(i){
let fn = middleware[i];
if(! fn)return;
returnfn(ctx, dispatch.bind(null,i+1)); }}let fn0 = function(ctx,next){
console.log(0);
ctx.push(0)
next()
}
let fn1 = function(ctx,next){
console.log(1);
ctx.push(1)
next();
}
let fn2 = function(ctx,next){
console.log(2);
ctx.push(2)
next();
}
let middleware = [fn0,fn1,fn2]
let ctx = []
myCompose(ctx,middleware);
console.log(ctx);
Copy the code
The middleware functions first execute one part and then execute the other middleware, and then come back and execute the rest
Can the above code achieve such a function? The tests are as follows:
let fn0 = function(ctx,next){
console.log(0);
ctx.push(0)
next()
console.log("fn0");
}
let fn1 = function(ctx,next){
console.log(1);
ctx.push(1)
next();
console.log("fn1");
}
let fn2 = function(ctx,next){
console.log(2);
ctx.push(2)
next();
console.log("fn2");
}
let middleware = [fn0,fn1,fn2]
let ctx = []
myCompose(ctx,middleware);
console.log(ctx);
Copy the code
Result returned:
0
1
2
fn2
fn1
fn0
(3) [0, 1, 2]
Copy the code
Obviously, the myCompose function above can do just that.
Middleware functions are asynchronous cases
Test asynchrony:
let fn0 = async function(ctx,next){
console.log(0);
ctx.push(0)
await next()
console.log("fn0");
}
let fn1 = async function(ctx,next){
console.log(1);
ctx.push(1)
await next();
console.log("fn1");
}
let fn2 = async function(ctx,next){
console.log(2);
ctx.push(2)
await next();
console.log("fn2");
}
let middleware = [fn0,fn1,fn2]
let ctx = []
myCompose(ctx,middleware);
console.log(ctx);
Copy the code
Result returned:
0
1
2
(3) [0, 1, 2]
fn2
fn1
fn0
Copy the code
Restrict one middleware execution to one time
function myCompose(ctx = [],middleware){
return dispatch(0)
function dispatch(i){
let fn = middleware[i];
if(! fn)return;
returnfn(ctx, dispatch.bind(null,i+1)); }}let fn0 = async function(ctx,next){
console.log(0);
ctx.push(0)
await next()
await next()
console.log("fn0");
}
let fn1 = async function(ctx,next){
console.log(1);
ctx.push(1)
await next();
console.log("fn1");
}
let fn2 = async function(ctx,next){
console.log(2);
ctx.push(2)
await next();
console.log("fn2");
}
let middleware = [fn0,fn1,fn2]
let ctx = []
myCompose(ctx,middleware);
console.log(ctx);
Copy the code
Result returned:
0
1
2
(3) [0, 1, 2]
fn2
fn1
1
2
fn2
fn1
fn0
Copy the code
Next () is called twice in the fn0 function, which causes all middleware functions after fn0 to be reexecuted. To avoid this, you need to limit one middleware to one execution. The myCompose function executes the middleware function by calling the Dispatch function, so you can prevent the middleware function from executing by blocking the Dispatch function. From the myCompose function, you can see that the middleware function must have called middleware.length dispatches by the second time it is executed in the middleware function to next, which means that all of the middleware function logic prior to next has been executed. Next () is called dispatch. Bind (null, I +1). As you can see, every middleware function that executes next’s function the second time passes the first argument I +1, which is in the range [0,3]. Therefore, it is possible to record how many dispatches have been executed to index, and then compare the value of the first parameter I +1 and index during each dispatch to determine whether the same middleware function has been executed multiple times. Therefore, myCompose is modified as follows:
function myCompose(ctx = [],middleware){
let index = -1;
return dispatch(0)
function dispatch(i){
if(i <= index) return;
index = i;
let fn = middleware[i];
if(! fn)return;
returnfn(ctx, dispatch.bind(null,i+1)); }}Copy the code
Result returned:
0
1
2
(3) [0, 1, 2]
fn2
fn1
fn0
Copy the code
Allows middleware to intercept information and handle errors
For a middleware, for example
let fn0 = async function(ctx,next){
console.log(0);
ctx.push(0)
await next()
console.log("fn0");
}
Copy the code
What if the next middleware has a problem, or the FN0 middleware function needs the next middleware function to return some data for processing? Use promise. Resolve and reject to process data and error messages. The modified code is:
function myCompose(ctx = [],middleware){
let index = -1;
return dispatch(0)
function dispatch(i){
if(i <= index) return Promise.reject(new Error('next() called multiple times in a middleware'));
index = i;
let fn = middleware[i];
if(! fn)return Promise.resolve();
returnPromise.resolve(fn(ctx, dispatch.bind(null,i+1))); }}Copy the code
Resolve (fn(context, dispatch.bind(null, I + 1))); An error occurs, which is caught with a try-catch and rejected to the upper-layer middleware. The modified code is:
function myCompose(ctx = [],middleware){
let index = -1;
return dispatch(0)
function dispatch(i){
if(i <= index) return Promise.reject(new Error('next() called multiple times in a middleware'));
index = i;
let fn = middleware[i];
if(! fn)return Promise.resolve();
try {
return Promise.resolve(fn(ctx, dispatch.bind(null,i+1)));
} catch(err){
return Promise.reject(err)
}
}
}
Copy the code
The official compose implementation is as follows:
function compose (middleware) {
if(! Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array! ')
for (const fn of middleware) {
if(typeof fn ! = ='function') throw new TypeError('Middleware must be composed of functions! ')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// last called middleware #
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)
}
}
}
}
Copy the code
If (I === middleware.length) fn = next; This is done to detect that the middleware will compose(this.middleware)(CTX,fnLast) if it executes to the last middleware; FnLast is the last middleware. Koa source code next is undefined.
Process Reques events
When a request is detected on port 3000, the request event is triggered and the middleware starts to execute. The order of middleware execution can be seen from the previous implementation of myCompose function.
conclusion
const Koa = require('koa');
const app = new Koa();
// logger
app.use(async (ctx, next) => {
await next();
const rt = ctx.response.get('X-Response-Time');
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});
// x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
// response
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
Copy the code
Here, CTX is processed step by step by middleware cascade. According to compose. Js, the next passed into middleware is actually:
dispatch.bind(null, i + 1)
Copy the code
The working principle of
- Create a KOA object and call use(fn) to push fn into the middleware array of the KOA object
- Listen is then called to create a server container, and this.callback() is then called, listening for the specified port
- This.callback () first calls compose to process the middleware array, returning an entry function to the Onion model. It then returns a handleRequest
- The handleRequest function is called when a port is listening for a request, which eventually calls the onion model entry function.
- The handleRequest function takes two arguments, req, res; This function first creates a CTX based on the req, RES passed in; The result is then returned by calling the handleRequest function of the KOA object.
- The handleRequest function of the KOA object takes two arguments (CTX, fn), which is the CTX created from req, RES, and fn, which is the onion model entry function returned by the compose call. The handleRequest function on the KOA object will eventually call fn(CTX)
When the middleware needs to be cascaded, the middleware needs to be passed a second parameter, next
skills
let fn0 = async function(ctx,next){
console.log(0);
ctx.push(0)
await next()
await next().catch(err=>console.log(err))
console.log("fn0");
}
Copy the code
Note the problem with the middleware system here. The middleware can define instance methods and attributes for CTX arbitrarily, which can lead to issues of overwriting between the middleware and making it difficult to check for errors. Error checking is done by catching next in the middleware.
await next().catch(err=>console.log(err))
Copy the code
It prints errors, but it doesn’t crash.