模块链接
模块链接(Module Linking)是 Esmx 提供的跨应用代码共享方案。它基于浏览器原生的 ESM(ECMAScript Modules)标准,让多个应用可以共享代码模块,无需任何额外的运行时库。
核心优势
- 零运行时开销:直接使用浏览器原生 ESM 加载器,不引入任何代理或包装层。
- 高效共享:通过导入映射(Import Maps)在构建时解析依赖,运行时直接加载。
- 版本隔离:不同应用可使用同一包的不同版本,共存的 major 版本被自动隔离。
- 简单易用:声明式配置,与原生 ESM 语法完全兼容。
简而言之,模块链接相当于一个"模块共享管理器",让不同应用能够像使用本地模块一样,安全、高效地共享代码。
模型:在 package.json esmx 中声明
所有协议事实都写在一个 package.json 字段 esmx 里,恰好有四个可选子字段。entry.node.ts 只保留行为(devApp、server、postBuild)以及环境链接 —— 把协议事实写进去会报错(E_PROTOCOL_IN_BEHAVIOR)。
一个模块的声明严格是本地知识:你可以在对其他任何模块一无所知的情况下写出它。三种角色覆盖所有工程。
角色 1 —— 供给方(共享平台包)
消费方 import 'shared/ui' —— 一个逻辑名。重命名 src/ui/index.ts 不再是破坏性变更。
角色 2 —— 消费方 + 供给方(功能远程)
注意没有什么:没有 imports 映射,没有逐 specifier 接线,esmx 里也没有版本字段。版本范围就放在 npm 本来就放的地方 —— dependencies(∪ peerDependencies)—— 并在构建时对照挂载产物的实际版本校验。
角色 3 —— 组合方(宿主)
uses 是传递的:如果 cart uses shared,那么 uses cart 的宿主会通过链条得到 shared 的供给。业务应用只声明一行,对链条深度毫不知情。
接线如何被推导(你永远不用写)
两条规则取代所有手写映射。
合并规则 —— 一句话:
supply(M) = merge(supply(uses[0]), …, supply(uses[n]), M.provides) —— 后面的条目覆盖前面的,模块自己的 provides 是隐含的最后一个元素,所以当两个模块供给同一个包时,uses 数组的顺序决定谁赢(按从通用到具体排列;具体层胜出,和 Object.assign 同一约定家族)。选举是逐 major 版本的 —— 共存的 major(如 vue 2 和 vue 3)是隔离的孤岛,各有自己的赢家,每个消费方接到满足自己声明范围的那个 major。
查找规则 —— 在打包器遍历你的代码时逐 specifier 应用,无需预处理、无需声明:
因此单实例共享是固有的(每个包一个赢家,整个闭包重接到它),多版本共存也不需要任何额外词汇 —— 一个自带副本的模块会被自动 scope 隔离。仅类型导入(import type)永远不产生接线。
挂载:产物从哪来
任何可通过 node_modules 解析的模块都会自动挂载在 node_modules/<name>/dist,无需路径配置。这覆盖了 registry 安装和 monorepo 兄弟包:pnpm workspace:* 依赖的软链会被跟随并 realpath 化,所以一条普通的 dependencies 加上 uses 名字就是全部。
只有对于不能经 npm 解析的产物目录(部署路径、远程拉取的产物),才需要加显式 links 条目。links 是环境事实而非协议事实,所以它是唯一还允许留在 entry.node.ts 里的 modules 键:
导入共享代码
一旦某模块进了你的 dependencies 和 uses,就用逻辑名导入它的导出:
axios 解析到你合并供给表里供给它的那个模块(通过 provides: ["axios"]);shared/ui 解析到 shared 模块声明的逻辑导出。你永远不用写物理路径,也不用手写 import map。
校验接线:esmx validate
esmx validate 是整个解析过程的免构建 dry run —— 挂载遍历、版本检查、供给合并、导出检查。每次改完声明就跑:
只有出现 error 级诊断时它才以非零码退出;只有警告则退出 0。没有 esmx 字段的包会报告 protocol: "legacy" 并退出 0。完整诊断分类(E_NOT_LINKED、E_VERSION、E_SCHEMA、W_MULTI_CANDIDATE 等)见 LLM 简报。
完整示例:多版本共存
一个共享基座并排供给两个 major 的 Vue;每个业务应用接到自己声明范围满足的那一个 —— 全程没有任何手写 imports。
共享基座(shared)
Vue 3 应用(vue3-app)
Vue 2 应用(vue2-app)
聚合宿主(host)
这展示了:
- 共享基座:提供多版本框架支持;版本隔离来自 npm 别名(
npm:vue@^2.7.0)加上provides声明 —— 每个 major 是一个隔离的选举分组。 - Vue 3 / Vue 2 应用:各自声明自己的 Vue 范围,只导出路由配置;解析器自动把它们接到匹配的 major。
- 聚合宿主:组合子应用的单一入口 —— 每个子应用一行
uses,没有手写 import map。
构建好子应用后跑 esmx validate --json,确认整张图能解析。
旧语法(下个大版本移除)
新代码请始终使用上面的
package.jsonesmx声明。 下面的语法在过渡期仍然有效,你在存量工程里也会见到 —— 认识它、维护它,并把它改写成新声明来现代化。不要为新模块编写它。
旧工程把所有协议事实放在 entry.node.ts 的 modules 键下:
新协议消除的旧语法陷阱:
- 公开导出名等于源路径。 旧消费方写
import { x } from 'shared/src/index'—— 目录结构就是 API,重命名一个源文件会破坏每个消费方。新协议下只有逻辑名('shared/ui')是公开的。 - 接线是手写的。 每个消费方都要手写
imports行,而新协议从声明中推导它们。 - 运行前不校验任何东西。 没有版本检查、没有导出检查、没有结构化诊断。
这一切的转换是一次机械改写(可写成 codemod,但没有随框架提供的命令)。按照 RFC 0001,旧语法将在后续阶段被完全移除 —— 不存在长期双语法。