koa源码学习

曾经看过不少源码,可是却没有本着刨根问底的精神,遇到不懂的问题老是轻易的放过。我知道掘金是个大神云集的地方,但愿把本身的学习过程记录下来,一方面督促本身,一方面也是为了能和你们一块儿学习,分享本身学习的心得。node

koa文件结构

├── application.js算法

├── context.js缓存

├── request.js安全

└── response.js服务器

koa一共只有四个文件,因此学习起来并不困难,稍微用一点时间就能够看完。从名称上就能够看出各个文件的功能。分别是请求,响应,上下文,应用四个文件。app

request.js

reuest.js是请求的封装,包含发请求相关的一系列操做。dom

~的运用

判断请求是否幂等。koa

get idempotent() {
    const methods = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE'];
    return !!~methods.indexOf(this.method);
  }
复制代码

首先解释下幂等概念,幂等函数,或幂等方法,是指可使用相同参数重复执行,并能得到相同结果的函数。在http中,是指不管调用这个url多少次,都不会有不一样的结果的HTTP方法。这一部分若是有不理解的地方,能够看看这篇文章HTTP请求方法及幂等性探究
比较好玩的地方是!!~methods.indexOf()。
!!的做用是转换为布尔值,~的做用是按位取反。举个例子js中-1的原码为10..001(62个0),因此补码为111..11(64)个1,按位取反后获得0。有这样一个规律,整数按位取反的结果等于-(x+1)。比较有意思的是 ~NaN === -1, ~Infinity === -1。异步

~~的运用

获取content-length的长度,return Number。async

get length() {
    const len = this.get('Content-Length');
    if (len == '') return;
    return ~~len;
  }
复制代码

由y=-(x+1),能够推出y==~~x。因此整数状况下,结果并不会发生改变。~~的参数不为整数时,会向下取整。当参数为NaN,或Infinity,以及非number类型时,都会返回0。这样能够保证,返回的输出的安全性。

X-Forwarded-For字段

相关代码以下:

get ips() {
   const proxy = this.app.proxy;
   const val = this.get('X-Forwarded-For');
   return proxy && val
     ? val.split(/\s*,\s*/)
     : [];
 }
复制代码

这一部分的做用是得到真实的用户ip。X-Forwarded-For:简称XFF头,它表明客户端,也就是HTTP的请求端真实的IP。若是一个 HTTP 请求到达服务器以前,通过了三个代理 Proxy一、Proxy二、Proxy3,IP 分别为 IP一、IP二、IP3,用户真实 IP 为 IP0,那么按照 XFF 标准,服务端最终会收到如下信息: X-Forwarded-For: IP0,IP1,IP2。IP3不在这个列表中,由于IP3会经过Remote Address 字段得到。

response.js

response.js是对原生req进行的封装。

Content-Disposition属性

attachment(filename) {
    if (filename) this.type = extname(filename);
    this.set('Content-Disposition', contentDisposition(filename));
  },
复制代码

其中extname是node的原生方法,得到文件的扩展名。主要须要搞清楚的是Content-Disposition字段。

    在常规的HTTP应答中,Content-Disposition 消息头指示回复的内容该以何种形式展现,是之内联的形式(即网页或者页面的一部分),仍是以附件的形式下载并保存到本地。 此时的第一个参数与可选值有inline,或者attachment。inline时,文件会以页面的一部分或者总体展示,而attachment则会弹出下载提示。

    在multipart/form-data类型的应答消息体中, Content-Disposition消息头能够被用在multipart消息体的子部分中,用来给出其对应字段的相关信息。第一个参数固定为form-data。详细文档能够参考MDN

etag字段

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

    etag是资源的指纹,用来标识资源是否更改。和etag相比较的是If-Match,和If-None-Match响应首部。有关响应首部有不理解的朋友能够看看http条件请求
    当首部是If-Match时,在请求方法为 GET 和 HEAD 的状况下,服务器仅在请求的资源知足此首部列出的 ETag 之一时才会返回资源。而对于 PUT 或其余非安全方法来讲,只有在知足条件的状况下才能够将资源上传。
    当响应首部是If-None-Match时,对于GET 和 HEAD 请求方法来讲,当且仅当服务器上没有任何资源的 ETag 属性值与这个首部中列出的相匹配的时候,服务器端会才返回所请求的资源,响应码为 200。对于get和head不对服务器状态发生改变的方法,若是相匹配返回304,其余的返回则返回 412。

Vary字段

vary(field) {
    vary(this.res, field);
  }
复制代码

这里主要讲一下vary的做用。

