美文网首页
Express源码分析

Express源码分析

作者: 我叫Aliya但是被占用了 | 来源:发表于2019-08-03 01:50 被阅读0次

官网

简单用法

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()
}

完整代码

相关文章

网友评论

      本文标题:Express源码分析

      本文链接:https://www.haomeiwen.com/subject/dtsurctx.html