由浅入深理解express源码(代码片段)

kaisela kaisela     2023-04-12     340

关键词:

 

回顾

上次迭代主要是实现了app.param,app.use,以及req.query中参数的提取工作。内容较多,篇幅也较长。

实现目标

git: github.com/kaisela/mye…

本次主要是完善router,实现错误处理中间件 和use更多用法实现。其实在上一次迭代的代码中已经完成了错误中间件的逻辑,但是由于上次迭代的篇幅较长,所以就放到这次的迭代中讲诉。对use则是加上了use子模块和router模块的实现。

项目结构

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使用。以及错误中间件的完善工作

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实现过程中,做了一些小小的包装处理。

错误中间件

app.use(function (err, req, res, next) 
  console.error(err.stack)
  res.status(500).send(‘Something broke!‘)
)
复制代码

错误处理是指如何表示同步和异步发生的捕获和处理错误。Express附带了一个默认的错误处理程序-->finalhandler。如果您将错误传递给next(),并且没有在自定义错误处理程序中处理它,那么它将由内置的错误处理程序处理;错误将通过堆栈跟踪写入客户端。

数据结构

 --------------                                     ----- ----------
| 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函数。

代码解析

和上次迭代一样,在文件的注释中前面加了一个“迭代编号:新增”的字样,来表示此段代码是在此迭代中新增的。

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的封装。

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

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

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

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

读node.js源码深入理解cjs模块系统(代码片段)

本文将对Node.js源码进行探索,深入理解cjs模块的加载过程。相信大家都知道如何在Node.js中加载一个模块:constfs=require('fs');constexpress=require('express');constanotherModule=require('./another-modu 查看详情

读node.js源码深入理解cjs模块系统(代码片段)

本文将对Node.js源码进行探索,深入理解cjs模块的加载过程。相信大家都知道如何在Node.js中加载一个模块:constfs=require('fs');constexpress=require('express');constanotherModule=require('./another-modu 查看详情

jvm简单理解,全局观理解(代码片段)

...解是不一样的.本文中说的很浅,适用于第一遍学习,学习JVM由浅入深,先看浅的,深入后面会陆续更新.1.什么是JVM(1) 查看详情

express源码分析之router(代码片段)

express作为nodejs平台下非常流行的web框架,相信大家都对其已经很熟悉了,对于express的使用这里不再多说,如有需要可以移步到www.expressjs.com自行查看express的官方文档,今天主要是想说下express的路由机制。最近抽时间看了下expres... 查看详情

java泛型-基础理解(代码片段)

...用到泛型,所以基于自己的理解,拆分几篇文章由浅入深记录一下。为什么需要泛型?泛型这个概念,是在JDK1.5引进来的,其实可以把它理解成一个语法糖,它解决的是什么问题呢?举个栗子:假... 查看详情

express中的中间件理解(代码片段)

express是轻量灵活的node.jsWeb应用框架”。它可以帮助你快速搭建web应用。express是一个自身功能极简,完全是由**路由**和**中间件**构成的一个web开发框架,本质上说,一个express应用就是在调用各种中间件。路由想必大家都有一定... 查看详情

由浅入深聊聊golang的sync.pool(代码片段)

前言今天在思考优化GC的套路,看到了sync.Pool,那就来总结下,希望可以有个了断。用最通俗的话,讲明白知识。以下知识点10s后即将到来。1.pool是什么?2.为什么需要sync.Pool?3.如何使用sync.Pool?4.走... 查看详情

javascript由浅到深含案例源码(代码片段)

1.什么是JavaScript1.1.JavaScript概念一门客户端脚本语言*运行在客户端浏览器中的。每一个浏览器都有JavaScript的解析引擎*脚本语言:不需要编译,直接就可以被浏览器解析执行了1.2.JavaScript功能可以来增强用户和html页面的交... 查看详情

深入理解jvm-分代的基本概念(代码片段)

前言​本次讲述jvm分代模型的基础概念,这个专栏会由浅入深的不断构建起来,循序渐进,是非常基础的内容概述:讲述JVM的基础分代模型以及版本升级的处理。对象分配的基础概念和知识。长期存活的对象是如... 查看详情

前端也能学算法:由浅入深讲解动态规划(代码片段)

...他的核心思想,并且多多练习还是可以掌握的。下面我们由浅入深的来讲讲动态规划。斐波拉契数列首先我们来看看斐波拉契数列,这是一个大家都很熟悉的数列://f=[1,1,2,3,5,8]f(1)=1;f(2)=1;f(n)=f(n-1)+f(n-2);//n>2有了上面的公式,... 查看详情

前端也能学算法:由浅入深讲解贪心算法(代码片段)

...且很好理解,因为它符合人们一般的思维习惯。下面我们由浅入深的来讲讲贪心算法。找零问题我们先来看一个比较简单的问题:假设你是一个商店老板,你需要给顾客找零n元钱,你手上有的钱的面值为:100元,50元,20元,5元... 查看详情

从源码切入透彻理解android的weight属性

...细节我们却不一定真正深入的进行过理解。今天我们就来由浅入深,从源码中去好好的研究研究这个东西。看看它有哪些可能被我们忽视的地方。以上述书中的案例来说,它的需求很简单,请实现“让一个按钮居中显示, 查看详情

由浅入深学mysql之事务全攻略(代码片段)

...少的一部分知识内容。也是非常重要的技术。本系列教程由浅入深,全面讲解数据库体系。非常适合零基础的小伙伴来学习。全文大约【1707】字,不说废话,只讲可以让你学到技术、明白原理的纯干货!本文带有丰富案例及配图... 查看详情

express的理解(代码片段)

express:基于node.js的前端开发框架,能快速地搭建一个项目的骨架。核心是对http模块的再包装。是一个自身功能极简单,完全由路由和中间件构成的web开发框架,从本质上说,一个express应用是在调用各种中间件。express安装步骤... 查看详情

「游戏引擎浅入浅出」4.着色器(代码片段)

...pp-game-engine-book着色器就是Shader,Shader就是一段GPU程序源码。我们大学就学过的C语言是CPU程序源码,Shader和C语言有很多相似之处,也要写代码、编译、链接。通过 查看详情

「游戏引擎浅入浅出」4.着色器(代码片段)

...pp-game-engine-book着色器就是Shader,Shader就是一段GPU程序源码。我们大学就学过的C语言是CPU程序源码,Shader和C语言有很多相似之处,也要写代码、编译、链接。通过 查看详情

03lifecycle源码分析——《android打怪升级之旅》(代码片段)

...要性不言而喻。本篇文章会从使用到原理依次进阶,由浅入深的剖析Lifecycle。文章目录一、Jetpack是什么二、Lifecycle是什么三、Lifecycle的使用3.1创建观察者3.2注册观察者四、Lifecy 查看详情

18.自定义标签及模板中的使用由浅入深(代码片段)

紧接上文——《17.自定义过滤器及模板中的使用(实战通过自定义过滤器实现内置过滤器lower和cut的功能)》,本文来讲一讲自定义标签!!!自定义标签:源码学习:template.Library().simple_tags():... 查看详情