从零到一解析Koa源码

炸鸡超人

抛砖引玉

先从官网示例开始,我们先简单实现启动服务的代码

const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000); 

在使用原始Nodejs,我们启动一个服务的写法是这样的

const http = require('http')

const server = http.createServer((req, res) => {
  res.end('hello world')
})

server.listen(3000)

我们大概可以看出几点不同

  1. 通过构造函数Koa实例化
  2. 新增use方法和ctx传参

构建Application

从官网看API说明

app.use(function)

将给定的中间件方法添加到此应用程序。app.use() 返回 this, 因此可以链式表达.

Koa 应用程序是一个包含一组中间件函数的对象,它是按照类似堆栈的方式组织和执行的。我们跟着这个思路实现一个基本类

application.js


const http = require('http')

class Koa {

  constructor() {
    // 中间件队列
    this.middlewares = []
  }

  // 启动服务器
  listen(...args) {
    const server = http.createServer((req, res) => {
      // 先遍历执行
      this.middlewares.forEach(middleware => middleware(req, res))
    })
    return server.listen(...args)
  }

  // 添加中间件
  use(middleware) {
    this.middlewares.push(middleware)
    // 返回链式调用
    return this
  }

}

module.exports = Koa

中间件执行方法后面再补充,先引入模块执行测试

// index.js

const Koa = require('./application');
const app = new Koa();

app.use((req, res) => {
  console.log('middleware1')
}).use((req, res) => {
  res.end('构建Application')
});

app.listen(3000); 

相关代码已上传至 lesson1

上下文(Context)

Koa Context 将 node 的 request 和 response 对象封装到单个对象中,为编写 Web 应用程序和 API 提供了许多有用的方法。 这些操作在 HTTP 服务器开发中频繁使用,它们被添加到此级别而不是更高级别的框架,这将强制中间件重新实现此通用功能。

每个 请求都将创建一个 Context,并在中间件中作为接收器引用,或者 ctx 标识符

我们先创建上下文,请求,和响应扩展原型,里面含有对应的扩展属性和方法,我们先留空占位

// context.js
module.exports = {}
// request.js
module.exports = {}
// response.js
module.exports = {}

application

引入上面模块,设置一个创建上下文方法

const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')

class Koa {

  constructor() {
    // 中间件队列
    this.middlewares = [];
    // 扩展属性
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }

  // 启动服务器
  listen(...args) {
    const server = http.createServer((req, res) => {
      const ctx = this.createContext(req, res)
      // 先遍历执行
      this.middlewares.forEach(middleware => middleware(ctx))
    })
    server.listen(...args)
  }

  // 创建上下文
  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.response = response
    response.request = request

    // 赋值url
    context.originalUrl = request.originalUrl = req.url
    // 上下文状态
    context.state = {}
    return context
  }

  // 添加中间件
  use(middleware) {
    this.middlewares.push(middleware)
    return this
  }

}

module.exports = Koa

index.js

const Koa = require('./application');
const app = new Koa();

app.use(ctx => {
  console.log('middleware1')
}).use(ctx => {
  ctx.res.end('上下文(Context)')
});

app.listen(3000);   

这里只是简单实现上下文,后面再补充细节实现

相关代码已上传至 lesson2

级联(洋葱圈模型及中间件传递)

Koa 中间件以更传统的方式级联,您可能习惯使用类似的工具 - 之前难以让用户友好地使用 node 的回调。然而,使用 async 功能,我们可以实现 “真实” 的中间件。对比 Connect 的实现,通过一系列功能直接传递控制,直到一个返回,Koa 调用“下游”,然后控制流回“上游”。

当一个中间件调用 next() 则该函数暂停并将控制传递给定义的下一个中间件。当在下游没有更多的中间件执行后,堆栈将展开并且每个中间件恢复执行其上游行为。

compose

koa里级联的源码都封装在koa-cpmose里,本身代码不多,我们可以直接跟着思路手写出来,主要实现几个点

  1. 要能够按照调用顺序来回执行中间件并且可以使用async功能,所以必须改造成Promise形式实现
  2. 利用第一点可以在中间件决定什么时候让出控制权,往下执行,当执行到最后中间件并且完成时返回控制权往上执行,这就是洋葱圈模型的原理,我们需要一个next函数告诉程序什么时候往下执行
  3. 为了保证不被部分中间件有意无意修改上下文影响,每个中间件需要得到同样的上下文
  4. 还有一些必须的容错处理防止代码崩溃(递归内存溢出,程序出错引起流程中断等)