http中有一个内容协商机制,为同一个URL指向的资源提供不一样的展示形式。好比文档的天然语言,编码形式,以及压缩算法等等。这种协商机制能够分为两种形式展示:

  • 客户端设置特定的 HTTP 首部 (又称为服务端驱动型内容协商机制或者主动协商机制);这是进行内容协商的标准方式;
  • 服务器返回 300 (Multiple Choices) 或者 406 (Not Acceptable) HTTP 状态码 (又称为代理驱动型协商机制或者响应式协商机制);这种方式通常用做备选方案。

vary字段就是标志服务器在服务端驱动型内容协商阶段所使用的首部清单,他能够通知缓存服务器决策的依据。常见的首部清单有Accept,Accept-Language,Accept-Charset,Accept-Encoding,User-Agent等。

content-length计算

get length() {
    const len = this.header['content-length'];
    const body = this.body;

    if (null == len) {
      if (!body) return;
      if ('string' == typeof body) return Buffer.byteLength(body);
      if (Buffer.isBuffer(body)) return body.length;
      if (isJSON(body)) return Buffer.byteLength(JSON.stringify(body));
      return;
    }

    return ~~len;
  }
复制代码

Buffer.byteLength方法返回字符串实际占据的字节长度,默认编码方式为utf8。即便对于string类型,也没有使用String.length来直接获取,由于String.length获取到的是字符的长度,而不是字节长度。好比汉字,utf8编码一个字符就要占三个字节。

context.js

ctx是咱们平常开发中最经常使用到的属性,好比ctx.req,ctx.res,ctx.response,ctx.request。以及开发中间件时的各类操做,都是在ctx上完成的。

context原型上有inspect,toJson,assert,throw,和onerror五个方法。剩下的就是response和request的代理。这里用了一个比较有意思的库delegates。写起来就像这样

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('set')
  .method('append')
  .method('flushHeaders')
  .access('status')
  .access('message')
  .access('body')
  .access('length')
  .access('type')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable');
复制代码

这里使用链式操做,看起来很是简单明了。 delegates中的getter和setter使用的是Object.prototype.defineGetter()和Object.prototype.defineSetter()方法。以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;
};
复制代码

当咱们为proto的某一属性赋值时,其实仍是调用target的set访问器,这里仅仅是一个代理。

application.js

Application继承于Emmiter类,包含request,response,context,subdomainOffset,proxy,middleware,subdomainOffset,env等属性。
listen方法实际调用了http.createServer(app.callback()).listen()。因此koa中最重要的就是callback函数的实现。

callback() {
    const fn = compose(this.middleware);
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };
    return handleRequest;
  }
复制代码

首先将middleware转化为function,并构建ctx对象,随后调用this.handleRequest传入ctx,fn,处理请求。

this.handleRequest函数主干以下所示:

handleRequest(ctx, fnMiddleware) {
    const handleResponse = () => respond(ctx);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
复制代码

fnMiddleware由compose函数得来,compose函数实现为下:

function compose (middleware) {
    return function (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
          let fn = middleware[i]
          if (i === middleware.length) fn = next
          if (!fn) return Promise.resolve()
          try {
            return Promise.resolve(fn(context, function next () {
              return dispatch(i + 1)
            }))
          } catch (err) {
            return Promise.reject(err)
          }
        }
    }
  }
复制代码

调用compose,返回一个(context,next)=>{}的函数,也就是this.handleRequest中的fnMiddleware。当执行fnMiddleware时,返回dispatch(0)。执行dispatch时,返回一个Promise,当Promise完成时,调用dispatch(1),以此类推,直到i === middleware.length时,fn = next,由于在this.handleRequest调用时,next并无传,因此,此时fn === undefined, return Promise.resolve();到这里compose的逻辑算是理清了。咱们在来看一下中间件是怎么书写的,举一个简单的例子:

const one = (ctx, next) => {
  console.log('>> one');
  next();
  console.log('<< one');
}

const two = (ctx, next) => {
  console.log('>> two');
  next(); 
  console.log('<< two');
}

app.use(one);
app.use(two);
复制代码

执行dispatch(0)时,返回

return Promise.resolve(one(context, function next () {
  return dispatch(1)
}))
复制代码

当执行到one函数的next函数时,此时return到dispatch(1)。此时dispatch(1)执行,当执行到two的next时,返回dispatch(2)。由于2 === middleware.length,又由于fn == undefined,固此时return Promise.resolve()。当two的next方法执行完毕,继续执行console.log('<< two')。当two的函数所有执行完毕后,程序回到one的next()结束部分,继续执行console.log('<< one')。async await异步函数执行同理。

这一块的逻辑确实难于理解,能够打断点调试下看看结果。

总结一下:学习不光要多看,还要多写,还要多实践,这样才能真正理解,并有所收获!

相关文章
相关标签/搜索