如何深入分析Koa源码,改写其核心机制为长尾词?
- 内容介绍
- 文章标签
- 相关推荐
本文共计2299个文字,预计阅读时间需要10分钟。
目录
1.Koa 的启动过程
2.中间件的加载
3.listen() 方法
4.next() 与 return next()
5.关于 Cant set headers after they are sent.
6.Context 对象的实现
7.Koa 的优缺点
- Koa 的主要代码位于根目录下的 lib 文件夹中目录
- 1. Koa 的启动过程
- 2. 中间件的加载
- 3. listen() 方法
- 4. next()与return next()
- 5. 关于 Can’t set headers after they are sent.
- 6. Context 对象的实现
- 7. Koa 的优缺点
Koa 的主要代码位于根目录下的 lib 文件夹中,只有 4 个文件,去掉注释后的源码不到 1000 行,下面列出了这 4 个文件的主要功能。
- request.js:对 github.com/Koajs/compose,我们可以看看其内部是如何实现的。
该模块只有一个方法
compose,调用方式为compose([a, b, c, ...]),该方法接受一个中间件的数组作为参数,返回的仍然是一个中间件(函数),可以将这个函数看作是之前加载的全部中间件的功能集合。/** * 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) } } } }
该方法的核心是一个递归调用的
dispatch函数,为了更好地说明这个函数的工作原理,这里使用一个简单的自定义中间件作为例子来配合说明。function myMiddleware(context, next) { process.nextTick(function () { console.log('I am a middleware'); }) next(); }
可以看出这个中间件除了打印一条消息,然后调用
next方法之外,没有进行任何操作,我们以该中间件为例,在 Koa 的 app.js 中使用app.use方法加载该中间件两次。const Koa = require('Koa'); const myMiddleware = require("./myMiddleware"); app.use(md1); app.use(dm2); app.listen(3000);
app真正实例化是在调用listen方法之后,那么中间件的加载同样位于listen方法之后。那么
compose方法的实际调用为compose[myMiddleware,myMiddleware],在执行dispatch(0)时,该方法实际可以简化为: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) } } }
可以看出
compose的本质仍是嵌套的中间件。3. listen() 方法
这是
app启动过程中的最后一步,读者会疑惑:为什么这么一行也要算作单独的步骤,事实上,上面的两步都是为了app的启动做准备,整个 Koa 应用的启动是通过listen方法来完成的。下面是 application.js 中listen方法的定义。/** * Shorthand for: * * localhost:3000/foo 会产生同样的错误,原因也很简单,在请求返回之后,
setTimeout内部的redirect会对一个已经发送出去的response进行修改,就会出现错误,在实际项目中不会像setTimeout这么明显,可能是一个数据库操作或者其他的异步操作,需要特别注意。6. Context 对象的实现
关于
ctx对象是如何得到request/response对象中的属性和方法的,可以阅读 context.js 的源码,其核心代码如下所示。此外,delegate模块还广泛运用在了 Koa 的各种中间件中。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是一个 Node 第三方模块,作用是把一个对象中的属性和方法委托到另一个对象上。读者可以访问该模块的项目地址 github.com/tj/node-delegates,然后就会发现该模块的主要贡献者还是TJ Holowaychuk。
这个模块的代码同样非常简单,源代码只有 100 多行,我们这里详细介绍一下。
在上面的代码中,我们使用了如下三个方法:
- method:用于委托方法到目标对象上。
- access:综合
getter和setter,可以对目标进行读写。 - getter:为目标属性生成一个访问器,可以理解成复制了一个只读属性到目标对象上。
getter和setter这两个方法是用来控制对象的读写属性的,下面是method方法与access方法的实现。/** * 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方法中使用apply方法将原目标的方法绑定到目标对象上。下面是
access方法的定义,综合了getter方法和setter方法。/** * 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; };
最后是
delegate的构造函数,该函数接收两个参数,分别是源对象和目标对象。/** * 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 = []; }
可以看出
deletgate对象在内部维持了一些数组,分别表示委托得到的目标对象和方法。关于动态加载中间件
在某些应用场景中,开发者可能希望能够动态加载中间件,例如当路由接收到某个请求后再去加载对应的中间件,但在 Koa 中这是无法做到的。原因其实已经包含在前面的内容了,Koa 应用唯一一次加载所有中间件是在调用
listen方法的时候,即使后面再调用app.use方法,也不会生效了。7. Koa 的优缺点
通过上面的内容,相信读者已经对 Koa 有了大概的认识,和 Express 相比,Koa 的优势在于精简,它剥离了所有的中间件,并且对中间件的执行做了很大的优化。
一个经验丰富的 Express 开发者想要转到 Koa 上并不需要很大的成本,唯一需要注意的就是中间件执行的策略会有差异,这可能会带来一段时间的不适应。
现在我们来说说 Koa 的缺点,剥离中间件虽然是个优点,但也让不同中间件的组合变得麻烦起来,Express 经过数年的沉淀,各种用途的中间件已经很成熟;而 Koa 不同,Koa2.0 推出的时间还很短,适配的中间件也不完善,有时单独使用各种中间件还好,但一旦组合起来,可能出现不能正常工作的情况。
举个例子,如果想同时使用
router和views两个中间件,就要在render方法前加上return关键字(和return next()一个道理),对于刚接触 Koa 的开发者可能要花很长时间才能定位问题所在。再例如前面的koa-session和Koa-router,我初次接触这两个中间件时也着实花了一些功夫来将他们正确地组合在一块。虽然中间件概念的引入让Node开发变得像搭积木一样,但积木之间如果不能很顺利地拼接在一块的话,也会增加开发成本。到此这篇关于Node.js深入分析Koa源码的文章就介绍到这了,更多相关Node.js Koa内容请搜索易盾网络以前的文章或继续浏览下面的相关文章希望大家以后多多支持易盾网络!
本文共计2299个文字,预计阅读时间需要10分钟。
目录
1.Koa 的启动过程
2.中间件的加载
3.listen() 方法
4.next() 与 return next()
5.关于 Cant set headers after they are sent.
6.Context 对象的实现
7.Koa 的优缺点
- Koa 的主要代码位于根目录下的 lib 文件夹中目录
- 1. Koa 的启动过程
- 2. 中间件的加载
- 3. listen() 方法
- 4. next()与return next()
- 5. 关于 Can’t set headers after they are sent.
- 6. Context 对象的实现
- 7. Koa 的优缺点
Koa 的主要代码位于根目录下的 lib 文件夹中,只有 4 个文件,去掉注释后的源码不到 1000 行,下面列出了这 4 个文件的主要功能。
- request.js:对 github.com/Koajs/compose,我们可以看看其内部是如何实现的。
该模块只有一个方法
compose,调用方式为compose([a, b, c, ...]),该方法接受一个中间件的数组作为参数,返回的仍然是一个中间件(函数),可以将这个函数看作是之前加载的全部中间件的功能集合。/** * 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) } } } }
该方法的核心是一个递归调用的
dispatch函数,为了更好地说明这个函数的工作原理,这里使用一个简单的自定义中间件作为例子来配合说明。function myMiddleware(context, next) { process.nextTick(function () { console.log('I am a middleware'); }) next(); }
可以看出这个中间件除了打印一条消息,然后调用
next方法之外,没有进行任何操作,我们以该中间件为例,在 Koa 的 app.js 中使用app.use方法加载该中间件两次。const Koa = require('Koa'); const myMiddleware = require("./myMiddleware"); app.use(md1); app.use(dm2); app.listen(3000);
app真正实例化是在调用listen方法之后,那么中间件的加载同样位于listen方法之后。那么
compose方法的实际调用为compose[myMiddleware,myMiddleware],在执行dispatch(0)时,该方法实际可以简化为: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) } } }
可以看出
compose的本质仍是嵌套的中间件。3. listen() 方法
这是
app启动过程中的最后一步,读者会疑惑:为什么这么一行也要算作单独的步骤,事实上,上面的两步都是为了app的启动做准备,整个 Koa 应用的启动是通过listen方法来完成的。下面是 application.js 中listen方法的定义。/** * Shorthand for: * * localhost:3000/foo 会产生同样的错误,原因也很简单,在请求返回之后,
setTimeout内部的redirect会对一个已经发送出去的response进行修改,就会出现错误,在实际项目中不会像setTimeout这么明显,可能是一个数据库操作或者其他的异步操作,需要特别注意。6. Context 对象的实现
关于
ctx对象是如何得到request/response对象中的属性和方法的,可以阅读 context.js 的源码,其核心代码如下所示。此外,delegate模块还广泛运用在了 Koa 的各种中间件中。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是一个 Node 第三方模块,作用是把一个对象中的属性和方法委托到另一个对象上。读者可以访问该模块的项目地址 github.com/tj/node-delegates,然后就会发现该模块的主要贡献者还是TJ Holowaychuk。
这个模块的代码同样非常简单,源代码只有 100 多行,我们这里详细介绍一下。
在上面的代码中,我们使用了如下三个方法:
- method:用于委托方法到目标对象上。
- access:综合
getter和setter,可以对目标进行读写。 - getter:为目标属性生成一个访问器,可以理解成复制了一个只读属性到目标对象上。
getter和setter这两个方法是用来控制对象的读写属性的,下面是method方法与access方法的实现。/** * 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方法中使用apply方法将原目标的方法绑定到目标对象上。下面是
access方法的定义,综合了getter方法和setter方法。/** * 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; };
最后是
delegate的构造函数,该函数接收两个参数,分别是源对象和目标对象。/** * 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 = []; }
可以看出
deletgate对象在内部维持了一些数组,分别表示委托得到的目标对象和方法。关于动态加载中间件
在某些应用场景中,开发者可能希望能够动态加载中间件,例如当路由接收到某个请求后再去加载对应的中间件,但在 Koa 中这是无法做到的。原因其实已经包含在前面的内容了,Koa 应用唯一一次加载所有中间件是在调用
listen方法的时候,即使后面再调用app.use方法,也不会生效了。7. Koa 的优缺点
通过上面的内容,相信读者已经对 Koa 有了大概的认识,和 Express 相比,Koa 的优势在于精简,它剥离了所有的中间件,并且对中间件的执行做了很大的优化。
一个经验丰富的 Express 开发者想要转到 Koa 上并不需要很大的成本,唯一需要注意的就是中间件执行的策略会有差异,这可能会带来一段时间的不适应。
现在我们来说说 Koa 的缺点,剥离中间件虽然是个优点,但也让不同中间件的组合变得麻烦起来,Express 经过数年的沉淀,各种用途的中间件已经很成熟;而 Koa 不同,Koa2.0 推出的时间还很短,适配的中间件也不完善,有时单独使用各种中间件还好,但一旦组合起来,可能出现不能正常工作的情况。
举个例子,如果想同时使用
router和views两个中间件,就要在render方法前加上return关键字(和return next()一个道理),对于刚接触 Koa 的开发者可能要花很长时间才能定位问题所在。再例如前面的koa-session和Koa-router,我初次接触这两个中间件时也着实花了一些功夫来将他们正确地组合在一块。虽然中间件概念的引入让Node开发变得像搭积木一样,但积木之间如果不能很顺利地拼接在一块的话,也会增加开发成本。到此这篇关于Node.js深入分析Koa源码的文章就介绍到这了,更多相关Node.js Koa内容请搜索易盾网络以前的文章或继续浏览下面的相关文章希望大家以后多多支持易盾网络!

