您现在的位置是:网站首页> 编程资料编程资料

Node.js深入分析Koa源码_node.js_

2023-05-24 410人已围观

简介 Node.js深入分析Koa源码_node.js_

Koa 的主要代码位于根目录下的 lib 文件夹中,只有 4 个文件,去掉注释后的源码不到 1000 行,下面列出了这 4 个文件的主要功能。

  • request.js:对 http request 对象的封装。
  • response.js:对 http response 对象的封装。
  • context.js:将上面两个文件的封装整合到 context 对象中
  • application.js:项目的启动及中间件的加载。

1. Koa 的启动过程

首先回忆一下一个 Koa 应用的结构是什么样子的。

const Koa = require('Koa'); const app = new Koa(); //加载一些中间件 app.use(...); app.use(....); app.use(.....); app.listen(3000);

Koa 的启动过程大致分为以下三个步骤:

  • 引入 Koa 模块,调用构造方法新建一个 app 对象。
  • 加载中间件。
  • 调用 listen 方法监听端口。

我们逐步来看上面三个步骤在源码中的实现。

首先是类和构造函数的定义,这部分代码位于 application.js 中。

// application.js const response = require('./response') const context = require('./context') const request = require('./request') const Emitter = require('events') const util = require('util') // ...... 其他模块 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 = [] // 下面的 context,request,response 分别是从其他三个文件夹中引入的 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 } } // ...... 其他类方法 }

首先我们注意到该类继承于 Events 模块,然后当我们调用 Koa 的构造函数时,会初始化一些属性和方法,例如以context/response/request为原型创建的新的对象,还有管理中间件的 middleware 数组等。

2. 中间件的加载

中间件的本质是一个函数。在 Koa 中,该函数通常具有 ctxnext 两个参数,分别表示封装好的 res/req 对象以及下一个要执行的中间件,当有多个中间件的时候,本质上是一种嵌套调用,就像洋葱图一样。

Koa 和 Express 在调用上都是通过调用 app.use() 的方式来加载一个中间件,但内部的实现却大不相同,我们先来看application.js 中相关方法的定义。

/** * 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 在 application.js 中维持了一个 middleware 的数组,如果有新的中间件被加载,就 push 到这个数组中,除此之外没有任何多余的操作,相比之下,Express 的 use 方法就麻烦得多,读者可以自行参阅其源码。

此外,之前版本中该方法中还增加了 isGeneratorFunction 判断,这是为了兼容 Koa1.x 的中间件而加上去的,在 Koa1.x 中,中间件都是 Generator 函数,Koa2 使用的 async 函数是无法兼容之前的代码的,因此 Koa2 提供了 convert 函数来进行转换,关于这个函数我们不再介绍。

if (isGeneratorFunction(fn)) { // ...... fn = convert(fn) } 

接下来我们来看看对中间件的调用。

/** * 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 }

可以看出关于中间件的核心逻辑应该位于 compose 方法中,该方法是一个名为 Koa-compose 的第三方模块https://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: * * 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) } 

上面的代码就是 listen 方法的内容,可以看出第 3 行才真正调用了 http.createServer 方法建立了 http 服务器,参数为上节 callback 方法返回的 handleRequest 方法,源码如下所示,该方法做了两件事:

  • 封装 requestresponse 对象。
  • 调用中间件对 ctx 对象进行处理。
/** * 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()与return next()

我们前面也提到过,Koa 对中间件调用的实现本质上是嵌套的 promise.resolve 方法,我们可以写一个简单的例子。

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() { //更多的中间件... }) ) }) ) p.then(function (ctx) { console.log(ctx); })

代码在第一行定义的变量 ctx,我们可以将其看作 Koa 中的 ctx 对象,经过中间件的处理后,ctx 的值会发生相应的变化。

我们定义了 md1md2 两个中间件,md1 没有做任何操作,只调用了 next 方法,md2 则是对 ctx 执行加一的操作,那么在最后的 then 方法中,我们期望 ctx 的值为 2。

我们可以尝试运行上面的代码,最后的结果却是 undefined,在 md1next 方法前加上 return 关键字后,就能得到正常的结果了。

在 Koa 的源码 application.js 中,callback 方法的最后一行:

/** * 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) }

中的 fnMiddleware(ctx) 相当于之前代码第 8 行声明的 Promise 对象 p,被中间件方法修改后的 ctx 对象被 then 方法传给 handleResponse 方法返回给客户端。

每个中间件方法都会返回一个 Promise 对象,里面包含的是对 ctx 的修改,通过调用 next 方法来调用下一个中间件。

fn(context, function next () { return dispatch(i + 1); }) 

再通过

-六神源码网