Template-system 之 加入状态管理:远程组件本地可测、宿主可控

Overview

实现了什么: CDN 远程 ESM + 宿主可插拔状态

  • 远程组件:本地开发时能自己跑,不依赖宿主。用 mock context 或本地的轻量状态,local 模式就能自测功能。

  • 宿主加载:生产环境下 external 掉 @ss/useCart 等端口,由宿主通过 import map 注入真实 Redux 实现。这样远程组件在宿主环境下“自然”调用宿主的状态管理,像本地写的 hook 一样。


技术关键点

  1. 按领域拆分端口@ss/useCart@ss/react-redux … 不搞一个大网关。这样远程只会下载自己用到的 proxy 文件,按需加载,缓存粒度也细。

  2. proxy 转发:无论 React、React Redux、还是自定义 useCart,都通过 proxy 把“宿主全局注册实例”惰性转发出去,保证上下文一致。

  3. import map 注入:让浏览器解析 @ss/useCart/externals/useCart.mjs,在远程组件看来就是正常 import { useCart } from '@ss/useCart',实现“解耦 + 替换”。

  4. 自测与宿主兼容:dev 阶段 alias 到 mock,prod 阶段 external 到 import map。保证“远程组件能单独测试,也能在宿主无缝使用”。


为什么很值得

  • 独立性:远程组件不依赖宿主就能开发调试。

  • 可复用性:宿主换状态管理实现(比如从 Redux → Zustand → Server Action),远程组件无需改动。

  • 架构优雅: Port/Adapter 模式(领域端口 + 宿主适配),是大规模前端模块化里最常见的“best practice”。

  • 未来扩展:不仅限于 useCart,你可以逐渐把 useAuth、useTheme 也抽成端口,形成一套“状态 DI(依赖注入)”机制。

核心思路

目标
远程组件在本地(template-system)能自测;在宿主环境里自动接入宿主的 Redux(或任意状态实现),而不改远程代码。

方法论:在“模块边界”做依赖注入(DI)。把状态能力抽象成端口(Port),例如 @ss/cart,远程只依赖端口;宿主在运行时通过 import map 把端口映射到自己的实现产物(ESM)。

1)Template-system(远程库)配置

开发(dev):用 Vite alias 指向本地 mock,远程组件可独立自测。

 
// vite.config.ts(核心片段)
export default defineConfig(({ command }) => {
  const isDev = command === 'serve';
  return {
    resolve: {
      alias: isDev ? { '@ss/cart': fileURLToPath(new URL('./dev/mock-cart.ts', import.meta.url)) } : {},
    },
    build: {
      rollupOptions: {
        external: [
          'react', 'react-dom', 'react/jsx-runtime',
          ...(isDev ? [] : ['@ss/cart', '@ss/react-redux']), // ✅ prod external
        ],
      },
    },
    define: {
      __DEV__: isDev,
      'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'),
    },
  };
});

生产(prod)@ss/cart(以及 @ss/react-redux)标记为 external;远程组件代码保持:

import { useCart } from '@ss/cart';

远程产物里 不会内嵌实现,运行时交给宿主通过 import map 注入。

2)宿主(Host)落地

2.1 写一个 proxy,把真实实现转成浏览器可用的 ESM

源文件(只转口,不写业务):

// host-ports/useCart.proxy.ts
export { useCart } from '../src/redux/hooks/useCart';

用 esbuild 产物到 public/externals/(按需加载):

npx esbuild host-ports/useCart.proxy.ts \
  --bundle --format=esm --platform=browser --target=esnext \
  --external:react --external:react-dom --external:react/jsx-runtime \
  --external:@ss/react-redux \
  --outfile=public/externals/useCart.mjs

 

2.2 处理额外依赖:共享 同一个 react-redux

不能在产物里 import "react-redux"(那会变成另一份实例或浏览器解析失败)。做法是把宿主已加载的 react-redux 暴露到全局,并让远程通过端口代理拿它。

注册全局(远程加载前):

import * as ReactRedux from 'react-redux';
(globalThis as any).__REACT_REDUX__ = ReactRedux;

端口代理(放 public/externals/react-redux.proxy.mjs):

const getRR = () => {
  const rr = self.__REACT_REDUX__;
  if (!rr) throw new Error('[react-redux-proxy] Host react-redux not ready.');
  return rr;
};
export const useDispatch = (...a) => getRR().useDispatch(...a);
export const useSelector = (...a) => getRR().useSelector(...a);
export default new Proxy({}, { get(_t, p, r){ return Reflect.get(getRR(), p, r); }});

useCart.proxy.ts 里改为从端口取 hooks:

 
import { useDispatch, useSelector } from 'react-redux';

2.3 import map 映射端口 → 产物

<script type="importmap">
{
  "imports": {
    "react": "/vendor/react-19.1.0.js",
    "react-dom": "/vendor/react-dom-19.1.0.js",
    "react/jsx-runtime": "/vendor/react-jsx-runtime-19.1.0.js",
    "react-redux": "/vendor/react-redux.proxy.mjs", // 先window注入实例,然后mjs就能找到
    "@ss/cart": "/externals/useCart.mjs"
  }
}
</script>

浏览器只在远程真正 import '@ss/cart' 时下载 /externals/useCart.mjs按需加载

 

posted @ 2025-08-31 02:57  PEAR2020  阅读(7)  评论(0)    收藏  举报