当前位置:网站首页>Koa source code analysis
Koa source code analysis
2022-06-29 19:50:00 【Xia'an】
Koa Source analysis
Koa Source analysis
This article mainly describes from the perspective of source code Koa, Especially how the middleware system is implemented .
Follow Express comparison ,Koa The source code of is very concise ,Express Because the routing related code is embedded in the main logic , So read Express The source code of may be missing the point for a long time , And direct reading Koa There are few obstacles to the source code of .
Koa The main code of is located in the root directory lib In the folder , Only 4 File , After removing the comments, the source code is less than 1000 That's ok , This is listed below 4 The main functions of the files .
- request.js: Yes http request Encapsulation of objects .
- response.js: Yes http response Encapsulation of objects .
- context.js: Integrate the encapsulation of the above two files into context In the object
- application.js: Start the project and load the middleware .
1. Koa Start up process of
First of all, recall a Koa What is the structure of the application .
const Koa = require('Koa');
const app = new Koa();
// Load some middleware
app.use(...);
app.use(....);
app.use(.....);
app.listen(3000);
Koa The start-up process of is roughly divided into the following three steps :
- introduce Koa modular , Call the constructor to create a new
appobject . - Loading middleware .
- call
listenMethod listening port .
Let's take a step-by-step look at the implementation of the above three steps in the source code .
The first is the definition of class and constructor , This part of the code is located in application.js in .
// application.js
const response = require('./response')
const context = require('./context')
const request = require('./request')
const Emitter = require('events')
const util = require('util')
// ...... Other modules
module.exports = class Application extends Emitter {
constructor (options) {
super()
options = options || {
}
this.proxy = options.proxy || false
this.subdomainOffset = options.subdomainOffset || 2
this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For'
this.maxIpsCount = options.maxIpsCount || 0
this.env = options.env || process.env.NODE_ENV || 'development'
if (options.keys) this.keys = options.keys
this.middleware = []
// Below context,request,response They are introduced from the other three folders
this.context = Object.create(context)
this.request = Object.create(request)
this.response = Object.create(response)
// util.inspect.custom support for node 6+
/* istanbul ignore else */
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect
}
}
// ...... Other class methods
}
First, we note that this class inherits from Events modular , And then when we call Koa When the constructor for , Some properties and methods will be initialized , For example context/response/request New objects created for the prototype , There are also management middleware middleware Array etc. .
2. Loading of Middleware
The essence of middleware is a function . stay Koa in , This function usually has ctx and next Two parameters , Respectively denotes the encapsulated res/req Object and the next middleware to execute , When there are multiple middleware , It is essentially a nested call , Just like the onion picture .