function compose(middleware) {
  return (context, next) => {
    // 最后一个被执行的中间件
    let index = -1
    // 开始执行
    return dispatch(0)
    function dispatch(i) {
      // 防止多次执行
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      // 当前赋值
      index = i
      const fn = middleware[i]
      // 执行到最后一个中间件时正常不应该执行next,这时候next=undefined,即使有调用后续也有容错处理
      if (i === middleware.length) fn = next
      // 如果没有声明next则终止执行,开始回溯执行
      if (!fn) return Promise.resolve()
      try {
        // 中间件最终按序执行的代码,并且每个中间件都传递相同的上下文,防止被其中某些中间件改变影响后续执行,next即传入的dispatch.bind(null, i + 1)等于下一个中间件执行函数,因为是Promise函数所以可以利用async await中断让出当前函数控制权往下执行
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      } catch (err) {
        // 容错中断
        return Promise.reject(err)
      }
    }
  }
}

module.exports = compose

applaction.js

我们再将原本listen方法按单一职责划分成启动服务,请求回调,中间件执行三个函数

const http = require('http')
const compose = require('./compose');
const context = require('./context')
const request = require('./request')
const response = require('./response')

class Koa {

  //...省略

  // 启动服务器
  listen(...args) {
    // 将启动回调抽离
    const server = http.createServer(this.callback())
    server.listen(...args)
  }

  // 启动回调
  callback() {
    // 洋葱圈模型流程控制的核心,下面详解
    const fn = compose(this.middlewares)

    return (req, res) => {
      // 强制中间件重新实现新上下文
      const ctx = this.createContext(req, res)
      return this.hadnleRequest(ctx, fn)
    }
  }

  // 回调请求
  hadnleRequest(ctx, fnMiddleware) {
    // 响应处理
    const handleResoponse = () => respond(ctx)
    return fnMiddleware(ctx).then(handleResoponse)
  }

}

// 响应增强
function respond(ctx) {
  const res = ctx.res
  let body = ctx.body
  body = JSON.stringify(body)
  res.end(body)
}

module.exports = Koa

这两块大家看来可能云里雾里,实际整个流程串联起来的结果就是

compose(this.middlewares)(ctx).then(() => (ctx) => ctx.res.end(JSON.stringify(ctx.body)))

可以看到由此至终ctx贯穿其中,而其中compose的核心代码是这段

return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))

还是的结合上下文运行一遍好好理解原理

index.js

最后我们再运行测试一下

const Koa = require('./application');
const app = new Koa();

app.use(async (ctx, next) => {
  console.log('middleware1 start')
  await next();
  console.log('middleware1 end')
});

app.use(async (ctx, next) => {
  console.log('middleware2 start')
  await next();
  console.log('middleware2 end')
});

app.use(async ctx => {
  ctx.body = '级联(洋葱圈模型及中间件传递)'
});

app.listen(3000); 

终端输出

middleware1 start
middleware2 start
middleware2 end
middleware1 end

相关代码已上传至 lesson3

引入Cookies模块

Cookies是一个设置和获取HTTP(S)的cookies的nodejs模块,可以使用Keygrip对cookie进行签名以防止篡改,它可以作为nodejs http库或者Connect/Express中间件

applaction.js

先增加可选配置参数

class Koa {
  constructor(options) {
    // 可选项
    options = options || {};
    // cookies签名
    if (options.keys) this.keys = options.keys;
    // 中间件队列
    this.middlewares = [];
    // 扩展属性
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }
  
  // 省略其他
}

上下文增加cookies方法

context.js

/* 
  https://www.npmjs.com/package/cookies
  cookie是一个node.js模块,用于获取和设置HTTP cookie。可以使用Keygrip对cookie进行签名以防止篡改。它可以与内置的node.js HTTP库一起使用,也可以作为Connect/Express中间件使用。 
*/
const Cookies = require('cookies');

// symbol值作为对象属性的标识符
const COOKIES = Symbol('context#cookies');

module.exports = {
  get cookies() {
    if (!this[COOKIES]) {
      this[COOKIES] = new Cookies(this.req, this.res, {
        // 使用凭证启用基于SHA1 HMAC的加密签名
        keys: this.app.keys,
        // 明确指定连接是否安全,而不是此模块检查请求
        secure: this.requset.secure
      })
    }
    return this[COOKIES]
  },

  set cookies(_cookies) {
    this[COOKIES] = _cookies
  }
}

相关代码已上传至 lesson4

解析/设置请求/响应头

request.js

// ------省略其他------
/* 
  https://www.npmjs.com/package/only
  返回对象的白名单属性。 
*/
const only = require('only');
/* 
  https://www.npmjs.com/package/accepts
  基于negotiator的高级内容协商,从koa提取用于常规使用 
*/
const accepts = require('accepts');
/* 
  https://www.npmjs.com/package/typeis
  类型检查 
*/
const typeis = require('type-is');
/* 
  https://www.npmjs.com/package/content-type
  根据RFC 7231创建解析HTTP Content-Type头 
*/
const contentType = require('content-type');

