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
服務,咱們就按照官方的設計返回一個函數 app
。express
// 文件: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
模塊建立了服務,並調用了建立服務 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
函數的參數傳入。app
接着就是循環 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
函數中。
以前的例子中咱們都是用原生的 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(); // 一、指定模板引擎,其實就是模板文件的後綴名 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;
在上面新增代碼中設置了兩個緩存 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
的源碼必定比讀這篇文章更須要耐心,固然若是你已經讀到了這裏證實困難都被克服了,繼續加油。