Skip to content

插件升级 ​

Egg 的插件承担了框架的大量实际能力:会话、上传、静态资源、错误处理,以及数据库和模板引擎集成。Egg 4 中,这些能力陆续迁入同一 Monorepo,并向 TypeScript 源码、@eggjs 命名空间和可导入的配置工厂靠拢。对使用者来说,变化的重点是插件更容易被发现、配置和验证;对作者来说,类型、依赖与初始化时机需要更明确地表达。

应用可以通过插件工厂启用可选能力,用 TypeBox 连接校验与类型,并在测试环境替换 Redis 客户端。插件作者则可以把类型、依赖和资源生命周期放在同一套接口中维护。

先分清内置能力与按需集成 ​

把插件放进官方仓库,首先改变的是维护方式。相关代码可以与核心一起调整、测试和发布,应用运行时是否加载它,仍由插件配置决定。当前 plugins 目录有 18 个包,传统内置配置引用其中 12 个,覆盖错误处理、会话、安全、文件上传、开发辅助等既有能力。development 还带有 local 环境限制,因此默认配置中出现的插件也不一定在所有环境执行。

Redis、TypeBox 校验、Nunjucks、CORS 和 tracer 是按需启用的插件;Mock 则服务于测试。这条边界有直接的使用意义:内置 view 提供视图抽象,并不意味着应用已经获得 Nunjucks 渲染器;仓库包含 redis,也不会让每个 Egg 应用自动创建 Redis 连接。Tegg 另有自己的默认集成,判断某个能力是否启用时,应同时查看框架与应用的最终配置。

官方插件采用 @eggjs 命名空间。已有应用无需为所有内置能力逐个补写启用代码;真正需要检查的是自己显式安装、覆写或直接导入的旧 egg-* 包,以及它们与当前 Egg 主版本的兼容范围。

把插件配置变成可导入的接口 ​

过去常见的配置依赖插件名、包名字符串和 package.json 中的元数据。现在,官方插件可以直接导出工厂函数,应用通过 import 找到插件,再展开工厂返回的配置。以 Redis 和 TypeBox 为例,下面的 config/plugin.ts 同时启用两个可选插件。导入路径能由编辑器和模块解析器检查,用户也可以沿定义跳转查看插件的真实声明。

typescript
// config/plugin.ts
import redisPlugin from '@eggjs/redis';
import typeboxPlugin from '@eggjs/typebox-validate';

export default {
  ...redisPlugin(),
  ...typeboxPlugin(),
};

工厂集中声明元数据并保留覆盖能力 ​

插件作者通过 Egg 的 definePluginFactory 声明 name、enable 和 path,还可以注明 dependencies、optionalDependencies 与 env。Redis 的实际入口将 path 设置为 import.meta.dirname,让插件随自身模块位置提供加载路径。工厂返回以插件名为键的配置对象,并设置 skipMerge,避免加载器再次合并 package.json 的 eggPlugin 字段。

应用可以调用 redisPlugin({ enable: false }) 覆盖启用状态,也能覆盖依赖或环境等元数据;插件 name 由工厂保持固定。实现使用浅层展开,覆盖数组时会替换原数组,不会自动追加。作者应把真正的启动依赖写进声明,应用也应谨慎覆盖 dependencies,避免删掉必要顺序。

工厂模式为新插件提供了明确入口,但旧的 enable 与 package 配置形式仍在兼容配置中使用。迁移可以逐项推进:先升级并核对包的导出,再改成工厂调用,最后检查合并后的配置。尤其要分开理解两类配置:redisPlugin() 决定插件是否加载,config.redis 中的连接信息才决定插件如何工作。下面是 Redis 的工厂定义节选。

typescript
import { definePluginFactory } from 'egg';

export default definePluginFactory({
  name: 'redis',
  enable: true,
  path: import.meta.dirname,
});

用同一份 Schema 连接校验与类型 ​

TypeScript 应用常遇到重复定义问题:HTTP 参数在运行时需要校验,业务函数又需要静态类型。两份定义分开维护时,新增字段或修改可选性很容易只更新一边。@eggjs/typebox-validate 用 TypeBox Schema 表达数据结构,再通过 Static 推导 TypeScript 类型,让运行时规则和业务类型引用同一份定义。

启用插件后,ctx.tValidate(schema, data) 调用应用的 Ajv 校验器;失败会抛出状态码 422、code 为 invalid_param 的错误。需要自行处理失败分支时,可以使用返回布尔值的 tValidateWithoutThrow。当前 tValidate 的类型签名也只是 boolean,并非 TypeScript 的 asserts 断言函数,因此未知输入在完成运行时校验后,仍需显式赋予 Schema 推导出的类型。

下面的控制器示例把 Schema 放在类外,避免每次请求重新创建定义。插件当前依赖 typebox 1.x,已从旧的 @sinclair/typebox 包迁移。已有项目若直接导入旧包,需要一起检查包名、Schema API 和自定义格式;示例采用插件提供的 typebox 子路径,它在插件入口中重新导出 typebox 的命名成员。

