動態

詳情 返回 返回

從淺入深瞭解Koa2源碼 - 動態 詳情

在前文我們介紹過什麼是 Koa2 的基礎

簡單回顧下

什麼是 koa2

  1. NodeJS 的 web 開發框架
  2. Koa 可被視為 nodejs 的 HTTP 模塊的抽象

源碼重點

中間件機制

洋葱模型

compose

源碼結構

Koa2 的源碼地址:https://github.com/koajs/koa

其中 lib 為其源碼

koa2源碼

可以看出,只有四個文件:application.jscontext.jsrequest.jsresponse.js

application

為入口文件,它繼承了 Emitter 模塊,Emitter 模塊是 NodeJS 原生的模塊,簡單來説,Emitter 模塊能實現事件監聽和事件觸發能力

application1

刪掉註釋,從整理看 Application 構造函數

Application構造函數

Application 在其原型上提供了 listen、toJSON、inspect、use、callback、handleRequest、createContext、onerror 等八個方法,其中

  • listen:提供 HTTP 服務
  • use:中間件掛載
  • callback:獲取 http server 所需要的 callback 函數
  • handleRequest:處理請求體
  • createContext:構造 ctx,合併 node 的 req、res,構造 Koa 的 參數——ctx
  • onerror:錯誤處理

其他的先不要在意,我們再來看看 構造器 constructor

Application的構造器

暈,這都啥和啥,我們啓動一個最簡單的服務,看看實例

const Koa = require('Koa')

const app = new Koa()

app.use((ctx) => {
  ctx.body = 'hello world'
})

app.listen(3000, () => {
  console.log('3000請求成功')
})

console.dir(app)

實例

能看出來,我們的實例和構造器一一對應,

打斷點看原型

斷點

哦了,除去非關鍵字段,我們只關注重點

Koa 的構造器上的 this.middleware、 this.context、 this.request、this.response

原型上有:listen、use、callback、handleRequest、createContext、onerror

注:以下代碼都是刪除異常和非關鍵代碼

先看 listen

...
  listen(...args) {
    const server = http.createServer(this.callback())
    return server.listen(...args)
  }
...

可以看出 listen 就是用 http 模塊封裝了一個 http 服務,重點是傳入的 this.callback()。好,我們現在就去看 callback 方法

callback

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

它包含了中間件的合併,上下文的處理,以及 res 的特殊處理

中間件的合併

使用了 koa-compose 來合併中間件,這也是洋葱模型的關鍵,koa-compose 的源碼地址:https://github.com/koajs/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, dispatch.bind(null, i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

一晃眼是看不明白的,我們需要先明白 middleware 是什麼,即中間件數組,那它是怎麼來的呢,構造器中有 this.middleware,誰使用到了—— use 方法

我們先跳出去先看 use 方法

use

use(fn) {
    this.middleware.push(fn)
    return this
}

除去異常處理,關鍵是這兩步,this.middleware 是一個數組,第一步往 this.middleware 中 push 中間件;第二步返回 this 讓其可以鏈式調用,當初本人被面試如何做 promise 的鏈式調用,懵逼臉,沒想到在這裏看到了

回過頭來看 koa-compose 源碼,設想一下這種場景

...
app.use(async (ctx, next) => {
    console.log(1);
    await next();
    console.log(6);
});
app.use(async (ctx, next) => {
    console.log(2);
    await next();
    console.log(5);
});

app.use(async (ctx, next) => {
    console.log(3);
    ctx.body = "hello world";
    console.log(4);
});
...

我們知道 它的運行是 123456

它的 this.middleware 的構成是

this.middleware = [
  async (ctx, next) => {
    console.log(1)
    await next()
    console.log(6)
  },
  async (ctx, next) => {
    console.log(2)
    await next()
    console.log(5)
  },
  async (ctx, next) => {
    console.log(3)
    ctx.body = 'hello world'
    console.log(4)
  },
]

不要感到奇怪,函數也是對象之一,是對象就可以傳值

const fn = compose(this.middleware)

我們將其 JavaScript 化,其他不用改,只需要把最後一個函數改成

async (ctx, next) => {
  console.log(3);
  -ctx.body = 'hello world';
  +console.log('hello world');
  console.log(4);
}

測試compose

測試compose2

逐行解析 koa-compose

這一段很重要,面試的時候常考,讓你手寫一個 compose ,淦它

//1. async (ctx, next) => { console.log(1); await next(); console.log(6); } 中間件
//2. const fn = compose(this.middleware) 合併中間件
//3. fn() 執行中間件

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, dispatch.bind(null, i + 1)));
            } catch (err) {
                return Promise.reject(err);
            }
        }
    };
}

執行 const fn = compose(this.middleware),即如下代碼

