Express 的實現(超詳細)

序:

由於公司 Node 方面業務都是基於一個小型框架寫的,這個框架是公司以前的一位同事根據 Express 的中間件思想寫的一個小型 Socket 框架,閱讀其源碼以後,對 Express 的中間件思想有了更深刻的瞭解,接下來就手寫一個 Express 框架 ,以做爲學習的產出 。javascript

在閱讀了同事的代碼與 Express 源碼以後,發現其實 Express 的核心就是中間件的思想,其次是封裝了更豐富的 API 供咱們使用,廢話很少說,讓咱們來一步一步實現一個可用的 Expresshtml

本文的目的在於驗證學習的收穫,大概細緻劃分以下: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

clipboard.png

能夠看到,程序已經在咱們的後臺跑起來了。瀏覽器

當咱們爲其添加一個路由:服務器

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

clipboard.png

這裏的亂碼是由於:服務器不知道你要怎樣去解析輸出,因此咱們須要指定響應頭:框架

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 ...')
})

clipboard.png

咱們先來實現上面的功能:

1. 服務器監聽的原理

新建一個 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');

2. 路由的解析與匹配:

接下來,

咱們先看看 routes 中的原理圖

clipboard.png

根據上圖,路由數組中存在多個 layer 層,每一個 layer 中包含了三個屬性, methodpathhandler 分別對應請求的方式、請求的路徑、執行的回調函數,代碼以下:

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 ,能夠看到:

clipboard.png

是否是和咱們上面圖中的如出一轍 ~

此時,咱們訪問對應的路徑,發現瀏覽器一直轉圈圈這是由於咱們只是完成了存的操做,把全部的 layer 層存到了 routes

那麼咱們該如何才能夠作的當訪問的時候,調用對應的 handle 函數呢?

思路:當咱們訪問路徑時,也就是獲取到請求對象 req 時,咱們須要遍歷所存入的 layer 與訪問的 methodpath 進行匹配,匹配成功,則執行對應的 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)
    }
  });
};
......

至此,路由的定義與解析也基本完成。

3. 中間件的定義與使用

接下來,就是重點了,中間件思想

中間件的定義其實與路由的定義差很少,也是存在 routes 中,可是,必須放到全部路由的 layer 以前,原理以下圖:

clipboard.png

其中,middle1middle2middle3 都是中間件,middle3 放在最後面,通常做爲錯誤處理中間件,而且,每次訪問服務器的時候,全部的請求先要通過 middle1middle2 作處理。

在中間件中,有一個 next 方法,其實 next 方法就是使 layerindex 標誌向後移一位,並進行匹配,匹配成功執行回調,匹配失敗則繼續向後匹配,有點像 回調隊列

核心 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 方法功能基本完成了。

4. 錯誤處理中間件定義與使用

如上面圖中所示,錯誤處理中間件放在最後,就像一個流水線工廠,錯誤處理就是最後一道工序,但並非全部的產品都須要跑最後一道工序,就像:只有不合格的產品,纔會進入最後一道工序,並被貼上不合格的標籤,以及不合格的緣由。

咱們先看看 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 了 ...

總結:

  1. 中間件的核心是 next 方法。
  2. 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
相關文章
相關標籤/搜索