由淺入深理解express源碼(二)

回顧

上一篇軟文主要是介紹了項目的搭建,實現了一個基本的框架。引入 mocha + chai + supertest 測試加入項目中,實現了服務器的啓動、app 對象get方法的簡化版本以及項目基本結構的肯定。對於get方法只實現到經過path找到對應的callback。稍簡單的功能javascript

實現目標

迭代二的實現目標主要是引入簡化版的router,並對/get/:id 式的路由進行解析。同時實現app.Methods相應的接口java

項目結構

express2
  |
  |-- lib
  |    |-- router // 實現簡化板的router
  |    |    |-- index.js // 實現路由的遍歷等功能
  |    |    |-- layer.js // 裝置path,method cb對應的關係
  |    |    |-- route.js // 將path和fn的關係實現一對多
  |    |-- express.js //負責實例化application對象
  |    |-- application.js //包裹app層
  |
  |-- examples
  |    |-- index.js // express1 實現的使用例子
  |
  |-- test
  |    |
  |    |-- index.js // 自動測試examples的正確性
  |
  |-- index.js //框架入口
  |-- package.json // node配置文件

複製代碼

問題分析

本迭代的重點在於理清method,url,callback之間的關係,先看express源碼開放出來的api:node

app.get(path, callback [, callback ...])

app.get('/', function (req, res, next) {
  next()
})

app.get('/', function (req, res) {
  res.send('GET request to homepage');
})
app.post('/', function (req, res) {
  res.send('POST request to homepage');
})
複製代碼

從上面的接口能夠看出:es6

一、一個path是能夠對應多個callbackexpress

二、一個path甚至能夠對應多個methodnpm

三、一個method能夠對應多個pathjson

四、一個callback只能對應一個method和pathapi

迭代二目標就是準確的經過url解析找到對應的path,並肯定method,在這兩個變量肯定的狀況下將其對應的callback逐個執行一遍,順帶將「/get/:id」形式的參數進行分析剝離一下數組

執行流程

express有兩大核心,第一個是路由的處理,第二個是中間件裝載。理解路由的實現尤其關鍵。在上一次迭代中,咱們是在appliction中定義了一個paths數組,用來預存path和callback的對應關係而且他們之間的關係咱們視爲簡單的一對一。在迭代二中,咱們path和callback的關係變得複雜起來,不但從一對一變成了一對多,並且還引入了method。對於迭代二的整個實現過程,咱們能夠分解爲如下幾個步驟:服務器

一、建立route實例肯定path和route實例的關係,path經過path-to-regexp包進行正則解析

二、在path必定的狀況下,在route實例中肯定method和callback對應預存

三、經過application的listen攔截全部請求

四、分析url,遍歷全部的path,與path的正則進行匹配找到path對應的route

五、匹配request中的method,遍歷path對應的route中全部的callback,method的關係,找到method對應的callback,逐個執行。

從以上的描述中path和route的關係,method和callback的關係須要地方存放。在此引入Layer類。而整個服務的route執行流程等放入Router類中管理。至此,路由由3個類組成:Router,Layer,Route。關係以下圖所示

實例關係圖以下:

--------------
| Application  |                                 ---------------------------------------------------------
|     |        |        ----- -----------        |     0     |     1     |     2     |     3     |  ...  |
|     |-router | ----> |     | Layer      |       ---------------------------------------------------------
--------------        |  0  |   |-path    |       | Layer     | Layer     | Layer     | Layer     |       |
 application          |     |   |-route   |---->  |  |- method|  |- method|  |- method|  |- method|  ...  |
                      |     |   |-dispatch|       |  |- callback||- callback||- callback||- callback|     |
                      |-----|-------------|       ---------------------------------------------------------
                      |     | Layer       |                               route
                      |  1  |   |-path    |                                  
                      |     |   |-route   |
                      |     |   |-dispatch|
                      |-----|-------------|       
                      |     | Layer       |
                      |  2  |   |-path    |
                      |     |   |-route   |
                      |     |   |-dispatch|
                      |-----|-------------|
                      | ... |   ...       |
                       ----- ------------- 
                            router
複製代碼

代碼解析

首先看看lib/application.js,迭代二中在app中加入了_router屬性,app[method]方法,lazyrouter方法:

_router: 存儲Router對應的實例

app[method]: 對應的app.get,app.post等方法,對應的參數爲path,callbacks。其中method對應的是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")中的方法。這個方法爲app在此次迭代中的主角,主要是對上面的實例關係圖進行註冊。每執行一次app[method]方法其實就是在註冊路由,將參數中的path和route對應起來,同時將method和callbacks對應。分別存在router的stack,route的stack中。

lazyrouter:實例化_router

源碼:

/** * 對路由實現裝載,實例化 */
app.lazyrouter = function () {
  if (!this._router) {
    this._router = new Router()
  }
}

/** * 實現post,get等http.METHODS 對應的方法 * 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" */

