Express 源碼分析及簡易封裝

在這裏插入圖片描述


原文出自:https://www.pandashen.com


前言

Express 是 NodeJS 的 Web 框架,與 Koa 的輕量相比,功能要更多一些,依然是當前使用最廣泛的 NodeJS 框架,本篇參考 Express 的核心邏輯來實現一個簡易版,Express 源碼較多,邏輯複雜,看一週可能也看不完,如果你已經使用過 Express,又想快速的瞭解 Express 常用功能的原理,那讀這篇文章是一個好的選擇,也可以爲讀真正的源碼做鋪墊,本篇內容每部分代碼較多,因爲按照 Express 的封裝思想很難拆分,所以建議以星號標註區域爲主其他代碼爲輔。


搭建基本服務

下面我們使用 Express 來搭建一個最基本的服務,只有三行代碼,只能訪問不能響應。

// 三行代碼搭建的最基本服務
// 引入 Express
const express = require("express");

// 創建服務
const app = express();

// 監聽服務
app.listen(3000);

從上面我們可以分析出,express 模塊給我們提供了一個函數,調用後返回了一個函數或對象給上面有 listen 方法給我們創建了一個 http 服務,我們就按照官方的設計返回一個函數 app

// 文件:express.js
const http = require("http");

function createApplication() {
    // 創建 app 函數,身份爲總管家,用於將請求分派給別人處理
    let app = function (req, res) {}

    // 啓動服務的 listen 方法
    app.listen = function () {
        // 創建服務器
        const server = http.createServer(app);

        // 監聽服務,可能傳入多個參數,如第一個參數爲端口號,最後一個參數爲服務啓動後回調
        server.listen(...arguments);
    }

    // 返回 app
    return app;
}

module.exports = createApplication;

我們創建一個模塊 express.js,導出了 createApplication 函數並返回在內部創建 app 函數,createApplication 等於我們引入 Express 模塊時所調用的那個函數,返回值就是我們接收的 app,在 createApplication 返回的 app 函數上掛載了靜態方法 listen,用於幫助我們啓動 http 服務。

createApplication 函數內我們使用引入的 http 模塊創建了服務,並調用了創建服務 serverlisten 方法,將 app.listen 的所有參數傳遞進去,這就等於做了一層封裝,將真正創建服務器的過程都包在了 app.listen 內部,我們自己封裝的 Express 模塊只有在調用導出函數並調用 app.listen 時纔會真正的創建服務器和啓動服務器,相當於將原生的兩步合二爲一。


路由的實現

Express 框架中有多個路由方法,方法名分別對應不同的請求方式,可以幫助我們匹配路徑和請求方式,在完全匹配時執行路由內部的回調函數,以、目的是在不同路由不同請求方法的情況下讓服務器做出不同的響應,路由的使用方式如下。

// 路由的使用方式
// 引入 Express
const express = require("express");

// 創建服務
const app = express();

// 創建路由
app.get("/", function (req, res) {
    res.end("home");
});

app.post("/about", function (req, res) {
    res.end("about");
});

app.all("*", function (req, res) {
    res.end("Not Found");
});

// 監聽服務
app.listen(3000);

如果啓動上面的服務,通過瀏覽器訪問定義的路由時可以匹配到 app.getapp.postapp.all 並執行回調,但其實我們可以發現這些方法的名字是與請求類型嚴格對應的,不僅僅這幾個,下面來看看實現路由的核心邏輯(直接找到星號提示新增或修改位置即可)。

// 文件:express.js
const http = require("http");

// ***************************** 以下爲新增代碼 *****************************
// methods 模塊返回存儲所有請求方法名稱的數組
const methods = require("methods");
// ***************************** 以上爲新增代碼 *****************************

function createApplication() {
    // 創建 app 函數,身份爲總管家,用於將請求分派給別人處理
    let app = function (req, res) {
// ***************************** 以下爲新增代碼 *****************************
        // 獲取方法名統一轉換成小寫
        let method = req.method.toLowerCase();

        // 訪問路徑解構成路由和查詢字符串兩部分 /user?a=1&b=2
        let [reqPath, query = ""] = req.url.split("?");

        // 循環匹配路徑
        for (let i = 0; i < app.routes.lenth; i++) {
            // 循環取得每一層
            let layer = app.routes[i];

            // 如果說路徑和請求類型都能匹配,則執行該路由層的回調
            if (
                (reqPath === layer.pathname || layer.pathname === "*") &&
                (method === layer.method || layer.method === "all")
            ) {
                return layer.hanlder(req, res);
            }
        }

        // 如果都沒有匹配上,則響應錯誤信息
        res.end(`CANNOT ${req.method} ${reqPath}`);
// ***************************** 以上爲新增代碼 *****************************
    }

// ***************************** 以下爲新增代碼 *****************************
    // 存儲路由層的請求類型、路徑和回調
    app.routes = [];

    // 返回一個函數體用於將路由層存入 app.routes 中
    function createRouteMethod(method) {
        return function (pathname, handler) {
            let layer = {
                method,
                pathname, // 不包含查詢字符串
                handler
            };

            // 把這一層放入存儲所有路由層信息的數組中
            app.routes.push(layer);
        }
    }

    // 循環構建所有路由方法,如 app.get app.post 等
    methods.forEach(function (method) {
        // 匹配路由的 get 方法
        app[method] = createRouteMethod(method);
    });

    // all 方法,通吃所有請求類型
    app.all = createRouteMethod("all");
// ***************************** 以上爲新增代碼 *****************************

    // 啓動服務的 listen 方法
    app.listen = function () {
        // 創建服務器
        const server = http.createServer(app);

        // 監聽服務,可能傳入多個參數,如第一個參數爲端口號,最後一個參數爲服務啓動後回調
        server.listen(...arguments);
    }

    // 返回 app
    return app;
}

module.exports = createApplication;

我們的邏輯大體可以分爲兩個部分,路由方法的創建以及路由的匹配,首先是路由方法的創建階段,每一個方法的內部所做的事情就是將路由的路徑、請求方式和回調函數作爲對象的屬性,並將對象存入一個數組中統一管理,所以我們創建了 app.routes 數組用來存儲這些路由對象。

方法名對應請求類型,請類型有很多,我們不會一一的創建每一個方法,所以選擇引入專門存儲請求類型名稱的 methods 模塊,其實路由方法邏輯相同,我們封裝了 createRouteMethod 方法用來生成不同路由方法的函數體,之所以這樣做是因爲有個特殊的路由方法 app.all,導致請求類型有差別,其他的可以從 methods 中取,app.all 我們定義類型爲 all 通過 createRouteMethod 函數的參數傳入。

接着就是循環 methods 調用 createRouteMethod 函數創建路由方法,並單獨創建 app.all 方法。

路由匹配階段實在函數 app 內完成的,因爲啓動服務接收到請求時會執行 createServer 中的回調,即執行 app,先通過原生自帶的 req.method 取出請求方式並處理成小寫,通過 req.path 取出完整路徑並分成路由名和查詢字符串兩個部分。

