专栏名称: SegmentFault思否
SegmentFault (www.sf.gg)开发者社区,是中国年轻开发者喜爱的极客社区,我们为开发者提供最纯粹的技术交流和分享平台。
目录
相关文章推荐
程序员的那些事  ·  GPU:DeepSeek ... ·  7 小时前  
程序员的那些事  ·  OpenAI ... ·  2 天前  
程序员小灰  ·  清华大学《DeepSeek学习手册》(全5册) ·  3 天前  
程序猿  ·  “未来 3 年内,Python 在 AI ... ·  5 天前  
51好读  ›  专栏  ›  SegmentFault思否

Koa 原理学习路径与设计哲学

SegmentFault思否  · 公众号  · 程序员  · 2018-04-02 08:00

正文

本文基于 [email protected]

Koa简介(废话篇)

Koa 是基于 Node . js HTTP 框架,由 Express 原班人马打造。是下一代的 HTTP 框架,更简洁,更高效。

我们来看一下下载量(2018.3.4)

Koa :471,451 downloads in the last month Express :18,471,701 downloads in the last month

说好的 Koa 是下一代框架呢,为什么下载量差别有这么大呢, Express 一定会说: 你大爷还是你大爷!

确实,好多知名项目还是依赖 Express 的,比如webpack的dev-server就是使用的 Express ,所以还是看场景啦,如果你喜欢DIY,喜欢绝对的控制一个框架,那么这个框架就应该什么功能都不提供,只提供一个基础的运行环境,所有的功能由开发者自己实现。

正式由于 Koa 的高性能和简洁,好多知名项目都在基于 Koa ,比如阿里的 eggjs ,360奇舞团的 thinkjs

所以,虽然从使用范围上来讲, Express 对于 Koa 你大爷还是你大爷! ,但是如果 Express 很好,为什么还要再造一个 Koa 呢?接下来我们来了解下 Koa 到底带给我们了什么, Koa 到底做了什么。

如何着手分析Koa

先来看两段demo。

下面是 Node 官方给的一个HTTP的示例。

  1. const http = require('http');

  2. const hostname = '127.0.0.1';

  3. const port = 3000;

  4. const server = http.createServer((req, res) => {

  5.  res.statusCode = 200;

  6.  res.setHeader('Content-Type', 'text/plain');

  7.  res.end('Hello World\n');

  8. });

  9. server.listen(port, hostname, () => {

  10.  console.log(`Server running at http://${hostname}:${port}/`);

  11. });

下面是最简单的一个 Koa 的官方实例。

  1. const Koa = require('koa');

  2. const app = new Koa();

  3. app.use(async ctx => {

  4.  ctx.body = 'Hello World';

  5. });

  6. app.listen(3000);

Koa 是一个基于 Node 的框架,那么底层一定也是用了一些 Node 的API。

jQuery 很好用,但是 jQuery 也是基于DOM,逃不过也会用 element . appendChild 这样的基础API。 Koa 也是一样,也是用一些 Node 的基础API,封装成了更好用的HTTP框架。

那么我们是不是应该看看 Koa http . createServer 的代码在哪里,然后顺藤摸瓜,了解整个流程。

Koa核心流程分析

Koa 的源码有四个文件:

  • application.js // 核心逻辑

  • context.js // 上下文,每次请求都会生成一个

  • request.js // 对原生HTTP的req对象进行包装

  • response.js // 对原生HTTP的res对象进行包装

我们主要关心 application . js 中的内容,直接搜索 http . createServer ,会搜到

  1.  listen(...args) {

  2.    debug('listen');

  3.    const server = http.createServer(this.callback());

  4.    return server.listen(...args);

  5.  }

刚好和 Koa 中的这行代码 app . listen ( 3000 ); 关联起来了。