module.exports = {
  // ------省略其他------


  // 检查实施
  inspect() {
    if (!this.req) return;
    return this.toJSON();
  },

  // 返回的指定配置的JSON表示数据
  toJSON() {
    return only(this, [
      'method',
      'url',
      'header'
    ]);
  },

  get header() {
    return this.req.headers
  },

  set header(val) {
    this.req.headers = val
  },

  // 同上效用,
  get headers() {
    return this.req.headers
  },

  set headers(val) {
    this.req.headers = val
  },

  // 返回对给定请求头值
  get(field) {
    const req = this.req;
    switch (field = field.toLowerCase()) {
      case 'referer':
      case 'referrer':
        return req.headers.referrer || req.headers.referer || '';
      default:
        return req.headers[field] || '';
    }
  },

  // 获取字符编码
  get charset() {
    try {
      const { parameters } = contentType.parse(this.req);
      return parameters.charset || '';
    } catch (e) {
      return '';
    }
  },

  // 返回解析后的内容长度
  get length() {
    const len = this.get('Content-Length');
    if (len === '') return;
    return ~~len;
  },

  /* 
    检查给定的“类型”是否可以接受,返回最佳匹配时为真,否则为假,其中情况你应该回应406“不可接受”。 
  */
  accepts(...args) {
    return this.accept.types(...args);
  },

  get accept() {
    return this._accept || (this._accept = accepts(this.req));
  },

  set accept(obj) {
    this._accept = obj;
  },

  // 根据“encodings”返回已接受的编码或最适合的编码(Accept-Encoding: gzip, deflate),返回['gzip', 'deflate']
  acceptsEncodings(...args) {
    return this.accept.encodings(...args);
  },

  // 根据“charsets”返回已接受的字符集或最适合的字符集(Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5), 返回['utf-8', 'utf-7', 'iso-8859-1']
  acceptsCharsets(...args) {
    return this.accept.charsets(...args);
  },

  // 根据“Language”返回已接受的语言或最适合的语言(Accept-Language: en;q=0.8, es, pt), 返回['es', 'pt', 'en']
  acceptsLanguages(...args) {
    return this.accept.languages(...args);
  },

  /* 
    检查进来的请求是否包含Content-Type头,其中是否包含给定的mime类型
      如果没有请求体,返回null
      如果没有内容类型, 返回false
      其他,返回第一个匹配的类型 
  */
  is(type, ...types) {
    return typeis(this.req, type, ...types);
  },

  // 返回请求mime类型void, 参数如“字符集”。
  get type() {
    const type = this.get('Content-Type');
    if (!type) return '';
    return type.split(';')[0];
  },
  
  get method() {
    return this.req.method;
  },

  set method(val) {
    this.req.method = val;
  },
  
  // 检查请求是否idempotent
  get idempotent() {
    const methods = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE'];
    return !!~methods.indexOf(this.method);
  },
}

上面引用了一个only库,它的作用就是筛选属性返回,例如

var obj = {
  name: 'tobi',
  last: 'holowaychuk',
  email: 'tobi@learnboost.com',
  _id: '12345'
};
 
var user = only(obj, 'name last email');
/* 返回结果
    {
      name: 'tobi',
      last: 'holowaychuk',
      email: 'tobi@learnboost.com'
    }
*/

其他大部分都是些解析类,就不细说了

response

这一章节代码会比较多,但是我尽量把有关系的代码块放一起上下文联系,,实际也是写解析处理的操作,慢慢看下来也没有什么难点

const extname = require('path').extname;
const Stream = require('stream');
/* 
  https://www.npmjs.com/package/only
  Return whitelisted properties of an object.
*/
const only = require('only');
/* 
  https://www.npmjs.com/package/type-is
  Infer the content-type of a request.
*/
const typeis = require('type-is').is;
/* 
  https://www.npmjs.com/package/vary
  Manipulate the HTTP Vary header
*/
const vary = require('vary');
/* 
  https://www.npmjs.com/package/cache-content-type
  The same as mime-types's contentType method, but with result cached.
*/
const getType = require('cache-content-type');
/* 
  https://www.npmjs.com/package/content-disposition
  reate and parse HTTP Content-Disposition header
*/
const contentDisposition = require('content-disposition');
/* 
  https://www.npmjs.com/package/assert
  With browserify, simply require('assert') or use the assert global and you will get this module.
  The goal is to provide an API that is as functionally identical to the Node.js assert API as possible. Read the official docs for API documentation.
*/
const assert = require('assert');
/* 
  https://www.npmjs.com/package/statuses
  HTTP status utility for node.
  This module provides a list of status codes and messages sourced from a few different projects:
*/
const statuses = require('statuses');
/* 
  https://www.npmjs.com/package/on-finished
  Execute a callback when a HTTP request closes, finishes, or errors.
*/
const onFinish = require('on-finished');
/* 
  https://www.npmjs.com/package/destroy
  Destroy a stream.
  This module is meant to ensure a stream gets destroyed, handling different APIs and Node.js bugs.
*/
const destroy = require('destroy');

