文章

人生代代无穷已,江月年年望相似

模子

站在 2023 年的现在回望过去 Node.js 对后端领域的进击,虽谈不上无疾而终,但也不能说有多大的起色。就我个人的体验来说,更多的是做中间层或者网关,然对大多数场景来说,网关有非常多的替换方案,而中间层的必要性也时常存疑(是不是徒增复杂性?)。这些领域 Node.js 虽有一战之力的,但是不多。不过当前 Node.js 依旧活跃,且在相当长的一段未来中,会活跃在前端的工具链之中,编译,打包,或者各种小工具,而在这些工具链中如果需要 http 容器则 Express 是通常的选择,再一次认识到这个事实之后,我决定梳理一下二者的区别,对于 Express 我从未系统的了解过,对于 Koa 我则有一些实践经验,所幸二者区别不大。

综述

Koa 和 Express 都是 Node.js 社区中相对热门的库,且是同一团队设计,我最初理解的 Koa 颇有种 Express “升级” 的味道,更好的 Es6 支持(在当时),基于 Promise 的中间件和更符合直觉的“洋葱模型”,仿佛昭示着未来,这也是我选择直接使用 Koa 而没怎么去了解 Express 的原因之一。实际上,现如今,如果非要在两个库中选一个去了解,我会推荐 Express 无它,社区更为活跃,应用的地方更多,同时学习资料更多(例子也更好找),可以说是更“现实”的选择,但既然 Koa 出现自然有它的道理,这里简单对比一下:

  • Express 内置了许多功能,比如路由,可直接使用,而 Koa 只提供基本的 http service 功能 。
  • Express 的中间件模型为线型,而 Koa 的中间件模型为 U 型,也就是“洋葱模型”构造中间件。
  • Express 通过回调实现异步函数,在多个回调、多个中间件中写起来容易逻辑混乱。而 Koa 利用 async/await 作为响应器,写起来更简洁清晰。

总体来说 Koa 是一个小而健壮的基石,可以根据自己的需要灵活地选择和组合各种中间件,对应的如果只需要一个简单的 http 容器,或者想提供几个简单的接口 Express 更合适,这可能也是它经久不衰的原因。

至于写法方面,没有什么正确答案,个人感觉 Koa 写起来负担更小,但也可能是我更熟悉 Koa 的缘故。

中间件

中间件的使用方式,可以说是二者的核心区别,只看写法上似乎差不多,写个例子试试。

const Koa = require('koa');
const app = new Koa();

app.use(async (ctx, next) => {
    // 中间件 1 记录整个链路用时
    console.log(`<-- ${ctx.method} ${ctx.url} `);
    const start = new Date().getTime();
    await next();
    const ms = new Date().getTime() - start;
    console.log(`--> ${ctx.method} ${ctx.url} - ${ms}ms - ${ctx.status}`);
});

app.use(async (ctx, next) => {
    // 中间件 2  具体的业务
    // 实际一般是使用 koa-router 来组织这里只是举个例子
    if (ctx.path === '/api/Hello') {    
        ctx.body = 'Hello';
    }
});

app.on('error', (err, ctx) => {
    // 错误监听,具体的业务中有错误则可以使用传统 js 的错误处理方式,非常符合直觉
    // Tip 需要统一的错误处理可以使用 koa-onerror
    console.log('main Error: ', err, ctx);
});

app.listen(8082);

Express 的话则是

const Express = require('express');
const app = new Express();

app.use((req, res, next) => {
    // 中间件 1 记录链路开始的时间
    console.log(`<-- ${req.method} ${req.path} `);
    req.start = new Date().getTime();
    next();
});

app.use((req, res, next) => {
    // 中间件 2  具体的业务
    // 实际上在 Express 中应该使用路由来做这个事情,这里只是举例
    if (req.path === '/api/Hello') { 
        res.send('hello');
    }
    next();
});

app.use((req, res, next) => {
    // 记录链路结束的时间
    const ms = new Date().getTime() - req.start;
    console.log(`--> ${req.method} ${req.path} - ${ms}ms - ${res.statusCode}`);
});