循環 app.routes 用取到請求的類型和路由名稱匹配,兩者都相等則執行對應路由對象上的回調函數,在判斷條件中,請求方式兼容了我們之前定義的 all,爲了所有的請求類型只要路由匹配都可以執行 app.all 的回調,請求路徑兼容了 *,因爲如果某個路由方法定義的路徑爲 *,則任意路由都可以執行這個路由對象上的回調。


擴展請求對象屬性

且在路由內部可以通過 req 訪問一些原生沒有的屬性如 req.pathreq.queryreq.hostreq.params,這說明 Express 在實現的過程中對 req 進行了處理。

// req 屬性的使用
// 引入 Express
const express = require("express");

// 創建服務
const app = express();

// 創建路由
app.get("/", function (req, res) {
    console.log(req.path);
    console.log(req.query);
    console.log(req.host);
    res.end("home");
});

app.get("/about/:id/:name", function (req, res) {
    console.log(req.params);
    res.end("about");
});

// 監聽服務
app.listen(3000);

在上面的使用中我們寫了兩個路由,分別打印了原生所不具備而 Express 幫我們處理並新增的屬性,下面我們就來在之前自己實現的 express.js 的基礎上增加這些屬性(直接找到星號提示新增或修改位置即可)。

// 文件:express.js
const http = require("http");

// methods 模塊返回存儲所有請求方法名稱的數組
const methods = require("methods");

// ***************************** 以下爲新增代碼 *****************************
const querystring = require("querystring");
// ***************************** 以上爲新增代碼 *****************************

function createApplication() {
    // 創建 app 函數,身份爲總管家,用於將請求分派給別人處理
    let app = function (req, res) {
        // 獲取方法名統一轉換成小寫
        let method = req.method.toLowerCase();

        // 訪問路徑解構成路由和查詢字符串兩部分 /user?a=1&b=2
        let [reqPath, query = ""] = req.url.split("?");

// *************************** 以下爲修改代碼 *****************************
        req.path = reqPath; // 將路徑名賦值給 req.path
        req.query = querystring.parse(query); // 將查詢字符串轉換成對象賦值給 req.query
        req.host = req.headers.host.split(":")[0]; // 將主機名賦值給 req.host

        // 循環匹配路徑
        for (let i = 0; i < app.routes.lenth; i++) {
            // 循環取得每一層
            let layer = app.routes[i];

            // 如果路由對象上存在正則說明存在路由參數,否則正常匹配路徑和請求類型
            if (layer.regexp) {
                let result = pathname.match(layer.regexp); // 使用路徑配置的正則匹配請求路徑

                // 如果匹配到結果且請求方式匹配
                if (result && (method === layer.method || layer.method === "all")) {
                    // 則將路由對象 paramNames 屬性中的鍵與匹配到的值構建成一個對象
                    req.params = layer.paramNames.reduce(function (memo, key, index) {
                        memo[key] = result[index + 1];
                        return memo;
                    }, {});

                    // 執行對應的回調
                    return layer.hanlder(req, res);
                }
            } else {
                // 如果說路徑和請求類型都能匹配,則執行該路由層的回調
                if (
                    (reqPath === layer.pathname || layer.pathname === "*") &&
                    (method === layer.method || layer.method === "all")
                ) {
                    return layer.hanlder(req, res);
                }
            }
// ***************************** 以上爲修改代碼 *****************************
        }

        // 如果都沒有匹配上,則響應錯誤信息
        res.end(`CANNOT ${req.method} ${reqPath}`);
    }

    // 存儲路由層的請求類型、路徑和回調
    app.routes = [];

    // 返回一個函數體用於將路由層存入 app.routes 中
    function createRouteMethod(method) {
        return function (pathname, handler) {
            let layer = {
                method,
                pathname, // 不包含查詢字符串
                handler
            };

// ***************************** 以下爲新增代碼 *****************************
            // 如果含有路由參數,如 /xxx/:aa/:bb,取出路由參數的鍵 aa bb 存入數組並掛在路由對象上
            // 並生匹配 /xxx/aa/bb 的正則掛在路由對象上
            if (pathname.indexOf(":") !== -1) {
                let paramNames = []; // 存儲路由參數

                // 將路由參數取出存入數組,並返回正則字符串
                let regStr = pathname.replace(/:(\w+)/g, function (matched, attr) {
                    paramNames.push(attr);
                    return "(\\w+)";
                });

                let regexp = new RegExp(regStr); // 生成正則類型
                layer.regexp = regexp; // 將正則掛在路由對象上
                layer.paramNames = paramNames; // 將存儲路由參數的數組掛載對象上
            }
// ***************************** 以上爲新增代碼 *****************************

            // 把這一層放入存儲所有路由層信息的數組中
            app.routes.push(layer);
        }
    }

    // 循環構建所有路由方法,如 app.get app.post 等
    methods.forEach(function (method) {
        // 匹配路由的 get 方法
        app[method] = createRouteMethod(method);
    });

    // all 方法,通吃所有請求類型
    app.all = createRouteMethod("all");

    // 啓動服務的 listen 方法
    app.listen = function () {
        // 創建服務器
        const server = http.createServer(app);

        // 監聽服務,可能傳入多個參數,如第一個參數爲端口號,最後一個參數爲服務啓動後回調
        server.listen(...arguments);
    }

    // 返回 app
    return app;
}

module.exports = createApplication;

上面代碼有些長,我們一點一點分析,首先是 req.path,就是我們瀏覽器地址欄裏查詢字符串前的路徑,值其實就是我們之前從 req.url 中解構出來的 pathname,我們只需要將 pathname 賦值給 req.path 即可。

req.query 是瀏覽器地址欄的查詢字符串傳遞的參數,就是我們從 req.url 解構出來的查詢字符串,藉助 querystring 模塊將查詢字符串處理成對象賦值給 req.query 即可。

req.host 是訪問的主機名,請求頭中的 host 包含了主機名和端口號,我們只要截取出前半部分賦值給 req.host 即可。

最複雜的是 req.params 的實現,大概分爲兩個步驟,首先是在路由方法創建時需要檢查定義的路由是否含有路由參數,如果有則取出參數的鍵存入數組 paramNames 中,然後創建一個匹配路由參數的正則,通過 replace 實現正則字符串的創建,再通過 RegExp 構造函數來創建正則,並掛在路由對象上,之所以使用 replace 是因爲創建的規則內的分組要和路由參數的個數是相同的,我們將這些邏輯完善進了 createRouteMethod 函數中。


實現響應方法 send 和 sendFile

之前的例子中我們都是用原生的 end 方法響應瀏覽器,我們知道 end 方法只能接收字符串和 Buffer 作爲響應的值,非常不方便,其實在 Express 中封裝了一個 send 方法掛在 res 對象下,可以接收數組、對象、字符串、Buffer、數字處理後響應給瀏覽器,在 Express 內部同樣封裝了一個 sendFile 方法用於讀取請求的文件。

// send 響應
// 引入 Express
const express = require("express");
const path = require("path");

// 創建服務
const app = express();

// 創建路由
app.get("/", function (req, res) {
    res.send({ name: "panda", age: 28 });
});

app.get("/test.txt", function (req, res) {
    // 必須傳入絕對路徑
    res.sendFile(path.join(__dirname, req.path));
});

// 監聽服務
app.listen(3000);