module.exports = {

  flushHeaders() {
    this.res.flushHeaders();
  },
  
  inspect() {
    if (!this.res) return;
    const o = this.toJSON();
    o.body = this.body;
    return o;
  },

  toJSON() {
    return only(this, [
      'status',
      'message',
      'header'
    ]);
  },

  get type() {
    const type = this.get('Content-Type');
    if (!type) return '';
    return type.split(';', 1)[0];
  },

  // 返回传入数据是否指定类型之一
  is(type, ...types) {
    return typeis(this.req, type, ...types);
  },

  get header() {
    const { res } = this;
    return typeof res.getHeaders === 'function'
      ? res.getHeaders()
      : res._headers || {}; // Node < 7.7
  },

  get headers() {
    return this.header;
  },

  get(field) {
    return this.header[field.toLowerCase()] || '';
  },

  has(field) {
    return typeof this.res.hasHeader === 'function'
      ? this.res.hasHeader(field)
      // Node < 7.7
      : field.toLowerCase() in this.headers;
  },

  set type(type) {
    type = getType(type);
    if (type) {
      this.set('Content-Type', type);
    } else {
      this.remove('Content-Type');
    }
  },

  attachment(filename, options) {
    // 获取扩展名
    if (filename) this.type = extname(filename);
    // 重设Content-Disposition头字段
    this.set('Content-Disposition', contentDisposition(filename, options));
  },

  get headerSent() {
    return this.res.headersSent;
  },

  // 操纵改变头字段
  vary(field) {
    if (this.headerSent) return;

    vary(this.res, field);
  },

  set(field, val) {
    if (this.headerSent) return;

    if (2 === arguments.length) {
      if (Array.isArray(val)) val = val.map(v => typeof v === 'string' ? v : String(v));
      else if (typeof val !== 'string') val = String(val);
      this.res.setHeader(field, val);
    } else {
      for (const key in field) {
        this.set(key, field[key]);
      }
    }
  },

  append(field, val) {
    const prev = this.get(field);

    if (prev) {
      val = Array.isArray(prev)
        ? prev.concat(val)
        : [prev].concat(val);
    }

    return this.set(field, val);
  },

  remove(field) {
    if (this.headerSent) return;

    this.res.removeHeader(field);
  },

  get status() {
    return this.res.statusCode;
  },

  set status(code) {
    if (this.headerSent) return;
    // 断言
    assert(Number.isInteger(code), 'status code must be a number');
    assert(code >= 100 && code <= 999, `invalid status code: ${code}`);
    // 是否有明确状态
    this._explicitStatus = true;
    // 根据状态码做不同处理
    this.res.statusCode = code;
    if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code];
    if (this.body && statuses.empty[code]) this.body = null;
  },

  get message() {
    return this.res.statusMessage || statuses[this.status];
  },

  set message(msg) {
    this.res.statusMessage = msg;
  },

  get body() {
    return this._body;
  },

  set body(val) {
    const original = this._body;
    this._body = val;

    // no content
    if (null == val) {
      if (!statuses.empty[this.status]) this.status = 204;
      if (val === null) this._explicitNullBody = true;
      this.remove('Content-Type');
      this.remove('Content-Length');
      this.remove('Transfer-Encoding');
      return;
    }

    // set the status
    if (!this._explicitStatus) this.status = 200;

    // set the content-type only if not yet set
    const setType = !this.has('Content-Type');

    // string
    if ('string' === typeof val) {
      if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text';
      this.length = Buffer.byteLength(val);
      return;
    }

    // buffer
    if (Buffer.isBuffer(val)) {
      if (setType) this.type = 'bin';
      this.length = val.length;
      return;
    }

    // stream
    if (val instanceof Stream) {
      // 当请求关闭,完成或者错误都会执行回调,这里是销毁Stream
      onFinish(this.res, destroy.bind(null, val));
      if (original != val) {
        val.once('error', err => this.ctx.onerror(err));
        // overwriting
        if (null != original) this.remove('Content-Length');
      }

      if (setType) this.type = 'bin';
      return;
    }

    // json
    this.remove('Content-Length');
    this.type = 'json';
  },

  set length(n) {
    this.set('Content-Length', n);
  },

  // 根据不同的响应体类型返回准确长度
  get length() {
    if (this.has('Content-Length')) {
      return parseInt(this.get('Content-Length'), 10) || 0;
    }

    const { body } = this;
    if (!body || body instanceof Stream) return undefined;
    if ('string' === typeof body) return Buffer.byteLength(body);
    if (Buffer.isBuffer(body)) return body.length;
    return Buffer.byteLength(JSON.stringify(body));
  },
  
  get writable() {
    // can't write any more after response finished
    // response.writableEnded is available since Node > 12.9
    // https://nodejs.org/api/http.html#http_response_writableended
    // response.finished is undocumented feature of previous Node versions
    // https://stackoverflow.com/questions/16254385/undocumented-response-finished-in-node-js
    if (this.res.writableEnded || this.res.finished) return false;

    const socket = this.res.socket;
    // There are already pending outgoing res, but still writable
    // https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486
    if (!socket) return true;
    return socket.writable;
  }, 

}