Koa and Express In terms of calling, it is through calling app.use() To load a middleware , But the internal implementation is very different , Let's see first application.js Definition of relevant methods in .
/** * Use the given middleware `fn`. * * Old-style middleware will be converted. * * @param {Function} fn * @return {Application} self * @api public */
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
debug('use %s', fn._name || fn.name || '-')
this.middleware.push(fn)
return this
}
Koa stay application.js One of them was maintained middleware Array of , If a new middleware is loaded , Just push Into this array , There's nothing else to do , by comparison ,Express Of use The method is much more troublesome , Readers can refer to its source code by themselves .
Besides , In the previous version, this method also added isGeneratorFunction Judge , This is for compatibility Koa1.x And the middleware , stay Koa1.x in , Middleware is all Generator function ,Koa2 The use of async Function is not compatible with previous code , therefore Koa2 Provides convert Function to convert , We will not introduce this function any more .
if (isGeneratorFunction(fn)) {
// ......
fn = convert(fn)
}
Next, let's look at the call to middleware .
/** * Return a request handler callback * for node's native http server. * * @return {Function} * @api public */
callback () {
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
}
It can be seen that the core logic about middleware should be located in compose In the method , The method is a Koa-compose Third party modules for https://github.com/Koajs/compose, We can see how it is implemented internally .
This module has only one method compose, The call mode is compose([a, b, c, ...]), This method accepts an array of middleware as a parameter , The return is still a middleware ( function ), This function can be regarded as the function set of all middleware loaded before .
/** * Compose `middleware` returning * a fully valid middleware comprised * of all those which are passed. * * @param {Array} middleware * @return {Function} * @api public */
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)
}
}
}
}
The core of this method is a recursively called dispatch function , To better illustrate how this function works , Here, a simple custom middleware is used as an example to illustrate .
function myMiddleware(context, next) {
process.nextTick(function () {
console.log('I am a middleware');
})
next();
}
It can be seen that the middleware can print a message , And then call next Out of the way , There was no operation , We take this middleware as an example , stay Koa Of app.js Use in app.use Method to load the middleware twice .
const Koa = require('Koa');
const myMiddleware = require("./myMiddleware");
app.use(md1);
app.use(dm2);
app.listen(3000);
app The real instantiation is to call listen After method , Then the middleware loading is also located in listen After method .
that compose The actual call to the method is compose[myMiddleware,myMiddleware], In execution dispatch(0) when , This method can be simplified to :
function compose(middleware) {
return function (context, next) {
try {
return Promise.resolve(md1(context, function next() {
return Promise.resolve(md2(context, function next() {
}))
}))
} catch (err) {
return Promise.reject(err)
}
}
}
It can be seen that compose The essence of is still nested middleware .
3. listen() Method
This is a app The last step in the startup process , Readers will wonder : Why should such a line count as a separate step , in fact , Both of the above steps are for app Prepare to start , Whole Koa The application is started by listen Method to complete . Here is application.js in listen Method definition .
/** * Shorthand for: * * http.createServer(app.callback()).listen(...) * * @param {Mixed} ... * @return {Server} * @api public */
listen(...args) {
debug('listen')
const server = http.createServer(this.callback())
return server.listen(...args)
}
The code above is listen Contents of the method , You can see the 3 Line to really call http.createServer The method establishes http The server , The parameter is the previous section callback Method handleRequest Method , The source code is shown below , The method does two things :
- encapsulation
requestandresponseobject . - Call the middleware pair
ctxObject to process .
/** * Handle request in callback. * * @api private */
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)
}
4. next() And return next()
We mentioned it before ,Koa The implementation of middleware calls is essentially nested promise.resolve Method , We can write a simple example .
let ctx = 1;
const md1 = function (ctx, next) {
next();
}
const md2 = function (ctx, next) {
return ++ctx;
}
const p = Promise.resolve(
mdl(ctx, function next() {
return Promise.resolve(
md2(ctx, function next() {
// More middleware ...
})
)
})
)
p.then(function (ctx) {
console.log(ctx);
})
The variables defined in the first line of code ctx, We can think of it as Koa Medium ctx object , After middleware processing ,ctx The value of will change accordingly .
We defined md1 and md2 Two Middleware ,md1 I didn't do anything , Only called next Method ,md2 That's right. ctx Perform the operation of adding one , So in the last then In the method , We expect ctx The value of is 2.
We can try to run the above code , The end result is undefined, stay md1 Of next Add... Before the method return The key word , You can get normal results .
stay Koa Source code application.js in ,callback The last line of the method :
/** * Return a request handler callback * for node's native http server. * * @return {Function} * @api public */
callback () {
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
}
/** * Handle request in callback. * * @api private */
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)
}
Medium fnMiddleware(ctx) Equivalent to the previous code No 8 Line declaration Promise object p, Modified by the middleware method ctx The object is then Method passed to handleResponse Method to return to the client .
Each middleware method will return a Promise object , What it contains is right ctx Modification of , By calling next Method to call the next middleware .
fn(context, function next () {
return dispatch(i + 1);
})
Re pass return The keyword will be modified ctx Object as resolve The parameter of returns .
If multiple middleware are operated at the same time ctx object , Then it is necessary to use return Keyword returns the operation result to the middleware called by the upper level .
in fact , If the reader reads Koa-router perhaps Koa-static Source code , You will also find that they all use return next Method .
5. About Can’t set headers after they are sent.
This is the use of Express perhaps Koa One of the common mistakes , The reason is as literal , For the same HTTP The request was sent repeatedly HTTP HEADER . The server is processing HTTP When requesting, a response header will be sent first ( Use writeHead or setHeader Method ), Then send the body content ( adopt send perhaps end Method ), If you have a HTTP The request called twice writeHead Method , Will appear Can't set headers after they are sent Error prompt for , Take the following example :
const http = require("http");
http.createServer(function (req, res) {
res.setHeader('Content-Type', 'text/html');
res.end('ok');
resend(req, res); // Send the response message again after the response is completed
}).listen(5000);
function resend(req, res) {
res.setHeader('Content-Type', 'text/html');
res.end('error');
}
Try to visit localhost:5000 You will get an error message , This example is too straightforward . Here's a Express Examples in , Because middleware may contain asynchronous operations , So sometimes the cause of the error is hidden .
const express = require('express');
const app = express();
app.use(function (req, res, next) {
setTimeout(function () {
res.redirect("/bar");
}, 1000);
next();
});
app.get("/foo", function (req, res) {
res.end("foo");
});
app.get("/bar", function (req, res) {
res.end("bar");
});
app.listen(3000);
Run the above code , visit http://localhost:3000/foo Will produce the same error , The reason is also simple , After the request returns ,setTimeout Inside redirect Will be sent to a response Make changes , There will be errors , In the actual project, it will not be like setTimeout So obvious , It may be a database operation or other asynchronous operation , Special attention required .
6. Context Object implementation
About ctx How the object gets request/response Of properties and methods in an object , You can read context.js Source code , The core code is as follows . Besides ,delegate The module is also widely used in Koa In various middleware of .
const delegate = require('delegates')
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('has')
.method('set')
.method('append')
.method('flushHeaders')
.access('status')
.access('message')
.access('body')
.access('length')
.access('type')
.access('lastModified')
.access('etag')
.getter('headerSent')
.getter('writable')
delegate It's a Node Third-party module , The function is to delegate the properties and methods of one object to another object .
Readers can access the project address of the module https://github.com/tj/node-delegates, Then you will find that the main contributor of the module is TJ Holowaychuk.
The code for this module is also very simple , The source code is only 100 Multiple lines , Let's go into details here .
In the code above , We used the following three methods :
- method: Used to delegate methods to the target object .
- access: comprehensive
getterandsetter, You can read and write to the target . - getter: Generate an accessor for the target property , It can be understood as copying a read-only attribute to the target object .
getter and setter These two methods are used to control the read and write properties of objects , Here is method Methods and access Method implementation .
/** * Delegate method `name`. * * @param {String} name * @return {Delegator} self * @api public */
Delegator.prototype.method = function(name){
var proto = this.proto;
var target = this.target;
this.methods.push(name);
proto[name] = function(){
return this[target][name].apply(this[target], arguments);
};
return this;
};
method Method used in apply Method binds the method of the original target to the target object .
Here is access Method definition , A combination of getter Methods and setter Method .
/** * Delegator accessor `name`. * * @param {String} name * @return {Delegator} self * @api public */
Delegator.prototype.access = function(name){
return this.getter(name).setter(name);
};
/** * Delegator getter `name`. * * @param {String} name * @return {Delegator} self * @api public */
Delegator.prototype.getter = function(name){
var proto = this.proto;
var target = this.target;
this.getters.push(name);
proto.__defineGetter__(name, function(){
return this[target][name];
});
return this;
};
/** * Delegator setter `name`. * * @param {String} name * @return {Delegator} self * @api public */
Delegator.prototype.setter = function(name){
var proto = this.proto;
var target = this.target;
this.setters.push(name);
proto.__defineSetter__(name, function(val){
return this[target][name] = val;
});
return this;
};
And finally delegate Constructor for , This function takes two arguments , They are the source object and the target object .
/** * Initialize a delegator. * * @param {Object} proto * @param {String} target * @api public */
function Delegator(proto, target) {
if (!(this instanceof Delegator)) return new Delegator(proto, target);
this.proto = proto;
this.target = target;
this.methods = [];
this.getters = [];
this.setters = [];
this.fluents = [];
}
It can be seen that deletgate Object maintains some arrays inside , Respectively represent the target object and method obtained by the delegation .
About dynamically loading middleware
In some application scenarios , Developers may want to be able to dynamically load middleware , For example, when the route receives a request, it loads the corresponding middleware , But in Koa This cannot be done . The reason has already been included in the previous content ,Koa The only time an application loads all middleware is when it invokes listen Method time , Even if it is called later app.use Method , It won't work .
7. Koa Advantages and disadvantages
Through the above , Believe that the reader is right Koa Have a general understanding , and Express comparison ,Koa The advantage of is to simplify , It strips away all the middleware , And the implementation of middleware has been greatly optimized .
An experienced one Express Developers want to go to Koa It doesn't cost much , The only thing to note is that the strategies implemented by the middleware are different , This may lead to a period of maladjustment .
Now let's talk about Koa The shortcomings of , Stripping middleware is an advantage , But it also makes the combination of different middleware troublesome ,Express After years of precipitation , Middleware for various purposes has been mature ; and Koa Different ,Koa2.0 The launch time is still very short , The middleware is not perfect , Sometimes it's OK to use various middleware alone , But once combined , It may not work properly .
for instance , If you want to use it at the same time router and views Two Middleware , Will be in render Add... Before the method return keyword ( and return next() A truth ), For rigid contact Koa It may take a long time for developers to locate the problem . Another example is the previous koa-session and Koa-router, When I first came into contact with these two middleware, I really spent some effort to correctly combine them . Although the introduction of the concept of middleware has made Node Development becomes like building blocks , But if the building blocks can not be smoothly spliced together , It will also increase the development cost .
边栏推荐
- 命令执行(RCE)漏洞
- [network orientation training] - Enterprise Park Network Design - [had done]
- npm ERR! fatal: early EOF npm ERR! fatal: index-pack failed
- 社区访谈丨一个IT新人眼中的JumpServer开源堡垒机
- MySQL remote connection
- What if the win11 policy service is disabled? Solution to disabling win11 policy service
- How to set a pod to run on a specified node
- JVM (4) Bytecode Technology + Runtime Optimization
- JVM(4) 字節碼技術+運行期優化
- [observation] softcom power liutianwen: embrace change and "follow the trend" to become an "enabler" of China's digital economy
猜你喜欢