通過我們的分析,封裝的 send 方法應該是將 end 不支持的類型數據轉換成了字符串,在內部再次調用 end,而 sendFile 方法規定參數必須爲絕對路徑,內部實現應該是利用可讀流讀取文件內容相應給瀏覽器,下面是兩個方法的實現(直接找到星號提示新增或修改位置即可)。

// 文件:express.js
const http = require("http");

// methods 模塊返回存儲所有請求方法名稱的數組
const methods = require("methods");
const querystring = require("querystring");

// ***************************** 以下爲新增代碼 *****************************
const util = require("util");
const httpServer = require("_http_server"); // 存儲 node 服務相關信息
const fs = require("fs");
// ***************************** 以上爲新增代碼 *****************************

function createApplication() {
    // 創建 app 函數,身份爲總管家,用於將請求分派給別人處理
    let app = function (req, res) {
        // 獲取方法名統一轉換成小寫
        let method = req.method.toLowerCase();

        // 訪問路徑解構成路由和查詢字符串兩部分 /user?a=1&b=2
        let [reqPath, query = ""] = req.url.split("?");

        req.path = reqPath; // 將路徑名賦值給 req.path
        req.query = querystring.parse(query); // 將查詢字符串轉換成對象賦值給 req.query
        req.host = req.headers.host.split(":")[0]; // 將主機名賦值給 req.host

// ***************************** 以下爲新增代碼 *****************************
        // 響應方法
        res.send = function (params) {
            // 設置響應頭
            res.setHeader("Content-Type", "text/plain;charset=utf8");

            // 檢測傳入值得數據類型
            switch (typeof params) {
                case "object":
                    res.setHeader("Content-Type", "application/json;charset=utf8");
                    params = util.inspect(params); // 將任意類型的對象轉換成字符串
                    break;
                case "number":
                    params = httpServer.STATUS_CODES[params]; // 數字則直接取出狀態嗎對應的名字返回
                    break;
                default:
                    break;
            }

            // 響應
            res.end(params);
        }

        // 響應文件方法
        res.sendFile = function (pathname) {
            fs.createReadStream(pathname).pipe(res);
        }
// ***************************** 以上爲新增代碼 *****************************

        // 循環匹配路徑
        for (let i = 0; i < app.routes.lenth; i++) {
            // 循環取得每一層
            let layer = app.routes[i];

            // 如果路由對象上存在正則說明存在路由參數,否則正常匹配路徑和請求類型
            if (layer.regexp) {
                let result = reqPath.match(layer.regexp); // 使用路徑配置的正則匹配請求路徑

                // 如果匹配到結果且請求方式匹配
                if (result && (method === layer.method || layer.method === "all")) {
                    // 則將路由對象 paramNames 屬性中的鍵與匹配到的值構建成一個對象
                    req.params = layer.paramNames.reduce(function (memo, key, index) {
                        memo[key] = result[index + 1];
                        return memo;
                    }, {});

                    // 執行對應的回調
                    return layer.hanlder(req, res);
                }
            } else {
                // 如果說路徑和請求類型都能匹配,則執行該路由層的回調
                if (
                    (reqPath === layer.pathname || layer.pathname === "*") &&
                    (method === layer.method || layer.method === "all")
                ) {
                    return layer.hanlder(req, res);
                }
            }
        }

        // 如果都沒有匹配上,則響應錯誤信息
        res.end(`CANNOT ${req.method} ${reqPath}`);
    }

    // 存儲路由層的請求類型、路徑和回調
    app.routes = [];

    // 返回一個函數體用於將路由層存入 app.routes 中
    function createRouteMethod(method) {
        return function (pathname, handler) {
            let layer = {
                method,
                pathname, // 不包含查詢字符串
                handler
            };

            // 如果含有路由參數,如 /xxx/:aa/:bb,取出路由參數的鍵 aa bb 存入數組並掛在路由對象上
            // 並生匹配 /xxx/aa/bb 的正則掛在路由對象上
            if (pathname.indexOf(":") !== -1) {
                let paramNames = []; // 存儲路由參數

                // 將路由參數取出存入數組,並返回正則字符串
                let regStr = pathname.replace(/:(\w+)/g, function (matched, attr) {
                    paramNames.push(attr);
                    return "(\\w+)";
                });

                let regexp = new RegExp(regStr); // 生成正則類型
                layer.regexp = regexp; // 將正則掛在路由對象上
                layer.paramNames = paramNames; // 將存儲路由參數的數組掛載對象上
            }

            // 把這一層放入存儲所有路由層信息的數組中
            app.routes.push(layer);
        }
    }

    // 循環構建所有路由方法,如 app.get app.post 等
    methods.forEach(function (method) {
        // 匹配路由的 get 方法
        app[method] = createRouteMethod(method);
    });

    // all 方法,通吃所有請求類型
    app.all = createRouteMethod("all");

    // 啓動服務的 listen 方法
    app.listen = function () {
        // 創建服務器
        const server = http.createServer(app);

        // 監聽服務,可能傳入多個參數,如第一個參數爲端口號,最後一個參數爲服務啓動後回調
        server.listen(...arguments);
    }

    // 返回 app
    return app;
}

module.exports = createApplication;

有一點需要注意,在 Node 環境中想把任何對象類型轉換成字符串應該使用 util.inspect 方法,而當 send 方法輸入數字類型時,要返回對應狀態碼的名稱,可通過 _http_server 模塊的 STATUS_CODES 對象獲取。


內置中間件的實現

Express 最大的特點就是中間件機制,中間件就是用來處理請求的函數,用來完成不同場景的請求處理,一箇中間件處理完請求後可以再傳遞給下一個中間件,具有回調函數 next,不執行 next 則會卡在一個位置,調用 next 則繼續向下傳遞。

// use 的使用
// 引入 Express
const express = require("express");
const path = require("path");

// 創建服務
const app = express();

// 創建路由
app.use(function (req, res, next) {
    res.setHeader("Content-Type", "text/html;charset=utf8");
    next();
});

// 創建路由
app.get("/", function (req, res) {
    res.send({ name: "panda", age: 28 });
});

// 監聽服務
app.listen(3000);

在上面代碼中使用 use 方法執行了傳入的回調函數,實現公共邏輯,起到了中間件的作用,調用回調參數的 next 方法向下繼續執行,下面來實現 use 方法(直接找到星號提示新增或修改位置即可)。

// 文件:express.js
const http = require("http");

// methods 模塊返回存儲所有請求方法名稱的數組
const methods = require("methods");
const querystring = require("querystring");
const util = require("util");
const httpServer = require("_http_server"); // 存儲 node 服務相關信息
const fs = require("fs");