相关代码已上传至 lesson5

解析/设置URL

request

// ------省略其他------
const stringify = require('url').format;
const net = require('net');
/* 
  https://www.npmjs.com/package/url
  这个模块具有与node.js核心URL模块相同的URL解析和解析工具。 
*/
const URL = require('url').URL;
/* 
  https://www.npmjs.com/package/parseurl
  Parse a URL with memoization 
*/
const parse = require('parseurl');
/* 
  https://www.npmjs.com/package/querystringjs
  一个查询字符串解析实用程序,可以正确处理一些边界情况。当您想要正确地处理查询字符串时,请使用此选项。 
*/
const qs = require('querystring');

module.exports = {
  // ------省略其他------
  // 缓存解析后的URL
  get URL() {
    /* 
      istanbul ignore else 
      */
    if (!this.memoizedURL) {
      const originalUrl = this.originalUrl || ''; // avoid undefined in template string
      try {
        this.memoizedURL = new URL(`${this.origin}${originalUrl}`);
      } catch (err) {
        this.memoizedURL = Object.create(null);
      }
    }
    return this.memoizedURL;
  },

  get url() {
    return this.req.url;
  },

  set url(val) {
    this.req.url = val;
  },

  // 当使用TLS请求返回http或者https协议字符串,当代理设置"X-Forwarded-Proto"头会被信任,如果你正在启用一个http反向代理这将被启动
  get protocol() {
    if (this.socket.encrypted) return 'https';
    if (!this.app.proxy) return 'http';
    const proto = this.get('X-Forwarded-Proto');
    return proto ? proto.split(/\s*,\s*/, 1)[0] : 'http';
  },

  // 是否https
  get secure() {
    return 'https' === this.protocol;
  },

  // 解析Host头,当启动代理支持X-Forwarded-Host
  get host() {
    const proxy = this.app.proxy;
    let host = proxy && this.get('X-Forwarded-Host');
    if (!host) {
      if (this.req.httpVersionMajor >= 2) host = this.get(':authority');
      if (!host) host = this.get('Host');
    }
    if (!host) return '';
    return host.split(/\s*,\s*/, 1)[0];
  },

  get hostname() {
    const host = this.host;
    if (!host) return '';
    if ('[' === host[0]) return this.URL.hostname || ''; // IPv6
    return host.split(':', 1)[0];
  },

  /* 
    返回数组类型子域名
  
    子域名是主机在主域名之前以点分隔的部分
    应用程序的域。默认情况下,应用程序的域是最后两个域
    主机的一部分。这可以通过设置“app.subdomainOffset”来改变。
  
    例如,如果域名是“tobi.ferrets.example.com”:
    如果app.subdomainOffset没有设置。子域是["ferrets", "tobi"]
    如果app.subdomainOffset是3。子域["tobi"]。 
  */
  get subdomains() {
    const offset = this.app.subdomainOffset;
    const hostname = this.hostname;
    if (net.isIP(hostname)) return [];
    return hostname
      .split('.')
      .reverse()
      .slice(offset);
  },

  get origin() {
    return `${this.protocol}://${this.host}`;
  },

  get href() {
    // support: `GET http://example.com/foo`
    if (/^https?:\/\//i.test(this.originalUrl)) return this.originalUrl;
    return this.origin + this.originalUrl;
  },

  get path() {
    return parse(this.req).pathname;
  },

  set path(path) {
    const url = parse(this.req);
    if (url.pathname === path) return;

    url.pathname = path;
    url.path = null;

    this.url = stringify(url);
  },

  get querystring() {
    if (!this.req) return '';
    return parse(this.req).query || '';
  },

  set querystring(str) {
    const url = parse(this.req);
    if (url.search === `?${str}`) return;

    url.search = str;
    url.path = null;

    this.url = stringify(url);
  },

  get query() {
    const str = this.querystring;
    const c = this._querycache = this._querycache || {};
    return c[str] || (c[str] = qs.parse(str));
  },

  set query(obj) {
    this.querystring = qs.stringify(obj);
  },

  get search() {
    if (!this.querystring) return '';
    return `?${this.querystring}`;
  },

  set search(str) {
    this.querystring = str;
  },
}

response

// ------省略其他------
/* 
  https://www.npmjs.com/package/escape-html
  Escape string for use in HTML
*/
const escape = require('escape-html');
/* 
  https://www.npmjs.com/package/encodeurl
  Encode a URL to a percent-encoded form, excluding already-encoded sequences
*/
const encodeUrl = require('encodeurl');

module.exports = {
  // ------省略其他------
  
  redirect(url, alt) {
    // location
    if ('back' === url) url = this.ctx.get('Referrer') || alt || '/';
    this.set('Location', encodeUrl(url));

    // status
    if (!statuses.redirect[this.status]) this.status = 302;

    // html
    if (this.ctx.accepts('html')) {
      url = escape(url);
      this.type = 'text/html; charset=utf-8';
      this.body = `Redirecting to <a href="${url}">${url}</a>.`;
      return;
    }

    // text
    this.type = 'text/plain; charset=utf-8';
    this.body = `Redirecting to ${url}.`;
  },
}

相关代码已上传至 lesson6

缓存机制

### request

// ------省略其他------
/* 
  https://www.npmjs.com/package/fresh
  HTTP响应测试 
*/
const fresh = require('fresh');

module.exports = {
  // ------省略其他------
  // 检查请求是否最新,Last-Modified或者ETag是否匹配
  get fresh() {
    const method = this.method;
    const s = this.ctx.status;

    // GET or HEAD for weak freshness validation only
    if ('GET' !== method && 'HEAD' !== method) return false;

    // 2xx or 304 as per rfc2616 14.26
    if ((s >= 200 && s < 300) || 304 === s) {
      return fresh(this.header, this.response.header);
    }

    return false;
  },

  // 检查是否旧请求,Last-Modified或者ETag是否改变
  get stale() {
    return !this.fresh;
  },
}

response

// ------省略其他------
/* 
  https://www.npmjs.com/package/fresh
  HTTP响应测试 
*/
const fresh = require('fresh');

module.exports = {
  // ------省略其他------
  
  set lastModified(val) {
    if ('string' === typeof val) val = new Date(val);
    this.set('Last-Modified', val.toUTCString());
  },

  get lastModified() {
    const date = this.get('last-modified');
    if (date) return new Date(date);
  },

  set etag(val) {
    if (!/^(W\/)?"/.test(val)) val = `"${val}"`;
    this.set('ETag', val);
  },

  get etag() {
    return this.get('ETag');
  },
}

相关代码已上传至 lesson7

IP相关

request

// ------省略其他------
const IP = Symbol('context#ip');

module.exports = {
  // ------省略其他------
  
  /* 
    当app.proxy是true,解析X-Forwarded-For ip地址列表
    例如如果值是"client, proxy1, proxy2",会得到数组`["client", "proxy1", "proxy2"]`
    proxy2是最远的下游 
  */
  get ips() {
    const proxy = this.app.proxy;
    const val = this.get(this.app.proxyIpHeader);
    let ips = proxy && val
      ? val.split(/\s*,\s*/)
      : [];
    if (this.app.maxIpsCount > 0) {
      ips = ips.slice(-this.app.maxIpsCount);
    }
    return ips;
  },

  // 返回请求的远程地址,当app.proxy是true,解析X-Forwarded-For ip地址列表并返回第一个
  get ip() {
    if (!this[IP]) {
      this[IP] = this.ips[0] || this.socket.remoteAddress || '';
    }
    return this[IP];
  },

  set ip(_ip) {
    this[IP] = _ip;
  },
}

相关代码已上传至 lesson8

错误规范处理

context

// ------省略其他------
const util = require('util');
/* 
  Create HTTP errors for Express, Koa, Connect, etc. with ease.
  https://github.com/jshttp/http-errors
*/
const createError = require('http-errors');
/* 
  Assert with status codes. Like ctx.throw() in Koa, but with a guard.
  https://github.com/jshttp/http-assert
*/
const httpAssert = require('http-assert');
/* 
  https://github.com/jshttp/statuses
  HTTP status utility for node.
  This module provides a list of status codes and messages sourced from a few different projects:
*/
const statuses = require('statuses');

module.exports = {
  // ------省略其他------
  
  /* 
    The API of this module is intended to be similar to the Node.js assert module.
    Each function will throw an instance of HttpError from the http-errors module when the assertion fails.
    * @param {Mixed} test
    * @param {Number} status
    * @param {String} message
    * @api public
  */
  assert: httpAssert,

  /* 
    * @param {String|Number|Error} err, msg or status
    * @param {String|Number|Error} [err, msg or status]
    * @param {Object} [props]
    * @api public
  */
  throw(...args) {
    throw createError(...args);
  },

  onerror(err) {
    // 这里之所以没用全等,我觉得可能是因为双等下null == undefined 也返回true
    if (null == err) return

    const isNativeError = Object.prototype.toString.call(err) === '[object Error]' || err instanceof Error;
    // 创建一个格式化后的字符串,使用第一个参数作为一个类似 printf 的格式的字符串,该字符串可以包含零个或多个格式占位符。 每个占位符会被对应参数转换后的值所替换
    if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err))

    let headerSent = false;
    if (this.headerSent || !this.writable) {
      headerSent = err.headerSent = true;
    }

    // delegate
    this.app.emit('error', err, this);

    // 在这里我们做不了任何事情,将其委托给应用层级的处理程序和日志
    if (headerSent) {
      return;
    }

    const { res } = this;
    // 清除头字段
    if (typeof res.getHeaderNames === 'function') {
      res.getHeaderNames().forEach(name => res.removeHeader(name));
    } else {
      res._headers = {}; // Node < 7.7
    }
    // 设置指定的
    this.set(err.headers);
    // 强制text/plain
    this.type = 'text';

    let statusCode = err.status || err.statusCode;
    // ENOENT support
    if ('ENOENT' === err.code) statusCode = 404;
    // default to 500
    if ('number' !== typeof statusCode || !statuses[statusCode]) statusCode = 500;
    // respond
    const code = statuses[statusCode];
    const msg = err.expose ? err.message : code;
    this.status = err.status = statusCode;
    this.length = Buffer.byteLength(msg);
    res.end(msg);
  }
}

