【koa 源码阅读(二)】手把手教你搞懂 koa 核心原理

我是小十七_,今天和大家一起阅读 koa 的结构和所有源代码,koa 是带有异步中间件的一个 web 框架。如果你不知道 Koa 的中间件是如何工作的,你可以先看看这篇文章:【koa 源码阅读(一)】手把手教你搞懂 koa 中间件(洋葱模型)原理

翻译自:itnext.io/readingkoa-…

我们将涵盖 Koa 中的所有文件,koa 源码仅包含四个文件(酷~):

文件 1: Application File (application.js)

这是 Koa 的入口文件。

我们一般这样初始化 koa 服务器:

const Koa = require('koa');
const app = new Koa();
app.listen(3000);
复制代码

new Koa() 实际上实例化了一个新的 Application 对象,这个是 application.js 中的构造函数:

module.exports = class Application extends Emitter {
  constructor() {
    super();    
    this.proxy = false;
    this.middleware = [];
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || 'development';
    this.context = Object.create(context); // 来自文件 2: context.js
    this.request = Object.create(request); // 来自文件 3: request.js
    this.response = Object.create(response); // 来自文件 4: response.js
    if (util.inspect.custom) {
      this[util.inspect.custom] = this.inspect;
    }
  }
复制代码

关于 Emitter

new Koa() 实例化了一个 Application 对象,它 extends 了 Emitter。扩展 Emitter 类后,它将暴露一个 eventEmitterObject.on() 函数,这意味着我们可以像这样将事件附加到 Koa:

const app = new Koa();
app.on('event', (data) => {
  console.log('an event occurred! ' + data); // an event occurred! 123
});
app.emit('event', 123);
复制代码

当 EventEmitter 对象发出事件时,附加到该特定事件的所有函数都被同步调用,被调用的侦听器返回的任何值都将被忽略并被丢弃。

Events | Node.js v12.4.0 Documentation

关于 Object.create()

我们也可以在构造函数中看到 Object.create(),它只是创建一个新对象,使用一个现有的对象作为新创建对象的原型。它们的引用地址不同。

下面是一些例子:

const person = {
  isHuman: false,
  printIntroduction: function () {
    console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
  }
};
const me = Object.create(person);
me.name = "Matthew"; // "name" 是 "me" 对象的一个属性, 但是不是 "person" 对象的
me.isHuman = true; // 继承的属性可以被重写
me.printIntroduction(); // "My name is Matthew. Am I human? true"
复制代码

Object.create()

启动服务器

说完 new Koa(),我们可以看看 app.listen(3000)。如果我们使用 app.listen(3000); 启动服务器,将执行以下代码:

listen(...args) {
  debug('listen');
  // Step 1: 调用 callback(), 创建一个 http 服务器
  const server = http.createServer(this.callback());
  // Step 5: http 服务器创建完成, 开始监听端口
  return server.listen(...args);
}
callback() {
  // Step 2: 准备中间件
  const fn = compose(this.middleware);  
  if (!this.listenerCount('error')) 
      this.on('error', this.onerror);  
  const handleRequest = (req, res) => {
    // Step 3: createContext, 我们会详细讨论它
    const ctx = this.createContext(req, res);
    // Step 4: handleRequest, 我们会详细讨论它
    return this.handleRequest(ctx, fn);
  };  
  return handleRequest;
}
复制代码

如果你想知道如何不使用 Koa 启动一个 http 服务器,这里是一个正常的方式,我们使用 http 这个包直接创建服务器:

const http = require('http');
http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.write('Hello World!');
    res.end();
}).listen(8080);
复制代码

关于 createContext(向代码添加了注释)

createContext(req, res) {
    // 以 this.context 为原型创建了新的对象
    const context = Object.create(this.context);
    // 创建新的对象,确保 request 和 response 对象可以在 context 对象中访问
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    // 确保 context, request, response, app 对象之间可以相互访问
    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;
    // 再次确保 response 对象可以在 request 对象内部访问
    request.response = response;
    response.request = request;
    context.originalUrl = request.originalUrl = req.url;
    context.state = {};
    // 返回 context 对象,这是我们可以在中间件中使用的 ctx 对象
    return context;
}
复制代码

关于 handleRequest(向代码添加了注释)

handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    // 当所有中间件都执行完成后, 调用 respond()
    const handleResponse = () => respond(ctx);
    // 如果来自 http 包 的 res 抛出了错误, 调用 onerror 函数
    onFinished(res, onerror);
    // 中间件部分已经在上一篇文章中讨论过了,我们不在这里讨论
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
复制代码

respond(向代码添加了注释)

// 只是把 ctx.body 附加到 res, 这里没什么特别的
function respond(ctx) {
  if (false === ctx.respond) return;  
  if (!ctx.writable) return;  
  const res = ctx.res;
  let body = ctx.body;
  const code = ctx.status;  
  // 忽略 body
  if (statuses.empty[code]) {
    // 去掉 headers
    ctx.body = null;
    return res.end();
  }  
  if ('HEAD' == ctx.method) {
    if (!res.headersSent && isJSON(body)) {
      ctx.length = Buffer.byteLength(JSON.stringify(body));
    }
    return res.end();
  }  
  // 如果 body 不存在, 返回
  if (null == body) {
    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);
  }  
  // 如果 body 的类型是 buffer, 直接返回 body
  if (Buffer.isBuffer(body)) return res.end(body);
  if ('string' == typeof body) return res.end(body);
  if (body instanceof Stream) return body.pipe(res);  
  // JSON 加密 body
  body = JSON.stringify(body);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}
复制代码

文件 2: Context (context.js)

这个文件使用了一个叫做 delegate 的包来导出 context.js 中的方法,我写了一篇文章来了解这个包是如何工作的:

这是 context.js 文件的底部:

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
.....

delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .access('querystring')
复制代码

这意味着当您访问 ctx.querystring 时,它实际上是在访问 ctx.request.querystring,并且在调用 createContext 时分配了 ctx.request

所以 delegate 主要让你通过在中间件中使用 ctx 轻松访问 responserequest 中的方法(因为所有中间件都有 ctx 作为输入)。这是前一篇文章中提到的中间件示例:

// Here is the ctx
app.use(async (ctx, next) => {
    console.log(3);
    ctx.body = 'Hello World';
    await next();
    console.log(4);
});
复制代码

文件 3: Request (request.js)

这是 ctx.request 的原型。这个文件主要让你从 this.req 访问所有关于 http 请求的数据,比如 header、ip、host、url 等……这里是一些例子:

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] || '';
    }
},
复制代码

文件 4: Response (response.js)

这是 ctx.response 的原型。这个文件主要让你访问 this.res 中的数据,比如 header 和 body,这里是部分源代码:

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]);
      }
    }
}
复制代码

感谢阅读,如果对你有帮助的话,欢迎点赞和讨论~

猜你喜欢

转载自juejin.im/post/7036231319084335112
koa
今日推荐