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

回顧

上次迭代主要是實現了app.param,app.use,以及req.query中參數的提取工做。內容較多,篇幅也較長。javascript

實現目標

git: github.com/kaisela/mye…java

本次主要是完善router,實現錯誤處理中間件 和use更多用法實現。其實在上一次迭代的代碼中已經完成了錯誤中間件的邏輯,可是因爲上次迭代的篇幅較長,因此就放到此次的迭代中講訴。對use則是加上了use子模塊和router模塊的實現。node

項目結構

express4
  |
  |-- lib
  |    |-- middleware // 中間件文件夾
  |    |    |-- query.js // 實現req.query提取的中間件
  |    |    |-- init.js // 新增 每次請求初始之時對app,req,res進行賦值關聯
  |
  |    |-- router // 實現簡化板的router
  |    |    |-- index.js // 實現路由的遍歷等功能
  |    |    |-- layer.js // 裝置path,method cb對應的關係
  |    |    |-- route.js // 將path和fn的關係實現一對多
  |    |-- express.js //負責實例化application對象
  |    |-- application.js //包裹app層
  |    |-- utils.js // 新增,目前只是用於query中間件的實現的所需的工具函數
  |
  |-- examples
  |    |-- index.js // express 實現的使用例子
  |
  |-- test
  |    |
  |    |-- index.js // 自動測試examples的正確性
  |
  |-- index.js //框架入口
  |-- package.json // node配置文件

複製代碼

問題分析

本次迭代主要是將router做爲中間件暴露給用戶,而且能夠做爲app.use的中間件使用。app自己也是中間件的一種,也能夠被app.use使用。以及錯誤中間件的完善工做git

app.use 使用app 和 router做爲中間件

// 路由做爲中間件
var router = express.Router();
router.get('/', function (req, res, next) {
  next();
});
app.use(router)

// app做爲中間件
var subApp = express();
subApp.get('/', function (req, res, next) {
  next();
});
app.use(subApp)
複製代碼

以上是在官方文檔上,router和子app分別做爲中間件在app中的使用示例。其實在application和router的實現中,最重要的就是handle函數,整個程序的執行入口就在這兩個函數當中,而這兩個函數的參數就是req,res,next,自己就是一箇中間件。所以在app.use實現過程當中,作了一些小小的包裝處理。github

錯誤中間件

app.use(function (err, req, res, next) {
  console.error(err.stack)
  res.status(500).send('Something broke!')
})
複製代碼

錯誤處理是指如何表示同步和異步發生的捕獲和處理錯誤。Express附帶了一個默認的錯誤處理程序-->finalhandler。若是您將錯誤傳遞給next(),而且沒有在自定義錯誤處理程序中處理它,那麼它將由內置的錯誤處理程序處理;錯誤將經過堆棧跟蹤寫入客戶端。express

數據結構

--------------                                     ----- ----------
| Application  | ------------------------------->   | params       |                         
|     |        |        ----- -------------         |   |-param    |
|     |-router | ----> |     | Layer       |        |   |-callbacks|
--------------        |  0  |   |-path     |        ----- ----------
 application          |     |   |-callbacks|           router
                      |-----|--------------|      
                      |     | Layer        |                     
                      |  1  |   |-path     |                                  
                      |     |   |-callbacks|
                      |-----|--------------|       
                      |     | Layer        |
                      |  2  |   |-path     |
                      |     |   |-callbacks|
                      |-----|--------------|
                      | ... |   ...        |
                       ----- -------------- 
                            router
複製代碼

對於子app做爲中間件的數據結構並未發生變化,只是對callback函數作了處理。而對於router做爲中間件,callback就是router的handle函數。npm

代碼解析

和上次迭代同樣,在文件的註釋中前面加了一個「迭代編號:新增」的字樣,來表示此段代碼是在此迭代中新增的。json

app.use

application.js中的use作了修改,主要是對子app的回調作個簡單處理。引入flatten處理一下use的參數,這個在其餘的一些相似參數的接口中也加入了處理數組

