在下面分别对express用法和koa用法简单进行简单展示
Express
import express from \'express\';
import routes from \'../Routes\';
import proxy from \'express-http-proxy\';
const app = express();
app.use(express.static(\'public\'));
app.use(\'/api\', proxy(\'http://localhost:4000\', {
proxyReqPathResolver: function(req) {
return \'/api\'+req.url
}
}));
app.use(\'/\', routes)
var server = app.listen(3001, function () {
var host = server.address().address;
var port = server.address().port;
console.log("应用实例,访问地址为 http://%s:%s", host, port);
})
Koa
import koa2 from \'koa2\';
import routes from \'../Routes\';
import proxy from \'koa2-proxy-middleware\';
const router = require(\'koa-router\')()
const proxyOptions = {
target: \'http://localhost:4000\', //后端服务器地址
changeOrigin: true //处理跨域
};
const app = koa2();
app.use(require(\'koa-static\')(__dirname + \'../public\'))
//api前缀的请求都走代理
app.use(proxy(\'/api/*\', proxyOptions));
app.use(router.routes())
var server = app.listen(3001, function () {
var host = server.address().address;
var port = server.address().port;
console.log("应用实例,访问地址为 http://%s:%s", host, port);
})
目前可以挂载中间件进去的有:(HTTP Method指代那些http请求方法,诸如Get/Post/Put等等)
- app.use
- app.[HTTP Method]
- app.all
- app.param
- router.all
- router.use
- router.param
- router.[HTTP Method]
express中间件
express代码中依赖于几个变量(实例):app、router、layer、route
Layer实例是path和handle互相映射的实体,每一个Layer便是一个中间件
++router.use++:使用app.use、router.use来挂载的,
app.use经过一系列处理之后最终也是调用router.use的
router.route:使用app.all、app.[Http Method]、app.route、router.all、router.[Http Method]、router.route来挂载的
router.use 源码分析
// mixin Router class functions
setPrototypeOf(router, proto)
proto.use = function use(fn) {
var offset = 0;
var path = \'/\';
// default path to \'/\'
// disambiguate router.use([fn])
if (typeof fn !== \'function\') {
var arg = fn;
while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0];
}
// first arg is the path
if (typeof arg !== \'function\') {
offset = 1;
path = fn;
}
}
var callbacks = flatten(slice.call(arguments, offset));
if (callbacks.length === 0) {
throw new TypeError(\'Router.use() requires a middleware function\')
}
for (var i = 0; i < callbacks.length; i++) {
var fn = callbacks[i];
if (typeof fn !== \'function\') {
throw new TypeError(\'Router.use() requires a middleware function but got a \' + gettype(fn))
}
// add the middleware
debug(\'use %o %s\', path, fn.name || \'<anonymous>\')
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);
layer.route = undefined;
this.stack.push(layer);
}
return this;
};
从以上代码可以看出, router.use 就是初始化多个 layer 实例
// setup router
this.lazyrouter();
在启动 router 的时候,调用了 this.lazyrouter() 方法
// lazily adds the base router if it has not yet been added
app.lazyrouter = function lazyrouter() {
if (!this._router) {
this._router = new Router({
caseSensitive: this.enabled(\'case sensitive routing\'),
strict: this.enabled(\'strict routing\')
});
this._router.use(query(this.get(\'query parser fn\')));
this._router.use(middleware.init(this));
}
};
两个系统自带的,看初始化实例图的Layer的名字分别是:query 和 middleware.init, 这两个方法最终调用的也是router.use方法
router.route
proto.route = function route(path) {
var route = new Route(path);
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true
}, route.dispatch.bind(route));
layer.route = route;
this.stack.push(layer);
return route;
};
从以上源码中可以看出, new Route 实例化了一个Route, layer.route = route,其他的跟 router.use 差不多 .源码中初始Layer,其中的回调是route.dispatch.bind(route)
function Layer(path, options, fn) {
if (!(this instanceof Layer)) {
return new Layer(path, options, fn);
}
debug(\'new %o\', path)
var opts = options || {};
this.handle = fn;
this.name = fn.name || \'<anonymous>\';
this.params = undefined;
this.path = undefined;
this.regexp = pathRegexp(path, this.keys = [], opts);
// set fast path flags
this.regexp.fast_star = path === \'*\'
this.regexp.fast_slash = path === \'/\' && opts.end === false
}
我们如果使用箭头函数,不存在函数名,打印出来的 layer 的 name 是 anonymous
Route.prototype.all = function all() {
var handles = flatten(slice.call(arguments));
for (var i = 0; i < handles.length; i++) {
var handle = handles[i];
if (typeof handle !== \'function\') {
var type = toString.call(handle);
var msg = \'Route.all() requires a callback function but got a \' + type
throw new TypeError(msg);
}
var layer = Layer(\'/\', {}, handle);
layer.method = undefined;
this.methods._all = true;
this.stack.push(layer);
}
return this;
};
新建的route实例,维护的是一个path,对应多个method的handle的映射。每一个method对应的handle都是一个layer,path统一为/
((req, res) => {
……
(async(req, res) => {
……
})(req, res)
})(req, res)
实际上 express 的表现形式大概如此
koa2中间件
当通过 require 去载入 koa 模块时,找到模块下的package.json中的:
"main": "lib/application.js",
最终指向
module.exports = class Application extends Emitter {
}
挂载中间件
/**
* 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!\');
if (isGeneratorFunction(fn)) {
deprecate(\'Support for generators will be removed in v3. \' +
\'See the documentation for examples of how to convert old middleware \' +
\'https://github.com/koajs/koa/blob/master/docs/migration.md\');
fn = convert(fn);
}
debug(\'use %s\', fn._name || fn.name || \'-\');
this.middleware.push(fn);
return this;
}
以上代码可以看出,首先检查是否是方法,其次判断是否是生成器函数,因为在Koa1.x版本中是通过Generator+Promise+Co实现的,因此将中间件定义成了Generator Function。但自从Koa v2版本起,它的异步控制方案就开始支持Async/Await,因此中间件也用普通函数就可以了
convert:即koa-convert,作用是加入了一层函数嵌套,并使用Co自动执行原Generator函数
最后一段代码的作用是把传入的函数,push到this.middleware属性的尾部,this.middleware是一个数组,用来存储中间件
this.middleware = [];
响应请求
/**
* 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);
}
通过 http.createServer 创建服务,this.callback() 方法中一般会res指定了响应头,响应体内容为node.js,用end结束,我们来继续看下源码。
/**
* 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;
}
其中ctx是网络请求上下文,我们继续看下this.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);
}
这段代码中,主要执行了:
- 错误处理:onerror函数
- onFinished监听response执行完成,以用来做一些资源清理工作。
- 执行传入的fnMiddleware
-最终等待中间件执行完,最终执行handleResponse函数,开始组织响应,代码如下:
/**
* Response helper.
*/
function respond(ctx) {
// allow bypassing koa
if (false === ctx.respond) return;
if (!ctx.writable) return;
const res = ctx.res;
let body = ctx.body;
const code = ctx.status;
// ignore body
if (statuses.empty[code]) {
// strip headers
ctx.body = null;
return res.end();
}
if (\'HEAD\' == ctx.method) {
if (!res.headersSent && isJSON(body)) {
ctx.length = Buffer.byteLength(JSON.stringify(body));
}
return res.end();
}
// status body
if (null == body) {
if (ctx.req.httpVersionMajor >= 2) {
body = String(code);
} else {
body = ctx.message || String(code);
}
if (!res.headersSent) {
ctx.type = \'text\';
ctx.length = Buffer.byteLength(body);
}
return res.end(body);
}
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if (\'string\' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// body: json
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}
刚刚我们似乎漏下了一个主要的方法compose(this.middleware),它是如何来组织中间件的呢?
const compose = require(\'koa-compose\');
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!\')
}
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)
}
}
}
}
在参数中context:是透传的网络对象上下文,next:目前是undefined,后面会说明,它是用来表示所有中间件走完之后,最后执行的一个函数。
首先检查数组类型及数组里每个元素的类型,标识了一个变量index,等下讲dispatch函数的时候会看到它的作用 —— 用于标识「上一次执行到了哪个中间件」
// 校验预期执行的中间件,其索引是否在已经执行的中间件之后
if (i <= index) return Promise.reject(new Error(\'next() called multiple times\'))
执行过了的就不再执行
let fn = middleware[i]
取预期执行的中间件函数
if (i === middleware.length) fn = next
预期执行的中间件索引,已经超出了middleware边界,说明中间件已经全部执行完毕,开始准备执行之前传入的next
if (!fn) return Promise.resolve()
没有fn的话,直接返回一个已经reolved的Promise对象
try {
// 对中间件的执行结果包裹一层Promise.resolve
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
通过递归,对中间件的执行结果包裹一层Promise.resolve,next相当于是上一个中间件对下一个中间件的执行权,调用 next(),执行下一个中间件。
而Promise有个特性,如果Promise.resolve接受的参数,也是个Promise,那么外部的Promise会等待该内部的Promise变成resolved之后,才变成resolved,例如下面的这段代码:
Promise.resolve(new Promise((resolve => {
setTimeout(() => {
console.log(\'A Resolved\');
resolve()
}, 0);
})))
.then(() => { console.log(\'B Resolved\')})
// 先输出:A Resolved
// 后输出:B Resolved
总结
koa2 源码中主要利用闭包和递归的性质,一个个执行,因为每次返回的时候promise.resolve()中的都是Promise对象,然后会去等待方法参数中的Promise执行完then然后再返回,因此如果中间键有await会先执行完异步,按顺序执行;而Express中不是通过promise.resolve()的方式因此无法保证中间件中的await按顺序执行。