typescript
import { Controller } from 'egg';
import { Type, type Static } from '@eggjs/typebox-validate/typebox';

const UserSchema = Type.Object({
  name: Type.String(),
  nickname: Type.Optional(Type.String()),
});

export default class UserController extends Controller {
  async create() {
    const input: unknown = this.ctx.request.body;
    this.ctx.tValidate(UserSchema, input);
    const user = input as Static<typeof UserSchema>;
    this.ctx.body = { name: user.name };
  }
}

Redis 测试可以替换客户端实现 ​

业务单测需要验证缓存读写逻辑时,真实 Redis 服务会增加本地和 CI 的准备成本。@eggjs/redis 允许通过 config.redis.Redis 注入客户端类,因此测试环境可以使用 ioredis-mock。应用仍通过 app.redis 访问实例,变化集中在测试配置,而不必为了测试修改业务调用方式。

替换客户端时需要配置启动等待行为。插件通常会等待客户端的 ready 或 error 事件,ioredis-mock 可能在构造阶段同步发出 ready,而启动检查注册时该事件已经结束。测试配置必须在 client 中显式设置 weakDependent: true,使该实例不阻塞应用启动。只替换 Redis 类不会自动打开这个选项,插件检查的是客户端 options.weakDependent。

下面的配置来自当前 README 的用法,可放在 config/config.unittest.ts;ioredis-mock 及其类型包应作为测试依赖安装。这个选择适合验证常用命令下的业务行为,连接故障、真实部署配置及客户端与服务端的兼容性仍应由集成测试覆盖。生产环境也不应为了绕过连接失败而照搬 weakDependent,它改变的是应用就绪对 Redis 的依赖程度。

typescript
import RedisMock from 'ioredis-mock';
import type { EggAppInfo, PartialEggConfig } from 'egg';

export default function (_appInfo: EggAppInfo): PartialEggConfig {
  return {
    redis: {
      Redis: RedisMock,
      client: {
        host: '127.0.0.1',
        port: 6379,
        password: '',
        db: 0,
        weakDependent: true,
      },
    },
  };
}

安全行为要连同配置一起理解 ​

新迁入的 @eggjs/cors 展示了官方插件之间的协作方式。应用没有显式配置 cors.origin 时,插件会结合 security 的安全域名检查决定是否返回请求 Origin;若安全检查方法不存在,则保留该 Origin。显式设置 origin 会覆盖这套默认逻辑。因此,升级 CORS 插件时,应结合现有 security 配置测试允许和拒绝的来源,尤其不要把一个宽泛的 origin 设置当成默认白名单的补充。

另一项实际修复发生在 @eggjs/security 的 escapeShellArg:实现修正了 POSIX shell 单引号的转义方式,保持输入作为一个参数。它的适用范围仍然是 POSIX shell 中的单个参数,不负责 Windows cmd 或 PowerShell 的转义。对于需要启动外部程序的应用,插件 README 建议优先使用 execFile 或 spawn 的参数数组,减少自己拼接命令字符串的需要。

这些变化说明,插件升级的检查点不止是 API 能否导入。应用还应验证自身依赖的安全行为,包括自定义 CORS Origin、错误响应和外部命令调用方式。CSRF、XSS 与安全响应头等能力也能与核心一起维护和回归测试,降低跨包修复的验证成本。

插件初始化开始兼顾打包与快照 ​

当应用从文件目录部署进一步走向 bundle 或启动快照,插件的隐含假设会显现出来。导入阶段读取模板、动态扫描实现文件,或者在构造器里启动 watcher,都可能妨碍构建。官方插件已经做出具体调整:onerror 与 development 内联默认页面模板,watcher 采用直接类导入,并将 clusterWrapper 与 watch 相关资源的创建移到 configDidLoad。

这对应一条插件作者需要遵守的生命周期边界。快照构建执行到 configWillLoad,恢复后才从 configDidLoad 继续。构造器和 configWillLoad 应准备可序列化配置与元数据,把套接字、文件监听器、定时器等运行资源留到后续阶段。如果一个插件需要另一个插件创建的运行实例,还要声明依赖,以保证初始化顺序。官方插件完成适配,并不能代替第三方插件检查自己的导入副作用和资源创建位置。

维护已有插件时,可以先核对 Node.js 22.18.0 这一运行基线和 Egg 4 兼容范围,再检查导出、元数据与类型声明。随后用真实应用夹具验证加载、错误与关闭流程,最后根据是否使用打包或快照增加对应检查。测试层若接入当前 Mock 的 Vitest 集成,还需对齐 Vitest ^5.0.1 的要求;这属于测试环境配置,不是线上服务必须安装的运行依赖。

应用开发者可以先从显式依赖的插件入手,保持内置能力的默认配置,逐项迁移可选集成;插件作者则应把加载元数据、类型和资源生命周期一起作为公共接口维护。两类工作在同一仓库中更容易互相验证,也更容易定位升级后究竟是哪一层发生了变化。

Born to build better enterprise frameworks and apps