/** * 3:新增 暴露給用戶註冊中間件的結構,主要調用router的use方法 * @param {*} fn */
app.use = function use(fn) {
  let offset = 0
  let path = '/'

  if (typeof fn !== 'function') {
    let arg = fn
    while (Array.isArray(arg) && arg.length !== 0) {
      arg = arg[0]
    }

    if (typeof arg !== 'function') {
      offset = 1
      path = fn
    }
  }
  // 4:新增 對傳入的參數進行處理,是參數能夠傳入數組
  let fns = flatten(slice.call(arguments, offset))
  if (fns.length === 0) {
    throw new TypeError('app.use() require a middlewaare function')
  }

  this.lazyrouter()
  let router = this._router
  fns.forEach(function (fn) {
    // 4:修改 一般use裏面是一個express對象,或者router對象時會包含handle和set,不包含爲普通中間件
    if (!fn || !fn.handle || !fn.set) {
      return router.use(path, fn)
    }
    // 4:新增 此時的fn爲express或router對象,將當前express對象關聯到fn
    fn.parent = this

    router.use(path, function mounted_app(req, res, next) {
      let orig = req.app
      // 4:新增 在中間件的回調函數中調用fn的handle
      fn.handle(req, res, function (err) {
        setPrototypeOf(req, orig.request)
        setPrototypeOf(res, orig.response)
        next(err)
      })
    })
    // 4:新增 觸發fn掛載完成事件
    fn.emit('mount', this)
  }, this)
}
複製代碼

在上面的代碼中有req.app的引用。此次在每次請求時出了原來的query中間件,還加入了一個init中間件,主要是對app,req,res進行賦值關聯數據結構

const setPrototypeOf = require('setprototypeof')

exports.init = function (app) {
  return function expressInit(req, res, next) {
    req.res = res
    res.req = req
    req.next = next
    setPrototypeOf(req, app.request)
    setPrototypeOf(res, app.response)

    res.locals = res.locals || Object.create(null)
    next()
  }
}
複製代碼

app.request是在程序初始化時加入的,並將app掛在在app.request上面。對應文件爲express.js

function createApplication() {
  ...
  // 4:新增 講app和新建立的req相互關聯
  app.request = Object.create(req, {
    app: {
      configurable: true,
      enumerable: true,
      writable: true,
      value: app
    }
  })
  // 4:新增 講app和新建立的res相互關聯
  app.response = Object.create(res, {
    app: {
      configurable: true,
      enumerable: true,
      writable: true,
      value: app
    }
  })
  app.init()
  return app
}
複製代碼

重點的實如今router的handle方法,主要是是將請求的連接進行分割。好比path:/sub/:id/getuser 實際是子app的基本路徑爲:/sub 而在子app中有註冊get:/:id/getuser路由,二者連起來造成path:/sub/:id/getuser。因此在router的handle中,將url分割稱兩部分:/sub , /12/getuser