function createApplication() {
    // 創建 app 函數,身份爲總管家,用於將請求分派給別人處理
    let app = function (req, res) {
// ***************************** 以下爲修改代碼 *****************************
        // 循環匹配路徑
        let index = 0;

        function next(err) {
            // 獲取第一個回調函數
            let layer = app.routes[index++];

            if (layer) {
                // 將當前中間件函數的屬性解構出來
                let { method, pathname, handler } = layer;

                if (err) { // 如果存在錯誤將錯誤交給錯誤處理中間件,否則
                    if (method === "middle", handle.length === 4) {
                        return hanlder(err, req, res, next);
                    } else {
                        next(err);
                    }
                } else { // 如果不存在錯誤則繼續向下執行
                    // 判斷是中間件還是路由
                    if (method === "middle") {
                        // 匹配路徑判斷
                        if (
                            pathname === "/" ||
                            pathname === req.path ||
                            req.path.startWidth(pathname)
                        ) {
                            handler(req, res, next);
                        } else {
                            next();
                        }
                    } else {
                        // 如果路由對象上存在正則說明存在路由參數,否則正常匹配路徑和請求類型
                        if (layer.regexp) {
                            let result = req.path.match(layer.regexp); // 使用路徑配置的正則匹配請求路徑

                            // 如果匹配到結果且請求方式匹配
                            if (result && (method === layer.method || layer.method === "all")) {
                                // 則將路由對象 paramNames 屬性中的鍵與匹配到的值構建成一個對象
                                req.params = layer.paramNames.reduce(function (memo, key, index) {
                                    memo[key] = result[index + 1];
                                    return memo;
                                }, {});

                                // 執行對應的回調
                                return layer.hanlder(req, res);
                            } else {
                                next();
                            }
                        } else {
                            // 如果說路徑和請求類型都能匹配,則執行該路由層的回調
                            if (
                                (req.path === layer.pathname || layer.pathname === "*") &&
                                (method === layer.method || layer.method === "all")
                            ) {
                                return layer.hanlder(req, res);
                            } else {
                                next();
                            }
                        }
                    }
                }
            } else {
                // 如果都沒有匹配上,則響應錯誤信息
                res.end(`CANNOT ${req.method} ${req.path}`);
            }
        }

        next();
// ***************************** 以上爲修改代碼 *****************************
    }

// ***************************** 以下爲新增代碼 *****************************
    function init() {
        return function (req, res, next) {
            // 獲取方法名統一轉換成小寫
            let method = req.method.toLowerCase();

            // 訪問路徑解構成路由和查詢字符串兩部分 /user?a=1&b=2
            let [reqPath, query = ""] = req.url.split("?");

            req.path = reqPath; // 將路徑名賦值給 req.path
            req.query = querystring.parse(query); // 將查詢字符串轉換成對象賦值給 req.query
            req.host = req.headers.host.split(":")[0]; // 將主機名賦值給 req.host

            // 響應方法
            res.send = function (params) {
                // 設置響應頭
                res.setHeader("Content-Type", "text/plain;charset=utf8");

                // 檢測傳入值得數據類型
                switch (typeof params) {
                    case "object":
                        res.setHeader("Content-Type", "application/json;charset=utf8");
                        params = util.inspect(params); // 將任意類型的對象轉換成字符串
                        break;
                    case "number":
                        params = httpServer.STATUS_CODES[params]; // 數字則直接取出狀態嗎對應的名字返回
                        break;
                    default:
                        break;
                }

                // 響應
                res.end(params);
            }

            // 響應文件方法
            res.sendFile = function (pathname) {
                fs.createReadStream(pathname).pipe(res);
            }

            // 向下執行
            next();
        }
    }
// ***************************** 以上爲新增代碼 *****************************

    // 存儲路由層的請求類型、路徑和回調
    app.routes = [];

    // 返回一個函數體用於將路由層存入 app.routes 中
    function createRouteMethod(method) {
        return function (pathname, handler) {
            let layer = {
                method,
                pathname, // 不包含查詢字符串
                handler
            };

            // 如果含有路由參數,如 /xxx/:aa/:bb,取出路由參數的鍵 aa bb 存入數組並掛在路由對象上
            // 並生匹配 /xxx/aa/bb 的正則掛在路由對象上
// ***************************** 以下爲修改代碼 *****************************
            if (pathname.indexOf(":") !== -1 && pathname.method !== "middle") {
// ***************************** 以上爲修改代碼 *****************************
                let paramNames = []; // 存儲路由參數

                // 將路由參數取出存入數組,並返回正則字符串
                let regStr = pathname.replace(/:(\w+)/g, function (matched, attr) {
                    paramNames.push(attr);
                    return "(\\w+)";
                });

                let regexp = new RegExp(regStr); // 生成正則類型
                layer.regexp = regexp; // 將正則掛在路由對象上
                layer.paramNames = paramNames; // 將存儲路由參數的數組掛載對象上
            }

            // 把這一層放入存儲所有路由層信息的數組中
            app.routes.push(layer);
        }
    }

    // 循環構建所有路由方法,如 app.get app.post 等
    methods.forEach(function (method) {
        // 匹配路由的 get 方法
        app[method] = createRouteMethod(method);
    });

    // all 方法,通吃所有請求類型
    app.all = createRouteMethod("all");

// ***************************** 以下爲新增代碼 *****************************
    // 添加中間件方法
    app.use = function (pathname, handler) {
        // 處理沒有傳入路徑的情況
        if (typeof handler !== "function") {
            handler = pathname;
            pathname = "/";
        }

        // 生成函數並執行
        createRouteMethod("middle")(pathname, handler);
    }

    // 將初始邏輯作爲中間件執行
    app.use(init());
// ***************************** 以上爲新增代碼 *****************************

    // 啓動服務的 listen 方法
    app.listen = function () {
        // 創建服務器
        const server = http.createServer(app);

        // 監聽服務,可能傳入多個參數,如第一個參數爲端口號,最後一個參數爲服務啓動後回調
        server.listen(...arguments);
    }

    // 返回 app
    return app;
}

module.exports = createApplication;

use 方法第一個參數爲路徑,與路由相同,不傳默認爲 /,如果不傳所有的路徑都會經過該中間件,如果傳入指定的值,則匹配後的請求才會通過該中間件。

中間件的執行可能存在異步的情況,但之前匹配路徑使用的是 for 循環同步匹配,我們將其修改爲異步並把路由匹配的邏輯與中間件路徑匹配的邏輯進行了整合,並創建了 use 方法,對是否傳了第一個參數做了一個兼容,其他將帶有請求方式、路徑和回調的邏輯統一使用 createRouteMethod 方法創建,並傳入 middle 類型,createRouteMethod 中路由參數匹配的邏輯對 middle 類型做了一個排除。

使用 Express 中間件調用 next 方法時,不傳遞參數和參數爲 null 代表執行成功,如果傳入了其他的參數,表示執行出錯,會跳過所有正常的中間件和路由,直接交給錯誤處理中間件處理,並將 next 傳入的參數作爲錯誤處理中間件回調函數的第一個參數 err,後面三個參數分別爲 reqresnext

代碼種創建了 index 變量,默認調用了一次 next 方法,每次然後取出數組 app.routes 中的路由對象的回調函數執行,並在內部執行 handler,而 handler 回調中又調用了 next 方法,就這樣將整個中間件和路由的回調串聯起來。

我們發現在第一次調用 next 之前的所有邏輯,如給 req 添加屬性,給 res 添加方法,都是公共邏輯,是任何中間件和路由在匹配之前都會執行的邏輯,我們既然有了中間件方法 app.user,可以將這些邏輯抽取出來作爲一個單獨的中間件回調函數執行,所以創建了 init 函數,內部返回了一個函數作爲回調函數,形參爲 reqresnext,並在init 調用返回的函數內部調用 next 向下執行。


