为了账号安全,请及时绑定邮箱和手机立即绑定

掌握Express.js:深度剖析与实战

说明
leapcell.io/?lc_t=d_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主要有三种关键方法:

  1. app.use([path,] callback [, callback...]): 当请求路径符合设定的规则时,相应的中间件函数会被执行。
  • path: 指定调用中间件函数的路径。

  • callback: 回调函数可以是多种形式。它可以是一个中间件函数,是多个中间件函数,以逗号分隔,或者是一个中间件函数的数组,或者任何形式的组合。
    1. app.get()app.post(): 这些方法类似于 use(),也是用于注册中间件。不过,它们是绑定到 HTTP 请求方法上的。只有在使用相应的 HTTP 请求方法时,才会触发相应的中间件。
    2. 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 中间件函数的参数是 reqresnext,其中 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

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消