找到源头,现在我们就可以梳理清楚主流程,大家对着源码看我写的这个流程

  1. fn:listen

  2. fn:callback

  3. [fn:compose] // 组合中间件 会生成后面的 fnMiddleware

  4. fn:handleRequest // (@closure in callback)

  5. [fn(req, res):createContext] // 创建上下文 就是中间件中用的ctx

  6. fn(ctx, fnMiddleware):handleRequest // (@koa instance)

  7. code:fnMiddleware(ctx).then(handleResponse).catch(onerror);

  8. fn:handleResponse

  9. fn:respond

  10. code:res.end(body);

从上面可以看到最开始是 listen 方法,到最后HTTP的 res . end 方法。

listen 可以理解为初始化的方法,每一个请求到来的时候,都会经过从 callback respond 的生命周期。

在每个请求的生命周期中,做了两件比较核心的事情:

  1. 将多个中间件组合

  2. 创建ctx对象

多个中间件组合后,会先后处理ctx对象,ctx对象中既包含的req,也包含了res,也就是每个中间件的对象都可以处理请求和响应。

这样,一次HTTP请求,接连经过各个中间件的处理,再到返回给客户端,就完成了一次完美的请求。

Koa中的ctx

  1. app.use(async ctx => {

  2.  ctx.body = 'Hello World';

  3. });

上面的代码是一个最简答的中间件,每个中间件的第一个参数都是 ctx ,下面我们说一下这个 ctx 是什么。

创建 ctx 的代码:

  1.  createContext(req, res) {

  2.    const context = Object.create( this.context);

  3.    const request = context.request = Object.create(this.request);

  4.    const response = context.response = Object.create(this.response);

  5.    context.app = request.app = response.app = this;

  6.    context.req = request.req = response.req = req;

  7.    context.res = request.res = response.res = res;

  8.    request.ctx = response.ctx = context;

  9.    request .response = response;

  10.    response.request = request;

  11.    context.originalUrl = request.originalUrl = req.url;

  12.    context.cookies = new Cookies(req, res, {

  13.      keys: this.keys,

  14.      secure: request.secure

  15.    });

  16.    request.ip = request.ips[0] || req.socket.remoteAddress || '';

  17.    context.accept = request.accept = accepts(req );

  18.    context.state = {};

  19.    return context;

  20.  }

直接上代码,Koa每次请求都会创建这样一个ctx对象,以提供给每个中间件使用。

参数的 req , res 是Node原生的对象。

下面解释下这三个的含义:

  • context :Koa封装的带有一些和请求与相应相关的方法和属性

  • request :Koa封装的req对象,比如提了供原生没有的 host 属性。

  • response :Koa封装的res对象,对返回的 body hook了getter和setter。

其中有几行一堆 xx = xx = xx ,这样的代码。

是为了让ctx、request、response,能够互相引用。

举个例子,在中间件里会有这样的等式

  1. ctx.request.ctx === ctx

  2. ctx.response.ctx === ctx

  3. ctx.request.app === ctx.app

  4. ctx.response.app === ctx.app

  5. ctx.req === ctx.response.req

  6. // ...

为什么会有这么奇怪的写法?其实只是为了互相调用方便而已,其实最常用的就是ctx。

打开 context . js ,会发现里面写了一堆的 delegate

  1. /**

  2. * Response delegation.

  3. */

  4. delegate(proto, 'response')

  5.  .method('attachment')

  6.  .method('redirect')

  7.  .method('remove')

  8.  .method('vary')

  9.  .method('set')

  10.  .method('append')

  11.  .method('flushHeaders')

  12.  .access('status')

  13.  .access('message')

  14.  .access('body')

  15.  .access('length')

  16.  .access('type')

  17.  .access('lastModified')

  18.  .access('etag')

  19.  .getter('headerSent')

  20.  .getter('writable');

  21. /**

  22. * Request delegation.

  23. */

  24. delegate(proto, 'request')

  25.  .method('acceptsLanguages')

  26.  .method('acceptsEncodings')

  27.  .method('acceptsCharsets')

  28.  .method('accepts')

  29.  .method('get')

  30.  .method('is')

  31.  .access('querystring')

  32.  .access('idempotent')

  33.  .access ('socket')

  34.  .access('search')

  35.  .access('method')

  36.  .access('query')

  37.  .access('path')

  38.  .access('url')

  39.  .getter('origin')

  40.  .getter('href')

  41.  .getter('subdomains')

  42.  .getter('protocol')

  43.  .getter('host')

  44.  .getter('hostname')

  45.  .getter ('URL')

  46.  .getter('header')

  47.  .getter('headers')

  48.  .getter('secure')

  49.  .getter('stale')

  50.  .getter('fresh')

  51.  .getter('ips')

  52.  .getter('ip');

