從零手寫一套 Express 的源碼

這裏我將從零實現一套簡易的Express源碼,提供給來年“金三銀四”跳槽高峯期的小夥伴們閱讀也詳細梳理一下自己對Express原理的理解。

起牀工作

什麼是Express

Express是一個簡潔、靈活的node.js Web 應用開發框架,它提供一系列強大的特性,幫助我們創建各種Web 和移動設備應用。豐富的 HTTP 快捷方法和任意排列組合的Connect 中間件,讓你創建健壯、友好的API 變得即快捷又簡單。

Express的使用

在瞭解 express 原理之前,我們首先要掌握其基本用法。Express 官方文檔寫的很詳細 [Express 官網](http://www.expressjs.com.cn/ Express 官網)

初始化項目

在命令行依次輸入:

mkdir express_self 新建項目目錄
cd express_self/ 切換到項目目錄
npm init 生成項目的一些信息,最終會生成一個package.json文件。注意:可以輸入npm init -y可以不用按回車

安裝 express

在項目根目錄輸入:

npm install express --save

初始化項目目錄和文件

在項目根目錄新建一下文件:

.gitignore 忽略文件上傳到遠程倉庫
express 改文件夾存放手寫express源碼的目錄
express/index.js 存放手寫express核心源碼文件
test.js 測試express常用功能文件夾

調用 get方法

在test.js文件中,使用express啓動一個本地服務

const express = require('express');
const app = express();

app.get('/name', (req, res) => {
    res.end({
        name: '前端'
    })
})

app.listen(3000, () => {
    console.log('Server at port 3000')
})

調用post方法

const express = require('express');
const app = express();

app.post('/front', (req, res) => {
    res.json({
        name: 'front'
    })
})

app.listen(3000, () => {
    console.log('Server at port 3000')
})

調用use方法

在上面調用get方法中,中文出現了亂碼,這是提示我們要設置響應頭使其以文本格式返回

單個路由設置

app.get('/name', (req, res) => {
+    res.setHeader('Content-Type', 'text/html;charset=UTF-8;');
   res.end('前端')
})

設置中間件

設置中間件的好處:當路由多,則設置一箇中間件即可解決所有路由中文亂碼問題

+ app.use('/', (req, res, next) => {
+    res.setHeader('Content-Type', 'text/html;charset=UTF-8;');
+    next();
+ })

注意

  1. use 方法第一個參數不寫默認就是 /

  2. 只要開頭能匹配到後面的就都匹配到和路由有區別,路由的路徑是完全相同

  3. 要隨機應變更改中間件的位置,本文案例,要放到第一個路由前面才解決能所有路由中文亂碼問題

擴展

什麼是路由

路由:根據方法和路徑執行匹配成功後的執行對應的回調函數

前端路由

在單頁面應用,大部分頁面結構不變,只改變部分內容的使用。前端路由是單頁面富應用(SPA)的核心。

優點:

  1. 前後端分離

  2. 用戶體驗度好

缺點:

  1. 使用瀏覽器的前進,後退鍵的時候會重新發送請求

  2. 單頁面無法記住之前滾動的位置,無法在前進,後退的時候記住滾動的位置

後端路由

通過用戶請求的url導航到具體的html頁面;每跳轉到不同的URL,都是重新訪問服務端,然後服務端返回頁面,頁面也可以是服務端獲取數據,然後和模板組合,返回HTML,也可以是直接返回模板HTML,然後由前端***js*再去請求數據,使用前端模板和數據進行組合,生成想要的HTM**

優點:

  1. 服務端渲染後,可以直接返回給瀏覽器
  2. SEO 友好

缺點:

  1. 頁面、數據與邏輯混合在一起
  2. 開發、維護成本高

中間件

中間件就是處理HTTP請求的函數,用來完成各種特定的任務;比如檢查用戶是否登錄、用戶是否有權限、計算請求的時間等,它的特點是:

  1. 一箇中間件處理完請求和響應可以把對應數據再傳遞給下一個中間件
  2. 回調函數的next參數,表示接受其他中間件的調用,函數體中的next()表示將請求是否傳遞下去
  3. 可以根據路徑來區分返回執行不同的中間件

Express中間件

實現自己的Express

接下來,我將一步一步的實現Express源碼中常見的核心方法

加油幹

返回核心函數

由於引入express後,返回的是一個函數,所有我們在編寫源碼時,需要導出一個函數app

function createApplication() {
    let app = (req, res) => {
        // 該方法內編寫核心邏輯
    }
    return app;
}

module.exports = createApplication;

監聽函數

由於監聽函數是導出函數上的一個方法,所以我們在app函數上掛載一個listen方法,實現其監聽原理。其原理就是調用原生node中http模塊來創建服務。

從上面Express只用中,我們可以通過擴展運算符來獲取所有的參數,巧妙的解決了用戶傳入一個還是兩個參數的問題。

+ let http = require('http');

+ app.listen = function() {
+ 	let server = http.createServer(app);
+ 	server.listen(...arguments);
+ }

由於http上有很多方法,編寫源碼講究的是高類聚、低耦合高可用的代碼結構,所有我們不能一個方法寫一段邏輯。http提供了METHODS屬性會返回http所有的方法,然後我們對其所有方法進行遍歷動態的解決方法高複用的代碼邏輯;

http.METHODS 輸出:
[ 'ACL', 'BIND', 'CHECKOUT','CONNECT', 'COPY', 'DELETE', 'GET', 'HEAD', 'LINK', 'LOCK', 'M-SEARCH', 'MERGE', 'MKACTIVITY', 'MKCALENDAR', 'MKCOL', 'MOVE', 'NOTIFY', 'OPTIONS', 'PATCH', 'POST', 'PROPFIND', 'PROPPATCH', 'PURGE', 'PUT', 'REBIND', 'REPORT', 'SEARCH', 'SOURCE', 'SUBSCRIBE', 'TRACE', 'UNBIND', 'UNLINK', 'UNLOCK', 'UNSUBSCRIBE' ]

然後對其獲取的所有方法進行遍歷即可。

注意:遍歷時需要將所有的方法名轉換爲小寫

http.METHODS.forEach(method => {
	method = method.toLowerCase(); // 將方法名轉換爲小寫
})

實現get、post

由express基本用法可知,所有路由方法是一個函數,改函數接受兩個參數:一個請求的路由路徑、一個匹配路由成功後執行的回調函數。

一個express項目必定會有很多路由,我們需要將所有路由存放在一個數組容器裏;且數組中的每一個路由對象都包括三個屬性:一個請求方法、一個請求的路由路徑、一個匹配路由成功後執行的回調函數。

核心邏輯:

+ app.routes = []; // 存放所有的路由對象

+ http.METHODS.forEach(method => {
+     method = method.toLowerCase(); // 將方法名轉換爲小寫
+     /**
+      * path: 路徑
+      * handler:監聽函數 
+     */
+     app[method] = function(path, handler) {
+         let layer = {
+             method,
+             path,
+             handler
+        }
+         app.routes.push(layer);
+     }
+ })

編寫所有方法的核心邏輯後,我們需要在上面核心函數中進行遍歷獲取其中屬性與其用戶傳入的參數是否一致。一致則執行對應的成功回調函數,不一致提示用戶具體錯誤原因。

let app = (req, res) => {
    // 該方法內編寫核心邏輯
+    // 1. 獲取請求的方法
+    let m = req.method.toLowerCase();
+    // 2. 獲取請求的路徑
+    let { pathname } = url.parse(req.url, true);

+   for(let i = 0; i < app.routes.length; i++) {
+        let { method, path, handler } = app.routes[i];
+        if((method === m) && (path === pathname)) {
+            handler(req, res);
+        }
+    }

+    res.end(`Cannont ${m} ${pathname} `);
}

從上述邏輯可知,若用戶傳入的請求方法名和請求路徑相同的話,則執行回調函數並傳入req、res兩個參數。

all

和get函數不同app.all()函數可以匹配所有的HTTP動詞,也就是說它可以過濾所有路徑的請求。當請求方法爲all時,所有路由都可以進行匹配。

格式:app.all(path,function(request, response));

用處:

  1. 在所有請求路由最前添加all方法設置中文亂碼問題
  2. 在所有請求路由最後添加404頁面

核心邏輯

+ app.all = function(path, handler) {
+ 		let layer = {
+         method: 'all', // 如果method是all,表示全部匹配
+         path,
+         handler
+     }
+     app.routes.push(layer);
+ }

由於http.MRTHODS返回所有方法中沒有all方法,故我們需要自定義一個all方法掛載在覈心函數上,此時需要修改對應的核心函數來兼容all方法。

let app = (req, res) => {

     for(let i = 0; i < app.routes.length; i++) {
          let { method, path, handler } = app.routes[i];
+          if((method === m || method === 'all') && (path === pathname || path === '*')) {
              handler(req, res);
          }
      }
        
 }

中間件

中間件作爲express最爲核心的功能之一,其邏輯也是最爲複雜的一塊,在編寫其核心邏輯之前,我們需要對express的中間件有幾點注意:

  1. 中間件方法都是use方法, 所以需要在覈心函數上掛載use方法
  2. 中間件可以只傳一個回調函數參數
  3. 若不傳請求路徑,系統則默認爲 ‘/’
  4. 中間件的回調函數有一個next參數
  5. 中間件只要開頭能匹配到後面的就都匹配到和路由有區別,路由是完全相同

核心邏輯

+ app.use = function(path, handler) {
+     // 處理中間件參數的問題
+     if(typeof handler !== 'function') {
+         handler = path;
+         path = '/';
+     }

+     let layer = {
+         method: 'middle', // method是middle就表示是一箇中間件
+         path,
+         handler
+     }
+     app.routes.push(layer); // 將中間件放到容器內
+ }

此時需要修改上面核心函數方法來兼容use方法。

+ let app = (req, res) => {

+     // 通過next方法進行迭代
+     let index = 0; // 取第一個路由
+     function next() {
+     // 如果數組全部迭代完成還沒有找到  說明路徑不存在
+     if(index === app.routes.length) return res.end(`Cannot ${m} ${pathname}`);

+        let { method, path, handler } = app.routes[index++];
+        if(method === 'middle') { // 處理中間件
+             if(path === '/' || path === pathname || pathname.startsWith(path + '/')) {
+                 handler(req, res, next);
+             } else {
+                 next(); // 如果這個中間件沒有匹配到  那麼就繼續走下一個layer
+             }
+        } else {
            // 處理路由
            if((method === m || method === 'all') && (path === pathname || path === '*')) {
                 handler(req, res);
             } else {
                 next();
             }
        }
+     }
    
+     next();   
+ }

錯誤中間件

一般中間件都只有3個參數:req、res、next。但是當一箇中間件有4個參數的話系統則會被視爲錯誤處理中間件。

+ function next(err) {
     if(method === 'middle') { // 處理中間件
         if(path === '/' || path === pathname || pathname.startsWith(path + '/')) {
             handler(req, res, next);
         } else {
+             next(err); // 如果這個中間件沒有匹配到  那麼就繼續走下一個layer
         }
     }
}

內置中間件

我們爲什麼覺得Express即好用又簡單呢;因爲它框架中跟我們開發者把一些常用的方法都封裝好了。接下來我就舉一個內置中間件的原理實現:

+ // express 內置中間件
+ app.use(function(req, res, next) {
+    let {pathname, query} = url.parse(req.url, true);
+    let hostname = req.headers['host'].split(':')[0];
+    req.path = pathname;
+    req.query = query;
+    req.hostname = hostname;
+    next()
+ })

項目倉庫地址

如果小夥伴覺得樓主寫的對小夥伴有所收穫,幫忙點個star https://github.com/tangmengcheng/express.git

總結

我們大家都知道Express即簡單又容易上手,但是很多小夥伴對其原理不瞭解,這對我們不管是面試還算自我提升都是不友好的。其實從頭到尾下來,你會發現Express使用簡單,其原理也不是很難,所有真心想提高自己技術功能的自己手動敲一遍,定會有所收穫!

最後

歡迎大家加入,一起學習前端,共同進步!
cmd-markdown-logo
cmd-markdown-logo

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