相关代码已上传至 lesson9

作用域委托

我们利用delegates实现不同的委托方式

getter: 外部对象可以通过该方法访问内部对象的值。
setter:外部对象可以通过该方法设置内部对象的值。
access: 该方法包含getter和setter功能。
method: 该方法可以使外部对象直接调用内部对象的函数

context

我们在这里把请求和响应的相关变量方法都委托到当前对象上,同时增加输出格式

// ------省略其他------
/* 
  Node method and accessor delegation utilty.
  https://github.com/tj/node-delegates
*/
const delegate = require('delegates');

// 赋值
const proto = module.exports = {
  // ------省略其他------
  
  inspect() {
    if (this === proto) return this;
    return this.toJSON();
  },

  // 这里我们在每个对象上显式地调用. tojson(),否则迭代将由于getter而失败,并导致诸如clone()之类的实用程序失败。
  toJSON() {
    return {
      request: this.request.toJSON(),
      response: this.response.toJSON(),
      app: this.app.toJSON(),
      originalUrl: this.originalUrl,
      req: '<original node req>',
      res: '<original node res>',
      socket: '<original node socket>'
    };
  },
}

/**
 * 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');

相关代码已上传至 lesson10

应用增加错误机制和补充上下文

Application

// ------省略其他------
const Stream = require('stream');
/* 
  A tiny JavaScript debugging utility modelled after Node.js core's debugging technique. Works in Node.js and web browsers.
  https://github.com/visionmedia/debug
*/
const debug = require('debug')('koa:application');
/* 
  Is this a native generator function?
  https://github.com/inspect-js/is-generator-function
*/
const isGeneratorFunction = require('is-generator-function');
/* 
  Convert koa legacy ( 0.x & 1.x ) generator middleware to modern promise middleware ( 2.x ).
  https://github.com/koajs/convert
 */