內置模板引擎的實現

Express 框架中內置支持了 ejsjade 等模板,使用方法 “三部曲” 如下。

// 模板的使用
// 引入 Express
const express = require("express");
const path = require("path");

// 創建服務
const app = express();

// 1、指定模板引擎,其實就是模板文件的後綴名
app.set("view engine", "ejs");

// 2、指定模板的存放根目錄
app.set("views", path.resolve(__dirname, "views"));

// 3、如果要自定義模板後綴和函數的關係
app.engine(".html", require("./ejs").__express);

// 創建路由
app.get("/user", function (req, res) {
    //使用指定的模板引擎渲染 user 模板
    res.render("user", { title: "用戶管理" });
});

// 監聽服務
app.listen(3000);

上面將模板根目錄設置爲 views 文件夾,並規定了模板類型爲 ejs,可以同時給多種模板設置,並不衝突,如果需要將其他後綴名的模板按照另一種模板的渲染引擎渲染則使用 app.engine 進行設置,下面看一下實現代碼(直接找到星號提示新增或修改位置即可)。

// 文件:express.js
const http = require("http");

// methods 模塊返回存儲所有請求方法名稱的數組
const methods = require("methods");
const querystring = require("querystring");
const util = require("util");
const httpServer = require("_http_server"); // 存儲 node 服務相關信息
const fs = require("fs");

// ***************************** 以下爲新增代碼 *****************************
const path = require("path");
// ***************************** 以上爲新增代碼 *****************************

function createApplication() {
    // 創建 app 函數,身份爲總管家,用於將請求分派給別人處理
    let app = function (req, res) {
        // 循環匹配路徑
        let index = 0;

        function next(err) {
            // 獲取第一個回調函數
            let layer = app.routes[index++];

            if (layer) {
                // 將當前中間件函數的屬性解構出來
                let { method, pathname, handler } = layer;

                if (err) { // 如果存在錯誤將錯誤交給錯誤處理中間件,否則
                    if (method === "middle", handle.length === 4) {
                        return hanlder(err, req, res, next);
                    } else {
                        next(err);
                    }
                } else { // 如果不存在錯誤則繼續向下執行
                    // 判斷是中間件還是路由
                    if (method === "middle") {
                        // 匹配路徑判斷
                        if (
                            pathname === "/" ||
                            pathname === req.path ||
                            req.path.startWidth(pathname)
                        ) {
                            handler(req, res, next);
                        } else {
                            next();
                        }
                    } else {
                        // 如果路由對象上存在正則說明存在路由參數,否則正常匹配路徑和請求類型
                        if (layer.regexp) {
                            let result = req.path.match(layer.regexp); // 使用路徑配置的正則匹配請求路徑

                            // 如果匹配到結果且請求方式匹配
                            if (result && (method === layer.method || layer.method === "all")) {
                                // 則將路由對象 paramNames 屬性中的鍵與匹配到的值構建成一個對象
                                req.params = layer.paramNames.reduce(function (memo, key, index) {
                                    memo[key] = result[index + 1];
                                    return memo;
                                }, {});

                                // 執行對應的回調
                                return layer.hanlder(req, res);
                            } else {
                                next();
                            }
                        } else {
                            // 如果說路徑和請求類型都能匹配,則執行該路由層的回調
                            if (
                                (req.path === layer.pathname || layer.pathname === "*") &&
                                (method === layer.method || layer.method === "all")
                            ) {
                                return layer.hanlder(req, res);
                            } else {
                                next();
                            }
                        }
                    }
                }
            } else {
                // 如果都沒有匹配上,則響應錯誤信息
                res.end(`CANNOT ${req.method} ${req.path}`);
            }
        }

        next();
    }

    function init() {
        return function (req, res, next) {
            // 獲取方法名統一轉換成小寫
            let method = req.method.toLowerCase();

            // 訪問路徑解構成路由和查詢字符串兩部分 /user?a=1&b=2
            let [reqPath, query = ""] = req.url.split("?");

            req.path = reqPath; // 將路徑名賦值給 req.path
            req.query = querystring.parse(query); // 將查詢字符串轉換成對象賦值給 req.query
            req.host = req.headers.host.split(":")[0]; // 將主機名賦值給 req.host

            // 響應方法
            res.send = function (params) {
                // 設置響應頭
                res.setHeader("Content-Type", "text/plain;charset=utf8");

                // 檢測傳入值得數據類型
                switch (typeof params) {
                    case "object":
                        res.setHeader("Content-Type", "application/json;charset=utf8");
                        params = util.inspect(params); // 將任意類型的對象轉換成字符串
                        break;
                    case "number":
                        params = httpServer.STATUS_CODES[params]; // 數字則直接取出狀態嗎對應的名字返回
                        break;
                    default:
                        break;
                }

                // 響應
                res.end(params);
            }

            // 響應文件方法
            res.sendFile = function (pathname) {
                fs.createReadStream(pathname).pipe(res);
            }

// ***************************** 以下爲新增代碼 *****************************
            // 模板渲染方法
            res.render = function (filename, data) {
                // 將文件名和模板路徑拼接
                let filepath = path.join(app.get("views"), filename);

                // 獲取擴展名
                let extname = path.extname(filename.split(path.sep).pop());

                // 如果沒有擴展名,則使用默認的擴展名
                if (!extname) {
                    extname = `.${app.get("view engine")}`
                    filepath += extname;
                }

                // 讀取模板文件並使用渲染引擎相應給瀏覽器
                app.engines[extname](filepath, data, function (err, html) {
                    res.setHeader("Content-Type", "text/html;charset=utf8");
                    res.end(html);
                });
            }
// ***************************** 以上爲新增代碼 *****************************

            // 向下執行
            next();
        }
    }

    // 存儲路由層的請求類型、路徑和回調
    app.routes = [];

    // 返回一個函數體用於將路由層存入 app.routes 中
    function createRouteMethod(method) {
        return function (pathname, handler) {
// ***************************** 以下爲修改代碼 *****************************
            // 滿足條件說明是取值方法
            if (method === "get" && arguments.length === 1) {
                return app.settings[pathname];
            }
// ***************************** 以上爲修改代碼 *****************************

            let layer = {
                method,
                pathname, // 不包含查詢字符串
                handler
            };

            // 如果含有路由參數,如 /xxx/:aa/:bb,取出路由參數的鍵 aa bb 存入數組並掛在路由對象上
            // 並生匹配 /xxx/aa/bb 的正則掛在路由對象上
            if (pathname.indexOf(":") !== -1 && pathname.method !== "middle") {
                let paramNames = []; // 存儲路由參數

                // 將路由參數取出存入數組,並返回正則字符串
                let regStr = pathname.replace(/:(\w+)/g, function (matched, attr) {
                    paramNames.push(attr);
                    return "(\\w+)";
                });

                let regexp = new RegExp(regStr); // 生成正則類型
                layer.regexp = regexp; // 將正則掛在路由對象上
                layer.paramNames = paramNames; // 將存儲路由參數的數組掛載對象上
            }

            // 把這一層放入存儲所有路由層信息的數組中
            app.routes.push(layer);
        }
    }

    // 循環構建所有路由方法,如 app.get app.post 等
    methods.forEach(function (method) {
        // 匹配路由的 get 方法
        app[method] = createRouteMethod(method);
    });

    // all 方法,通吃所有請求類型
    app.all = createRouteMethod("all");

    // 添加中間件方法
    app.use = function (pathname, handler) {
        // 處理沒有傳入路徑的情況
        if (typeof handler !== "function") {
            handler = pathname;
            pathname = "/";
        }

        // 生成函數並執行
        createRouteMethod("middle")(pathname, handler);
    }

    // 將初始邏輯作爲中間件執行
    app.use(init());

// ***************************** 以下爲新增代碼 *****************************
    // 存儲設置的對象
    app.setting ={};

    // 存儲模板渲染方法
    app.engines = {};

    // 添加設置的方法
    app.set = function (key, value) {
        app.use[key] = value;
    }

    // 添加渲染引擎的方法
    app.engine = function (ext, renderFile) {
        app.engines[ext] = renderFile;
    }
// ***************************** 以上爲新增代碼 *****************************

    // 啓動服務的 listen 方法
    app.listen = function () {
        // 創建服務器
        const server = http.createServer(app);

        // 監聽服務,可能傳入多個參數,如第一個參數爲端口號,最後一個參數爲服務啓動後回調
        server.listen(...arguments);
    }

    // 返回 app
    return app;
}

