Template-system 之 从Context 到 Service Registry 的迁移
自述:
在next环境的时候要把nextLink/image的实现注入到远程组件。
远程组件用本地实现mock这两个组件的注入。
最开始实现思路:用Context,主题系统和宿主分别注入value的不同
发现问题:页面的软link,点击居然硬跳转了,说明并没有使用宿主注入的value
查原因:虽然注入value不同,但是context并没有共用一个!
分析:
1. 为什么我的redux可以通过importmap加上react-redux的行为规避context问题?因为react-redux里面就相当于是构建了一个context。
2. 那么我的nextLink/image 的注入是否可以通过redux?
- 错误的,redux适合简单的状态管理,必须纯函数
3. 那么应该如何实现主题系统一套,宿主一套?
- service registry,适用于全局单例
实现:
0. service.mjs应该是host-ports/里通过script生成到public/
1. client provider里,import { getVersion, provided, register } from '@ss/services';
,加useEffect, 注册NextLinkImplApi/NextImageImplApi服务进入service.mjs的全局变量;
并且进行标记+广播
2. 在远程组件加载前await,确保重要加载顺序的稳健性!
3. importmap中 @ss/services 映射到public/
4. 编译期:turbopack 掌握dev时期,直接resolve到本地文件。webpack掌握build时期,浏览器阶段排除掉@ss/services。(使用importmap外部cdn的都应该在编译阶段external)
在 Next.js 里给远程组件“注入” Link / Image:一场从 Context 到 Service Registry 的迁移记
目标:在 Next 环境里,把 NextLink / NextImage 能力注入到远程组件(remote)中;在本地模板运行时又能用 mock 实现自测;同时兼顾主题/租户差异与构建产线的可维护性。
背景与最初设计
-
远程组件:不直接依赖 Next,而是通过一层“注入”拿到链接跳转与图片能力。
-
宿主(Host):在运行时提供 Next 的具体实现。
-
模板开发:希望在 Vite 本地仓库里能用轻量的 mock 跑通 UI 与交互。
最开始的方案:用 React Context。
-
远程用
useLink()/useImage()消费 Context; -
宿主与主题系统分别用
LinkProvider/ImageProvider注入不同的value。
症状:软跳变硬跳
点击“站内链接”竟然出现 硬跳转(location.assign),说明远程并没有用到宿主注入的 Next 路由,而是走了回退逻辑。
根因:不是同一个 Context 实例
Context 在 React 里是按“实例身份”比对的。
我的宿主 Provider 引用了一份 LinkContext,远程组件使用的是 另一份(不同 URL / 打包副本)——看名字一样,但不是同一个对象。
于是 useContext() 得到 null,或落到兜底实现,自然“软跳不生效”。
为什么 Redux 当年没踩坑?
当我把 react-redux external 出去,并且让宿主和远程都从 同一个 URL 加载它时:
-
react-redux自己内部就有单例 Context; -
Provider 与
useSelector/useDispatch来自同一份包实例,身份一致,所以“看起来一切顺利”。
这并不意味着“把 Link/Image 塞进 Redux 就对”。Redux 适合可序列化的应用状态,而 Link/Image 是“服务能力”(函数、类、DOM 相关),会破坏纯函数与 DevTools 回放。
方案转向:Service Registry(注册表)
我最终采用了 Service Registry:把 Link / Image 当“全局单例服务”注入,远程按需读取。它解决了“上下文身份不一致”的根问题,也简化了布线。
Registry 的角色分工
-
Registry(全局单例):
register({ link, image })→getLink()/getImage(); -
宿主:在 Client Provider 里注册 NextLinkImplApi / NextImageImplApi;
-
远程:只依赖
@ss/services的接口,在 mock 环境下用本地实现,在宿主环境下用宿主实现。
为什么它更适合这里
-
不依赖 Context 身份:跨包副本也不怕“上下文不相等”的坑;
-
无需 Provider 布线:不受 Portal 或多 root 干扰;
-
开发与生产解耦:dev 用源码别名,prod 用 importmap 的外链产物;
-
与 Redux 解耦:服务是服务,状态是状态。
运行时注入与就绪顺序
宿主 Client Provider:
-
注册服务(幂等)
register({ link: NextLinkImplApi, image: NextImageImplApi });
-
标记 + 广播事件
/host-ports/ # 宿主“端口/适配层”源码(脚本也可生成) ├─ services.ts # @ss/services 的注册表实现(单一源码) └─ …其它 proxy /stubs/ └─ ss-services.server.ts # server 假实现(防止 node 侧误用) /public/externals/ └─ services.v1.(m)js # 由 /host-ports/services.ts 构建出的浏览器产物
构建产物生成(任选其一):
-
tsup:
tsup host-ports/services.ts --format esm --platform=browser --target=es2022 --minify --sourcemap --out-dir public/externals -
esbuild:
esbuild host-ports/services.ts --bundle --format=esm --platform=browser --target=es2022 --outfile=public/externals/services.v1.mjs --minify --sourcemap
常见坑 & 速修
-
软跳变硬跳
-
根因:Provider 和 Consumer 用的是不同实例的 Context;
-
方案:用 Registry;或确保宿主与远程从同一 URL导入 Context(更麻烦)。
-
-
Module not found: '@ss/services'
-
根因:打包期解析不到;
-
Dev:
turbopack.resolveAlias['@ss/services'] = './host-ports/services.ts'; -
Build:client → module external;server → alias 到 stub。
-
-
windows imports are not implemented yet
-
根因:把 alias 指到了
D:\...public\externals\*.mjs这样的 Windows 绝对路径; -
修复:dev 下 别指 public;用源码相对路径(posix),或改用动态导入。
-
-
事件错过
-
先判定再监听:
flag/provided()快速路径 + 事件兜底;必要时再加 rAF 轮询。
-
-
把服务塞进 Redux
-
不要。Redux 面向可序列化状态;服务(函数/类/DOM)会破坏纯度与工具链。
-
最终方案
-
注入方式:Service Registry(全局单例)。
-
宿主职责:在 Client Provider 里
register(NextLinkImplApi, NextImageImplApi);设置__SS_SERVICES_READY__并广播ss-services:ready。 -
远程职责:依赖
@ss/services,加载前await waitForReactGlobal({ alsoServices: true })。 -
构建流程:
-
Dev / Turbopack:别名到源码(
./host-ports/services.ts); -
Build / Webpack:client → module external;server → alias 到 stub;
-
Runtime:importmap 映射到
public/externals/services.v1.mjs(或 CDN)。
-
-
多主题/多租户:默认全局 Registry;必要时可加 Context 做局部覆写(Registry 作兜底)。
心得与收获
-
服务 ≠ 状态。Link/Image 是“能力”,不该放在 Redux/Context 里硬扛全局一致性;Registry 更合适。
-
唯一真身是分布式前端的首要原则:同一个 URL、同一个实例,Context/单例/缓存才会工作。
-
构建与运行解耦:importmap 是运行时机制;打包器需要 alias / externals 的协同配置。
-
稳健优先:注册后广播、加载前等待,是前端平台级能力注入的“黄金组合”。

浙公网安备 33010602011771号