This is the 25th day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021
I am small seventeen _, today with you read koA middleware source code, easy to understand, package education package will ~
- IO /how-koa-mid…
Koa’s middleware is different from Express in that Koa uses the Onion model principle. Its source code contains only four files, very friendly for the students who read the source code for the first time. Today we will only look at the main file – application.js, which already contains the core logic of how the middleware works.
Lead to
First clone the KOA source code
git clone [email protected]:koajs/koa.git
npm install
Copy the code
We then add an index.js to the root of the project for testing
// index.js
// Include koA entry files
const Koa = require('./lib/application.js');
const app = new Koa();
const debug = require('debug') ('koa');
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(6);
const rt = ctx.response.get('X-Response-Time');
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});
// Time log
app.use(async (ctx, next) => {
console.log(2);
const start = Date.now();
await next();
console.log(5);
const ms = Date.now() - start;
ctx.set('X-Response-Time'.`${ms}ms`);
});
app.use(async (ctx, next) => {
console.log(3);
ctx.body = 'Hello World';
await next();
console.log(4);
});
app.listen(3000);
Copy the code
Run the following command to start the server:
node index.js
Copy the code
Then go to http://localhost:3000 and you’ll see 1, 2, 3, 4, 5, 6 output. This is called the Onion model
How the Onion model works
Let’s read koA’s core code to see how the middleware works. In index.js, we use middleware like this:
const app = new Koa();
app.use(// middleware);
app.use(// middleware);
app.listen(3000);
Copy the code
Let’s take a look at application.js, which is in the lib directory of the source code. Here is the middleware related code. I have simplified the code, keeping the core middleware logic and adding some comments to the code.
module.exports = class Application extends Emitter {
constructor() {
super(a);this.proxy = false;
// Step 0: Initialize the middleware array
this.middleware = [];
}
use(fn) {
// Step 1: Push middleware into array
this.middleware.push(fn);
return this;
}
listen(. args) {
debug('listen');
// Step 2: Call this.callback() to combine all middleware
const server = http.createServer(this.callback());
returnserver.listen(... args); }callback() {
// Step 3: The most important part of the compose function is the compose function
// The middleware is combined into one big function that returns a promise, which we'll discuss later
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);
Step 4: Resolve this promise
returnfnMiddleware(ctx).then(handleResponse).catch(onerror); }}Copy the code
Let’s simplify the code to pseudo-code for the compose function only:
listen(. args) {
const server = http.createServer(this.callback());
}
callback() {
/ / compose function
const fn = compose(this.middleware);
return this.handleRequest(ctx, fn);
}
handleRequest(ctx, fnMiddleware) {
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
Copy the code
The compose function, for example, returns a function called fn, which returns a promise.
About compose Function
For more information about the compose function, look at the source code for the KOa-compose package
module.exports = compose
function compose (middleware) {
// Skip the type checking code here
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
As we guessed above, the function that compose returns is called fn, and all middleware is passed by compose to the FN function, which returns dispatch(0), which immediately executes the dispatch function and returns a promise. Before we look at the contents of the Dispatch function, we need to look at the syntax of Promises.
On the Promise
We usually use promises like this:
const promise = new Promise(function(resolve, reject) {
if (success){
resolve(value);
} else{ reject(error); }});Copy the code
In Koa, it is used like this:
let testPromise = new Promise((resolve, reject) = > {
setTimeout(() = > {
resolve('test success');
}, 1000);
});
Promise.resolve(testPromise).then(function (value) {
console.log(value); // "test success"
});
Copy the code
Therefore, we know that in the compose function, it returns a promise.
Back to the KOa-compose middleware
module.exports = compose
function compose (middleware) {
// Skip the type checking code here
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
Dispatch is a recursive function that loops through all middleware. Let’s simplify the recursion:
let fn = middleware[i]
fn(context, dispatch.bind(null, i + 1))
Copy the code
Here fn is the current middleware function, which executes fn with context and dispatch.bind(null, I + 1) (which we passed to next), and the middleware executes this function, which recursively executes dispatch. Look at the following analysis:
In our test file index.js we have 3 middleware and all 3 middleware will execute this code before await next();
app.use(async (ctx, next) => {
console.log(2);
const start = Date.now();
await next(); // <- stop here and wait for the next middleware to execute
console.log(5);
const ms = Date.now() - start;
ctx.set('X-Response-Time'.`${ms}ms`);
});
Copy the code
We can look at the execution order of the three middleware in index.js:
- perform
dispatch(0)
When,Resolve (fn(context, dispatch.bind(null, 0 + 1)))
- The first middleware content will run until
await next()
next()
=dispatch.bind(null, 0 + 1)
This is the second middleware- The second middleware will run until
await next()
next()
=dispatch.bind(null, 1 + 1)
This is the third middleware- The third middleware will run until
await next()
next() = dispatch.bind(null, 2 + 1)
, no fourth middleware will pass immediatelyif (! fn) return Promise.resolve()
Returns, in the third middlewareawait next()
It is parsed and the remaining code in the third middleware is executed.- In the second middleware
await next()
The remaining code in the second middleware is executed. - In the first middleware
await next()
The remaining code in the first middleware is parsed and executed.
Why the Onion model?
If we had async/await in the middleware, coding would be easier. When we want to write a time logger for API requests, it is very easy to add this middleware:
app.use(async (ctx, next) => {
const start = Date.now();
await next(); // Your API logic
const ms = Date.now() - start;
console.log('API response time:' + ms);
});
Copy the code