const convert = require('koa-convert');
const deprecate = require('depd')('koa');
/* 
  Execute a callback when a HTTP request closes, finishes, or errors.
  https://www.npmjs.com/package/on-finished
*/
const onFinished = require('on-finished');
/* 
  HTTP status utility for node.
  This module provides a list of status codes and messages sourced from a few different projects:
  https://www.npmjs.com/package/statuses
*/
const statuses = require('statuses');
/* 
  https://www.npmjs.com/package/only
  返回对象的白名单属性。 
*/
const only = require('only');

class Koa {
  // ------省略其他------
    
  /**
  * @param {object} [options] Application options
  * @param {string} [options.env='development'] Environment
  * @param {string[]} [options.keys] Signed cookie keys
  * @param {boolean} [options.proxy] Trust proxy headers
  * @param {number} [options.subdomainOffset] Subdomain offset
  * @param {boolean} [options.proxyIpHeader] proxy ip header, default to X-Forwarded-For
  * @param {boolean} [options.maxIpsCount] max ips read from proxy ip header, default to 0 (means infinity)
  */
  constructor(options) {
    super();
    // 可选项
    options = options || {};
    // cookies签名
    if (options.keys) this.keys = options.keys;
    // 中间件队列
    this.middlewares = [];
    // 扩展属性
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
    // 增加配置项
    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';
  }
  
