从源码入手探索koa2应用的实现

xdtt1946 2年前
   <pre>  A Koa application is an object containing an array of middleware functions which are composed and executed in a stack-like manner upon request.  </pre>    <ul>     <li>只提供封装好http上下文、请求、响应,以及基于async/await的中间件容器</li>     <li>基于koa的app是由一系列中间件组成,原来是generator中间件,现在被async/await代替(generator中间件,需要通过中间件koa-convert封装一下才能使用)</li>     <li>按照app.use(middleware)顺序依次执行中间件数组中的方法</li>    </ul>    <pre>  1.0 版本是通过组合不同的 generator,可以免除重复繁琐的回调函数嵌套,并极大地提升错误处理的效率。  2.0版本Koa放弃了generator,采用Async 函数实现组件数组瀑布流式(Cascading)的开发模式。  </pre>    <h2>源码文件</h2>    <pre>  ├── lib  │   ├── application.js  │   ├── context.js  │   ├── request.js  │   └── response.js  └── package.json  </pre>    <p>核心代码就是lib目录下的四个文件</p>    <ul>     <li>application.js 是整个koa2 的入口文件,封装了context,request,response,以及最核心的中间件处理流程。</li>     <li>context.js 处理应用上下文,里面直接封装部分request.js和response.js的方法</li>     <li>request.js 处理http请求</li>     <li>response.js 处理http响应</li>    </ul>    <h2>koa流程</h2>    <p><img src="https://simg.open-open.com/show/edff4ee8869023ef27cba34cdbd6a8d3.jpg"></p>    <p>koa的流程分为三个部分: <strong>初始化 -> 启动Server -> 请求响应</strong></p>    <ul>     <li> <p>初始化</p>      <ul>       <li>初始化koa对象之前我们称为初始化</li>      </ul> </li>     <li> <p>启动server</p>      <ul>       <li>初始化中间件(中间件建立联系)</li>       <li>启动服务,监听特定端口,并生成一个新的上下文对象</li>      </ul> </li>     <li> <p>请求响应</p>      <ul>       <li>接受请求,初始化上下文对象</li>       <li>执行中间件</li>       <li>将body返回给客户端</li>      </ul> </li>    </ul>    <h3>初始化</h3>    <p>定义了三个对象, context , response , request</p>    <ul>     <li>request 定义了一些set/get访问器,用于设置和获取请求报文和url信息,例如获取query数据,获取请求的url(详细API参见 <a href="/misc/goto?guid=4959756186209502791" rel="nofollow,noindex">Koa-request文档</a> )</li>     <li>response 定义了一些set/get操作和获取响应报文的方法(详细API参见 <a href="/misc/goto?guid=4959756186302825213" rel="nofollow,noindex">Koa-response 文档</a> )</li>     <li> <p>context 通过第三方模块 delegate 将 koa 在 Response 模块和 Request 模块中定义的方法委托到了 context 对象上,所以以下的一些写法是等价的:</p> <pre>  //在每次请求中,this 用于指代此次请求创建的上下文 context(ctx)  this.body ==> this.response.body  this.status ==> this.response.status  this.href ==> this.request.href  this.host ==> this.request.host  ......  </pre> <p>为了方便使用,许多上下文属性和方法都被委托代理到他们的 ctx.request 或 ctx.response ,比如访问 ctx.type 和 ctx.length 将被代理到 response 对象, ctx.path 和 ctx.method 将被代理到 request 对象。</p> <p>每一个请求都会创建一段上下文,在控制业务逻辑的中间件中, ctx 被寄存在 this 中(详细API参见 <a href="/misc/goto?guid=4959756186391607608" rel="nofollow,noindex">Koa-context 文档</a> )</p> </li>    </ul>    <h3>启动Server</h3>    <ol>     <li>初始化一个koa对象实例</li>     <li>监听端口</li>    </ol>    <pre>  var koa = require('koa');  var app = koa()    app.listen(9000)  </pre>    <p>解析启动流程,分析源码</p>    <p>application.js 是koa的入口文件</p>    <pre>  // 暴露出来class,`class Application extends Emitter`,用new新建一个koa应用。  module.exports = class Applicationextends Emitter{        constructor() {          super();            this.proxy = false; // 是否信任proxy header,默认false // TODO          this.middleware = [];   // 保存通过app.use(middleware)注册的中间件          this.subdomainOffset = 2;          this.env = process.env.NODE_ENV || 'development';   // 环境参数,默认为 NODE_ENV 或 ‘development’          this.context = Object.create(context);  // context模块,通过context.js创建          this.request = Object.create(request);  // request模块,通过request.js创建          this.response = Object.create(response);    // response模块,通过response.js创建      }      ...  </pre>    <p>Application.js 除了上面的的构造函数外,还暴露了一些公用的api,比如常用的 listen 和 use (use放在后面讲)。</p>    <p>listen</p>    <p>作用: 启动koa server</p>    <p>语法糖</p>    <pre>  // 用koa启动server  const Koa = require('koa');  const app = new Koa();  app.listen(3000);    // 等价于    // node原生启动server  const http = require('http');  const Koa = require('koa');  const app = new Koa();  http.createServer(app.callback()).listen(3000);  https.createServer(app.callback()).listen(3001); // on mutilple address  </pre>    <pre>  // listen  listen(...args) {      const server = http.createServer(this.callback());      return server.listen(...args);  }  </pre>    <p>封装了nodejs的创建http server,在监听端口之前会先执行 this.callback()</p>    <pre>  // callback    callback() {      // 使用koa-compose(后面会讲) 串联中间件堆栈中的middleware,返回一个函数      // fn接受两个参数 (context, next)      const fn = compose(this.middleware);        if (!this.listeners('error').length) this.on('error', this.onerror);        // this.callback()返回一个函数handleReqwuest,请求过来的时候,回调这个函数      // handleReqwuest接受参数 (req, res)      const handleRequest = (req, res) => {          // 为每一个请求创建ctx,挂载请求相关信息          const ctx = this.createContext(req, res);          // handleRequest的解析在【请求响应】部分          return this.handleRequest(ctx, fn);      };        return handleRequest;  }  </pre>    <p>const ctx = this.createContext(req, res); 创建一个最终可用版的 context</p>    <p><img src="https://simg.open-open.com/show/a72d3f9fae7c8a310d66e875ded57c86.jpg"></p>    <p>ctx上包含5个属性,分别是request,response,req,res,app</p>    <p>request和response也分别有5个箭头指向它们,所以也是同样的逻辑</p>    <p>补充了解 各对象之间的关系</p>    <p><img src="https://simg.open-open.com/show/a83003cb95247cd9bb4fd36a9f7029d3.png"></p>    <p>最左边一列表示每个文件的导出对象</p>    <p>中间一列表示每个Koa应用及其维护的属性</p>    <p>右边两列表示对应每个请求所维护的一些列对象</p>    <p>黑色的线表示实例化</p>    <p>红色的线表示原型链</p>    <p>蓝色的线表示属性</p>    <h3>请求响应</h3>    <p>回顾一下,koa启动server的代码</p>    <pre>  app.listen = function(){      var server = http.createServer(this.callback());      return server.listen.apply(server, arguments);  };  </pre>    <pre>  // callback  callback() {      const fn = compose(this.middleware);      ...      const handleRequest = (req, res) => {          const ctx = this.createContext(req, res);          return this.handleRequest(ctx, fn);      };      return handleRequest;  }  </pre>    <p>callback() 返回了一个请求处理函数 this.handleRequest(ctx, fn)</p>    <pre>  // handleRequest    handleRequest(ctx, fnMiddleware) {      const res = ctx.res;        // 请求走到这里标明成功了,http respond code设为默认的404 TODO 为什么?      res.statusCode = 404;        // koa默认的错误处理函数,它处理的是错误导致的异常结束      const onerror = err=> ctx.onerror(err);        // respond函数里面主要是一些收尾工作,例如判断http code为空如何输出,http method是head如何输出,body返回是流或json时如何输出      const handleResponse = ()=> respond(ctx);        // 第三方函数,用于监听 http response 的结束事件,执行回调      // 如果response有错误,会执行ctx.onerror中的逻辑,设置response类型,状态码和错误信息等      onFinished(res, onerror);        // 执行中间件,监听中间件执行结果      // 成功:执行response      // 失败,捕捉错误信息,执行对应处理      // 返回Promise对象      return fnMiddleware(ctx).then(handleResponse).catch(onerror);  }  </pre>    <p>Koa处理请求的过程:当请求到来的时候,会通过 req 和 res 来创建一个 context (ctx) ,然后执行中间件</p>    <p>koa中另一个常用API - use</p>    <p>作用: 将函数推入middleware数组</p>    <pre>  use(fn) {      // 首先判断传进来的参数,传进来的不是一个函数,报错      if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');      // 判断这个函数是不是 generator      // koa 后续的版本推荐使用 await/async 的方式处理异步      // 所以会慢慢不支持 koa1 中的 generator,不再推荐大家使用 generator      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');          // 如果是 generator,控制台警告,然后将函数进行包装          fn = convert(fn);      }      debug('use %s', fn._name || fn.name || '-');      // 将函数推入 middleware 这个数组,后面要依次调用里面的每一个中间件      this.middleware.push(fn);      // 保证链式调用      return this;  }  </pre>    <p>koa-compose</p>    <p>const fn = compose(this.middleware)</p>    <p>app.use([MW])仅仅是将函数推入middleware数组,真正让这一系列函数组合成为中间件的,是koa-compose,koa-compose是Koa框架中间件执行的发动机</p>    <pre>  'use strict'    module.exports = 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){          // 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              // 如果中间件中没有 await next ,那么函数直接就退出了,不会继续递归调用              if (!fn) return Promise.resolve()              try {                  return Promise.resolve(fn(context, function next(){                      return dispatch(i + 1)                  }))              } catch (err) {                  return Promise.reject(err)              }          }      }  }  </pre>    <p>Koa2.x的compose方法虽然从纯generator函数执行修改成了基于Promise.all,但是中间件加载的中心思想没有发生改变,依旧是从第一个中间件开始,遇到await/yield next,就中断本中间件的代码执行,跳转到对应的下一个中间件执行期内的代码…一直到最后一个中间件,然后逆序回退到倒数第二个中间件await/yield next下部分的代码执行,完成后继续会退…一直会退到第一个中间件await/yield next下部分的代码执行完成,中间件全部执行结束</p>    <p>级联的流程,V型加载机制</p>    <p><img src="https://simg.open-open.com/show/2972bdca4b0af6e6e9522bd4f5c5c72f.png"></p>    <h2>koa2常用中间件</h2>    <h3>koa-router 路由</h3>    <p>对其实现机制有兴趣的可以戳看看 -> <a href="/misc/goto?guid=4959756186466776263" rel="nofollow,noindex">Koa-router路由中间件API详解</a></p>    <pre>  const Koa = require('koa')  const fs = require('fs')  const app = new Koa()    const Router = require('koa-router')    // 子路由1  let home = new Router()  home.get('/', async ( ctx )=>{    let html = `      <ul>        <li><a href="/page/helloworld">/page/helloworld</a></li>        <li><a href="/page/404">/page/404</a></li>      </ul>    `    ctx.body = html  })    // 子路由2  let page = new Router()  page.get('hello', async (ctx) => {      ctx.body = 'Hello World Page!'  })    // 装载所有子路由的中间件router  let router = new Router()  router.use('/', home.routes(), home.allowedMethods())  router.use('/page', page.routes(), page.allowedMethods())    // 加载router  app.use(router.routes()).use(router.allowedMethods())    app.listen(3000, () => {    console.log('[demo] route-use-middleware is starting at port 3000')  })  </pre>    <h3>koa-bodyparser 请求数据获取</h3>    <p>GET请求数据获取</p>    <p>获取GET请求数据有两个途径</p>    <ol>     <li> <p>是从上下文中直接获取</p>      <ul>       <li>请求对象ctx.query,返回如 { a:1, b:2 }</li>       <li>请求字符串 ctx.querystring,返回如 a=1&b=2</li>      </ul> </li>     <li> <p>是从上下文的request对象中获取</p>      <ul>       <li>请求对象ctx.request.query,返回如 { a:1, b:2 }</li>       <li>请求字符串 ctx.request.querystring,返回如 a=1&b=2</li>      </ul> </li>    </ol>    <p>POST请求数据获取</p>    <p>对于POST请求的处理,koa2没有封装获取参数的方法需要通过解析上下文context中的原生node.js请求对象req,将POST表单数据解析成query string(例如:a=1&b=2&c=3),再将query string 解析成JSON格式(例如:{“a”:”1”, “b”:”2”, “c”:”3”})</p>    <p>对于POST请求的处理,koa-bodyparser中间件可以把koa2上下文的formData数据解析到ctx.request.body中</p>    <pre>  ...  const bodyParser = require('koa-bodyparser')    app.use(bodyParser())    app.use( async ( ctx ) => {      if ( ctx.url === '/' && ctx.method === 'POST' ) {      // 当POST请求的时候,中间件koa-bodyparser解析POST表单里的数据,并显示出来      let postData = ctx.request.body      ctx.body = postData    } else {      ...    }  })    app.listen(3000, () => {    console.log('[demo] request post is starting at port 3000')  })  </pre>    <h3>koa-static 静态资源加载</h3>    <p>为静态资源访问创建一个服务器,根据url访问对应的文件夹、文件</p>    <pre>  ...  const static = require('koa-static')  const app = new Koa()    // 静态资源目录对于相对入口文件index.js的路径  const staticPath = './static'    app.use(static(    path.join( __dirname,  staticPath)  ))      app.use( async ( ctx ) => {    ctx.body = 'hello world'  })    app.listen(3000, () => {    console.log('[demo] static-use-middleware is starting at port 3000')  })  </pre>    <p>参考</p>    <ul>     <li><a href="/misc/goto?guid=4959756186551668309" rel="nofollow,noindex">koa文档</a></li>     <li><a href="/misc/goto?guid=4959756186629007682" rel="nofollow,noindex">深入浅出koa #2</a></li>     <li><a href="/misc/goto?guid=4959756186719743638" rel="nofollow,noindex">深入浅出koa2 #11</a></li>     <li><a href="/misc/goto?guid=4959756186799905134" rel="nofollow,noindex">Node.js Koa 之Async中间件</a></li>     <li><a href="/misc/goto?guid=4959653348251716696" rel="nofollow,noindex">koa中文文档</a></li>     <li><a href="/misc/goto?guid=4959756186917879360" rel="nofollow,noindex">koa2 源码分析 (一)</a></li>     <li><a href="/misc/goto?guid=4959756187003371505" rel="nofollow,noindex">Koa2源码阅读笔记</a></li>     <li><a href="/misc/goto?guid=4959756187082612218" rel="nofollow,noindex">Koa2进阶学习笔记</a></li>     <li><a href="/misc/goto?guid=4959756186466776263" rel="nofollow,noindex">Koa-router路由中间件API详解</a></li>     <li><a href="/misc/goto?guid=4959756187173274714" rel="nofollow,noindex">跨入Koa2.0,从Compose开始</a></li>    </ul>    <p> </p>    <p>来自:https://blog.kaolafed.com/2017/12/29/从源码入手探索koa2应用的实现/</p>    <p> </p>