掌握 Express.js:深入探究
Express 是 Node.js 中非常常用的 Web 服务器应用程序框架。本质上,框架是一种遵循特定规则的代码结构,具有两个关键特征:
- 它封装了API,使开发人员能够更加专注于编写业务代码。
- 有既定的流程和标准规范。
Express框架的核心特性如下:
- 它可以配置中间件来响应各种HTTP请求。
- 它定义了用于执行不同类型的 HTTP 请求操作的路由表。
- 支持向模板传递参数,实现HTML页面的动态渲染。
本文将通过实现一个简单的LikeExpress类来分析Express如何实现中间件注册、next机制以及路由处理。
快速分析
我们先通过两个 Express 代码示例来探索一下它提供的功能:
Express 官网 Hello World 示例
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
入口文件app.js分析
app.js
以下是脚手架生成的Express项目入口文件的代码express-generator
:
// Handle errors caused by unmatched routes
const createError = require('http-errors');
const express = require('express');
const path = require('path');
const indexRouter = require('./routes/index');
const usersRouter = require('./routes/users');
// `app` is an Express instance
const app = express();
// View engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
// Parse JSON format data in post requests and add the `body` field to the `req` object
app.use(express.json());
// Parse the urlencoded format data in post requests and add the `body` field to the `req` object
app.use(express.urlencoded({ extended: false }));
// Static file handling
app.use(express.static(path.join(__dirname, 'public')));
// Register top-level routes
app.use('/', indexRouter);
app.use('/users', usersRouter);
// Catch 404 errors and forward them to the error handler
app.use((req, res, next) => {
next(createError(404));
});
// Error handling
app.use((err, req, res, next) => {
// Set local variables to display error messages in the development environment
res.locals.message = err.message;
// Decide whether to display the full error according to the environment variable. Display in development, hide in production.
res.locals.error = req.app.get('env') === 'development'? err : {};
// Render the error page
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的实现重点关注三点:
- 中间件功能的注册过程。
- 中间件中的核心next机制实现功能。
- 路线处理,重点是路径匹配。
基于这些要点,下面我们将实现一个简单的LikeExpress类。
1. 类的基本结构
首先明确该类需要实现的主要方法:
use()
:实现通用中间件注册。get()
andpost()
:实现与HTTP请求相关的中间件注册。listen()
:本质上就是listen()
httpServer的功能,在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 service started successfully");
});
因此,LikeExpress类的基本结构如下:
const http = require('http');
class LikeExpress {
constructor() {}
use() {}
get() {}
post() {}
// httpServer callback function
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() {
// List of stored middleware
this.routes = {
all: [], // General middleware
get: [], // Middleware for get requests
post: [], // Middleware for post requests
};
}
中间件注册功能
中间件注册就是将中间件存放到对应的中间件数组中。中间件注册函数需要解析传入的参数,第一个参数可能是路由,也可能是中间件,所以需要先判断是否是路由,如果是则原样输出;否则默认为根路由,然后将剩余的中间件参数转换成数组。
register(path) {
const info = {};
// If the first parameter is a route
if (typeof path === "string") {
info.path = path;
// Convert to an array starting from the second parameter and store it in the middleware array
info.stack = Array.prototype.slice.call(arguments, 1);
} else {
// If the first parameter is not a route, the default is the root route, and all routes will execute
info.path = '/';
info.stack = Array.prototype.slice.call(arguments, 0);
}
return info;
}
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 = [];
// Ignore the browser's built-in icon request
if (url === "/favicon") {
return stack;
}
// Get routes
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;
}
然后在httpServer的回调函数中callback()
,提取需要执行的中间件:
callback() {
return (req, res) => {
res.json = function (data) {
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);
};
}
4. 下一机制的实施
Express 中间件函数的参数为req
、res
和next
,其中next
是一个函数。只有调用它,中间件函数才能按顺序执行,类似next()
ES6 Generator 中的操作。在我们的实现中,我们需要编写一个next()
满足以下要求的函数:
- 每次按顺序从中间件队列数组中提取一个中间件。
- 将
next()
函数传入提取出来的中间件中,由于中间件数组是公共的,每次next()
执行时都会取出数组中第一个中间件函数执行,从而达到顺序执行中间件的效果。
// Core next mechanism
handle(req, res, stack) {
const next = () => {
const middleware = stack.shift();
if (middleware) {
middleware(req, res, next);
}
};
next();
}
快递代码
const http = require('http');
const slice = Array.prototype.slice;
class LikeExpress {
constructor() {
// List of stored middleware
this.routes = {
all: [],
get: [],
post: [],
};
}
register(path) {
const info = {};
// If the first parameter is a route
if (typeof path === "string") {
info.path = path;
// Convert to an array starting from the second parameter and store it in the stack
info.stack = slice.call(arguments, 1);
} else {
// If the first parameter is not a route, the default is the root route, and all routes will execute
info.path = '/';
info.stack = slice.call(arguments, 0);
}
return info;
}
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);
}
match(method, url) {
let stack = [];
// Browser's built-in icon request
if (url === "/favicon") {
return stack;
}
// Get routes
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;
}
// Core next mechanism
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) {
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:用于 Web 托管、异步任务和 Redis 的下一代无服务器平台
最后介绍一个非常适合部署Express的平台:Leapcell。
Leapcell是一个无服务器平台,具有以下特点:
1.多语言支持
- 使用 JavaScript、Python、Go 或 Rust 进行开发。
2. 免费部署无限项目
- 仅按使用量付费 — 无请求,无费用。
3.无与伦比的成本效益
- 按使用量付费,无闲置费用。
- 例如:25 美元支持 694 万个请求,平均响应时间为 60 毫秒。
4. 简化的开发人员体验
- 直观的用户界面,轻松设置。
- 全自动 CI/CD 管道和 GitOps 集成。
- 实时指标和日志记录可提供可操作的见解。
5.轻松的可扩展性和高性能
- 自动扩展以轻松处理高并发。
- 零运营开销——只需专注于建设。
Leapcell Twitter:https://x.com/LeapcellHQ
鏂囩珷鏉ユ簮锛�https://dev.to/leapcell/mastering-expressjs-a-deep-dive-4ef5