Express 源码分析-脉络梳理

Http 搭建服务 vs Express 搭建服务

Http 搭建服务

通过 http 我们可以快速搭建起一个服务器。

ini 复制代码
const http = require("http");
const routes = [
  {
    url: "/",
    handle(req, res) {
      res.writeHead(200, {
        "Content-type": "application/json",
      });
      res.end(
        JSON.stringify({
          code: 1000,
          message: "成功",
        })
      );
    },
  },
  {
    url: "/list",
    handle(req, res) {
      res.writeHead(200, {
        "Content-type": "application/json",
      });
      res.end(
        JSON.stringify({
          code: 1000,
          message: "成功",
          data: [
            { name: "张三", age: 12 },
            { name: "李四", age: 15 },
          ],
        })
      );
    },
  },
];
const server = http.createServer((req, res) => {
  const url = req.url;
  const targetRoute = routes.find((item) => item.url === url);
  if (targetRoute) {
    targetRoute.handle(req, res);
  } else {
    res.end("NOT FOUND " + req.method + " " + url);
  }
});
server.listen(3000);

Express 搭建服务

有如下好处

    1. 内置路由管理。
    1. Express 支持多种中间件,如 body-parsermorgancors 等,可以轻松地处理请求和响应。扩展性和可维护性更好。
    1. 提供丰富的 HTTP 实用工具,如 res.json()res.send()res.redirect() 等,简化了响应处理。使得开发更加方便。
javascript 复制代码
const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.json({
    code: 1000,
    message: "成功"
  });
});

app.get('/list', (req, res) => {
  res.json({
    code: 1000,
    message: "成功",
    data: [
      { name: "张三", age: 12 },
      { name: "李四", age: 15 }
    ]
  });
});

// 日志记录中间件
app.use((req, res, next) => {
  console.log('Request received for path:', req.url);
  next();
});

// 身份验证中间件
app.use((req, res, next) => {
  if (req.headers.authorization === 'Bearer valid-token') {
    next();
  } else {
    res.status(401).send('Unauthorized');
  }
});

// 错误处理中间件
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send("Internal Server Error");
});;

app.listen(3000, () => {
  console.log('Server is running on http://localhost:3000');
});

Express 模块总览

来梳理下 Express 各个模块,本章不会涉及代码实现细节,会先把 Express 初始化过、响应请求的相关流程梳理清楚。下章会针对具体的代码实现。来看下源码各个模块都有哪些功能。

css 复制代码
express/
├── lib/
│   ├── application.js // app 方法挂载例如: app.init()、app.use()、app.handle()等
│   ├── express.js     // 入口 app.init() 初始化
│   ├── request.js     // 请求体属性拦截,增加 req.query、req.path、req.host、req.ip 等
│   ├── response.js    // 响应体方法挂载, res.json()、res.status()、res.send()、res.header 等
│   ├── utils.js
│   └── view.js        // 模板引擎相关

内置路由

首先来看下一张总的关系图,其中 Router、Layer、Route 都是路由系统中重要的概念。

【图1】

给出相关代码

javascript 复制代码
var app = module.exports = express()

app.get('/', function middleware1(req, res, next) {
  console.log('Middleware 1');
  next();
}, function helloWorld(req, res){
  res.send('Hello World');
});

app.get('/foo', function log(req, res) {
  const params = req.params;
  const host = req.host;
  const query = req.query;
  console.log(params, host, query);
  res.end('foo')
})

/* istanbul ignore next */
if (!module.parent) {
  app.listen(3000);
  console.log('Express started on port 3000');
}

接下来目标就是把上面这个例子讲明白,就对 Express 中内置路由有大概的了解了。

Router 对象

Router 实例结构如下

router.stack 对应【图1】外层的 stack 部分。当我们每次调用 app.get() 时候就会往 router.stack 推送一个 Layer 实例对象。回调函数可以支持多个,对应的就是 middle1Fn、middle2Fn。

scss 复制代码
app[method](path, middle1Fn, middle2Fn) // 这里的 method 常见有 get 请求、post 请求
总结

Router 对象就是维护所有路由的容器。具体路由信息会存放在 router.stack 中。

Layer 对象

Layer 实例结构如下

前面说过,router.stack 中存放的就是一个个 Layer 实例对象。layer 对象上有以下几个重要的属性。

  • matchers 维护了匹配规则,判断 layer 对象是否匹配某个路径
  • route 对象,维护了 layer 对象和一个 route 对象关联关系,正是同个这个关联关系,layer 对象可以找到 middle1Fn、middle2Fn回调中间件函数。
总结
  • 外层 Layer 主要维护了 Route 关系。
  • 通过 matchers 属性判断路由是否和 layer 是否匹配,如果匹配它会找到与他关联的 Route 实例对象。
  • 通过 handle 调用 route dispatch 调度函数,来遍历 route stack 中的 layer 对象。

Route 结构

Route 实例结构如下