module.exports = createApplication;

在上面新增代碼中設置了兩個緩存 settingsengines,前者用來存儲模板相關的設置,如渲染成什麼類型的文件、讀取模板文件的根目錄,後者用來存儲渲染引擎,即渲染模板的方法,這所以設置這兩個緩存對象是爲了實現 Express 多種不同模板共存的功能,可以根據需要進行設置和使用,而設置的方法分別爲 app.setapp.engine,有設置值的方法就應該有取值的方法,但是 app.get 方法已經被設置爲路由方法了,爲了語義我們在 app.get 方法邏輯中進行了兼容,當參數爲 1 個時,從 settings 中取值並返回,否則執行添加路由方法的邏輯。

之前都是準備工作,在使用時無論是中間件還是路由中都是靠調用 res.render 方法並傳入模板路徑和渲染數據來真正實現渲染和響應的,render 方法是在 init 函數初始化時就掛在了 res 上,核心邏輯是取出傳入的模板文件後綴名,如果存在則使用後綴名,將文件名與默認讀取模板的文件夾路徑拼接傳遞給設置的渲染引擎的渲染方法,如果不存在後綴名則默認拼接 .html 當作後綴名,再與默認讀取模板路徑進行拼接,在渲染函數的回調中將渲染引擎渲染的模板字符串響應給瀏覽器。


內置靜態資源中間件的實現

Express 內部可以通過路由處理靜態文件,但是如果可能請求多個文件不可能一個文件對應一個路由,因此 Express 內部實現了靜態文件中間件,使用如下。

// 靜態文件中間件的使用
// 引入 Express
const express = require("express");
const path = require("path");

// 創建服務
const app = express();

// 使用處理靜態文件中間件
app.use(express.static(path.resolve(__dirname, "public")));

// 監聽服務
app.listen(3000);

從上面使用可以看出,express.static 是一個函數,執行的時候傳入了一個參數,爲默認查找文件的根路徑,而添加中間件的 app.use 方法傳入的參數正好是回調函數,這說明 express.static 方法需要返回一個函數,形參爲 reqresnext,通過調用方式我們能看出 static 是靜態方法,掛在了模塊返回的函數上,實現代碼如下(直接找到星號提示新增或修改位置即可)。

// 文件:express.js
const http = require("http");

// methods 模塊返回存儲所有請求方法名稱的數組
const methods = require("methods");
const querystring = require("querystring");
const util = require("util");
const httpServer = require("_http_server"); // 存儲 node 服務相關信息
const fs = require("fs");
const path = require("path");

// ***************************** 以下爲新增代碼 *****************************
const mime = require("mime");
// ***************************** 以上爲新增代碼 *****************************

