原文出自: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
模塊創建了服務,並調用了創建服務 server
的 listen
方法,將 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.get
、app.post
或 app.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.path
、req.query
、req.host
和 req.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
,後面三個參數分別爲 req
、res
和 next
。
代碼種創建了 index
變量,默認調用了一次 next
方法,每次然後取出數組 app.routes
中的路由對象的回調函數執行,並在內部執行 handler
,而 handler
回調中又調用了 next
方法,就這樣將整個中間件和路由的回調串聯起來。
我們發現在第一次調用 next
之前的所有邏輯,如給 req
添加屬性,給 res
添加方法,都是公共邏輯,是任何中間件和路由在匹配之前都會執行的邏輯,我們既然有了中間件方法 app.user
,可以將這些邏輯抽取出來作爲一個單獨的中間件回調函數執行,所以創建了 init
函數,內部返回了一個函數作爲回調函數,形參爲 req
、res
和 next
,並在init
調用返回的函數內部調用 next
向下執行。
內置模板引擎的實現
在 Express
框架中內置支持了 ejs
、jade
等模板,使用方法 “三部曲” 如下。
// 模板的使用
// 引入 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;
在上面新增代碼中設置了兩個緩存 settings
和 engines
,前者用來存儲模板相關的設置,如渲染成什麼類型的文件、讀取模板文件的根目錄,後者用來存儲渲染引擎,即渲染模板的方法,這所以設置這兩個緩存對象是爲了實現 Express
多種不同模板共存的功能,可以根據需要進行設置和使用,而設置的方法分別爲 app.set
和 app.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
方法需要返回一個函數,形參爲 req
、res
和 next
,通過調用方式我們能看出 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);
看到上面的使用方式,我們根據前面的套路知道是 Express
在 res
對象上給掛載了一個 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
的源碼一定比讀這篇文章更需要耐心,當然如果你已經讀到了這裏證明困難都被克服了,繼續加油。