• 由浅入深理解express源码(三)


     

    回顾

    上一次的迭代中,主要是实现了简化版的router,并对/get/:id 式的路由进行解析。同时实现app.Methods相应的接口

    实现目标

    git:github.com/kaisela/mye…

    gitee: gitee.com/kaisela/mye…

    本次迭代主要是实现了app.param,app.use,以及req.query中参数的提取工作。其实在本次迭代中app.param和query足以形成一个迭代,再加上app.use内容就比较多,不过我还是将它们放在一个迭代中,还请读者多费些时间去理解。因为理解到这一层了,express的真面目已经揭开一大半了

    项目结构

    express3
      |
      |-- lib
      |    |-- middleware // 新增 中间件文件夹
      |    |    |-- query.js // 新增 实现req.query提取的中间件
      |
      |    |-- 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配置文件
    
    复制代码

    重要概念引入

    中间件

    在express中,中间件其实是一个介于web请求来临后到调用处理函数前整个流程体系中间调用的组件。其本质是一个函数,内部可以访问修改请求和响应对象,并调整接下来的处理流程。

    express官方给出的解释如下:

    Express 是一个自身功能极简,完全是由路由和中间件构成一个的 web 开发框架:从本质上来说,一个 Express 应用就是在调用各种中间件。

    中间件(Middleware) 是一个函数,它可以访问请求对象(request object (req)), 响应对象(response object (res)), 和 web 应用中处于请求-响应循环流程中的中间件,一般被命名为 next 的变量。

    中间件的功能包括:

    • 执行任何代码。
    • 修改请求和响应对象。
    • 终结请求-响应循环。
    • 调用堆栈中的下一个中间件。

    如果当前中间件没有终结请求-响应循环,则必须调用 next() 方法将控制权交给下一个中间件,否则请求就会挂起。

    Express 应用可使用如下几种中间件:

    使用可选则挂载路径,可在应用级别或路由级别装载中间件。另外,你还可以同时装在一系列中间件函数,从而在一个挂载点上创建一个子中间件栈。

    所以对于迭代二来说Router和Route类中的this.stack属性内部的每个handle都是一个中间件,根据使用接口不同区别了应用级中间件路由级中间件,而四个参数的处理函数就是错误处理中间件,对于内置中间件我们暂时还未涉及,而app.use接口将要实现的就是嵌入第三方中间件

    在express中的中间件其实和java中面相切面编程中的拦截器的作用基本一致。可以在某一类接口调用之前,使用中间件做统一处理。比如:app.param 也是一种中间件,只是它针对的只是对参数处理。而use和router都是针对请求路径来处理。

    问题分析

    本次迭代主要是实现了app.param,app.use,以及req.query中参数的提取工作

    app.use的官方api

    app.use(function(req, res, next) { // 将会拦截所有请求
      res.send('Hello World');
    });
    app.use('/abcd', function (req, res, next) { // 将会拦截路径为 /abcd 的请求
      next();
    });
    app.use('/abc?d', function (req, res, next) { // 将会拦截路径为 /abcd 和 /abd 的请求
      next();
    });
    app.use(//abc|/xyz/, function (req, res, next) { // 将会拦截路径为 /abc 和 /xyz 的请求
      next();
    });
    复制代码

    以上为app.use的一些用法示例,由于use方法和router的参数很相似,只是少了method这个变量。所以在express的源码中,use方法注册的中间件的数据结构将使用router的第一层(Router)中的stack存储,只是use注册的Layer中少了route对象

    app.param的官方api

    app.param('id', function (req, res, next, id) { // 当注册路由为 .../:id/...形式时会被此中间件拦截
      console.log('CALLED ONLY ONCE');
      next();
    });
    
    app.param(['id', 'page'], function (req, res, next, value) { // 拦截含有id 或者 page参数的路由请求
      console.log('CALLED ONLY ONCE with', value);
      next();
    });
    
    // 以上两种方式的另一种写法,二者选其一 ,文中和测试用例中我们以上一种为例
    app.param(function(param, option) {
      return function (req, res, next, val) {
        if (val == option) {
          next();
        }
        else {
          next('route');
        }
      }
    });
    
    // using the customized app.param()
    app.param('id', 1337);
    
    复制代码

    param的方法的结构就较为简单,分为参数param和callback两种,其二者的关系为一对多的关系,在express的源码中实现是放在Router类中,数据结构由params对象和_params数组两种方式存储,第一种书写方式只需要用到params对象,第二种书写方式则是后面所有的param注册,都是使用前面return的中间件函数。此文中对第二种书写方式不做详解,请自行看源码理解

    Router中params的结构为{param:[fn,fn...]}

    req.query

    主要是对请求路径中的query部分进行解析,主要使用的方法为parseurl,querystring.parse。url转换后的结构示例如下

    url.parse (user:pass@host.com:8080/users/user.…);

    属性名
    href user:pass@host.com:8080/users/user.…
    protocol http
    slashes true
    host host.com:8080
    auth user:pass
    hostname host.com
    port 8080
    pathname /users/user.php
    search ?userName=Lulingniu&age=40&sex=male
    path /users/user.php?userName=Lulingniu&age=40&sex=male
    query userName=Lulingniu&age=40&sex=male
    hash #namel

    数据结构

     --------------                                     ----- ----------
    | Application  | ------------------------------->   | params       |                         
    |     |        |        ----- -------------         |   |-param    |
    |     |-router | ----> |     | Layer       |        |   |-callbacks|
    --------------        |  0  |   |-path     |        ----- ----------
     application          |     |   |-callbacks|           router
                          |-----|--------------|      
                          |     | Layer        |                     
                          |  1  |   |-path     |                                  
                          |     |   |-callbacks|
                          |-----|--------------|       
                          |     | Layer        |
                          |  2  |   |-path     |
                          |     |   |-callbacks|
                          |-----|--------------|
                          | ... |   ...        |
                           ----- -------------- 
                                router
    复制代码

    对于query的实现,其实就是在所有路由注册前面加上了一个处理query的中间件,和中间件的结构图一样,只是这里的中间件是一个特定的函数

    代码解析

    此次迭代中新增的代码比较多,也比较零碎,因此我在文件的注释中前面加了一个“迭代编号:新增”的字样,来表示此段代码是在此迭代中新增的。

    app.use

    application.js中新增use接口,主要是调用router中的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
        }
      }
      let fns = 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) {
        router.use(path, fn)
      })
    }
    复制代码

    router中新增use方法,主要是完成对中间件的注册,在handle中遍历

    /**
     * 3:新增 主要用于注册路由相关的中间件,此迭代中,在注册query中间件中使用到
     * @param {*} fn
     */
    proto.use = function use(fn) {
      let path = '/'
      let offset = 0
      // 为app.use 接口准备,第一个参数可能时路径的正则表达式
      if (typeof fn !== 'function') {
        let arg = fn
        while (Array.isArray(arg) && arg.length != 0) {
          arg = arg[0]
        }
        if (typeof arg !== 'function') {
          offset = 1
          path = arg
        }
      }
    
      let callbacks = slice.call(arguments, offset)
      if (callbacks.length === 0) {
        throw new TypeError('Router.use() requires a middleware function')
      }
    
      // 将中间件加入到stack栈中,方便handle函数遍历中执行
      for (let i = 0; i < callbacks.length; i++) {
        let fn = callbacks[i]
        if (typeof fn !== 'function') {
          throw new TypeError('Router.use() requires a middleware function but not a ' + gettype(fn))
        }
        let layer = new Layer(path, {
          strict: false,
          end: false
        }, fn)
        layer.route = undefined
        this
          .stack
          .push(layer)
      }
    }
    复制代码

    application.js中新增param接口,主要是调用router中的param方法

    /**
     * 3:新增 实现app的param接口
     * @param {*} name 参数名称 可以是数组 或者 字符串
     * @param {*} fn 需要处理的中间件
     */
    app.param = function param(name, fn) {
      this.lazyrouter()
      // 如果name是数组时,分割调用自身
      if (Array.isArray(name)) {
        for (let i = 0; i < name.length; i++) {
          this.param(name[i], fn)
        }
        return this
      }
      this
        ._router
        .param(name, fn)
      return this
    }
    复制代码

    router中新增param方法,主要是完成对param中间件的注册,在handle中处理

    /**
     * 3:新增 对传过来的参数进行拦截,将参数拦截相关存入到params中,在handle中进行分解执行
     */
    proto.param = function param(name, fn) {
      if (typeof name === 'function') {
        this
          ._params
          .push(name)
        return
      }
      if (name[0] === ':') {
        name = name.substr(1)
      }
      let params = this._params
      let len = this._params.length
      let ret
      for (let i = 0; i < len; i++) {
        if (ret = params[i](name, fn)) {
          fn = ret
        }
      }
      (this.params[name] = this.params[name] || []).push(fn)
    }
    复制代码

    router中的handle方法中新增对use中间件的遍历逻辑,主要是通过是否有route来判断。新增process_params方法对params对象的处理,主要是和layer.keys进行比较,匹配到的时候逐个执行param所对应的callbacks。在process_params中使用param递归遍历keys,使用paramCallback的递归对param对应的callbacks进行遍历。这里就不具体贴代码了,大家自行移步git看代码

    /**
     * 遍历stack数组,并处理函数, 将res req 传给route
     */
    
    proto.handle = function handle(req, res, out) {
      ...
      next() //第一次调用next
      function next(err) {
        ...
        // 3:修改 对req调用handle时的初始值进行保存,返回处理函数,以便随时恢复初始值
          let done = restore(out, req, 'baseUrl', 'next', 'params') 
        ...
        // 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) { //从数组中找到匹配的路由
          layer = stack[idx++]
          match = matchLayer(layer, path)
          route = layer.route
          if (typeof match !== 'boolean') {
            layerError = layerError || match
          }
    
          if (match !== true) {
            continue
          }
          // 3:新增,原逻辑中不可能存在route没有的情况,在3中加入中间件,其route为undefined
          if (!route) {
            continue
          }
        ...
        }
        if (match !== true) { // 循环完成没有匹配的路由,调用最终处理函数
          return done(layerError)
        }
        req.params = Object.assign({}, layer.params) // 将解析的‘/get/:id’ 中的id剥离出来
        // 3:新增,主要是处理params
        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, '', 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)
        }
      }
    
    }
    复制代码

    restore方法为一个高阶函数,主要作用是对一个对象的初始值进行存储,在返回的函数中以便随时恢复

    /**
     * 3:新增 对obj对象的一些属性进行恢复出厂设置
     * @param {*} fn 恢复值之后需要调用的函数
     * @param {*} obj 需要恢复值的对象
     * @param {*}  augments[i+2] obj需要恢复的属性
     */
    function restore(fn, obj) {
      let props = new Array(arguments.length - 2)
      let vals = new Array(arguments.length - 2)
    
      // 保存函数调用时,obj对应属性的值
      for (let i = 0; i < props.length; i++) {
        props[i] = arguments[i + 2]
        vals[i] = obj[props[i]]
      }
    
      return function () {
        // 调用函数时,对obj属性值进行恢复
        for (let i = 0; i < props.length; i++) {
          obj[props[i]] = vals[i]
        }
        fn.apply(this, arguments);
      }
    
    }
    复制代码

    还有一个是query中间件介绍,在utils中通过compileQueryParser来确定querysting调用的是那个方法,默认值是在qs和querystring中做选择,当然你也可以自己写处理方法。在路由初始化的时候进行中间件的注册

    
    /**
     * 对路由实现装载,实例化
     */
    app.lazyrouter = function () {
      if (!this._router) {
        this._router = new Router()
        // 3:新增 注册处理query的中间件
        this
          ._router
          .use(query(this.get('query parser fn')))
      }
    }
    
    /**
     * 3:新增 处理req.url query部分的中间件
     */
    let merge = require('utils-merge')
    let parseUrl = require('parseurl')
    let qs = require('qs')
    
    module.exports = function query(options) {
      let opts = merge({}, options)
      let queryparse = qs.parse
    
      if (typeof options === 'function') {
        queryparse = options
        opts = undefined
      }
    
      if (opts !== undefined && opts.allowPrototypes === undefined) {
        opts.allowPrototypes = true
      }
    
      return function query(req, res, next) {
        if (!req.query) {
          let val = parseUrl(req).query
          req.query = queryparse(val, opts)
        }
        next()
      }
    }
    复制代码

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

    // 3:新增 输出传入的id,和name时拦截处理参数
    app.param([
      'id', 'name'
    ], function (req, res, next, val, name) {
      if (name == 'id') {
        req.params.id = ((val - 0) + 3) + ''
      }
      if (name == 'name') {
        req.params[name] = req.params[name] + ' param'
      }
      next()
    })
    // 3:新增 当路径为/get 时拦截处理query
    app.use('/get', function (req, res, next) {
      for (key in req.query) {
        req.query[key] = req.query[key] + ' use'
      }
      next()
    })
    
    // 测试param处理id ,name
    app.post('/user/:id/:name', function (req, res) {
      res.end(JSON.stringify(req.params))
    })
    
    // 测试param处理id
    app.post('/user/:id', function (req, res) {
      res.end(JSON.stringify(req.params))
    })
    
    // 测试param处理name
    app.post('/name/:name', function (req, res) {
      res.end(JSON.stringify(req.params))
    })
    
    app.get('/get', function (req, res) {
      res.end(JSON.stringify(req.query))
    })
    // 输出传入的id
    app.get('/get/:id', function (req, res) {
      res.end(`{"id":${res.params.id}}`)
    })
    复制代码

    test/index.js 测试exapmles中的代码,验证是否按照地址的不同,进了不同的回调函数

    
      // 测试get: /get 带query
      it('GET /get', (done) => {
        request
          .get('/get?test=once')
          .expect(200)
          .end((err, res) => {
            if (err) 
              return done(err)
            let params = JSON.parse(res.text)
            assert.equal(params.test, 'once use', 'res.text must has prototype test and the value must be once use') // 经过use方法处理后的test为once+ use = once use
            done()
          })
      })
    
      // 如果走的不是examples中的post:/user/:id/:name 测试不通过
      it('POST /user/12/kaisela', (done) => {
        request
          .post('/user/12/kaisela')
          .expect(200)
          .end((err, res) => {
            if (err) 
              return done(err)
            let params = JSON.parse(res.text)
            assert.equal(params.id, '15', 'id must be 15') // 经过param方法处理后的id为12+3 = 15
            assert.equal(params.name, 'kaisela param', 'name must be kaisela param')
            // 经过param方法处理后的id为kaisela+ param = kaisela param
            done()
          })
      })
    
      // 如果走的不是examples中的post:/user/:id测试不通过
      it('POST /user/17', (done) => {
        request
          .post('/user/17')
          .expect(200)
          .end((err, res) => {
            if (err) 
              return done(err)
            let params = JSON.parse(res.text)
            // 经过param方法处理后的id为17+3 = 20
            assert.equal(params.id, '20', 'id must be 20')
            done()
          })
      })
    
      // 如果走的不是examples中的post:/name/:name测试不通过
      it('POST /name/ke', (done) => {
        request
          .post('/name/ke')
          .expect(200)
          .end((err, res) => {
            if (err) 
              return done(err)
            let params = JSON.parse(res.text)
            // 经过param方法处理后的id为ke+ param = ke param
            assert.equal(params.name, 'ke param', 'name must be ke param')
            done()
          })
      })
    
    复制代码

    test测试结果如下:

    写在最后

    到此为止,express的两个比较重要的功能算是基本完成,虽然还有很多细节要完善。对于use方法可以路由嵌套的功能也许还要花一个篇幅讲解,看后面的时间吧。还有request,response的封装,模版引擎以及错误处理中间件。尤其是模版引擎,目前算是一点未引入

    下期预告

    完善router,实现错误处理中间件 和use更多用法实现

    由浅入深理解express源码(一)

    由浅入深理解express源码(二)

    由浅入深理解express源码(三)

    由浅入深理解express源码(四)

  • 相关阅读:
    不同权限访问详细细节
    文件搜索器
    java中依赖、关联、聚合
    子类重写父类成员方法
    java中子类对父类中的私有变量和私有方法有拥有权无使用权
    shell脚本在CentOS7自动更包
    LR虚拟用户已设置集合点,但controller无法设置集合点策略的解决方案
    LR工具使用之场景设置
    LR集合点策略
    LR录制附件上传后,回放报错
  • 原文地址:https://www.cnblogs.com/kaisela/p/12486202.html
Copyright © 2020-2023  润新知