// 最后一个中间件做错误处理
app.use((err, req, res, next) => {
    // 返回 500 以及 错误信息
    // 任意使用这个方法的地方都会直接进行反回
    res.status(500).send(err.message || 'Service Err');
});

app.listen(8082);

可以很明显的看出二者的区别

  1. 执行上,koa 的 next 可以通过 await 等待后续的链路先执行,express 的 next 似乎只是单纯的调用下一个中间件。
  2. 参数上。

    • Koa 把 req,res 都放在第一个参数里,可以通过(ctx.req ,ctx.res 获取),同时常见操作做了聚合,改对应的变量会直接影响结果。
    • Express 则区分开 req,res 并且通常不是通过改变量而是调用对应的方法影响结果。
  3. 错误监听和处理上,koa 使用 on 来区分,express 直接使用最后一个中间件默认处理。

个人认为 Express 的认知负担主要集中在链路很长的情况下,哪些方法会直接“响应”,为了实现一些,比如执行前后记录之类的功能,变量需要在链路里全局传递。
而 Koa 的认知负担主要集中在,对洋葱模型和响应点的理解。
共通点都在于,使用 next 调用下一中间件,区别则是在于当前中间件调用 next 之后执行的代码的作用,从这点看,Koa 可以很容易的兼容 Express 中间件。反过来估计不行,因为没法控制 Koa 中间件调用 next 后的代码在 Express 里在预期的地方执行。

中间件的兼容

为了验证这一点,我们来试一个常见库 http-proxy-middleware

const Express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');

const app = Express();
const port = 8083;

const apiProxy = createProxyMiddleware({
    target: 'http://192.168.10.106:5678',
    changeOrigin: true,
    ws:true
});

app.use(apiProxy);

app.listen(8083, () => {
    console.log(`Example app listening on port ${port}`);
});

这段代码将 192.168.10.106:5678 上的一个 http 服务代理到了,本地的 8083 端口。这里我们来试下让 Koa 来使用.

const Koa = require('koa');
const { createProxyMiddleware } = require('http-proxy-middleware');

const app = new Koa();
const port = 8083;

const apiProxy = createProxyMiddleware({
    target: 'http://192.168.10.106:5678',
    changeOrigin: true,
    ws: true
});

function withCallbackHandler(ctx, connectMiddleware, next) {
    return new Promise((resolve, reject) => {
        connectMiddleware(ctx.req, ctx.res, err => {
            if (err) reject(err);
            else resolve(next());
        });
    });
}

app.use(async (ctx, next) => {
    await withCallbackHandler(ctx, apiProxy, next);
});

app.listen(8083, () => {
    console.log(`Example app listening on port ${port}`);
});

平淡无奇,转接下参数,并控制等待下 Express 的中间件执行完就可以正常工作了,代码来自 koa-connet 这个库可以方便的进行这种兼容。

总结

总的来说,二者都是成熟的 web 库,如果是抱着学习为目的,想做一些自己用的小工具,我建议用 Koa 相对来说会灵活写的更爽,会有更多的东西可以研究一些。而如果抱着“实用”或者出“活”这类想法,还是看 Express 吧,更成熟,应用更广。

题外话,其实还有很多比如路由使用,取值,传值的区别,或者静态资源托管的区别这种,纯写法/选择库的问题,随着 chatGPT 的普及影响应该越来越少了,只要知道自己要什么,并能准确描述,搜索引擎(比如 new bing)的聊天机器人,即使不能准确的回答也能找到大差不差的参考,这种纯苦力(记忆的),编程学习转换的成本从来没有像今天这样这么低(容易)。

评论

  1. contrails contrails
    Chrome 129

    文章写得很好,像读小说一样流畅。捉个虫:总结倒数第四行 chatGPT 拼错了

    1. zephyru zephyru
      Chrome 130

      感谢捉虫,虽然内容不是很丰富吧,收到评论还真有种意外之喜的感觉

This is just a placeholder img.