由於公司 Node
方面業務都是基於一個小型框架寫的,這個框架是公司以前的一位同事根據 Express
的中間件思想寫的一個小型 Socket
框架,閱讀其源碼以後,對 Express
的中間件思想有了更深刻的瞭解,接下來就手寫一個 Express
框架 ,以做爲學習的產出 。javascript
在閱讀了同事的代碼與 Express
源碼以後,發現其實 Express
的核心就是中間件的思想,其次是封裝了更豐富的 API
供咱們使用,廢話很少說,讓咱們來一步一步實現一個可用的 Express
。html
本文的目的在於驗證學習的收穫,大概細緻劃分以下:java
next()
方法API
的封裝在手寫框架以前,咱們有必要去回顧一下 Express
的簡單使用,從而對照它給咱們提供的 API
去實現其相應的功能:node
新建一個 app.js
文件,添加以下代碼:express
// app.js let express = require('express'); let app = express(); app.listen(3000, function () { console.log('listen 3000 port ...') })
如今,在命令行中執行:數組
node app.js
能夠看到,程序已經在咱們的後臺跑起來了。瀏覽器
當咱們爲其添加一個路由:服務器
let express = require('Express'); let app = express(); app.get('/hello', function (req, res) { res.setHeader('Content-Type', 'text/html; charset=utf-8') res.end('我是新添加的路由,只有 get 方法才能夠訪問到我 ~') }) app.listen(3000, function () { console.log('listen 3000 port ...') })
再次重啓:在命令行中執行啓動命令:(每次修改代碼都須要從新執行腳本)並訪問瀏覽器本地 3000 端口:app
這裏的亂碼是由於:服務器不知道你要怎樣去解析輸出,因此咱們須要指定響應頭:框架
let express = require('Express'); let app = express(); app.get('/hello', function (req, res) { res.setHeader('Content-Type', 'text/html; charset=utf-8') // 指定 utf-8 res.end('我是新添加的路由,只有 get 方法才能夠訪問到我 ~') }) app.post('/hi', function (req, res) { res.end('我是新添加的路由,只有 post 方法才能夠訪問到我 ~') }) app.listen(3000, function () { console.log('listen 3000 port ...') })
咱們先來實現上面的功能:
新建一個 MyExpress.js
,定義一個入口函數:
let http = require('http'); function createApplication () { // 定義入口函數,初始化操做 let app = function (req, res) { } // 定義監聽方法 app.listen = function () { // 經過 http 模塊建立一個服務器實例,該實例的參數是一個函數,該函數有兩個參數,分別是 req 請求對象和 res 響應對象 let server = http.createServer(app); // 將參數列表傳入,爲實例監聽配置項 server.listen(...arguments); } // 返回該函數 return app } module.exports = createApplication;
如今,咱們代碼中的 app.listen()
其實就已經實現了,能夠將引入的 express
替換爲咱們寫的 MyExpress
作驗證:
let express = require('Express'); // 替換爲 let express = require('./MyExpress');
接下來,
咱們先看看 routes
中的原理圖
根據上圖,路由數組中存在多個 layer
層,每一個 layer
中包含了三個屬性, method
、path
、handler
分別對應請求的方式、請求的路徑、執行的回調函數,代碼以下:
const http = require('http') function createApp () { let app = function (req, res) { }; app.routes = []; // 定義路由數組 let methods = http.METHODS; // 獲取全部請求方法,好比常見的 GET/POST/DELETE/PUT ... methods.forEach(method => { method = method.toLocaleLowerCase() // 小寫轉換 app[method] = function (path, handler) { let layer = { method, path, handler, } // 將每個請求保存到路由數組中 app.routes.push(layer) } }) // 定義監聽的方法 app.listen = function () { let server = http.createServer(app); server.listen(...arguments) } return app; } module.exports = createApp
到這裏,仔細思考下,當腳本啓動時,咱們把全部的路由都保存到了 routes
,打印 routes
,能夠看到:
是否是和咱們上面圖中的如出一轍 ~
此時,咱們訪問對應的路徑,發現瀏覽器一直轉圈圈,這是由於咱們只是完成了存的操做,把全部的 layer
層存到了 routes
中。
那麼咱們該如何才能夠作的當訪問的時候,調用對應的 handle
函數呢?
思路:當咱們訪問路徑時,也就是獲取到請求對象 req
時,咱們須要遍歷所存入的 layer
與訪問的 method
、path
進行匹配,匹配成功,則執行對應的 handler
函數
代碼以下:
const url = require('url') ...... let app = function (req, res) { let reqMethod = req.method.toLocaleLowerCase() // 獲取請求方法 let pathName = url.parse(req.url, true).pathname // 獲取請求路徑 console.log(app.routes); app.routes.forEach(layer => { let { method, path, handler } = layer; if (method === reqMethod && path === pathName) { handler(req, res) } }); }; ......
至此,路由的定義與解析也基本完成。
接下來,就是重點了,中間件思想。
中間件的定義其實與路由的定義差很少,也是存在 routes
中,可是,必須放到全部路由的 layer
以前,原理以下圖:
其中,middle1
、middle2
、middle3
都是中間件,middle3 放在最後面,通常做爲錯誤處理中間件,而且,每次訪問服務器的時候,全部的請求先要通過 middle1
、middle2
作處理。
在中間件中,有一個 next
方法,其實 next
方法就是使 layer
的 index
標誌向後移一位,並進行匹配,匹配成功執行回調,匹配失敗則繼續向後匹配,有點像 回調隊列。
next()
方法接下來咱們實現一個 next
方法:
由於只有中間件的回調中才具備 next
方法,可是咱們的中間件和路由的 layer
層都是存在 routes
中的,因此首先要判斷 layer
中的 method
是否爲 middle
初次以外,還要判斷,中間件的路由是否相匹配,由於有些中間件是針對某個路由的。
let reqMethod = req.method.toLocaleLowerCase() let pathName = url.parse(req.url, true).pathname let index = 0; function next () { // 中間件處理 if (method === 'middle') { // 檢測 path 是否匹配 if (path === '/' || pathName === path || pathName.startsWith(path + '/')) { handler(req, res, next) // 執行中間件回調 } else { next() } // 路由處理 } else { // 檢測 method 與 path 是否匹配 if (method === reqMethod && path === pathName) { handler(req, res) // 執行路由回調 } else { next() } } } next() // 這裏必需要調用一次 next ,意義在於初始化的時候,取到第一個 layer,
若是遍歷完 routes
,都沒有匹配的 layer
,該怎麼辦呢?因此要在 next
方法最早判斷是否邊已經遍歷完:
function next () { // 判斷是否遍歷完 if (app.routes.length === index) { return res.end(`Cannot ${reqMethod} ${pathName}`) } let { method, path, handler } = app.routes[index++]; // 中間件處理 if (method === 'middle') { if (path === '/' || pathName === path || pathName.startsWith(path + '/')) { handler(req, res, next) } else { next() } } else { // 路由處理 if (method === reqMethod && path === pathName) { handler(req, res) } else { next() } } } next()
這樣,一個 next 方法功能基本完成了。
如上面圖中所示,錯誤處理中間件放在最後,就像一個流水線工廠,錯誤處理就是最後一道工序,但並非全部的產品都須要跑最後一道工序,就像:只有不合格的產品,纔會進入最後一道工序,並被貼上不合格的標籤,以及不合格的緣由。
咱們先看看 Express
中的錯誤是怎麼被處理的:
// 中間件1 app.use(function (req, res, next) { res.setHeader('Content-Type', 'text/html; charset=utf-8') console.log('middle1') next('這是錯誤') }) // 中間件2 app.use(function (req, res, next) { console.log('middle2') next() }) // 中間件3(錯誤處理) app.use(function (err, req, res, next) { if (err) { res.end(err) } next() })
如上圖所示:有三個中間件,當 next
方法中拋出錯誤時,會把錯誤當作參數傳入 next
方法,而後,next
指向的下一個方法就是錯誤處理的回調函數,也就是說:next
方法中的參被當作了錯誤處理中間件的 handler
函數的參數傳入。代碼以下:
function next (err) { // 判斷是否遍歷完成 if (app.routes.length === index) { return res.end(`Cannot ${reqMethod} ${pathName}`) } let { method, path, handler } = app.routes[index++]; if (err) { console.log(handler.length) // 判斷是否有 4 個參數:由於錯誤中間件與普通中間件最直觀的區別就是參數數量不一樣 if (handler.length === 4) { // 錯誤處理回調 handler(err, req, res, next) } else { // 一直向下傳遞 next(err) } } else { // 中間件處理 if (method === 'middle') { if (path === '/' || pathName === path || pathName.startsWith(path + '/')) { handler(req, res, next) } else { next() } } else { // 路由處理 if (method === reqMethod && path === pathName) { handler(req, res) } else { next() } } } }
麻雀雖小五臟俱全,至此,一個簡單的 Express
就完成了。你能夠根據本身的興趣來封裝本身的 API
了 ...
next
方法。next
方法只負責維護 routes
數組和取出 layer
,根據條件去決定是否執行回調。const http = require('http') const url = require('url') function createApp () { let app = function (req, res) { let reqMethod = req.method.toLocaleLowerCase() let pathName = url.parse(req.url, true).pathname let index = 0; function next (err) { if (app.routes.length === index) { return res.end(`Cannot ${reqMethod} ${pathName}`) } let { method, path, handler } = app.routes[index++]; if (err) { console.log(handler.length) if (handler.length === 4) { console.log(1) handler(err, req, res, next) } else { next(err) } } else { if (method === 'middle') { if (path === '/' || pathName === path || pathName.startsWith(path + '/')) { handler(req, res, next) } else { next() } } else { if (method === reqMethod && path === pathName) { handler(req, res) } else { next() } } } } next() }; let methods = http.METHODS; app.routes = []; methods.forEach(method => { method = method.toLocaleLowerCase() app[method] = function (path, handler) { let layer = { method, path, handler, } app.routes.push(layer) } }) app.use = function (path, handler) { if (typeof path === 'function') { handler = path; path = '/'; } let layer = { method: 'middle', handler, path } app.routes.push(layer) } app.listen = function () { let server = http.createServer(app); server.listen(...arguments) } return app; } module.exports = createApp