掌握 Express.js:深入探究

2025-06-09

掌握 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}`);
});
Enter fullscreen mode Exit fullscreen mode

入口文件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;
Enter fullscreen mode Exit fullscreen mode

从上面两段代码段我们可以看出Express实例app主要有三个核心方法:

  1. app.use([path,] callback [, callback...]):用于注册中间件,当请求路径符合设置的规则时,会执行相应的中间件函数。
    • path:指定中间件函数调用的路径。
    • callback:回调函数可以采用多种形式。它可以是单个中间件函数、一系列以逗号分隔的中间件函数、中间件函数数组,或者以上所有形式的组合。
  2. app.get()app.post():这些方法与 类似use(),也用于注册中间件。但它们与 HTTP 请求方法绑定。只有使用相应的 HTTP 请求方法时,才会触发相关中间件的注册。
  3. app.listen():负责创建httpServer并传递所需的参数server.listen()

代码实现

通过对Express代码功能的分析,我们知道Express的实现重点关注三点:

  • 中间件功能的注册过程。
  • 中间件中的核心next机制实现功能。
  • 路线处理,重点是路径匹配。

基于这些要点,下面我们将实现一个简单的LikeExpress类。

1. 类的基本结构

首先明确该类需要实现的主要方法:

  • use():实现通用中间件注册。
  • get()and post():实现与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");
});
Enter fullscreen mode Exit fullscreen mode

因此,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();
};
Enter fullscreen mode Exit fullscreen mode

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
    };
}
Enter fullscreen mode Exit fullscreen mode

中间件注册功能

中间件注册就是将中间件存放到对应的中间件数组中。中间件注册函数需要解析传入的参数,第一个参数可能是路由,也可能是中间件,所以需要先判断是否是路由,如果是则原样输出;否则默认为根路由,然后将剩余的中间件参数转换成数组。

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

然后在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);
    };
}
Enter fullscreen mode Exit fullscreen mode

4. 下一机制的实施

Express 中间件函数的参数为req​​ 、resnext,其中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();
}
Enter fullscreen mode Exit fullscreen mode

快递代码

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();
};
Enter fullscreen mode Exit fullscreen mode

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
PREV
该使用哪种身份验证?四种常用方法的比较
NEXT
深入探究 Microsoft MarkItDown