methods.forEach((method) => {
  method = method.toLowerCase()
  app[method] = function (path) {
    if (method === 'get' && arguments.length === 1) { // 當爲一個參數時app的get方法,返回settings中的屬性值
      return this.set(path)
    }
    this.lazyrouter()
    let route = this
      ._router
      .route(path) // 調用_router的route方法,對path和route註冊
    route[method].apply(route, slice.call(arguments, 1)) // 調用route的method方法,對method和callbacks註冊
  }
})
複製代碼

application對原來的handle方法也作出了修改,調用的是_router.handle 對url進行精肯定位和匹配。在handle中還引入了finalhandler方法,對http請求發生錯誤時作最後的處理,具體查看: www.npmjs.com/package/fin…

/** * http.createServer 中的回調函數最終執行 * 調用的是_router.handle 對url進行精肯定位和匹配 */
app.handle = function handle(req, res) {
  let router = this._router
  let done = finalhandler(req, res, {
    env: this.get('env'),
    onerror: logerror.bind(this)
  })
  if (!router) {
    done()
  }
  router.handle(req, res, done)
}

function logerror(err) {
  if (this.get('env') !== 'test') 
    console.error(err.stack || err.toString());
  }
複製代碼

Router類的實現主要關注在route方法和handle兩個方法中,一個是用來註冊,一個是遍歷註冊的數組.

route方法簡單明瞭一看就明白,最後將route返回到app中,再調用當前的實例route[method]方法註冊method和callbacks的關係

handle方法就比較複雜,主要分爲兩塊,一個是對錯誤的處理,發生錯誤是調用app中的finalhandle,一個是對stack數組的遍歷,找到url匹配的路由。對stack遍歷的方式採用的是next方法遞歸調用的方式。這種思想相似於es6中的Iterator接口的實現

/** * 將path和route對應起來,並放進stack中,對象實例爲layer */
proto.route = function route(path) {
  let route = new Route(path)
  let layer = new Layer(path, {
    end: true
  }, route.dispatch.bind(route))
  layer.route = route
  this
    .stack
    .push(layer)
  return route
}

/** * 遍歷stack數組,並處理函數, 將res req 傳給route */

proto.handle = function handle(req, res, out) {
  let self = this
  debug('dispatching %s %s', req.method, req.url)
  let idx = 0
  let stack = self.stack
  let url = req.url
  let done = out
  next() //第一次調用next
  function next(err) {
    let layerError = err === 'route'
      ? null
      : err
    if (layerError === 'router') { //若是錯誤存在,再當前任務結束前調用最終處理函數
      setImmediate(done, null)
      return
    }

    if (idx >= stack.length) { // 遍歷完成以後調用最終處理函數
      setImmediate(done, layerError)
      return
    }

    let layer
    let match
    let route
    while (match !== true && idx < stack.length) { //從數組中找到匹配的路由
      layer = stack[idx++]
      match = matchLayer(layer, url)
      route = layer.route
      if (typeof match !== 'boolean') {
        layerError = layerError || match
      }

      if (match !== true) {
        continue
      }
      if (layerError) {
        match = false
        continue
      }
      let method = req.method
      let has_method = route._handles_method(method)
      if (!has_method) {
        match = false
        continue
      }
    }
    if (match !== true) { // 循環完成沒有匹配的路由,調用最終處理函數
      return done(layerError)
    }
    res.params = Object.assign({}, layer.params) // 將解析的‘/get/:id’ 中的id剝離出來
    layer.handle_request(req, res, next) //調用route的dispatch方法,dispatch完成以後在此調用next,進行下一次循環

  }
}

複製代碼

Route類的實現主要是在route[method]方法和dispatch,和Router中的route和handle的功能相似,只是route[method]註冊是的method和callback的對應關係,而dispatch遍歷的則是callbacks

route[method]一樣比較簡單,主要是將app中對應method的第二個之後的參數進行遍歷,並將其和method對應起來

dispatch採用的是和router中的handle同樣的方式--> next遞歸遍歷stack。處理完成後回調router的next

/** * 對同一path對應的methods進行註冊,存放入stack中 */
methods.forEach((method) => {
  method = method.toLowerCase()

  Route.prototype[method] = function () {
    let handles = arguments

    for (let i = 0; i < handles.length; i++) {
      let handle = handles[i]
      if (typeof handle !== 'function') {// 若是handle不是function,則對外拋出異常
        let msg = `Route.${method}() requires a callback function but not a ${type}`
        throw new Error(msg)
      }

      debug('%s %o', method, this.path)

      let layer = new Layer('/', {}, handle) // 註冊method和handle的關係
      layer.method = method

      this.methods[method] = true
      this
        .stack
        .push(layer)
    }
    return this
  }
})

