在下面分别对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按顺序执行。