const fn = 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, dispatch.bind(null, i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

執行 fn(),即如下代碼:

const fn = 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    // index = 0
      let fn = middleware[i] // fn 為第一個中間件
      if (i === middleware.length) fn = next // 當弄到最後一箇中間件時,最後一箇中間件賦值為 fn
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
          // 返回一個 Promise 實例,執行 遞歸執行 dispatch(1)
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

也就是第一個中間件,要先等第二個中間件執行完才返回,第二個要等第三個執行完才返回,直到中間件執行執行完畢

Promise.resolve 就是個 Promise 實例,之所以使用 Promise.resolve 是為了解決異步,之所以使用 Promise.resolve 是為了解決異步

拋去 Promise.resolve,我們先看一下遞歸的使用,執行以下代碼

const fn = function () {
    return dispatch(0);
    function dispatch(i) {
        if (i > 3) return;
        i++;
        console.log(i);
        return dispatch(i++);
    }
};
fn(); // 1,2,3,4

回過頭來再看一次 compose,代碼類似於

// 假設 this.middleware = [fn1, fn2, fn3]
function fn(context, next) {
    if (i === middleware.length) fn = next // fn3 沒有 next
    if (!fn) return Promise.resolve() // 因為 fn 為空,執行這一行
    function dispatch (0) {
        return Promise.resolve(fn(context, function dispatch(1) {
            return Promise.resolve(fn(context, function dispatch(2) {
                return Promise.resolve()
            }))
        }))
    }
  }
}

這種遞歸的方式類似執行棧,先進先出

執行棧

這裏要多思考一下,遞歸的使用,對 Promise.resolve 不用太在意

上下文的處理

上下文的處理即調用了 createContext

createContext(req, res) {
    const context = Object.create(this.context)
    const request = (context.request = Object.create(this.request))
    const response = (context.response = Object.create(this.response))
    context.app = request.app = response.app = this
    context.req = request.req = response.req = req
    context.res = request.res = response.res = res
    request.ctx = response.ctx = context
    request.response = response
    response.request = request
    context.originalUrl = request.originalUrl = req.url
    context.state = {}
    return context
}

傳入原生的 request 和 response,返回一個 上下文——context,代碼很清晰,不解釋

res 的特殊處理

callback 中是先執行 this.createContext,拿到上下文後,再去執行 handleRequest,先看代碼:

handleRequest(ctx, fnMiddleware) {
    const res = ctx.res
    res.statusCode = 404
    const onerror = (err) => ctx.onerror(err)
    const handleResponse = () => respond(ctx)
    onFinished(res, onerror)
    return fnMiddleware(ctx).then(handleResponse).catch(onerror)
}

一切都清晰了

const Koa = require('Koa');
const app = new Koa();

console.log('app', app);
app.use((ctx, next) => {
    ctx.body = 'hello world';
});
app.listen(3000, () => {
    console.log('3000請求成功');
});

這樣一段代碼,實例化後,獲得了 this.middleware、this.context、this.request、this.response 四大將,你使用 app.use() 時,將其中的函數推到 this.middleware。再使用 app.listen() 時,相當於起了一個 HTTP 服務,它合併了中間件,獲取了上下文,並對 res 進行了特殊處理

錯誤處理

onerror(err) {
    if (!(err instanceof Error))
        throw new TypeError(util.format('non-error thrown: %j', err))

    if (404 == err.status || err.expose) return
    if (this.silent) return

    const msg = err.stack || err.toString()
    console.error()
    console.error(msg.replace(/^/gm, '  '))
    console.error()
}

context.js

引入我眼簾的是兩個東西

// 1.
const proto = module.exports = {
    inspect(){...},
    toJSON(){...},
    ...
}
// 2.
delegate(proto, 'response')
  .method('attachment')
  .access('status')
  ...

第一個可以理解為,const proto = { inspect() {...} ...},並且 module.exports 導出這個對象

第二個可以這麼看,delegate 就是代理,這是為了方便開發者而設計的

// 將內部對象 response 的屬性,委託至暴露在外的 proto 上
delegate(proto, 'response')
  .method('redirect')
  .method('vary')
  .access('status')
  .access('body')
  .getter('headerSent')
  .getter('writable');
  ...

而使用 delegate(proto, 'response').access('status')...,就是在 context.js 導出的文件,把 proto.response 上的各個參數都代理到 proto 上,那 proto.response 是什麼?就是 context.response,context.response 哪來的?

回顧一下, 在 createContext 中

createContext(req, res) {
    const context = Object.create(this.context)
    const request = (context.request = Object.create(this.request))
    const response = (context.response = Object.create(this.response))
    ...
}

context.response 有了,就明瞭了, context.response = this.response,因為 delegate,所以 context.response 上的參數代理到了 context 上了,舉個例子

  • ctx.header 是 ctx.request.header 上代理的
  • ctx.body 是 ctx.response.body 上代理的

request.js 和 response.js

一個處理請求對象,一個處理返回對象,基本上是對原生 req、res 的簡化處理,大量使用了 ES6 中的 get 和 post 語法

大概就是這樣,瞭解了這麼多,怎麼手寫一個 Koa2 呢,請看下一篇——手寫 Koa2

參考資料

  • KOA2 框架原理解析和實現
  • 可能是目前最全的 koa 源碼解析指南
user avatar nihaojob 頭像 xiaoxxuejishu 頭像 zxl20070701 頭像 yuzhihui 頭像 ccVue 頭像 yangxiansheng_5a1b9b93a3a44 頭像 yuxl01 頭像 code500g 頭像 ailvyoudetiebanshao 頭像 zbooksea 頭像 fannaodeliushu 頭像 morimanong 頭像
點贊 54 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.