  // 启动服务器
  listen(...args) {
    debug('listen');
    // 将启动回调抽离
    const server = http.createServer(this.callback())
    server.listen(...args)
  }
  
  // 启动回调
  callback() {
    // 洋葱圈模型流程控制的核心,下面详解
    const fn = compose(this.middlewares)

    // 增加监听事件
    if (!this.listenerCount('error')) this.on('error', this.onerror);

    return (req, res) => {
      // 强制中间件重新实现新上下文
      const ctx = this.createContext(req, res)
      return this.hadnleRequest(ctx, fn)
    }
  }
  
  // 回调请求
  hadnleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    // 响应处理
    const handleResoponse = () => respond(ctx)
    onFinished(res, onerror);
    return fnMiddleware(ctx).then(handleResoponse)
  }
  
  // 创建上下文
  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

    // 赋值url
    context.originalUrl = request.originalUrl = req.url
    // 上下文状态
    context.state = {}
    return context
  }
  
  // 添加中间件
  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.middlewares.push(fn)
    return this
  }

  onerror(err) {
    // When dealing with cross-globals a normal `instanceof` check doesn't work properly.
    // See https://github.com/koajs/koa/issues/1466
    // We can probably remove it once jest fixes https://github.com/facebook/jest/issues/2549.
    const isNativeError =
      Object.prototype.toString.call(err) === '[object Error]' ||
      err instanceof Error;
    if (!isNativeError) throw new TypeError(util.format('non-error thrown: %j', err));

    if (404 === err.status || err.expose) return;
    if (this.silent) return;

    const msg = err.stack || err.toString();
    console.error(`\n${msg.replace(/^/gm, '  ')}\n`);
  }

  toJSON() {
    return only(this, [
      'subdomainOffset',
      'proxy',
      'env'
    ]);
  }

  inspect() {
    return this.toJSON();
  }
}

// 响应增强
function respond(ctx) {

  // allow bypassing koa
  if (false === ctx.respond) return;

  if (!ctx.writable) return;

  const res = ctx.res
  let body = ctx.body
  const code = ctx.status;

  // 如果状态码需要一个空的主体
  if (statuses.empty[code]) {
    // strip headers
    ctx.body = null;
    return res.end();
  }

  if ('HEAD' === ctx.method) {
    if (!res.headersSent && !ctx.response.has('Content-Length')) {
      const { length } = ctx.response;
      if (Number.isInteger(length)) ctx.length = length;
    }
    return res.end();
  }

  // status body
  if (null == body) {
    if (ctx.response._explicitNullBody) {
      ctx.response.remove('Content-Type');
      ctx.response.remove('Transfer-Encoding');
      return res.end();
    }
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code);
    } else {
      body = ctx.message || String(code);
    }
    if (!res.headersSent) {
      ctx.type = 'text';
      ctx.length = Buffer.byteLength(body);
    }
    return res.end(body);
  }

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

相关代码已上传至 lesson11

继承事件机制和导出错误类

// ------省略其他------
const Emitter = require('events');
/* 
  Create HTTP errors for Express, Koa, Connect, etc. with ease.
  https://github.com/jshttp/http-errors
*/
const { HttpError } = require('http-errors');

module.exports = class Application extends Emitter {
    // ------省略其他------
}

/**
 * Make HttpError available to consumers of the library so that consumers don't
 * have a direct dependency upon `http-errors`
 */
module.exports.HttpError = HttpError;

相关代码已上传至 lesson12

结束

我们大概从基本起步了解整个架构流程,然后逐渐补充结构脉络,最后结合成一个完整的Koa库,总的来说不管是代码量还是难度都不算复杂,更多是里面各种标准化处理操作,有些细节讲解不够清晰或者理解有误的话也希望有人可以指出

阅读 391

原QDFuns关闭,只能换个社区重新出发,愿各自安好.

421 声望
27 粉丝
0 条评论
你知道吗?

原QDFuns关闭,只能换个社区重新出发,愿各自安好.

421 声望
27 粉丝
宣传栏