外层的 layer 会指向一个 Route 实例对象,route对象通过 stack 维护一个 Layer 列表。这里可能会有人疑惑了???怎么这里又出现了 Layer 对象。

其实 Layer 对象你可以理解为一个数据结构。

  • 外层的 Layer 实例有这样几个关键属性 matchers、route 、handle 对应的是 route dispatch调度函数。
  • 内层的 Layer 实例和外层的区别是,有 method、(name 指向中间件回调函数名)、没有 route 对象。 handle 对应的就是 middleware1 这样的回调函数。所以内存的 layer 就是实际处理路由回调函数的地方。
总结
  1. 你可以通过 router.stack 找到你匹配的外层 layer 对象。一次请求只会匹配一个路由,如果你定义了两个相同的路由,后面的会被忽略

  2. 然后通过 layer.route 找到 route 对象

  3. route 对象通过 dispatch 调度函数遍历 stack 数组,取出 layer 对象,判断是否符合路由和方法进行执行

    • dispatch 调度函数中核心就是实现了一个 next() 函数。该函数会遍历 route.stack 数组
  4. 内层 layer 对象就是对应 app.get('/', function middleware1(req, res, next) {}, function helloWorld() {}) middleware1 和 helloWorld 这两个中间件回调函数

  5. 调用内层 layer 对象中的 handle 函数相当于执行了 middleware1 中间件回调函数,middleware1 中执行 next() 函数继续遍历 route.stack 数组中的下一个 layer

中间件

Express是一个路由和中间件web框架,它自己的功能很少:Express应用程序本质上是一系列中间件函数调用。

中间件函数是可以访问请求对象(req)、响应对象(res)以及应用程序请求-响应周期中的 next 中间件函数。

Express应用程序可以使用以下类型的中间件:

  • 应用程序中间件
  • 路由器级别中间件
  • 错误处理中间件
  • 内置的中间件
  • 第三方中间件

中间件基本使用

php 复制代码
var express = require('express')
var app = express()
// 应用中间件
app.use(function (req, res, next) {
  console.log('Time:', Date.now())
  next()
})


// 内置中间件
// 设置静态文件目录
app.use(express.static('public'));

// 解析 URL 编码的请求体
app.use(express.urlencoded({ extended: true }));

// 路由中间键
router.get('/user/:id', function (req, res) {
  res.send('User Info')
})

// 错误中间件
app.use(function (err, req, res, next) {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

中间件源码梳理

有了上面路由系统的认识后看中间件就简单多了

  1. 使用 app.use(middleFn) 后也会根据这个回调去创建一个外层的 layer
  2. 创建的 layer 有个重要的属性 slash 为 true 表示他可以匹配任意路径
  3. 当我们访问路由时候,还是会去调用Router.handle 中的 next() 函数去匹配对应外层的 layer 对象
  4. 中间件的 layer 对象会匹配任意路由,所以它会直接 app.use() 传入的回调函数
  5. 回调函数中执行了 next() 方法,该方法就是继续遍历 router.stack, 寻找下一个匹配的 layer 对象。如果是中间件的 layer则执行回调。如果是 app.[method] 注册的,则会遍历 route.stack 执行对应的逻辑

可以看到如果使用 app.use(middleFn1, middleFn2) 会在 router.stack 栈中加入两个 layer 对象。效果就像执行了两次 router.get('/', fn) 。只不过这个 layer 可以匹配任意路径和方法。

接口访问流程图。

总结

本文梳理了 Express 最核心的路由和中间件源码流程,剩下还有 request、response、模板引擎这些会下下一篇源码细节中再探讨 # Express 源码分析-代码讲解

相关推荐
码蜂窝编程官方10 分钟前
【含开题报告+文档+PPT+源码】基于SpringBoot+Vue的虎鲸旅游攻略网的设计与实现
java·vue.js·spring boot·后端·spring·旅游
gqkmiss10 分钟前
Chrome 浏览器 131 版本开发者工具(DevTools)更新内容
前端·chrome·浏览器·chrome devtools
Summer不秃16 分钟前
Flutter之使用mqtt进行连接和信息传输的使用案例
前端·flutter
旭日猎鹰20 分钟前
Flutter踩坑记录(二)-- GestureDetector+Expanded点击无效果
前端·javascript·flutter
Viktor_Ye26 分钟前
高效集成易快报与金蝶应付单的方案
java·前端·数据库
hummhumm28 分钟前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
J老熊38 分钟前
JavaFX:简介、使用场景、常见问题及对比其他框架分析
java·开发语言·后端·面试·系统架构·软件工程
AuroraI'ncoding1 小时前
时间请求参数、响应
java·后端·spring
乐闻x1 小时前
Vue.js 性能优化指南:掌握 keep-alive 的使用技巧
前端·vue.js·性能优化
好奇的菜鸟1 小时前
Go语言中的引用类型:指针与传递机制
开发语言·后端·golang