function createApplication() {
    // 創建 app 函數,身份爲總管家,用於將請求分派給別人處理
    let app = function (req, res) {
        // 循環匹配路徑
        let index = 0;

        function next(err) {
            // 獲取第一個回調函數
            let layer = app.routes[index++];

            if (layer) {
                // 將當前中間件函數的屬性解構出來
                let { method, pathname, handler } = layer;

                if (err) { // 如果存在錯誤將錯誤交給錯誤處理中間件,否則
                    if (method === "middle", handle.length === 4) {
                        return hanlder(err, req, res, next);
                    } else {
                        next(err);
                    }
                } else { // 如果不存在錯誤則繼續向下執行
                    // 判斷是中間件還是路由
                    if (method === "middle") {
                        // 匹配路徑判斷
                        if (
                            pathname === "/" ||
                            pathname === req.path ||
                            req.path.startWidth(pathname)
                        ) {
                            handler(req, res, next);
                        } else {
                            next();
                        }
                    } else {
                        // 如果路由對象上存在正則說明存在路由參數,否則正常匹配路徑和請求類型
                        if (layer.regexp) {
                            let result = req.path.match(layer.regexp); // 使用路徑配置的正則匹配請求路徑

                            // 如果匹配到結果且請求方式匹配
                            if (result && (method === layer.method || layer.method === "all")) {
                                // 則將路由對象 paramNames 屬性中的鍵與匹配到的值構建成一個對象
                                req.params = layer.paramNames.reduce(function (memo, key, index) {
                                    memo[key] = result[index + 1];
                                    return memo;
                                }, {});

                                // 執行對應的回調
                                return layer.hanlder(req, res);
                            } else {
                                next();
                            }
                        } else {
                            // 如果說路徑和請求類型都能匹配,則執行該路由層的回調
                            if (
                                (req.path === layer.pathname || layer.pathname === "*") &&
                                (method === layer.method || layer.method === "all")
                            ) {
                                return layer.hanlder(req, res);
                            } else {
                                next();
                            }
                        }
                    }
                }
            } else {
                // 如果都沒有匹配上,則響應錯誤信息
                res.end(`CANNOT ${req.method} ${req.path}`);
            }
        }

        next();
    }

    function init() {
        return function (req, res, next) {
            // 獲取方法名統一轉換成小寫
            let method = req.method.toLowerCase();

            // 訪問路徑解構成路由和查詢字符串兩部分 /user?a=1&b=2
            let [reqPath, query = ""] = req.url.split("?");

            req.path = reqPath; // 將路徑名賦值給 req.path
            req.query = querystring.parse(query); // 將查詢字符串轉換成對象賦值給 req.query
            req.host = req.headers.host.split(":")[0]; // 將主機名賦值給 req.host

            // 響應方法
            res.send = function (params) {
                // 設置響應頭
                res.setHeader("Content-Type", "text/plain;charset=utf8");

                // 檢測傳入值得數據類型
                switch (typeof params) {
                    case "object":
                        res.setHeader("Content-Type", "application/json;charset=utf8");
                        params = util.inspect(params); // 將任意類型的對象轉換成字符串
                        break;
                    case "number":
                        params = httpServer.STATUS_CODES[params]; // 數字則直接取出狀態嗎對應的名字返回
                        break;
                    default:
                        break;
                }

                // 響應
                res.end(params);
            }

            // 響應文件方法
            res.sendFile = function (pathname) {
                fs.createReadStream(pathname).pipe(res);
            }

            // 模板渲染方法
            res.render = function (filename, data) {
                // 將文件名和模板路徑拼接
                let filepath = path.join(app.get("views"), filename);

                // 獲取擴展名
                let extname = path.extname(filename.split(path.sep).pop());

                // 如果沒有擴展名,則使用默認的擴展名
                if (!extname) {
                    extname = `.${app.get("view engine")}`
                    filepath += extname;
                }

                // 讀取模板文件並使用渲染引擎相應給瀏覽器
                app.engines[extname](filepath, data, function (err, html) {
                    res.setHeader("Content-Type", "text/html;charset=utf8");
                    res.end(html);
                });
            }

            // 向下執行
            next();
        }
    }

    // 存儲路由層的請求類型、路徑和回調
    app.routes = [];

    // 返回一個函數體用於將路由層存入 app.routes 中
    function createRouteMethod(method) {
        return function (pathname, handler) {
            // 滿足條件說明是取值方法
            if (method === "get" && arguments.length === 1) {
                return app.settings[pathname];
            }

            let layer = {
                method,
                pathname, // 不包含查詢字符串
                handler
            };

            // 如果含有路由參數,如 /xxx/:aa/:bb,取出路由參數的鍵 aa bb 存入數組並掛在路由對象上
            // 並生匹配 /xxx/aa/bb 的正則掛在路由對象上
            if (pathname.indexOf(":") !== -1 && pathname.method !== "middle") {
                let paramNames = []; // 存儲路由參數

                // 將路由參數取出存入數組,並返回正則字符串
                let regStr = pathname.replace(/:(\w+)/g, function (matched, attr) {
                    paramNames.push(attr);
                    return "(\\w+)";
                });

                let regexp = new RegExp(regStr); // 生成正則類型
                layer.regexp = regexp; // 將正則掛在路由對象上
                layer.paramNames = paramNames; // 將存儲路由參數的數組掛載對象上
            }

            // 把這一層放入存儲所有路由層信息的數組中
            app.routes.push(layer);
        }
    }

    // 循環構建所有路由方法,如 app.get app.post 等
    methods.forEach(function (method) {
        // 匹配路由的 get 方法
        app[method] = createRouteMethod(method);
    });

    // all 方法,通吃所有請求類型
    app.all = createRouteMethod("all");

    // 添加中間件方法
    app.use = function (pathname, handler) {
        // 處理沒有傳入路徑的情況
        if (typeof handler !== "function") {
            handler = pathname;
            pathname = "/";
        }

        // 生成函數並執行
        createRouteMethod("middle")(pathname, handler);
    }

    // 將初始邏輯作爲中間件執行
    app.use(init());

    // 存儲設置的對象
    app.setting ={};

    // 存儲模板渲染方法
    app.engines = {};

    // 添加設置的方法
    app.set = function (key, value) {
        app.use[key] = value;
    }

    // 添加渲染引擎的方法
    app.engine = function (ext, renderFile) {
        app.engines[ext] = renderFile;
    }

    // 啓動服務的 listen 方法
    app.listen = function () {
        // 創建服務器
        const server = http.createServer(app);

        // 監聽服務,可能傳入多個參數,如第一個參數爲端口號,最後一個參數爲服務啓動後回調
        server.listen(...arguments);
    }

    // 返回 app
    return app;
}

// ***************************** 以下爲新增代碼 *****************************
createApplication.static = function (staticRoot) {
    return function (req, res, next) {
        // 獲取文件的完整路徑
        let filename = path.join(staticRoot, req.path);

        // 如果沒有權限就向下執行其他中間件,如果有權限讀取文件並響應
        fs.access(filename, function (err) {
            if (err) {
                next();
            } else {
                // 設置響應頭類型和響應文件內容
                res.setHeader("Content-Type", `${mime.getType()};charset=utf8`);
                fs.createReadStream(filename).pipe(res);
            }
        });
    }
}
// ***************************** 以上爲新增代碼 *****************************

module.exports = createApplication;

這個方法的核心邏輯是獲取文件的路徑,檢查文件的權限,如果沒有權限,則調用 next 交給其他中間件,這裏注意的是 err 錯誤對象不要傳遞給 next,因爲後面的中間件還要執行,如果傳遞後會直接執行錯誤處理中間件,有權限的情況下就正常讀取文件內容,給 Content-Type 響應頭設置文件類型,並將文件的可讀流通過 pipe 方法傳遞給可寫流 res,即響應給瀏覽器。


實現重定向

Express 中有一個功能在我們匹配到的某一個路由中調用可以直接跳轉到另一個路由,即 302 重定向。

// 使用重定向
// 引入 Express
const express = require("express");
const path = require("path");

// 創建服務
const app = express();

// 創建路由
app.get("/user", function (req, res, next) {
    res.end("user");
});

app.get("/detail", function (req, res, next) {
    // 訪問 /detail 重定向到 /user
    res.redirect("/user");
});

// 監聽服務
app.listen(3000);

看到上面的使用方式,我們根據前面的套路知道是 Expressres 對象上給掛載了一個 redirect 方法,參數爲狀態碼(可選)和要跳轉路由的路徑,並且這個方法應該在 init 函數調用時掛在 res 上的,下面是實現的代碼(直接找到星號提示新增或修改位置即可)。

// 文件:express.js
const http = require("http");

// methods 模塊返回存儲所有請求方法名稱的數組
const methods = require("methods");
const querystring = require("querystring");
const util = require("util");
const httpServer = require("_http_server"); // 存儲 node 服務相關信息
const fs = require("fs");
const path = require("path");
const mime = require("mime");

