上一篇軟文主要是介紹了項目的搭建,實現了一個基本的框架。引入 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接口