/** * 遍歷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
  // 3:修改 對req調用handle時的初始值進行保存,返回處理函數,以便隨時恢復初始值
  let done = restore(out, req, 'baseUrl', 'next', 'params')
  let paramcalled = {}
  // 4:新增 用於存放url中和中間件中的path相匹配的部分
  let removed = ''
  // 4:新增 在移除中間件部分以後,是否給url加過 /
  let slashAdded = false
  // 4:新增 若是是子路由,或者子app 會存在父app的params
  let parentPrarms = req.params
  // 4:新增 若是是子路由,或者子app url 存於baseUrl中
  let parentUrl = req.baseUrl || ''
  req.next = next

  req.baseUrl = parentUrl
  req.originalUrl = req.originalUrl || req.url
  next() //第一次調用next
  function next(err) {
    let layerError = err === 'route'
      ? null
      : err
    // 4:新增 若是添加過 / 則移除
    if (slashAdded) {
      req.url = req
        .url
        .substr(1)
      slashAdded = false
    }
    // 4:新增 若是移除過中間件匹配到的部分,則還原
    if (removed.length !== 0) {
      req.baseUrl = parentUrl
      req.url = removed + req.url
      removed = ''
    }

    if (layerError === 'router') { //若是錯誤存在,再當前任務結束前調用最終處理函數
      setImmediate(done, null)
      return
    }

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

    // 3: 新增path ,用於獲取除query以外的path
    let path = getPathname(req)
    if (!path) {
      return done(layerError)
    }
    let layer
    let match
    let route
    while (match !== true && idx < stack.length) { //從數組中找到匹配的路由
      ...
    }
    if (match !== true) { // 循環完成沒有匹配的路由,調用最終處理函數
      return done(layerError)
    }
    req.params = mixin(parentPrarms || {}, layer.params) // 將解析的‘/get/:id’ 中的id剝離出來
    // 4:新增
    let layerPath = layer.path

    // 3:新增,主要是處理app.param
    self.process_params(layer, paramcalled, req, res, function (err) {
      ...
      // 3:新增,加入handle_error處理
      trim_prefix(layer, layerError, layerPath, path)
    })
  }

  function trim_prefix(layer, layerError, layerPath, path) {
    if (layerPath.length !== 0) {
      let c = path[layerPath.length]
      if (c && c !== '/' && c !== '.') 
        return next(layerError)
        // 4:新增 移除中間件中帶的path,在父子app中,剝離出子app須要匹配的url 經過req帶入子app的handle中
      removed = layerPath
      req.url = req
        .url
        .substr(removed.length)
      if (req.url[0] !== '/') {
        req.url = '/' + req.url
        slashAdded = true
      }
      req.baseUrl = parentUrl + (removed[removed.length - 1] === '/'
        ? removed.substr(0, removed.length - 1)
        : removed)
    }
    if (layerError) {
      layer.handle_error(layerError, req, res, next)
    } else {
      layer.handle_request(req, res, next)
    }
  }

}

複製代碼

對於錯誤中間件的處理,主要是放在layer.js中,當出現錯誤的時候,將error傳給next,若是layerError存在就走錯誤邏輯

// router
proto.handle = function handle(req, res, 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
    }

    // 3: 新增path ,用於獲取除query以外的path
    let path = getPathname(req)
    if (!path) {
      return done(layerError)
    }
   ...
    if (match !== true) { // 循環完成沒有匹配的路由,調用最終處理函數
      return done(layerError)
    }
    req.params = mixin(parentPrarms || {}, layer.params) // 將解析的‘/get/:id’ 中的id剝離出來
    // 4:新增
    let layerPath = layer.path

    // 3:新增,主要是處理app.param
    self.process_params(layer, paramcalled, req, res, function (err) {
      if (err) {
        return next(layerError || err)
      }
      if (route) {
        //調用route的dispatch方法,dispatch完成以後在此調用next,進行下一次循環
        return layer.handle_request(req, res, next)
      }
      // 3:新增,加入handle_error處理
      trim_prefix(layer, layerError, layerPath, path)
    })
  }

  function trim_prefix(layer, layerError, layerPath, path) {
    if (layerPath.length !== 0) {
      let c = path[layerPath.length]
      if (c && c !== '/' && c !== '.') 
        return next(layerError)
    ...
    }
    if (layerError) {
      layer.handle_error(layerError, req, res, next)
    } else {
      layer.handle_request(req, res, next)
    }
  }
}

// layer
/** * 3:新增 加入handle_error的處理 * @param {*} err 錯誤信息 * @param {*} req * @param {*} res * @param {*} next */
Layer.prototype.handle_error = function handle_error(err, req, res, next) {
  let fn = this.handle
  if (fn.length !== 4) {
    return next(err)
  }
  try {
    fn(err, req, res, next)
  } catch (err) {
    next(err)
  }
}
複製代碼

exammple/index.js 在入口文件中加入了一些新的測試用例

let router = express.Router({mergeParams: true})
router.get('/getname/:like', function (req, res, next) {
  res.end(JSON.stringify(req.params))
})
app.use('/:userId', router)
let subApp = express()
subApp.get('/getuser', function (req, res, next) {
  res.end(JSON.stringify(req.params))
})
app.use('/sub/:id', subApp)
複製代碼

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

// 4:新增 測試get: /:userId/getname/:like
  it('GET /:userId/getname/:like', (done) => {
    request
      .get('/12/getname/ll')
      .expect(200)
      .end((err, res) => {
        if (err) 
          return done(err)
        let params = JSON.parse(res.text)
        assert.equal(params.userId, '12', 'res.text must has prototype userId and the value must be 12') // 通過use方法處理後的test爲once+ use = once use
        assert.equal(params.like, 'll', 'res.text must has prototype like and the value must be ll')
        done()
      })
  })

  // 4:新增 測試get: /:userId/getname/:like
  it('GET /sub/:id/getuser', (done) => {
    request
      .get('/sub/13/getuser')
      .expect(200)
      .end((err, res) => {
        if (err) 
          return done(err)
        let params = JSON.parse(res.text)
        assert.equal(params.id, '16', 'res.text must has prototype id and the value must be 13')
        done()
      })
  })

複製代碼

test測試結果以下:

回顧總體結構

寫在最後

這節對應的邏輯相對來講比較簡單,主要是對之前的邏輯進行完善處理。讓router獨立出來,可作中間件。讓app亦可獨立做爲中間件應用於另外一個app中。這樣就造成了嵌套關係。此次迭代完成以後,算是把express的主要邏輯造成了一個完整的鏈條。以後的功能能夠說是圍繞當前的數據結構作存取,總體的數據結構不會發生大的變化。對於express的解讀想暫時寫到此,若是後面有時間,就寫一下模版的渲染和req,res的封裝。

相關文章
相關標籤/搜索