koa原始碼學習

guoliangli發表於2018-02-03

曾經看過很多原始碼,但是卻沒有本著刨根問底的精神,遇到不懂的問題總是輕易的放過。我知道掘金是個大神雲集的地方,希望把自己的學習過程記錄下來,一方面督促自己,一方面也是為了能和大家一起學習,分享自己學習的心得。

koa檔案結構

├── application.js

├── context.js

├── request.js

└── response.js

koa一共只有四個檔案,所以學習起來並不困難,稍微用一點時間就可以看完。從名稱上就可以看出各個檔案的功能。分別是請求,響應,上下文,應用四個檔案。

request.js

reuest.js是請求的封裝,包含發請求相關的一系列操作。

~的運用

判斷請求是否冪等。

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。

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 請求到達伺服器之前,經過了三個代理 Proxy1、Proxy2、Proxy3,IP 分別為 IP1、IP2、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非同步函式執行同理。

這一塊的邏輯確實難於理解,可以打斷點除錯下看看結果。

koa原始碼學習

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

相關文章