koa学习笔记

我们从以下几个方面来学习Koa

  • 创建应用程序函数
  • 扩展res和req
  • 中间件实现原理
创建应用程序函数 Koa 是依赖 node 原生的 http 模块来实现 http server 的能力,原生 http 模块可以通过几行代码就启动一个监听在 8080 端口的http服务,createServer 的第一个参数是一个回调函数,这个回调函数有两个参数,一个是请求对象,一个是响应对象,可以根据请求对象的内容来决定响应数据的内容;
const http = require("http"); const server = http.createServer((req, res) => { // 每一次请求处理的方法 console.log(req.url); res.writeHead(200, { "Content-Type": "text/plain" }); res.end("Hello NodeJS"); }); server.listen(8080);

在 Koa 中,createServer 回调函数中的 req 和 res 会被保存到 ctx 对象上,伴随整个处理请求的生命周期,Koa 源码中的 request.js 和 response.js 就是对这两个对象添加了大量便捷获取数据和设置数据的方法,如获取请求的方法、请求的路径、设置返回数据体、设置返回状态码等操作。
而Koa在封装创建应用程序的方法中主要执行了以下流程:
  • 组织中间件(监听请求之前)
  • 生成context上下文对象
  • 执行中间件
  • 执行默认响应方法或者异常处理方法
// application.js// 这个方法是封装了http模块提供的http.createServer和listen方法 listen(...args) { debug('listen'); const server = http.createServer(this.callback()); return server.listen(...args); }callback() { //组织中间件,在监听请求之前完成的 const fn = compose(this.middleware); if (!this.listenerCount('error')) this.on('error', this.onerror); const handleRequest = (req, res) => { //创建context上下文对象 const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; return handleRequest; }handleRequest(ctx, fnMiddleware) { const res = ctx.res; // 默认状态码为404 res.statusCode = 404; const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); // 执行中间件 return fnMiddleware(ctx).then(handleResponse).catch(onerror); }// 创建context上下文对象 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.state = {}; return context; }

扩展res和req NodeJS中原生的res和req是http.IncomingMessage和http.ServerResponse的实例,Koa中则是自定义request和response对象,保持对原生的res和req引用,然后通过getter和setter方法实现扩展。
// application.jscreateContext(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; // 保存原生 req 对象 context.res = request.res = response.res = res; // 保存原生 res 对象 request.ctx = response.ctx = context; request.response = response; // response 拓展 response.request = request; // request 拓展 context.originalUrl = request.originalUrl = req.url; context.state = {}; // 最终返回完整的context上下文对象 return context; }

在Koa中要区别这两组对象:
  • request、response: Koa扩展的对象
  • res、req: NodeJS原生对象
// request.js get header() { return this.req.headers; },set header(val) { this.req.headers = val; }

此时已经可以采用这样的方式访问header属性:
ctx.request.header

delegates 属性代理 【koa学习笔记】koa对response和request使用了属性代理,使我们可以直接在context中使用request和response的方法,其中method方法是委托方法,getter方法用来委托getter,access方法委托getter+setter
// context.js /** * Response delegation. */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'); /** * 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') .access('accept') .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');

delegates 实现 对于 setter 和 getter方法,是通过调用对象上的 __defineSetter__ 和 __defineGetter__ 来实现的
// delegates/index.js// getter 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; }; // setter 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; }; // access Delegator.prototype.access = function(name){ return this.getter(name).setter(name); }; // method 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; };

中间件实现原理 在Koa中,通过app.use() 来注册中间件,Koa支持三种不同类型的中间件:普通函数,async 函数,Generator函数,如果是Generator函数,那就用 convert 把函数包起来,然后在push到 this.middleware
use(fn) { if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); if (isGeneratorFunction(fn)) { fn = convert(fn); } debug('use %s', fn._name || fn.name || '-'); this.middleware.push(fn); return this; }

convert作用 convert是用于将koa中以前基于generator写法的中间件转为基于promise写法。convert()的源码实现逻辑如下:
  1. convert 方法首先判断传入的中间件是否是一个函数,如果不是就抛出异常;
  2. 接着判断是否是一个 generator 函数,如果不是就直接返回,不做处理;
  3. 利用co将 generator 函数形式的中间件转成 promise 形式的中间件。
    function convert (mw) { if (typeof mw !== 'function') { throw new TypeError('middleware must be a function') } // assume it's Promise-based middleware if ( mw.constructor.name !== 'GeneratorFunction' && mw.constructor.name !== 'AsyncGeneratorFunction' ) { return mw } const converted = function (ctx, next) { return co.call( ctx, mw.call( ctx, (function * (next) { return yield next() })(next) )) } converted._name = mw._name || mw.name return converted }

    中间件的执行Koa中间件的执行流程主要通过koa-compose中的compose函数完成,基于洋葱圈模型:
    koa学习笔记
    文章图片

    koa-compose 的代码很短,一共才不到50行,主要执行顺序如下:
    // koa-compose function compose (middleware) { //传入middleware数组 // 不是数组抛出异常 if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!') // 判断每个middleware中的每一项是否为函数 // 不是函数抛出异常 for (const fn of middleware) { if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!') } // 返回一个函数 return function (context, next) { //index计数 let index = -1 return dispatch(0) //调用dispatch,从第一个中间件开始 function dispatch (i) { // i小于index,证明在中间件内调用了不止一次的next(),抛出错误 if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i // 更新index的值 let fn = middleware[i] //middleware中的函数,从第i个开始 if (i === middleware.length) fn = next //如果i走到最后一个的后面,就让fn为next,此时fn为undefined if (!fn) return Promise.resolve()// 那么这时候就直接resolve try { // 把下一个中间件作为当前中间件的next传入 return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } } }

    推荐阅读