掌握Express.js:深度剖析与实战
Express 是 Node.js 中一个非常常用的 web 服务器框架。本质上,框架是一种遵循特定规则的代码结构,它有两个关键特性:
- 它封装了API接口,让开发者更专注于业务逻辑。
- 它建立了流程和标准。
Express框架的核心特性如下:
- 它可以配置中间件以响应各种HTTP请求。
- 它定义了一个路由表,用于执行不同类型HTTP请求的操作。
- 它支持将参数传递给模板,以动态生成HTML页面。
本文将通过实现一个简单的 Express 类似功能来说明路由处理、中间件注册以及 next 机制的实现方式。
即时分析让我们通过两个Express代码例子首先看看它的功能。
你好,世界 示例程序
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(port, () => {
console.log(`示例应用正在 http://localhost:${port} 监听呢`);
});
切换到全屏模式 切换退出全屏模式
分析启动文件 app.js
以下是 express-generator
搭架子时生成的 Express 项目入口文件 app.js
的代码:
// 处理未匹配路由引起的错误
const createError = require('http-errors');
const express = require('express');
const path = require('path');
const indexRouter = require('./routes/index');
const usersRouter = require('./routes/users');
// `app` 是一个 Express 实例
const app = express();
// 视图引擎设置
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
// 解析 POST 请求中的 JSON 数据并将 `body` 字段添加到 `req` 对象中
app.use(express.json());
// 解析 POST 请求中的 urlencoded 数据并将 `body` 字段添加到 `req` 对象中
app.use(express.urlencoded({ extended: false }));
// 处理静态文件
app.use(express.static(path.join(__dirname, 'public')));
// 注册顶层路由
app.use('/', indexRouter);
app.use('/users', usersRouter);
// 捕获 404 错误并将其传递给错误处理器
app.use((req, res, next) => {
next(createError(404));
});
// 错误处理
app.use((err, req, res, next) => {
// 设置局部变量以在开发环境中显示错误消息
res.locals.message = err.message;
// 根据环境变量决定是否显示完整的错误信息。开发环境中显示,生产环境中隐藏
res.locals.error = req.app.get('env') === 'development' ? err : {};
// 渲染错误页面
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
全屏模式 退出全屏
从上述两个代码段中,我们可以看到Express的实例app
主要有三种关键方法:
app.use([path,] callback [, callback...])
: 当请求路径符合设定的规则时,相应的中间件函数会被执行。
-
path
: 指定调用中间件函数的路径。 callback
: 回调函数可以是多种形式。它可以是一个中间件函数,是多个中间件函数,以逗号分隔,或者是一个中间件函数的数组,或者任何形式的组合。app.get()
和app.post()
: 这些方法类似于use()
,也是用于注册中间件。不过,它们是绑定到 HTTP 请求方法上的。只有在使用相应的 HTTP 请求方法时,才会触发相应的中间件。app.listen()
: 负责创建一个 httpServer 并调用server.listen()
传递所需参数。
根据对Express代码功能分析的结果,我们了解到Express的实现主要集中在三个方面:
- 中间件函数的注册过程。
- 中间件函数的核心机制。
- 路由处理,重点关注路径匹配。
根据这些要点,我们下面来实现一个简单的LikeExpress类。
1. 类的基本构成
明确这个类需要实现的主要功能,首先,咱们先来明确一下,好吗?
use()
: 实现通用中间件注册。get()
和post()
: 用于处理与 HTTP 请求相关的中间件注册。listen()
: 实质上等同于 httpServer 的listen()
函数。在这个类的listen()
函数中,创建了一个 httpServer,传递了相关参数,监听请求,并执行了回调函数(req, res) => {}
。
检查原生 Node httpServer 的用法:
const http = require("http");
const server = http.createServer((req, res) => {
res.end("hello");
});
server.listen(3003, "127.0.0.1", () => {
console.log("在控制台上输出,Node服务启动成功了");
});
点击进入全屏 点击退出全屏
因此,LikeExpress 类的结构如下:
const http = require('http');
class LikeExpress {
constructor() {}
use() {}
get() {}
post() {}
// HTTP 服务器回调函数
callback() {
return (req, res) => {
res.json = function (data) {
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify(data));
};
};
}
listen(...args) {
const server = http.createServer(this.callback());
server.listen(...args);
}
}
module.exports = () => {
return new LikeExpress();
};
切换到全屏模式 退出全屏
2. 中间件的注册流程
从 app.use([path,] callback [, callback...])
我们能发现,中间件可以是一个函数数组或单个函数。为了简化实现过程,我们统一将中间件处理为一个函数数组。在 LikeExpress 类中,use()
、get()
和 post()
这三个方法都可以实现中间件注册。只是因为不同的请求方法,触发的中间件会依据请求方法的不同而变化。因此我们考虑:
- 抽象一个通用的中间件注册函数。
- 创建数组来存储与这三个方法对应的中间件函数。因为
use()
作为一个通用的中间件注册函数适用于所有请求,所以存储use()
中间件的数组实际上是get()
和post()
数组的结合。
中间件队列集
中间件数组需要放置在公共区域,以便类中的方法可以轻松访问其中的内容。因此,我们将中间件数组放置在 constructor() 构造函数中。
constructor() {
// 中间件列表
this.routes = {
all: [], // 所有请求的中间件
get: [], // GET 请求中间件
post: [], // POST 请求中间件
};
}
全屏显示 退出全屏
中间件注册功能
中间件注册就是将中间件存放在相应的中间件数组中。中间件注册函数需要解析传入的参数。第一个参数可能是路由或中间件之一,因此需要首先确定它是否是一个路由。如果是,就原样输出;否则,默认是根路由,然后将剩下的中间件参数转换成数组。
register(path) {
const info = {};
// 如果第一个参数是一个路径
if (typeof path === "string") {
info.path = path;
// 将从第二个参数开始的所有参数转换为数组,并将其存储在中间件列表中
info.stack = Array.prototype.slice.call(arguments, 1);
} else {
// 如果第一个参数不是一个路径,则默认为根路径,并执行所有路径
info.path = '/';
info.stack = Array.prototype.slice.call(arguments, 0);
}
return info;
}
全屏模式 按 Esc 退出
关于 use()
, get()
, 和 post()
的实现方法
有了通用的中间件注册函数 register()
,实现 use()
、get()
和 post()
就变得很容易,只需将对应的中间件存入相应的数组中。
use() {
const info = this.register.apply(this, arguments);
this.routes.all.push(info);
}
get() {
const info = this.register.apply(this, arguments);
this.routes.get.push(info);
}
post() {
const info = this.register.apply(this, arguments);
this.routes.post.push(info);
}
点击全屏 点击退出全屏
3. 路线匹配处理
当注册函数的第一个参数是路由时,只有在请求路径与该路由匹配或为其子路由的情况下,对应的中间件函数才会被触发。因此,我们需要一个路由匹配函数,根据请求方法和请求路径来提取匹配路由的中间件列表,以便后续的 callback()
函数能顺利执行:
match(method, url) {
let stack = [];
// 如果请求的是浏览器自带的 favicon,就直接返回,不用管它。
if (url === "/favicon") {
return stack;
}
// 获取路由列表
let curRoutes = [];
curRoutes = curRoutes.concat(this.routes.all);
// 将所有路由和当前方法的路由合并
curRoutes = curRoutes.concat(this.routes[method]);
curRoutes.forEach((route) => {
// 如果当前 URL 以路由的路径开头
if (url.indexOf(route.path) === 0) {
// 将该路由的处理函数加入到处理栈中
stack = stack.concat(route.stack);
}
});
return stack;
}
全屏模式 退出全屏
然后,在 HTTP 服务器的回调函数(callback()
)中,取出需要执行的中间件部分:
callback() {
return (req, 响应) => {
响应.json = function (data) {
响应.setHeader('Content-Type', 'application/json');
响应.end(JSON.stringify(data));
};
const url = req.url;
const 方法 = req.method.toLowerCase();
// `this.match` 是自定义方法,用于匹配请求方法和URL
const resultList = this.match(方法, url);
// `this.handle` 是处理请求和响应的方法
this.handle(req, 响应, resultList);
};
}
切换到全屏,然后退出全屏
4. 下一个机制的实施情况
Express 中间件函数的参数是 req
、res
和 next
,其中 next
是一个函数。只有调用它,中间件函数才能依次执行,类似于 ES6 Generator 中的 next()
。在我们的实现中,我们需要实现一个 next()
函数,其需要满足以下要求:
- 每次从中间件队列数组中按顺序提取一个中间件。
- 将
next()
函数传给提取出的中间件。因为中间件数组是公开的,每次调用next()
,数组中的第一个中间件函数会被调用并执行,从而达到中间件顺序执行的效果。
// 核心 next 机制
handle(req, res, stack) {
const next = () => {
const middleware = stack.shift(); // 取出
如果有中间件 {
middleware(req, res, next);
}
};
next();
}
进入全屏 退出全屏
快速代码 const http = require('http');
const slice = Array.prototype.slice;
class LikeExpress {
constructor() {
// 中间件列表
this.routes = {
all: [],
get: [],
post: [],
};
}
register(path) {
const info = {};
// 如果第一个参数是路由
if (typeof path === "string") {
info.path = path;
// 将从第二个参数开始的参数转换为数组并存储
info.stack = slice.call(arguments, 1);
} else {
// 默认是根路由,所有路由执行
info.path = '/';
info.stack = slice.call(arguments, 0);
}
return info;
}
use() {
// 将中间件添加到all列表中
this.routes.all.push(this.register.apply(this, arguments));
}
get() {
// 注册get方法的中间件
this.routes.get.push(this.register.apply(this, arguments));
}
post() {
// 注册post方法的中间件
this.routes.post.push(this.register.apply(this, arguments));
}
match(method, url) {
let stack = [];
// 浏览器内置的图标请求(例如/favicon.ico)
if (url === "/favicon") {
return stack;
}
// 获取路由
let curRoutes = [];
curRoutes = curRoutes.concat(this.routes.all);
curRoutes = curRoutes.concat(this.routes[method]);
curRoutes.forEach((route) => {
if (url.indexOf(route.path) === 0) {
stack = stack.concat(route.stack);
}
});
return stack;
}
// 核心next机制
handle(req, res, stack) {
const next = () => {
const middleware = stack.shift();
if (middleware) {
middleware(req, res, next);
}
};
next();
}
callback() {
// 返回一个处理请求的函数
return (req, res) => {
res.json = function (data) {
// 设置Content-Type头部为application/json
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify(data));
};
const url = req.url;
const method = req.method.toLowerCase();
const resultList = this.match(method, url);
this.handle(req, res, resultList);
};
}
listen(...args) {
// 启动服务器并监听指定端口
const server = http.createServer(this.callback());
server.listen(...args);
}
}
module.exports = () => {
return new LikeExpress();
};
进入全屏 / 退出全屏
Leapcell: 新一代无服务器计算平台,适合网页托管、异步任务处理以及 Redis 集成最后来介绍一下一个非常适合部署 Express 的平台:[Leapcell](https://leapcell.io/?lc_t=d_js),)。
Leapcell(https://leapcell.io/?lc_t=d_js)是一个具有以下特点的无服务器平台:
1. 支持多种语言
- 用 JavaScript、Python、Go 或 Rust 开发即可。
2. 免费部署项目,数量不限
- 只按实际使用计费,不用不收钱。
3. 超高的性价比
- 按使用量付费,无空闲费用。
- 例如:$25可以支持6.94M请求次数,平均响应时间仅为60毫秒。
4. 流畅的开发体验
- 直观的用户界面,轻松的设置过程。
- 完全自动化的CI/CD流水线和GitOps集成。
- 实时指标和日志,提供实用的数据洞察。
5. 轻松扩展性和高性能
- 自动扩展以轻松应对高并发。
- 零运营负担 — 只需专注于开发。
更多详情查看文档!
Leapcell:推特:https://x.com/LeapcellHQ
共同学习,写下你的评论
评论加载中...
作者其他优质文章