function createApplication() {
    // 創建 app 函數,身份爲總管家,用於將請求分派給別人處理
    let app = function (req, res) {
        // 循環匹配路徑
        let index = 0;

        function next(err) {
            // 獲取第一個回調函數
            let layer = app.routes[index++];

            if (layer) {
                // 將當前中間件函數的屬性解構出來
                let { method, pathname, handler } = layer;

                if (err) { // 如果存在錯誤將錯誤交給錯誤處理中間件,否則
                    if (method === "middle", handle.length === 4) {
                        return hanlder(err, req, res, next);
                    } else {
                        next(err);
                    }
                } else { // 如果不存在錯誤則繼續向下執行
                    // 判斷是中間件還是路由
                    if (method === "middle") {
                        // 匹配路徑判斷
                        if (
                            pathname === "/" ||
                            pathname === req.path ||
                            req.path.startWidth(pathname)
                        ) {
                            handler(req, res, next);
                        } else {
                            next();
                        }
                    } else {
                        // 如果路由對象上存在正則說明存在路由參數,否則正常匹配路徑和請求類型
                        if (layer.regexp) {
                            let result = req.path.match(layer.regexp); // 使用路徑配置的正則匹配請求路徑

                            // 如果匹配到結果且請求方式匹配
                            if (result && (method === layer.method || layer.method === "all")) {
                                // 則將路由對象 paramNames 屬性中的鍵與匹配到的值構建成一個對象
                                req.params = layer.paramNames.reduce(function (memo, key, index) {
                                    memo[key] = result[index + 1];
                                    return memo;
                                }, {});

                                // 執行對應的回調
                                return layer.hanlder(req, res);
                            } else {
                                next();
                            }
                        } else {
                            // 如果說路徑和請求類型都能匹配,則執行該路由層的回調
                            if (
                                (req.path === layer.pathname || layer.pathname === "*") &&
                                (method === layer.method || layer.method === "all")
                            ) {
                                return layer.hanlder(req, res);
                            } else {
                                next();
                            }
                        }
                    }
                }
            } else {
                // 如果都沒有匹配上,則響應錯誤信息
                res.end(`CANNOT ${req.method} ${req.path}`);
            }
        }

        next();
    }

    function init() {
        return function (req, res, next) {
            // 獲取方法名統一轉換成小寫
            let method = req.method.toLowerCase();

            // 訪問路徑解構成路由和查詢字符串兩部分 /user?a=1&b=2
            let [reqPath, query = ""] = req.url.split("?");

            req.path = reqPath; // 將路徑名賦值給 req.path
            req.query = querystring.parse(query); // 將查詢字符串轉換成對象賦值給 req.query
            req.host = req.headers.host.split(":")[0]; // 將主機名賦值給 req.host

            // 響應方法
            res.send = function (params) {
                // 設置響應頭
                res.setHeader("Content-Type", "text/plain;charset=utf8");

                // 檢測傳入值得數據類型
                switch (typeof params) {
                    case "object":
                        res.setHeader("Content-Type", "application/json;charset=utf8");
                        params = util.inspect(params); // 將任意類型的對象轉換成字符串
                        break;
                    case "number":
                        params = httpServer.STATUS_CODES[params]; // 數字則直接取出狀態嗎對應的名字返回
                        break;
                    default:
                        break;
                }

                // 響應
                res.end(params);
            }

            // 響應文件方法
            res.sendFile = function (pathname) {
                fs.createReadStream(pathname).pipe(res);
            }

            // 模板渲染方法
            res.render = function (filename, data) {
                // 將文件名和模板路徑拼接
                let filepath = path.join(app.get("views"), filename);

                // 獲取擴展名
                let extname = path.extname(filename.split(path.sep).pop());

                // 如果沒有擴展名,則使用默認的擴展名
                if (!extname) {
                    extname = `.${app.get("view engine")}`
                    filepath += extname;
                }

                // 讀取模板文件並使用渲染引擎相應給瀏覽器
                app.engines[extname](filepath, data, function (err, html) {
                    res.setHeader("Content-Type", "text/html;charset=utf8");
                    res.end(html);
                });
            }

// ***************************** 以下爲新增代碼 *****************************
            // 重定向方法
            res.redirect = function (status, target) {
                // 如果第一個參數是字符串類型說明沒有傳狀態碼
                if (typeof status === "string") {
                    // 將第二個參數(重定向的目標路徑)設置給 target
                    target = status;

                    // 再把狀態碼設置成 302
                    status = 302;
                }

                // 響應狀態碼,設置重定向響應頭
                res.statusCode = status;
                res.setHeader("Location", target);
                res.end();
            }
// ***************************** 以上爲新增代碼 *****************************

            // 向下執行
            next();
        }
    }

    // 存儲路由層的請求類型、路徑和回調
    app.routes = [];

    // 返回一個函數體用於將路由層存入 app.routes 中
    function createRouteMethod(method) {
        return function (pathname, handler) {
            // 滿足條件說明是取值方法
            if (method === "get" && arguments.length === 1) {
                return app.settings[pathname];
            }

            let layer = {
                method,
                pathname, // 不包含查詢字符串
                handler
            };

            // 如果含有路由參數,如 /xxx/:aa/:bb,取出路由參數的鍵 aa bb 存入數組並掛在路由對象上
            // 並生匹配 /xxx/aa/bb 的正則掛在路由對象上
            if (pathname.indexOf(":") !== -1 && pathname.method !== "middle") {
                let paramNames = []; // 存儲路由參數

                // 將路由參數取出存入數組,並返回正則字符串
                let regStr = pathname.replace(/:(\w+)/g, function (matched, attr) {
                    paramNames.push(attr);
                    return "(\\w+)";
                });

                let regexp = new RegExp(regStr); // 生成正則類型
                layer.regexp = regexp; // 將正則掛在路由對象上
                layer.paramNames = paramNames; // 將存儲路由參數的數組掛載對象上
            }

            // 把這一層放入存儲所有路由層信息的數組中
            app.routes.push(layer);
        }
    }

    // 循環構建所有路由方法,如 app.get app.post 等
    methods.forEach(function (method) {
        // 匹配路由的 get 方法
        app[method] = createRouteMethod(method);
    });

    // all 方法,通吃所有請求類型
    app.all = createRouteMethod("all");

    // 添加中間件方法
    app.use = function (pathname, handler) {
        // 處理沒有傳入路徑的情況
        if (typeof handler !== "function") {
            handler = pathname;
            pathname = "/";
        }

        // 生成函數並執行
        createRouteMethod("middle")(pathname, handler);
    }

    // 將初始邏輯作爲中間件執行
    app.use(init());

    // 存儲設置的對象
    app.setting ={};

    // 存儲模板渲染方法
    app.engines = {};

    // 添加設置的方法
    app.set = function (key, value) {
        app.use[key] = value;
    }

    // 添加渲染引擎的方法
    app.engine = function (ext, renderFile) {
        app.engines[ext] = renderFile;
    }

    // 啓動服務的 listen 方法
    app.listen = function () {
        // 創建服務器
        const server = http.createServer(app);

        // 監聽服務,可能傳入多個參數,如第一個參數爲端口號,最後一個參數爲服務啓動後回調
        server.listen(...arguments);
    }

    // 返回 app
    return app;
}

createApplication.static = function (staticRoot) {
    return function (req, res, next) {
        // 獲取文件的完整路徑
        let filename = path.join(staticRoot, req.path);

        // 如果沒有權限就向下執行其他中間件,如果有權限讀取文件並響應
        fs.access(filename, function (err) {
            if (err) {
                next();
            } else {
                // 設置響應頭類型和響應文件內容
                res.setHeader("Content-Type", `${mime.getType()};charset=utf8`);
                fs.createReadStream(filename).pipe(res);
            }
        });
    }
}

module.exports = createApplication;

其實 res.redirect 方法的核心邏輯就是處理參數,如果沒有傳狀態碼的時候將參數設置給 target,將狀態碼設置爲 302,並設置重定向響應頭 Location


總結

到此爲止 Express 的大部分內置功能就都簡易的實現了,由於 Express 內部的封裝思想,以及代碼複雜、緊密的特點,各個功能代碼很難單獨拆分,總結一下就是很難表述清楚,只能通過大量代碼來堆砌,好在每一部分實現我都標記了 “重點”,但看的時候還是要經歷 “痛苦”,這已經將 Express 中的邏輯 “閹割” 到了一定的程度,讀 Express 的源碼一定比讀這篇文章更需要耐心,當然如果你已經讀到了這裏證明困難都被克服了,繼續加油。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章