50行程式碼學會koa2中介軟體原理

船頭尺發表於2021-09-09

圖片描述

Koa 是 nodejs 開發的下一代 web 開發框架,可參考 。說是“下一代”,其實在實際開發中早就用在專案中了。特別是 nodejs 新版本開始正式支援 async/await 語法之後,Koa2 正在被大量使用。

關於 Koa2 的基本使用和中介軟體機制的使用,大家可以去官網查閱和學習,本文主要講解 Koa2 的中介軟體原理 —— 而且是透過非常簡短的 50 行程式碼。程式碼寫完之後,要能實現官網中的一段中介軟體示例,如下:

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

// logger
app.use(async (ctx, next) => {
  await next();
  const rt = ctx.response.get('X-Response-Time');
  console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});

// x-response-time
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});

// response
app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

這段程式碼的意圖是:第一,記錄服務開始的時間戳;第二,返回 hello word ;第三,記錄返回之後的時間戳,然後算出時間間隔並列印。接下來我們就透過這段程式碼,分析一下 Koa2 的中介軟體實現原理。

上述示例程式碼中,有三個 app.use ,從使用角度分析它的用意,其實就是註冊中介軟體函式。因此,首先可以這樣定義我們自己的 Koa 程式碼,新建一個 like-koa2.js ,開始編寫:

class LikeKoa2 {
    constructor() {
        this.middlewareList = []
    }

    // 核心方法
    use(fn) {
        this.middlewareList.push(fn)
        return this
    }
}

首先定一個類,然後建構函式中初始化 middelwareList 陣列,用以儲存所有的中介軟體函式。use 中接收中介軟體函式,然後放到 middelwareList 陣列中,就算是註冊完成。最後 return this 是為了能實現鏈式操作,例如 app.use(fn1).use(fn2).use(fn3) ,實際是否這樣用看自己的需求。

示例程式碼的最後使用 app.listen(3000) 啟動服務監聽,可以轉化為 nodejs 原生的 http 處理方式。程式碼如下:

const http = require('http');

class LikeKoa2 {
    constructor() {
        this.middlewareList = []
    }

    // 核心方法
    use(fn) {
        this.middlewareList.push(fn)
        return this
    }

	// 將 req res 組合成為 ctx
    createContext(req, res) {
        // 簡單模擬 koa 的 ctx ,不管細節了
        const ctx = {
            req,
            res
        }
        return ctx
    }

	// 生成 http.createServer 需要的回撥函式
	callback() {
        return (req, res) => {
            const ctx = this.createContext(req, res)

        }
    }

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

需要簡單解釋兩點。第一,nodejs 原生的 http.createServer 需要傳入一個回撥函式,在 callback() 中返回。第二,示例程式碼中中介軟體函式的第一個引數都是 ctx ,其實可以簡答理解為 resreq 的集合,透過 createContext 合併一下即可。

上述程式碼,獲取到 ctx 之後,並沒有做下一步處理,下文會繼續解釋。

上文一開始使用 use 來註冊中介軟體,再就是用 listen 去啟動並監聽服務,即剛開始就直接結束了。其實中間漏下很重要的一個步驟 —— 中介軟體組合,即如何讓中間有 next 機制,將中介軟體一個一個的串起來。

Koa2 中透過一個 compose 函式來組合中介軟體,以及實現了 next 機制。具體程式碼有點繞,不太好解釋,我儘量用通俗的語言、結合程式碼註釋,解釋清楚。先看程式碼:

// 傳入中介軟體列表
function compose(middlewareList) {
	// 返回一個函式,接收 ctx (即 res 和 req 的組合)—— 記住了,下文要用
    return function (ctx) {
	    // 定義一個派發器,這裡面就實現了 next 機制
        function dispatch(i) {
	        // 獲取當前中介軟體
            const fn = middlewareList[i]
            try {
                return Promise.resolve(
	                // 透過 i + 1 獲取下一個中介軟體,傳遞給 next 引數
                    fn(ctx, dispatch.bind(null, i + 1))
                )
            } catch (err) {
                return Promise.reject(err)
            }
        }
        // 開始派發第一個中介軟體
        return dispatch(0)
    }
}

我們按步驟解釋一下上述程式碼。

