koa源碼解讀

koa是有express原班人馬打造的基於node.js的下一代web開發框架。koa 1.0使用generator實現異步,相比於回調簡單和優雅和很多。koa團隊並無止步於koa 1.0, 隨着node.js開始支持async/await,他們又快馬加鞭的發佈了koa 2.0,koa2徹底使用Promise並配合async/await來實現異步,使得異步操做更臻完美。javascript

 

1、快速開始前端

koa使用起來很是簡單,安裝好node.js後執行如下命令安裝koa:java

npm initnode

npm install --save koagit

 

一個簡單的Hello World程序開場,github

//index.js

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


app.use(async ctx => {
    ctx.body = 'Hello World'

})


app.listen(3000, () => {
    console.log("server is running at 3000 port");
})

  

在命令行執行web

node index.jsexpress

打開瀏覽器查看http://localhost:3000就能夠看到頁面輸出的 Hello World。npm

 

中間件 middlewareapi

Koa中使用 app.use()用來加載中間件,基本上Koa 全部的功能都是經過中間件實現的。

中間件的設計很是巧妙,多箇中間件會造成一個棧結構(middle stack),以」先進後出」(first-in-last-out)的順序執行。每一箇中間件默認接受兩個參數,第一個參數是 Context 對象,第二個參數是 next函數。只要調用 next函數,就能夠把執行權轉交給下一個中間件,最裏層的中間件執行完後有會把執行權返回給上一級調用的中間件。整個執行過程就像一個剝洋蔥的過程。

 

 

 

好比你能夠經過在全部中間件的頂端添加如下中間件來打印請求日誌到控制檯:

app.use(async function (ctx, next) {

let start = new Date()

await next()

let ms = new Date() - start

console.log('%s %s - %s', ctx.method, ctx.url, ms)

})

 

經常使用的中間件列表能夠在這裏找到: https://github.com/koajs/koa/wiki

 

2、koa源碼解讀

打開項目根目錄下的node_modules文件夾,打開並找到koa的文件夾,以下所示:

 

打開lib文件夾,這裏一共有4個文件,

  • application.js - koa主程序入口

  • context.js - koa中間件參數ctx對象的封裝

  • request.js - request對象封裝

  • response.js - response對象封裝

 

咱們這裏主要看下application.js,我這裏摘取了主要功能相關的 代碼以下:

  /**
   * Shorthand for:
   *
   *    http.createServer(app.callback()).listen(...)
   *
   * @param {Mixed} ...
   * @return {Server}
   * @api public
   */

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


  /**
   * Use the given middleware `fn`.
   *
   * Old-style middleware will be converted.
   *
   * @param {Function} fn
   * @return {Application} self
   * @api public
   */

  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    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');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }

  /**
   * Return a request handler callback
   * for node's native http server.
   *
   * @return {Function}
   * @api public
   */

  callback() {
    const fn = compose(this.middleware);

    if (!this.listenerCount('error')) this.on('error', this.onerror);

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

    return handleRequest;
  }

  /**
   * Handle request in callback.
   *
   * @api private
   */

  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);
  }

 

經過註釋咱們能夠看出上面代碼主要乾的事情是初始化http服務對象並啓動。咱們注意到 callback()方法裏面有這樣一段代碼 :

const fn = compose(this.middleware);

 

compose實際上是Node模塊koa-compose,它的做用是將多箇中間件函數合併成一個大的中間件函數,而後調用這個中間件函數就能夠依次執行添加的中間件函數,執行一系列的任務。遇到await next()時就中止當前中間件函數的執行並把執行權交個下一個中間件函數,最後next()執行完返回上一個中間件函數繼續執行下面的代碼。

 

它是用了什麼黑魔法實現的呢?咱們打開node_modules/koa-compose/index.js,代碼以下 :

function compose(middleware) {

    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

            if (!fn) return Promise.resolve()

            try {

                return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

            } catch (err) {

                return Promise.reject(err)

            }

        }

    }

}

 

 

乍一看好難好複雜,沒事,咱們一步一步的來梳理一下。

 

這個方法裏面的核心就是dispatch函數(廢話,整個compose方法就返回了一個函數)。沒有辦法簡寫,可是咱們能夠將dispatch函數相似遞歸的調用展開,以三個中間件爲例:

第一次,此時第一個中間件被調用,dispatch(0),展開:

Promise.resolve(function(context, next){

    //中間件一第一部分代碼

    await/yield next();

    //中間件一第二部分代碼}());

 

很明顯這裏的next指向dispatch(1),那麼就進入了第二個中間件;

第二次,此時第二個中間件被調用,dispatch(1),展開:

Promise.resolve(function(context, 中間件2){

    //中間件一第一部分代碼

    await/yield Promise.resolve(function(context, next){

        //中間件二第一部分代碼

        await/yield next();

        //中間件二第二部分代碼

    }())

    //中間件一第二部分代碼}());

 

很明顯這裏的next指向dispatch(2),那麼就進入了第三個中間件;

第三次,此時第二個中間件被調用,dispatch(2),展開:

Promise.resolve(function(context, 中間件2){

    //中間件一第一部分代碼

    await/yield Promise.resolve(function(context, 中間件3){

        //中間件二第一部分代碼

        await/yield Promise(function(context){

            //中間件三代碼

        }());

        //中間件二第二部分代碼

    })

    //中間件一第二部分代碼}());

 

此時中間件三代碼執行完畢,開始執行中間件二第二部分代碼,執行完畢,開始執行中間一第二部分代碼,執行完畢,全部中間件加載完畢。

再舉一個例子加深下理解。新建index.js並粘貼以下代碼:

const compose = require('koa-compose')

const middleware1 = (ctx, next) => {
    console.log('here is in middleware1, before next:');
    next();
    console.log('middleware1 end');
}

const middleware2 = (ctx, next) => {
    console.log('here is in middleware2, before next:');
    next();
    console.log('middleware2 end');
}

const middleware3 = (ctx, next) => {
    console.log('here is in middleware3, before next:');
    next();
    console.log('middleware3 end');
}

const middlewares = compose([middleware1, middleware2, middleware3])
console.dir(middlewares())

 

 

在命令行輸入node index.js執行,輸出結果以下:

here is in middleware1, before next:

here is in middleware2, before next:

here is in middleware3, before next:

middleware3 end

middleware2 end

middleware1 end

Promise { undefined }

 

能夠看到每一箇中間件都按照「剝洋蔥」的流程一次執行。當咱們初始化app對象並調用app.use()時,就是在不斷往app.middleware數組裏添加中間件函數,當調用app.listen()再執行組合出來的函數。

 

-END-

 

轉載請註明來源

掃描下方二維碼,或者搜索 前端提升班 關注公衆號,便可獲取最新走心文章

記得把我設爲星標或置頂哦

在公衆號後臺回覆 前端資源 便可獲取最新前端開發資源

相關文章
相關標籤/搜索