/** * 遍歷stack數組,並處理函數 */
Route.prototype.dispatch = function dispatch(req, res, done) {
  let idx = 0
  let stack = this.stack
  if (stack.length === 0) {
    return done() // 函數出來完成以後,將執行入口交給母函數管理,此處的done爲router handle中的next
  }

  let method = req
    .method
    .toLowerCase()
  req.route = this
  next()
  function next() {
    let layer = stack[idx++]
if (!layer) { // 當循環完成,調回router handle中的next
      return done()
    }
    if (layer.method && layer.method !== method) { // 不符合要求,繼續調用next進行遍歷
      return next()
    }

    layer.handle_request(req, res, next)
  }

}
複製代碼

Layer類的做用主要是關係的關聯,path和route的關聯,path對應的route中method和callback的關聯。再有就是對path的處理,主要的方法也有兩個:match、handle_request

handle_request:主要是執行layer中的handle,在router中layer對應的handle爲layer.route對應的dispatch,在route中的handle對應的則是app的method傳進來的callback函數

match:對uri和path進行匹配,匹配上了返回true否側false。中間還對'/get/:id'式的路由中的id進行參數剝離,存入params中.在這個類中用到了path-to-regexp包,主要是對path進行解析,具體查看:www.npmjs.com/package/pat…

Layer.prototype.handle_request = function handle(req, res, next) {
  let fn = this.handle
  fn(req, res, next)
}


Layer.prototype.match = function match(path) {
  let match
  if (path) {
    match = this
      .regexp
      .exec(path)
  }

  if (!match) {
    this.params = undefined
    this.path = undefined
    return false
  }

  this.params = {}
  this.path = match[0]
  if (this.keys) {
    let keys = this.keys
    let params = this.params
    for (let i = 1; i < match.length; i++) {
      let key = keys[i - 1]
      let prop = key.name
      let val = decode_param(match[i])

      if (val !== undefined) {
        params[prop] = val
      }
    }
  }
  return true
}
複製代碼

exammple/index.js 在入口文件中加入了一些新的路由

// localhost:3000/path 時調用
app.get('/path', function (req, res, next) {
  console.log('visite /path , send : path')
  // res.end('path')
  pathJson.index = 1
  next()
})
// localhost:3000/path 時調用,先走第一個,再走這個
app.get('/path', function (req, res) {
  console.log('visite /path , send : path')
  pathJson.end = true
  res.end(JSON.stringify(pathJson))
})
// localhost:3000/ 時調用
app.get('/', function (req, res) {
  console.log('visite /, send: root')
  res.end('root')
})
// 發生post請求的時候調用
app.post('/post/path', function (req, res) {
  res.end('post path')
})
// 輸出傳入的id
app.get('/get/:id', function (req, res) {
  res.end(`{"id":${res.params.id}}`)
})
複製代碼

test/index.js 測試exapmles中的代碼,驗證是否按照地址的不一樣,進了不一樣的回調函數

// 若是走的不是examples中的get:/path 測試不經過;
  it('GET /path', (done) => {
    request
      .get('/path')
      .expect(200)
      .end((err, res) => {
        if (err) 
          return done(err)
        let json = JSON.parse(res.text)
        assert.equal(json.index, 1, 'didn`t visite the first route /path') // 查看是否調用了第一次的註冊
        assert.equal(json.end, true, 'res is wrong') // 查看是否調用了第二次註冊
        done()
      })
  })
  // 測試get: /get/:id 並輸出{id:12}
  it('GET /get/:id', (done) => {
    request
      .get('/get/12')
      .expect(200)
      .end((err, res) => {
        if (err) 
          return done(err)
        let params = JSON.parse(res.text)
        assert.equal(params.id, 12, 'id is wrong') // 若是輸出的不是傳入的12,測試不經過
        done()
      })
  })
  // 若是走的不是examples中的post:/post/path 測試不經過
  it('POST /post/path', (done) => {
    request
      .post('/post/path')
      .expect(200)
      .end((err, res) => {
        if (err) 
          return done(err)
        assert.equal(res.text, 'post path', 'res is wrong') // 根據response調用end方法時的輸出爲: post path
        done()
      })
  })


複製代碼

test測試結果以下:

寫在最後

總結一下當前expross各個部分的工做。

application表明一個應用程序,expross是一個工廠類負責建立application對象。Router表明路由組件,負責應用程序的整個路由系統。組件內部由一個Layer數組構成,每一個Layer表明一組路徑相同的路由信息,具體信息存儲在Route內部,每一個Route內部也是一個Layer對象,可是Route內部的Layer和Router內部的Layer是存在必定的差別性。

Router內部的Layer,主要包含path、route、handle(route.dispatch)屬性。 Route內部的Layer,主要包含method、handle屬性。 若是一個請求來臨,會現從頭到尾的掃描router內部的每一層,而處理每層的時候會先對比URI,相同則掃描route的每一項,匹配成功則返回具體的信息,沒有任何匹配則返回未找到。

下期預告

完善router,實現app.use 和app.params接口

由淺入深理解express源碼(一)

相關文章
相關標籤/搜索