Express 源碼分析及簡易封裝

在這裏插入圖片描述


閱讀原文


前言

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


搭建基本服務

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

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

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

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

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

// 文件: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 服務。json

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 函數的參數傳入。app

接着就是循環 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();

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

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

// 三、若是要自定義模板後綴和函數的關係
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 的源碼必定比讀這篇文章更須要耐心,固然若是你已經讀到了這裏證實困難都被克服了,繼續加油。

相關文章
相關標籤/搜索