4-1 port scanning technology

lock4j--分布式锁中间件--自定义获取锁失败的逻辑

创作者基金会 6 月份亮点

以其他组件为代价的性能提升不是好提升
![[USB flash disk test] in order to transfer the data at the bottom of the pressure box, I bought a 2T USB flash disk, and the test result is only 47g~](/img/c3/e0637385d35943f1914477bb9f2b54.png)
[USB flash disk test] in order to transfer the data at the bottom of the pressure box, I bought a 2T USB flash disk, and the test result is only 47g~

Flume配置3——拦截器过滤

As the "only" privacy computing provider, insight technology is the "first" to settle in the Yangtze River Delta data element circulation service platform

Deficiencies and optimization schemes in Dao

通过MeterSphere和DataEase实现项目Bug处理进展实时跟进
MSYQL, redis, mongodb visual monitoring tool grafana
随机推荐
Win11 system component cannot be opened? Win11 system widget cannot be opened solution
Have you mastered all the testing methods of technology to ensure quality and software testing?
Tiger painter mengxiangshun's digital collection is on sale in limited quantities and comes with Maotai in the year of the tiger
创作者基金会 6 月份亮点
JVM (4) bytecode technology + runtime optimization
In 2022, the financial interest rate has dropped, so how to choose financial products?
剑指 Offer 59 - II. 队列的最大值
[USB flash disk test] in order to transfer the data at the bottom of the pressure box, I bought a 2T USB flash disk, and the test result is only 47g~
如何设置 Pod 到指定节点运行
命令执行(RCE)漏洞
Lingyun going to sea | Wenhua online &huawei cloud: creating a new solution for smart teaching in Africa
Inception 新结构 | 究竟卷积与Transformer如何结合才是最优的?
剑指 Offer 66. 构建乘积数组
KDD 2022 | 协同过滤中考虑表征对齐和均匀性
1404万!四川省人社厅关系型数据库及中间件软件系统升级采购招标!
There is no small green triangle on the method in idea
Performance improvement at the cost of other components is not good
3 - 3 découverte de l'hôte - découverte à quatre niveaux
KDD 2022 | 協同過濾中考慮錶征對齊和均勻性
2022年理财利率都降了,那该如何选择理财产品?