  • 第一,定義 compose 函式,並接收中介軟體列表。
  • 第二,compose 函式中返回一個函式,該函式接收 ctx ,下文會用這個返回的函式。
  • 第三,再往內部,定義了一個 dispatch 函式,就是一箇中介軟體的派發器,引數 i 就代表派發第幾個中介軟體。執行 dispatch(0) 就是開發派發第一個中介軟體。
  • 第四,派發器內部,透過 i 獲取當前的中介軟體,然後執行。執行時傳入的第一個引數是 ctx ,第二個引數是 dispatch.bind(null, i + 1) 即下一個中介軟體函式 —— 也正好對應到示例程式碼中中介軟體的 next 引數。
  • Promise.resolve 封裝起來,是為了保證函式執行的結果必須是 Promise 型別。

就是這麼多步驟,感覺自己已經解釋的很詳細了,但確實比較繞。如果有看不懂的同學,我建議多看幾遍,或者自己親自動手寫幾遍,熟練了也就掌握了。

有了 compose 之後,callback 即可被完善起來,相關程式碼(並不是全部的程式碼)如下,其中新增的 handleRequest 看看註釋應該也能明白了。

    // 處理中介軟體的 http 請求
    handleRequest(ctx, middleWare) {
        // 這個 middleWare 就是 compose 函式返回的 fn
        // 執行 middleWare(ctx) 其實就是執行中介軟體函式,然後再用 Promise.resolve 封裝並返回
        return middleWare(ctx)
    }
  
    callback() {
        const fn = compose(this.middlewareList)

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

以上就是分析的全部內容,下面列出完整的程式碼,但希望大家不要直接複製,而是自己親手寫出來。

const http = require('http');

// 組合中介軟體
function compose(middlewareList) {
    return function (ctx) {
        function dispatch(i) {
            const fn = middlewareList[i]
            try {
                return Promise.resolve(
                    fn(ctx, dispatch.bind(null, i + 1))
                )
            } catch (err) {
                return Promise.reject(err)
            }
        }
        return dispatch(0)
    }
}

class LikeKoa2 {
    constructor() {
        this.middlewareList = []
    }

    // 核心方法
    use(fn) {
        this.middlewareList.push(fn)
        return this
    }

    // 處理中介軟體的 http 請求
    handleRequest(ctx, middleWare) {
        // 這個 middleWare 就是 compose 函式返回的 fn
        // 執行 middleWare(ctx) 其實就是執行中介軟體函式,然後再用 Promise.resolve 封裝並返回
        return middleWare(ctx)
    }

    // 將 req res 組合成為 ctx
    createContext(req, res) {
        // 簡單模擬 koa 的 ctx ,不管細節了
        const ctx = {
            req,
            res
        }
        return ctx
    }

    callback() {
        const fn = compose(this.middlewareList)

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

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

module.exports = LikeKoa2

標題說“50行程式碼”其實有點誇張了,因為算上程式碼的空行和註釋,一共 60 多行 —— 空行和註釋也是程式碼的一部分嘛!

新建一個 test.js 然後開始編寫:

const Koa = require('./like-koa2');
const app = new Koa();

// logger
app.use(async (ctx, next) => {
  await next();
  const rt = ctx['X-Response-Time'];
  console.log(`${ctx.req.method} ${ctx.req.url} - ${rt}`);
});

// x-response-time
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx['X-Response-Time'] = `${ms}ms`;
});

// response
app.use(async ctx => {
  ctx.res.end('hello world')
});

app.listen(8000);

請大家注意,我們這裡的程式碼示例和一開始官網的示例是有一些區別的,例如這裡的 ctx['X-Response-Time'] 和官網示例的 ctx.set('X-Response-Time', ...) 。這是因為我們的 ctx 是簡單的將 resreq 拼接而成,而 Koa2 中的 ctx 還做了一些 API 的擴充套件和處理,但是這並不是我們理解中介軟體原理的障礙,因此可以忽略。

最後,node test.js 執行起來,然後瀏覽器訪問 ,看控制檯能否列印出日誌記錄。提示,nodejs 版本必須 >= 8.0 。

·····································

大家可以關注一下課程:

【新課】

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3016/viewspace-2821800/,如需轉載,請註明出處,否則將追究法律責任。

相關文章