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異步函數執行同理。

這一塊的邏輯確實難於理解,能夠打斷點調試下看看結果。

總結一下:學習不光要多看,還要多寫,還要多實踐,這樣才能真正理解,並有所收穫!

相關文章
相關標籤/搜索