简单用法
let Express = require('express')
let app = Express()
// app.get('/', (req, res, next) => {
// console.log(1)
// next()
// }, (req, res, next) => {
// console.log(11)
// next()
// }, (req, res, next) => {
// console.log(111)
// res.end('ok')
// next()
// });
// app.all('/', (req, res) => {
// console.log(2)
// res.end('post ok too')
// });
app.use((req, res, next) => {
console.log(1)
res.end('ok')
next()
}, (req, res, next) => {
console.log(11)
next()
}, (req, res, next) => {
console.log(111)
next()
});
app.use('/ha', (req, res, next) => {
console.log('shit')
res.end('ha')
next()
});
app.listen(3000, () => {
console.log('端口3000已监听')
});
- express中的next可以重复调用,有穿透效果 weird, why they do that
- use与get(post等)的区别
1)use不对method做处理,路由会(虽然有all)
2)use会尽可能多匹配路径(/user/center会匹配 /、/user、/user/center 这三个use),路由不会
源码
目录结构大概如下
|- router
|- index.js
|- layer.js
|- route.js
|- application.js
他们分别干的事,可以这么理解
你去电子城工作,你会修手机:app.修('手机', 你)
express就是电子城,里面结构如下
前台(appliction)有个小姐姐(router/index)
小姐姐(router)手里有导购表(_stack),里面写着多个
{ path: 手机, handler: 部门主管(route.js => dispatch)} (layer)
{ path: 电脑, handler: 部门主管(route.js => dispatch)} (layer)
手机部门(route)有
员工表(_stack2)[{ method: 修, handler: 你 }] (layer)
部门主管(dispatch)给员工分配工作
当客户上门时
“小姐姐(router),我手机坏了。”
小姐姐根据导购表(_stack),把客户给部门主管(dispatch)
部门主管根据员工表(_stack2),把客户给 你
你可以不招待客户(调用next方法),部门主管把客户给 员工表上的下个员工,如果店里没人了,部门主管把客户还给小姐姐,小姐姐把客户给 导购表上的另家店,如果没有店招待客户,小姐姐说 “cannot 手机 修”
以上是路由(get\post...)的逻辑,use可以这么理解
电子城特约专家,专家了解和手机相关的一切:app.use('手机', 专家)
前台(appliction)有个小姐姐(router/index)
小姐姐(router)手里有导购表(_stack),专家也在里面
{ path: 手机, handler: 专家 } (layer)
{ path: 电脑, handler: 部门主管(route.js => dispatch)} (layer)
“小姐姐(router),我手机坏了。”
小姐姐根据导购表(_stack),把客户给专家
专家不招待客户(调用next方法)
扯的够多了,上代码
- application.js
let http = require('http')
let Router = require('./router/index')
let methods = require('methods').concat('all')
function Application() {
// this.router = new Router
}
Application.prototype._lazyRoute = function () {
// 为什么要做这个懒加载,完全没用啊,谁创建了应用不监听不使用呀
if (!this.router) {
this.router = new Router
}
}
methods.forEach(item => {
Application.prototype[item] = function (path, ...fns) {
// 不做实际处理,全部交给路由
this._lazyRoute()
this.router[item](path, fns)
}
})
Application.prototype.use = function () {
// 不做实际处理,全部交给路由
this._lazyRoute()
this.router.use(...arguments)
}
Application.prototype.listen = function (port, callback) {
http.createServer((req, res) => {
this._lazyRoute()
// 不做实际处理,全部交给路由
let done = function () {
res.end(`cannot ${req.url} ${req.method}`)
}
this.router.handler(req, res, done)
}).listen(port, callback)
}
module.exports = Application;
- router/index.js
let Layer = require('./layer')
let Route = require('./route')
let methods = require('methods').concat('all')
function Router () {
this._stack = []
}
Router.prototype.route = function (path) {
// 创建path层级
let layer = new Layer
let route = new Route
layer.path = path // path
layer.handler = route.dispatch.bind(route) // 有method层的dispath
layer.route = route // 有method层指针
this._stack.push(layer)
return route
}
methods.forEach(item => {
Router.prototype[item] = function (path, fns) {
let route = this.route(path)
route[item](fns)
}
})
Router.prototype.use = function (path, ...args) {
if (typeof path == 'function') {
args.unshift(path)
path = '/'
}
args.forEach(fn => {
let layer = new Layer
layer.path = path
layer.handler = fn
this._stack.push(layer)
})
}
Router.prototype.handler = function (req, res, out) {
let idx = 0
let next = () => {
if (idx == this._stack.length) return out()
let layer = this._stack[idx++]
if (layer.match(req.url)) {
if (!layer.route) {
// 中间件
layer.handler(req, res, next)
} else {
// 路由
if (layer.route.match_method(req.method)) {
layer.handler(req, res, next) // dispatch
} else {
next()
}
}
} else {
next()
}
}
next()
}
module.exports = Router
- router/route.js
let methods = require('methods').concat('all')
function Route () {
this._stack = []
this.method = {} // (其实一个实例只有一种method)
}
methods.forEach(item => {
Route.prototype[item] = function (fns) {
// 创建method层级
fns.forEach(fn => {
let layer = new Layer
layer.method = item // method(其实实例上有)
layer.handler = fn // 有用户的callback
this._stack.push(layer)
});
this.method[item] = true
}
})
/**
* path层的layer.handler
* @param out path层的next
*/
Route.prototype.dispatch = function (req, res, out) {
let idx = 0
let next = () => {
if (idx == this._stack.length) return out()
let layer = this._stack[idx++]
if (this.match_method(layer.method)) {
// if (layer.called) throw new Error('next只能调用一次')
// else layer.called = true 源码没这判断,WTF
layer.handler(req, res, next) // 用户callback fn
} else {
next()
}
}
next()
}
Route.prototype.match_method = function (method) {
method = method.toLowerCase()
return this.method['all'] || this.method[method]
}
module.exports = Route
- router/layer.js
function Layer () {
this.path // type:string
this.method // type:string
this.handler // type:function
this.route // 仅路由有
}
Layer.prototype.match = function (path) {
if (this.path == path) return true
if (!this.route && path.startsWith(this.path)) {
// 中间件,尽可能多的匹配
return true
}
return false
}
module.exports = Layer
错误处理
express中没有promise和async函数,catch不到异步的错误,所以错误依靠next方法的入参处理。用法如下:
let app = Express()
app.use((req, res, next) => {
console.log('请求来自', req.url)
next('大家都先停下')
});
app.get('/user', (req, res, next) => {
console.log('here is get')
res.end('here is get')
next()
});
app.use('/user', (req, res, next) => {
console.log('here is use')
res.end('here is get')
next()
});
// 错误处理
app.use((err, req, res, next) => {
console.error('出错啦',err)
next()
});
如果没有错误监听处理,next里的错误信息会被end出去
实现
- 路由层根据next入参判断是否出错,错误向外抛out(error)
- 中间件根据next入参判断是否出错,出错依次调用use的Layer中handler_error方法
- handler_error方法,根据this.handler入参是否4个,判断当前中间件是否错误处理中间件,如果是调用,不是next
- 错误监听处理,next里的错误信息会被end出去
为什么错误处理中间件要和正常中间件放一个数组中呢?分开多好,是为了不再多维护一个种类的Layer?
还有,如果错误处理放在了最上面,不会不管用么?
- router/layer.js 添加 handler_error
Layer.prototype.match = function (path) {
if (this.handler.length == 4) return false
...
Layer.prototype.handler_error = function (err, req, res, next) {
// 根据handler入参是否4个,判断当前中间件是否错误处理中间件
if (this.handler.length == 4) {
this.handler(err, req, res, next)
} else {
next(err)
}
}
- router/route.js 的 dispatch 方法
...
let next = (err) => {
// 路由层错误处理,向外抛
if (err) return out(err)
...
- router/index.js 的 handler 方法
...
let next = (err) => {
if (idx == this._stack.length) return out(err)
let layer = this._stack[idx++]
if (err) {
// 错误处理,出错依次调用use的Layer中handler_error方法
if (!layer.route) {
// 中间件
layer.handler_error(err, req, res, next)
} else {
next(err)
}
return false
}
...
- application.js 的 listen 方法中的 done 方法
let done = function (err) {
if (err) {
// 有没有处理的错误
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
res.end(err.stack || err.toString())
} else {
res.end(`cannot ${req.url} ${req.method}`)
}
}
完整代码在这里
二级路径
相当于一个完整的路由系统,当做中间件挂载在父路由上。
智能家居部门自己请了个小哥哥,和小姐姐对接
小哥哥有自己的导购表(_stack)
{ path: 扫地机器人, handler: 二级部门主管(route.js => dispatch)}
{ path: 音响, handler: 二级部门主管(route.js => dispatch)}
。。。
用法:
let subrouter = Express.Router()
subrouter.get('/index', (req, res, next) => {
res.end('this is sub index')
next()
})
app.use('/sub', subrouter)
实现:
- Express静态属性Router是个方法
- 此方法返回一个中间件回调方法
function (req, res, next) {}
- 中间件要push到外层路由(中间件_stack)上
二级路由是一个中间件,中间件内容是一个路由系统(router),匹配时:
- 进内层(route)前,先删除一级路径,如 /sub
- 出来后(route的out),再加上
代码:
- index.js
let Applicaton = require('./application')
let Router = require('./router/index')
function createApplicaton () {
return new Applicaton
}
createApplicaton.Router = Router
module.exports = createApplicaton
- router/index.js
function Router () {
function sub (req, res, next) {
sub.handler(req, res, next)
}
sub._stack = []
sub.__proto__ = proto
// console.log('0000000000', sub.handler)
return sub // 类返回引用类型,此引用类型即类的this指定
}
let proto = Object.create(null)
当类返回引用类型,此引用类型即类的this指定。
这样处理,application.js中当类使new Router()、创建二级路由时当方法使Express.Router(),都行。
但是!好怪,后面尝试一下不这么弄
let idx = 0
let prefix = '' // 路径前缀,用于二级路由
let next = (err) => {
if (idx == this._stack.length) return out(err)
if (prefix) { req.url = prefix + req.url; prefix = ''; }
let layer = this._stack[idx++]
if (err) {
// 错误处理,出错依次调用use的Layer中handler_error方法
if (!layer.route) {
// 中间件
layer.handler_error(err, req, res, next)
} else {
next(err)
}
return false
}
if (layer.match(req.url)) {
if (!layer.route) {
// 中间件
if(layer.path !== '/'){
// 删除路径前缀(中间件的路径),以在二级路由中使用
prefix = layer.path;
req.url = req.url.slice(prefix.length);
}
layer.handler(req, res, next)
} else {
// 路由
if (layer.route.match_method(req.method)) {
layer.handler(req, res, next) // dispatch
} else {
next()
}
}
} else {
next()
}
}
next()
处理 进内层(route)前,先删除一级路径;出来后(route的out),再加上
- router/route.js
methods.forEach(item => {
Route.prototype[item] = function (fns) {
// 二级路由过来的,没有在application中被整合过,不是数组
fns = Array.isArray(fns) ? fns : [fns]
铛铛铛铛,我实现二级路由的方法(非express源码)
- index.js
let Applicaton = require('./application')
let Router = require('./router/index')
function createApplicaton () {
return new Applicaton
}
// createApplicaton.Router = Router
createApplicaton.Router = function () {
function sub (req, res, next) {
sub.handler(req, res, next)
}
sub.__proto__ = new Router()
return sub
}
module.exports = createApplicaton
- router/index.js 只处理 prefix 路径前缀
- router/route.js 处理办法同上
路由参数
用法:
app.get('/user/:id/:name', (req, res, next) => {
res.end('here is get' + JSON.stringify(req.params))
next()
});
app.use('/user/:id/:name', (req, res, next) => {
res.end('here is use' + JSON.stringify(req.params))
next()
});
use可以匹配 /user/123/aliya/xxxx
实现:
使用path-to-regexp
这个包,将绑定的path转化成正则,match时使用正则匹配,匹配到的参数挂载到req上
- router/layer.js
let PathToRegexp = require('path-to-regexp')
function Layer (path, options) {
this.path = path// type:string
this.method // type:string
this.handler // type:function
this.route // 仅路由有
this.param_keys = []
this.regexp = this.path && PathToRegexp(this.path, this.param_keys, options)
// 有options.end就是中间件
this.fast_slash = path === '/' && options.end == false
this.fast_star = path === '*'
}
Layer.prototype.match = function (path) {
if (this.handler.length == 4) return false
if (this.fast_star || this.fast_slash) return true
// 中间件的话,正则不是$结尾的
let vals = path.match(this.regexp)
if (vals) {
this.params = {}
this.param_keys.forEach((key, index) => {
this.params[key.name] = vals[index + 1]
})
}
return vals != null
}
Layer.prototype.handler_error = function (err, req, res, next) {
// 根据handler入参是否4个,判断当前中间件是否错误处理中间件
if (this.handler.length == 4) {
this.handler(err, req, res, next)
} else {
next(err)
}
}
module.exports = Layer
构造函数添加options入参,{end: false}
为中间件,在创建的正则不校验结尾。
- router/index.js
...
Router.prototype.use = function (path, ...args) {
...
// 中间件,指定不校验结尾
let layer = new Layer(path, { end: false })
...
if (layer.match(req.url)) {
req.params = layer.params || {} // 匹配成功后,挂载params
...
本来到这里就行完美了,但是!express源码又开始作妖了
作妖:app.param
用法:
app.param('id', (req, res, next, value, name) => {
console.log(33, value, name) // => 33 '1234' 'id'
next()
})
app.param('id', (req, res, next, value, name) => {
console.log(44, value, name) // => 44 '1234' 'id'
next()
})
param的执行在use和get之前
实现:
可见param是application原型链上的方法,且也是基于发布订阅,有next(又是洋葱模型)
- application.js
...
Application.prototype.param = function () {
this._lazyRoute()
this.router.param(...arguments)
}
...
需要用到路由参数,所以具体处理还得放router
- router/index.js
...
// 构造函数加属性
this.paramsCallback = {} // 订阅的 路由参数回调
...
Router.prototype.param = function (name, fn) { // 订阅路由参数回调
if (this.paramsCallback[name]) {
this.paramsCallback[name].push(fn)
} else {
this.paramsCallback[name] = [fn]
}
}
Router.prototype.process_params = function (req, res, done) { // 订阅路由参数回调
let allnames = Object.keys(this.paramsCallback)
let idx = 0
let next = () => {
if (idx == allnames.length) return done()
let name = allnames[idx++]
let value = req.params[name]
let cbs = this.paramsCallback[name]
if (cbs) {
handler_params_cds(req, res, cbs, next, value, name)
} else {
next()
}
}
next()
}
let handler_params_cds = (req, res, cbs, out, value, name) => {
let idx = 0
let next = () => {
if (idx == cbs.length) return out()
cbs[idx++](req, res, next, value, name)
}
next()
}
...
Router.prototype.handler = function (req, res, out) {
...
// 先执行(发布)路由参数回调
this.process_params(req, res, () => {
layer.handler(req, res, next)
})
...
完整代码在这里
模板引擎和默认中间件
用法:
app.set('views', path.resolve(__dirname, 'views'))
app.set('view engine', 'html') // 默认后缀名
app.engine('.html', require('ejs').__express) // 指定后缀名文件的解析方法
console.log('模板引擎本地路径',app.get('views'))
app.get('/index', (req, res, next) => {
console.log(req.path, req.query) // => /index {id: 123}
res.render('haha', { name: 'haha'})
next()
})
实现:
- application中添加 set、engine方法,处理对应set的get
Application.prototype._lazyRoute = function () {
// 为什么要做这个懒加载,完全没用啊,谁创建了应用不监听不使用呀
if (!this.router) {
this.router = new Router
this.settings = {
ext: {}
}
// 挂载默认中间件
this.init()
}
}
Application.prototype.set = function (key, value) {
this._lazyRoute()
if (arguments.length == 2) {
this.settings[key] = value
} else {
return this.settings[key] // 入参只1个,即为get
}
}
Application.prototype.engine = function (ext, fn) {
this._lazyRoute()
if (!ext.startsWith('.')) ext = '.' + ext.substr
this.settings['ext'][ext] = fn
}
Application.prototype.init = function () {
this._lazyRoute()
// 挂载默认中间件
this.use(require('./middleware/init').bind(this))
this.use(require('./middleware/query').bind(this))
}
- middleware/init.js 模板引擎中间件
let path = require('path')
module.exports = function (req, res, next) {
res.render = (file, data, callback) => {
file = path.parse(file)
if (!file.ext && this.get('view engine')) {
file.ext = '.' + this.get('view engine')
}
let dir = path.join(this.get('views'), file.name + file.ext)
let render = this.get('ext')[file.ext]
// console.log(dir)
render(dir, data, (err, html) => {
if (err) {
return console.error(err)
}
if (callback) {
return callback(html)
} else {
res.end(html)
}
})
}
next()
}
- middleware/query.js
let url = require('url')
module.exports = function (req, res, next) {
let { pathname, query } = url.parse(req.url, true)
req.path = pathname
req.query = query
next()
}
网友评论