最近一直都在开发基于 node 的前后端项目, 分享一下 Koa 的源码
Koa 自己的说法是 next generation web framework for node.js
Koa 算是比较主流的 node 的 web 框架了, 前身是 express 相比于 express,koa 去除了多余的 middleware, 只留下了最基本的对 node 的网络模块的继承和封装, 并且提供了方便的中间件调用机制, Koa 的源码总共加起来就 1600+, 很快就可以看完
基础知识
在分析 koa 的源码之前需要先了解一下 node 的 http 模块
- const http = require('http');
- const server = http.createServer((req, res) => {
- res.statusCode = 200;
- res.setHeader(Content-Type, text/plain);
- res.end(Hello World);
- }
- server.listen(3000);
node 的 http 模块主要负责了 node 对 HTTP 处理的封装以上这段代码启动了一个监听 3000 端口的 web server, 并且返回'Hello World'给接收到的请求
每一次接收到一个新的请求的时候会调用回调函数, 参数 req 和 res 分别是请求的实体和返回的实体, 操作 req 可以获取收到的请求, 操作 res 对应的是将要返回的 packet
如果你需要对接收到的请求进行一系列处理的话, 则需要按顺序写在回调函数里面
同样的功能对应的 Koa 的写法如下:
- const Koa = require('koa');
- const app = new Koa();
- app.use(async ctx => {
- ctx.body = 'Hello World';
- });
- app.listen(3000);
这里的 user 实际上是 Koa 的中间件机制提供的一个方便的处理请求的接口请求会被 use 后面的函数依次按照 use 的顺序被处理, 通常称这些函数为中间件, 他们的参数 ctx 为 koa 基于 node 的 http 模块的 req 和 res 封装的一个对象, 集合了 req 和 res 的功能为一体, 并且增加了一些简单的操作可以通过 ctx.req 和 ctx.res 获取到原生的 req 和 res, 与此同时 ctx.request 和 ctx.response 是 Koa 基于 req 和 res 封装的拥有一些新的功能的请求和返回的实体 以下代码是 Koa 中间件使用的栗子:
- const Koa = require('koa');
- const app = new Koa();
- app.use(async (ctx, next) => {
- console.log(pre1)
- await next();
- console.log(post1);
- });
- app.use(async (ctx, next) => {
- console.log(pre2);
- await next();
- console.log(post2)
- });
- app.use(async ctx => {
- console.log(pre3)
- ctx.body = 'Hello World';
- console.log(post3);
- });
- app.listen(3000);
next()表示将请求的处理交给下一个中间件如果没有 next(), 在该中间件函数执行结束后, 将返回执行上一个中间件的 next()后续的内容直到最开始的中间件的 next()后面的内容执行完毕
上面的代码的结果是
- pre1
- pre2
- pre3
- post3
- post2
- post1
执行的结果和函数递归调用何其相似, 之后了解了 Koa 的中间件机制后自然会明白这个结果的原因
正题
在了解了 koa 的基本的使用和带着以上中间件执行的结果, 我们来看看 koa 的源码吧
对 Koa 的理解主要分为两个部分:
Koa 对 node 的 http 模块的封装
Koa 的中间件机制
按照栗子代码的顺序从上到下:
初始化 Koa
const app = new Koa();
使用 use 加载中间件
对应的源码中
- 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;
- }
将中间件函数 push 到 middleware 数组中
调用 listen 方法启动 web server
- listen(...args) {
- debug('listen');
- const server = http.createServer(this.callback());
- return server.listen(...args);
- }
回调函数:
- callback() {
- const fn = compose(this.middleware);
- if (!this.listeners('error').length) this.on('error', this.onerror);
- const handleRequest = (req, res) = >{
- res.statusCode = 404;
- const ctx = this.createContext(req, res);
- const onerror = err = >ctx.onerror(err);
- const handleResponse = () = >respond(ctx);
- onFinished(res, onerror);
- return fn(ctx).then(handleResponse).
- catch(onerror);
- };
- return handleRequest;
- }
这里的 compose 函数是实现 Koa 的中间件机制的地方之后再细说
Koa 对 node 的 http 模块的封装
- const ctx = this.createContext(req, res);
- 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;
- }
createContext 实际上只是创建之前说的集合了 req & res & request & response 的一个对象, 作为参数传递给中间件 对于 request 和 response 其中除了一些拓展的方便的接口之外大部分都是直接继承的 http 的 req 和 response 实现的新的接口也是比较简单的封装, 举个栗子(response.js):
- set status(code) {
- assert('number' == typeof code, 'status code must be a number');
- assert(statuses[code], `invalid status code: ${code}`);
- assert(!this.res.headersSent, 'headers have already been sent');
- this._explicitStatus = true;
- this.res.statusCode = code;
- this.res.statusMessage = statuses[code];
- if (this.body && statuses.empty[code]) this.body = null;
- },
- /**
- * Get response status message
- *
- * @return {String}
- * @api public
- */
- get message() {
- return this.res.statusMessage || statuses[this.status];
- }
以上是 response 中实现的两个新的接口, 实际上也就是 res 的接口再简单封装了一下然后返回 再看 (context.js) 的最底部
- /**
- * Response delegation.
- */
- delegate(proto, 'response')
- .method('attachment')
- .method('redirect')
- .method('remove')
- .method('vary')
- .method('set')
- .method('append')
- .method('flushHeaders')
- .access('status')
- .access('message')
- .access('body')
- .access('length')
- .access('type')
- .access('lastModified')
- .access('etag')
- .getter('headerSent')
- .getter('writable');
- /**
- * Request delegation.
- */
- delegate(proto, 'request')
- .method('acceptsLanguages')
- .method('acceptsEncodings')
- .method('acceptsCharsets')
- .method('accepts')
- .method('get')
- .method('is')
- .access('querystring')
- .access('idempotent')
- .access('socket')
- .access('search')
- .access('method')
- .access('query')
- .access('path')
- .access('url')
- .getter('origin')
- .getter('href')
- .getter('subdomains')
- .getter('protocol')
- .getter('host')
- .getter('hostname')
- .getter('URL')
- .getter('header')
- .getter('headers')
- .getter('secure')
- .getter('stale')
- .getter('fresh')
- .getter('ips')
- .getter('ip');
delegate 的作用是将对应的对象上的 method,getter,setter 继承到另一个对象上 可以看到, 直接继承了大部分 req 和 res 的方法
接下来就是看看 Koa 的中间件机制的实现了
Koa 的中间件机制
compose 的源码也是非常简单的:
- 'use strict'
- /**
- * Expose compositor.
- */
- module.exports = compose
- /**
- * 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, function next () {
- return dispatch(i + 1)
- }))
- } catch (err) {
- return Promise.reject(err)
- }
- }
- }
- }
可以看到其实就是按照顺序将 middleware 数组中的中间件 s 按照顺序递归执行, 每次执行 next()的时候就是执行下一个中间件, 最后一个 next 也就是第一个中间件的第二个参数, 因为是 undefined, 所以会结束递归调用反向依次执行每个中间件 next 后续的代码每次 return 的都是一个 Promise 对象, 因此我们写的时候是 await 来等待这个异步调用的结束, 然后执行下一个中间件而我们一般的写法是将 await next()写在中间件函数的最后, 从而用伪递归的方式来实现每个请求依次被中间件函数处理的效果
恩, Koa 的主要的概念就是这些, 它的目的就是一个极简的框架, 只提供最基本的接口, 大部分的功能, 开发者根据需求使用 use 添加中间件来实现
来源: https://juejin.im/post/5a951e446fb9a0634f40c2c1