Express是Node.js中非常常用的Web服务器框架。从本质上讲,框架是遵循特定规则的代码结构,具有两个关键特征:
- 它封装了API,使开发人员专注于编写业务代码。
- 它建立了流程和标准规范。
Express框架的核心功能如下:
- 它可以配置中间件来响应各种HTTP请求。
- 它定义了一个路由表,用于执行不同类型的HTTP请求操作。
- 它支持向模板传递参数以实现HTML页面的动态呈现。
本文将分析Express如何实现中间件注册和路由处理。
Express快速分析
让我们通过两个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
以下是express-generator生成的app.js的代码:
// 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 }));
// 静态文件处理
app.use(express.static(path.join(__dirname, 'public')));
// 注册路由
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));
});
// 错误处理
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...]):用于注册中间件。当请求路径与设置的规则匹配时,将执行相应的中间件功能。app.get()和app.post():这些方法类似于use()。但是,它们绑定到HTTP请求方法。只有当使用相应的HTTP请求方法时,才会触发相关中间件的注册的方法。app.listen():负责创建一个httpServer并传递server.listen()所需的参数。
代码实现
对Express代码功能的分析,我们知道Express的实现有三个方面:
- 中间件功能的注册。
- 中间件的核心next。
- 路由处理,重点是路径匹配。
基于这些要点,我们将在下面实现一个简单的LikeExpress类。
class基本结构
首先,明确这个类需要实现的哪些方法:
use():实现中间件注册。get()和post():实现与HTTP请求相关的中间件。listen():本质上,它是httpServer的listen()函数。
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();
};
中间件注册
从 app.use([path,] callback [, callback...]) 开始,我们可以看到中间件可以是一个函数数组,也可以是一个函数。
为了简化实现,我们统一将中间件作为数组。在LikeExpress类中,use()、get()和post()这三个方法都可以实现中间件注册.
中间件队列数组
中间件数组需要放在公共区域,以便类中的方法轻松访问。因此,我们将中间件数组放在构造函数中。
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);
}
路由匹配处理
我们需要一个路由匹配函数,根据请求方法和请求路径匹配路由的中间件数组,供后续的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);
};
}
next机制的执行情况
Express中间件函数的参数是req、res和next,其中next是一个函数。只有调用它,中间件函数才能按顺序执行,类似于ES6 Generator中的next()。在我们的实现中,我们需要编写一个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();
};