koa koa 是由 Express 原班人马打造的,致力于成为一个更小、更富有表现力、更健壮的 Web 框架。 使用 koa 编写 web 应用,通过组合不同的 generator,可以免除重复繁琐的回调函数嵌套, 并极大地提升错误处理的效率。koa 不在内核方法中绑定任何中间件, 它仅仅提供了一个轻量优雅的函数库,使得编写 Web 应用变得得心应手。
安装、版本依赖请点我点我 ~ O(∩_∩)O 哈哈~
开篇怎能没有 hello world 呢?→_→
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const Koa = require ('koa' );const app = new Koa();app.use(async ctx => { ctx.body = 'Hello World' ; }); app.listen(3000 ); const http = require ('http' );const Koa = require ('koa' );const app = new Koa();app.use(async ctx => { ctx.body = 'Hello World' ; }); http.createServer(app.callback()).listen(3000 );
koa 图解 图中一层一层的洋葱圈,其实就是每一个中间件函数middlewarefn(ctx, next)
,有的是系统中间件,有的是用户中间件,这取决于用户如何定义中间件在系统中的功能层级(都是通过 app.use()加载,koa 内部不封装任何中间件函数)。
路线分析:外层到内层-对 request 请求进行处理,内层再到外层-对 response 请求进行处理。
代码分析:
1 2 3 4 5 6 7 8 async middlewarefn (ctx, next) { ...... await next(); ...... }
koa 核心文件 有且仅有 4 个(精简、流畅、易用)
application.js context.js request.js response.js application.js 今天主要解剖一下这个货 ^_^
继承自 Emitter 类,主要用于监听 error。
利用require('debug')('koa:application')
模块,把所有的 debug 都输出在 koa:application 域下,便于查看。
构造函数的参数简单易懂:
proxy false middleware [] subdomainOffset 2 env process.env.NODE_ENV || ‘development’ context 上下文对象 request 请求级别对象 response 请求级别对象 应用实例。从用户的角度出发,从事的工作是暴露给用户可使用的方法;
例如:
listen() use() toJSON() inspect() listen() 不言而喻,最基本的功能。可以看出,核心是调用了this.callback()
方法。
1 2 3 4 5 listen (...args ) { debug('listen' ); const server = http.createServer(this .callback()); return server.listen(...args); }
use() require('is-generator-function')
模块,可以判定该函数是否是生成器函数,进而使用require('koa-convert')
模块把生成器函数转换为可解析的 Promise 函数。有兴趣的同学可以研究一下这 2 个模块。
1 2 3 4 5 if (isGeneratorFunction(fn)) { fn = convert(fn); } this .middleware.push(fn);return this ;
toJSON() require('only')
模块,返回该对象中指定的,形成一个新的对象。参数可以使数组(数组中是 key),也可以是字符串(用空格隔开 key)。
1 return only(this , ['subdomainOffset' , 'proxy' , 'env' ]);
inspect() 直观理解:返回 toJSON()方法。
从功能性角度来讲,他主要的使命是:
callback() createContext() handleRequest() respond() onerror() callback() require('koa-compose')
模块,把列表中的中间件按先后顺序用 Promise 封装;next()方法返回的恰恰是上一个函数执行的环境,只不过就是把中间件函数变成了 middleware 数组中的下一个,说白了就是递归执行。该模块的核心代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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, function next ( ) { return dispatch(i + 1 ) })) } catch (err) { return Promise .reject(err) } } }
接着就是创建上下文环境 createContext(),然后执行 handlerequest()方法。
1 2 3 4 5 6 7 8 9 callback() const fn = compose(this .middleware); if (!this .listeners('error' ).length) this .on('error' , this .onerror); const handleRequest = (req, res ) => { const ctx = this .createContext(req, res); return this .handleRequest(ctx, fn); }; return handleRequest; }
createContext() 乍一看,我的个神啊,这么多,密密麻麻,不用害怕,其实真正核心的就只有这几行:
把 application 的实例赋值给上下文 context,把 http 中的 req,res 赋值给上下文 context;接下来把 req,res 同时赋值给 request.js 和 response.js 暴露出来的静态对象(便于使用)。
1 2 3 4 5 6 context.app = request.app = response.app = this ; context.req = request.req = response.req = req; context.res = request.res = response.res = res; request.ctx = response.ctx = context; request.response = response; response.request = request;
其余的代码很好理解,上下文对象 ctx 加入 cookie,ip, originalUrl, accept, state 参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 createContext (req, res ) { const context = Object .create(this .context); const request = context.request = Object .create(this .request); const response = context.response = Object .create(this .response); context.app = request.app = response.app = this ; context.req = request.req = response.req = req; context.res = request.res = response.res = res; request.ctx = response.ctx = context; request.response = response; response.request = request; context.originalUrl = request.originalUrl = req.url; context.cookies = new Cookies(req, res, { keys: this .keys, secure: request.secure }); request.ip = request.ips[0 ] || req.socket.remoteAddress || '' ; context.accept = request.accept = accepts(req); context.state = {}; return context; }
respond() 分别根据 buffer、string 和 stream 流来区分对待;默认 JSON 封装数据,再顺便求一个字节长度。
1 2 3 4 5 6 7 8 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 .stringify(body); if (!res.headersSent) { ctx.length = Buffer.byteLength(body); } res.end(body);
onerror() 只打印 stack,默认在开头加一个空格,自己也不能解惑。
1 2 3 4 const msg = err.stack || err.toString();console .error();console .error(msg.replace(/^/gm , ' ' ));console .error();
敬请期待下一节 koa 源码分析(2) request.js ^_^