是为了把大多数的 request response 中的属性也挂在 ctx 下,我们为了拿到请求的路径需要 ctx . request . path ,但是由于代理过 path 这个属性, ctx . path 也是可以的,即 ctx . path === ctx . request . path

ctx 模块大概就是这样,没有讲的特别细,这块是重点不是难点,大家有兴趣自己看看源码很方便。

一个小tip: 有时候我也会把 context . js 中最下面的那些 delegate 当成文档使用,会比直接看文档快一点。

Koa中间件机制

中间件函数的参数解释
  • ctx :上面讲过的在请求进来的时候会创建一个给中间件处理请求和响应的对象,比如读取请求头和设置响应头。

  • next :暂时可以理解为是下一个中间件,实际上是被包装过的下一个中间件。

一个小栗子

我们来看这样的代码:

  1. // 第一个中间件

  2. app.use(async(ctx, next) => {

  3.  console.log('m1.1', ctx.path);

  4.  ctx.body = 'Koa m1';

  5.  ctx.set('m1', 'm1');

  6.  next();

  7.  console. log('m1.2', ctx.path);

  8. });

  9. // 第二个中间件

  10. app.use(async(ctx, next) => {

  11.  console.log('m2.1', ctx.path);

  12.  ctx.body = 'Koa m2';

  13.  ctx.set('m2', 'm2');

  14.  next();

  15.  debugger

  16.  console.log('m2.2', ctx.path);

  17. });

  18. // 第三个中间件

  19. app .use(async(ctx, next) => {

  20.  console.log('m3.1', ctx.path);

  21.  ctx.body = 'Koa m3';

  22.  ctx.set('m3', 'm3');

  23.  next();

  24.  console.log('m3.2', ctx.path);

  25. });

会输出什么呢?来看下面的输出:

  1. m1.1 /

  2. m2.1 /

  3. m3.1 /

  4. m3.2 /

  5. m2 .2 /

  6. m1.2 /

来解释一下上面输出的现象,由于将 next 理解为是下一个中间件,在第一个中间件执行 next 的时候,第一个中间件就将 执行权限 给了第二个中间件,所以 m1 . 1 后输出的是 m2 . 1 ,在之后是 m3 . 1

那么为什么 m3 . 1 后面输出的是 m3 . 2 呢?第三个中间件之后已经没有中间件了,那么第三个中间件里的 next 又是什么?

我先偷偷告诉你,最后一个中间件的 next 是一个立刻resolve的Promise,即 return Promise . resolve () ,一会再告诉你这是为什么。

所以第三个中间件(即最后一个中间件)可以理解成是这样子的:

  1. app.use(async (ctx, next) => {

  2.    console.log('m3.1', ctx.path);

  3.    ctx.body = 'Koa m3';

  4.    ctx.set('m3', 'm3');

  5.    new Promise.resolve(); // 原来是next

  6.    console.log('m3.2', ctx.path);

  7. });

从代码上看, m3 . 1 后面就会输出 m3 . 2

那为什么 m3 . 2 之后又会输出 m2 . 2 呢?,我们看下面的代码。

  1. let f1 = () => {

  2.  console.log(1.1);

  3.  f2 ();

  4.  console.log(1.2);

  5. }

  6. let f2 = () => {

  7.  console.log(2.1);

  8.  f3();

  9.  console.log(2.2);

  10. }

  11. let f3 = () => {

  12.  console.log(3.1);

  13.  Promise.resolve();

  14.  console.log(3.2);

  15. }

  16. f1();

  17. /*

  18.  outpout

  19.  1.1







请到「今天看啥」查看全文