万星入坞:我们如何用三层插件体系干掉巨石应用

0 阅读15分钟

在大规模企业级前端应用的实践中,有三个老生常谈却始终避不开的问题:
如何保证代码质量,如何提高开发效率,如何降低维护成本?

笔者所在团队维护的运营管理后台,从最初一个几万行代码的 SPA,逐步膨胀到了几十万行的"巨无霸"。随着业务扩张和组织调整,一个曾经完全由单一团队维护的系统,慢慢变成了多部门协作的巨型应用——公共组件改一处牵动全局、发布一次需要全量回归、团队之间互相等待代码合并……这些问题叠加在一起,开发效率直线下降,维护成本指数级上升。

面对这些痛点,微前端是业界主流的解法。但社区里的微前端框架(qiankun、single-spa 等)多数聚焦在"如何把多个独立应用拼在一起",对于配置驱动、生命周期编排、跨插件状态共享、UI 组件级别的插件化等企业级诉求,要么没有覆盖,要么需要大量胶水代码。

笔者的团队决定造一个轮子——星坞(Xingwu)一个面向现代浏览器的插件化前端框架
它的核心设计思想是:壳层 + 子应用 + SDK 三层插件体系,配置驱动,按需加载。

本文聚焦星坞框架的主应用(Shell)设计与实现,拆解壳层的核心模块、启动流程、本地开发模式与构建部署方案。


巨石应用之痛

先来聊聊我们遇到的具体问题,这些也是星坞设计的驱动力。

巨石应用示例图生成.png

构建

巨石应用最直观的痛就是构建慢。几十万行代码的 Webpack 项目,冷启动动辄两三分钟,HMR 一次更新要等好几秒。CI 构建更惨,十来分钟起步是常态。更要命的是,这种慢是"全局性"的——你只改了商品模块的一个按钮文案,整个应用都得重新打包。

发布

单体应用只有一条发布流水线,所有功能必须一起上线。商品团队修了个 bug,但订单团队还没测完?那就等着吧。更危险的是,一个模块的线上故障可能导致整个应用不可用,影响范围远超故障本身的业务边界。

协作

多团队在同一个仓库开发,代码冲突是家常便饭。更隐蔽的问题是依赖纠缠——A 模块 import 了 B 模块的内部函数,B 重构时 A 就挂了。久而久之,没人敢动公共代码,技术债越积越多。

小结

痛点根因星坞解法
构建慢全量打包Shell + 按需加载,子应用独立构建
发布耦合单一发布流水线子应用/SDK 独立部署,配置驱动
代码冲突同仓库强耦合Monorepo + 三层插件边界
依赖纠缠无隔离的模块引用AppContext/SdkContext 受限 API

三层插件体系

星坞框架的架构可以概括为四个字:万星入坞。 壳层是"坞",子应用和 SDK 是"星"。

graph TB
  subgraph Shell["Shell(壳层)"]
    subgraph Core["核心模块"]
      Bootstrap
      Router
      PluginRegistry
      ConfigCenter
      SharedStateBus
      LifecycleManager
      Infrastructure
    end
    subgraph Apps["子应用"]
      AppProduct["App: product\n/product/*"]
      AppOrder["App: order\n/order/*"]
      AppMore["App: ...\n独立路由段"]
    end
    subgraph SDKs["SDK"]
      SDKAuth["SDK: auth-guard\n纯逻辑 · 鉴权"]
      SDKRegion["SDK: region-selector\n逻辑+UI · 区域"]
    end
  end
层级职责关键约束
Shell(壳层)应用初始化、路由分发、插件注册表、配置中心、共享状态、基础设施禁止反向依赖子应用或 SDK
App(子应用)独立业务模块,拥有路由段和完整 UI 树通过 AppContext 消费 Shell 能力,禁止直接访问 Shell 内部
SDK(轻量插件)不占路由段的功能模块;可纯逻辑,也可提供 UI 组件通过 SdkContext 消费 Shell 能力,能力是 AppContext 的受约束子集

依赖方向严格单向:Shell → typesApp/SDK → types。子应用与 SDK 之间通过 SharedStateBusSdkRegistry 通信,禁止直接 import 对方模块

有人可能会问:
为什么还要区分 App 和 SDK?
直接都用子应用不行吗?
还真不行。实际业务中有很多能力——区域选择器、鉴权守卫、审计日志——它们既不属于某个特定子应用,又需要渲染 UI 或者拦截逻辑。如果强制归入子应用,会引入不必要的路由和加载开销;如果复制到每个子应用,则违背 DRY 原则。
SDK 就是解决这个矛盾的轻量形态:声明式 UI 契约 + 按需/预加载 + 自主渲染或组件暴露,灵活且聚焦。


Shell 启动流程

Shell 的启动遵循 创建 → 挂载 → 渲染 三步,清晰且有层次感。

graph TD
  Start(["应用启动"]) --> Create["① createShell(config)\n初始化基础设施 → 核心模块"]
  Create --> Mount["② shell.mount('#root')\n加载插件配置 → 注册插件 → 预加载 SDK"]
  Mount --> Render["③ render(ShellApp)\n挂载全局 React 实例 → 渲染壳层 UI"]
  Render --> Done(["启动完成,等待路由"])

  Create -. "依赖顺序" .-> Mount
  Mount -. "React 单实例" .-> Render

第一步:创建 Shell 实例

const shell = createShell(config);

createShell 内部按依赖顺序初始化所有核心模块:

export class Shell {
  readonly registry: PluginRegistry;
  readonly configCenter: ConfigCenter;
  readonly sharedState: SharedStateBus;
  readonly sdkRegistry: SdkRegistry;
  readonly lifecycle: LifecycleManager;
  readonly monitor: MonitorImpl;
  readonly i18n: I18nImpl;
  readonly net: NetClientImpl;
  readonly permission: PermissionCheckerImpl;

  constructor(config: ShellConfig) {
    // 基础设施先行(被其他模块引用)
    this.monitor = new MonitorImpl(config.monitor);
    this.i18n = new I18nImpl(config.i18n);
    this.net = new NetClientImpl();
    this.permission = new PermissionCheckerImpl(config.permission);

    // 核心模块后行(存在依赖关系)
    this.registry = new PluginRegistry();
    this.configCenter = new ConfigCenter(config.configCenter, this.monitor);
    this.sharedState = new SharedStateBus();
    this.lifecycle = new LifecycleManager(this.registry, { ... });
    this.sdkRegistry = new SdkRegistry(this.registry, { ... });
  }
}

这里有个细节值得说下:初始化顺序不是随便写的。ConfigCenter 依赖 Monitor 做错误上报,SdkRegistry 依赖 PluginRegistryLifecycleManager,而 LifecycleManager 又依赖 PluginRegistry。这些依赖关系决定了基础设施必须先于核心模块初始化。

第二步:挂载并初始化

await shell.mount('#root');

mount 内部调用 init(),完成三件事:

  1. 加载插件配置 — 从 JSON 文件或内联数组读取 App/SDK 描述符,经 Zod Schema 校验后注册
  2. 注册插件 — 将描述符写入 PluginRegistry
  3. 预加载 SDK — 对标记了 preload: true 的 SDK,提前 resolve + activate
async init(): Promise<void> {
  const { apps, sdks, preloadSdkNames } = await loadPluginConfig(plugins);
  this.registry.registerApps(apps);
  this.registry.registerSdks(sdks);

  if (preloadSdkNames.length > 0) {
    await this.sdkRegistry.preload(preloadSdkNames);
  }

  this.configCenter.startRefresh();
}

这里用了一个比较实用的设计:插件配置以 JSON 文件外置,开发时从本地 shell/config/ 目录读取,生产环境则从 ConfigMap 拉取。好处是增减插件无需改代码,只改配置即可。配合 Zod Schema 做运行时校验,防止配置错误在下游引发难以定位的问题。

第三步:渲染 React 应用

const root = createRoot(document.getElementById('root')!);
root.render(
  <React.StrictMode>
    <ConfigProvider locale={zhCN}>
      <AntdApp>
        <ShellApp shell={shell} config={config} />
      </AntdApp>
    </ConfigProvider>
  </React.StrictMode>,
);

在渲染之前,Shell 做了一件关键的事——将 React、ReactDOM、antd 子集挂载到全局

(window as any).__REACT_SHARED__ = { React, ReactDOM };
(window as any).__ANTD_SHARED__ = {
  antd: { Breadcrumb, Button, Dropdown, Empty, Select, Space, Typography },
  icons: { GlobalOutlined },
};

为什么要这么做?因为 React 的 Hooks 机制要求整个应用使用同一份 React 实例,否则 useStateuseContext 等 Hook 会因实例不一致而崩溃。壳层先把 React 挂到全局,后续动态加载的子应用和 SDK 就能复用同一份实例。


核心模块设计

PluginRegistry —— 插件注册表

PluginRegistry 是整个框架的"人口管理局",所有插件(App + SDK)的注册、解析与模块缓存都由它管理,是唯一的注册事实来源

这里有个设计决策值得一提:SDK 有自己的 SdkRegistry,但它不持有任何状态,只是 PluginRegistry 的门面(Facade)。为什么?因为如果 App 和 SDK 各自维护注册表,同一个插件可能被两边注册了不同版本,依赖拓扑无法完整计算。统一注册表 + 门面模式,既保证了全局一致性,又让 SDK 消费侧的 API 保持简洁。

模块缓存与 SRI 校验

resolve(name)moduleCache: Map<string, Promise<unknown>> 缓存已加载的模块 Promise。这意味着同一插件只会被 import() 一次,后续调用直接返回缓存。

对于安全要求更高的场景,PluginRegistry 支持 SRI(Subresource Integrity)校验——当描述符中携带 integrity 字段时,不直接 import(entry),而是先 fetch 资源、通过 SubtleCrypto 计算哈希校验、再通过 Blob URL 导入,防止静态资源被篡改:

async function importWithSri(entry: string, integrity: string): Promise<unknown> {
  const response = await fetch(entry);
  const buffer = await response.arrayBuffer();
  // 校验哈希
  const hashBuffer = await crypto.subtle.digest(normalizedAlgo, buffer);
  if (actualBase64 !== expectedBase64) {
    throw new Error(`[Xingwu] SRI check failed for "${entry}"`);
  }
  // 校验通过,用 Blob URL 导入
  const blob = new Blob([buffer], { type: 'application/javascript' });
  return await import(/* @vite-ignore */ URL.createObjectURL(blob));
}

路由系统 —— 权限前置与离开拦截

Shell 的路由基于 React Router v6 扩展,核心流程是:URL 变更 → 查重定向规则 → 执行 beforeNavigate 守卫 → 查找插件 → 权限校验 → resolve → mount

graph TD
  URLChange["URL 变更"] --> Redirect{"匹配重定向规则?"}
  Redirect -->|是| DoRedirect["执行重定向"] --> URLChange
  Redirect -->|否| Guard["beforeNavigate 守卫"]
  Guard --> GuardResult{"守卫通过?"}
  GuardResult -->|否| Abort["阻止导航"]
  GuardResult -->|是| FindPlugin["查找插件描述符"]
  FindPlugin --> Permission{"权限校验\n(描述符中的 permission 字段)"}
  Permission -->|无权限| Deny["403 / 跳转登录"]
  Permission -->|有权限| Resolve["import() 加载模块"]
  Resolve --> Mount["mount 挂载子应用"]

  style Permission fill:#fff3cd
  style Resolve fill:#d4edda

这里有两个关键设计:

权限前置:权限检查在 import() 加载之前执行。如果先加载模块再校验权限,敏感内容已进入浏览器内存,且浪费了网络带宽与 JS 解析开销。Shell 的策略是先查描述符中的权限声明,再决定是否加载——PluginDescriptor 中的 navItempermission 字段足够壳层做出权限判断,无需加载模块本体。

路由离开拦截:基于 React Router v6 的 useBlocker API 统一实现。子应用通过 ctx.router.beforeLeave 注册守卫函数,Shell 在 beforeUnmount 阶段聚合所有守卫结果:任一守卫返回 false → 阻止离开,弹出确认对话框。

ConfigCenter —— 配置中心

ConfigCenter 提供类型安全的运行时配置管理,三个核心能力:响应式更新、插件级作用域隔离、Zod Schema 校验

作用域隔离是配置中心的关键设计。底层用一个扁平的 Map<string, unknown> 存储所有配置,key 遵循 pluginName.configKey 格式。forPlugin(pluginName) 返回一个 PluginConfigScope 门面对象,自动为所有读写操作加上 pluginName. 前缀。这样插件只能操作自己的命名空间,无法读写其他插件的配置。

forPlugin(pluginName: string): PluginConfigScope {
  const prefix = `${pluginName}.`;
  return {
    get: <T>(key: string): T => this.get<T>(`${prefix}${key}`),
    set: <T>(key: string, value: T): void => this.set(`${prefix}${key}`, value),
    watch: <T>(key: string, cb: (v: T, old: T) => void): (() => void) =>
      this.watch<T>(`${prefix}${key}`, cb),
  };
}

远程刷新失败时,ConfigCenter 采用指数退避重试(1s → 2s → 4s,最多 3 次),全部失败后降级使用本地缓存并上报监控,而不是直接让配置失效。

graph TD
  Refresh["远程刷新配置"] --> Result{"请求成功?"}
  Result -->|是| Update["更新本地缓存\n通知订阅者"]
  Result -->|否| Retry1["重试 1(延迟 1s)"]
  Retry1 --> R1{"成功?"}
  R1 -->|是| Update
  R1 -->|否| Retry2["重试 2(延迟 2s)"]
  Retry2 --> R2{"成功?"}
  R2 -->|是| Update
  R2 -->|否| Retry3["重试 3(延迟 4s)"]
  Retry3 --> R3{"成功?"}
  R3 -->|是| Update
  R3 -->|否| Fallback["降级:使用本地缓存\n上报监控"]

  style Fallback fill:#f8d7da
  style Update fill:#d4edda

SharedStateBus —— 共享状态总线

跨插件状态共享有三种经典方案:① 全局 Store(如 Redux)—— 强依赖单一状态管理库;② window 全局变量 —— 零约束,命名冲突与隐式依赖无法控制;③ 受控 EventBus —— 在灵活性与约束之间取平衡。

星坞选择方案③,并增加了以下约束使其从"松散 EventBus"升级为"受控状态总线":

  • 命名空间强制:所有 key 遵循 pluginName.stateKey 格式
  • 写入审计:每次 setState 自动记录 { key, value, timestamp } 到滚动窗口(上限 5000 条),供运维调试
  • 函数式更新setState 支持 (prev: T) => T 回调,避免竞态条件下基于旧值计算

LifecycleManager —— 生命周期管理器

LifecycleManager 编排插件的挂载、更新、卸载流程,保证任意时刻最多一个子应用处于 active,并通过串行锁避免 mount/unmount 竞态。

App 和 SDK 的生命周期有差异,这是由定位决定的:

阶段AppSDK
初始化beforeMount → mount → afterMountactivate
更新update(路由参数变化)无(不参与路由)
卸载beforeUnmount → unmountdeactivate
stateDiagram-v2
  state App {
    [*] --> BeforeMount: 路由匹配
    BeforeMount --> Mount: 钩子通过
    Mount --> AfterMount: 挂载完成
    AfterMount --> Update: 路由参数变化
    Update --> Update: 参数再次变化
    Update --> BeforeUnmount: 路由离开
    AfterMount --> BeforeUnmount: 路由离开
    BeforeUnmount --> Unmount: 钩子通过 / 超时熔断
    BeforeUnmount --> AfterMount: 返回 false(阻止卸载)
    Unmount --> [*]
  }

  state SDK {
    [*] --> Activate: 预加载 / 按需加载
    Activate --> Deactivate: 壳层卸载
    Deactivate --> [*]
  }

几个关键设计点:

  • beforeUnmount 可中断beforeMountafterMount 是通知型钩子,但 beforeUnmount 返回 false 可阻止卸载——处理"表单未保存"等场景
  • 钩子超时熔断:每个钩子有 10 秒超时限制,超时后 reject 并释放串行锁,防止死锁
  • ESM 模块驱逐:子应用 unmount 后可选择性驱逐模块缓存(evictOnUnmount),释放内存;下次进入时重新 import()

SdkRegistry —— SDK 门面

前面说了 SdkRegistry 是 PluginRegistry 的门面,但它的门面不是简单的方法转发,在三个关键点增加了业务语义:

  1. API 获取语义get<T>(name) 不是直接返回模块导出,而是从 SharedStateBus 读取 {name}.api——SDK 在 activate 阶段发布,消费者通过订阅感知 API 就绪
  2. UI 组件缓存activate 后提取 getComponents() 返回的组件映射并缓存,避免每次 getComponent() 重新调用
  3. preload = resolve + activate:预加载不仅是加载模块,还要执行初始化,因为预加载的目的是"首屏即可用"

SDK UI 组件机制

SDK 的 UI 能力是星坞相比传统微前端框架的一个亮点。传统方案中,插件只能是纯逻辑或纯页面,无法表达"提供可复用 UI 片段"的需求。SDK 通过三种互补方式解决这一问题:

方式一:壳层插槽自主渲染

壳层在布局中预留 SdkSlotHost,SDK 通过 render(container, ctx) 将 UI 渲染到宿主提供的 DOM:

// Shell 布局中预留插槽
<SdkSlotHost shell={shell} sdkName="region-selector" slot="header-slot" />

// SDK 侧实现 render
render(container, ctx) {
  return renderSdkUi(container, ctx); // 读取 data-xingwu-slot 选择组件并 createRoot
}

这里的设计哲学是布局权与渲染权分离——宿主决定"UI 出现在哪",SDK 决定"插槽里画什么"。

当 SDK 内部状态变更需要刷新 UI 时,通过 ctx.ui.requestRerender(componentName) 通知宿主,SdkSlotHost 会递增 renderVersion,重新执行 renderTo 流程。

方式二:子应用显式引用

子应用通过 ctx.sdk.getComponent('region-selector', 'RegionPicker') 获取组件,自行放入 JSX 树。适用于需要精细控制位置与 props 的场景。

方式三:仅消费 API

不渲染 UI,只调用 RegionSelectorApi 等逻辑能力。


本地开发模式

星坞的本地开发体验是设计时重点考虑的维度,目标是让开发者在壳层和子应用之间无缝切换。

graph TD
  subgraph Standalone["独立开发模式"]
    DevApp["子应用 dev server\nlocalhost:5174"] --> DevPage["独立页面\n无需 Shell"]
  end

  subgraph Joint["联调模式"]
    ShellDev["Shell dev server\nlocalhost:3000"] -->|"动态 import()"| DevApp
    ShellDev -->|"entry 覆盖为\nlocalhost:5174"| LoadPlugin["loadPluginConfig"]
    LoadPlugin --> SharedReact["window.__REACT_SHARED__\n共享 React 单实例"]
  end

  DevApp -.->|"需要联调时切换"| ShellDev

独立开发

子应用可以独立启动开发服务器,不依赖 Shell:

cd packages/apps/product
pnpm dev
# 访问 http://localhost:5174

联调模式

启动 Shell 后,通过 JSON 配置中声明的 entry 定位到子应用的本地开发服务器:

cd packages/shell
pnpm dev
# 访问 http://localhost:3000/product
# Shell 从 localhost:5174 动态 import 子应用

开发态下,loadPluginConfig 会自动将 JSON 中的生产 entry 覆盖为本地开发地址:

const DEV_ENTRY_OVERRIDES: Record<string, string> = {
  product: 'http://localhost:5174/src/index.tsx',
  'auth-guard': 'http://localhost:5175/src/index.ts',
  'region-selector': 'http://localhost:5176/src/index.tsx',
};

开发模式共享 React 实例

联调模式下的一个棘手问题是:Shell 和 SDK 各自有 Vite 开发服务器,如果不做处理,SDK 通过 import() 加载时会拿到另一份 React 实例,导致 Hooks 崩溃。

星坞的解法是 createSharedReactPlugin:在 SDK 的 Vite 配置中,将 react 系裸导入重定向到虚拟模块,从 window.__REACT_SHARED__ 获取 Shell 提供的 React 单实例。

需要拦截的裸导入包括:

裸导入虚拟模块说明
reactvirtual:shared-reactReact 核心
react-domvirtual:shared-react-domReactDOM
react-dom/clientvirtual:shared-react-dom-clientcreateRoot、hydrateRoot
react/jsx-runtimevirtual:shared-react-jsx-runtime生产态 JSX 转换
react/jsx-dev-runtimevirtual:shared-react-jsx-dev-runtime开发态 JSX 转换

其中 react-dom/client 的拦截最容易踩坑——如果不单独拦截,Shell 通过 import() 动态加载 SDK 时,react-dom/client 会落到 Vite 的 CJS→ESM 预构建路径,而预构建转换无法正确暴露 createRoot 命名导出,运行时会报:

SyntaxError: The requested module '.../react-dom/client.js' does not provide an export named 'createRoot'

配合 resolve.dedupeoptimizeDeps.disabled: true,确保所有 react 系模块走 Vite 的正常 transform → resolve pipeline,由 createSharedReactPlugin 统一拦截。


构建与部署

分层构建策略

graph LR
  subgraph Artifacts["构建产物"]
    ShellJs["shell.js\nShell 包(首屏加载)"]
    VendorJs["vendor.js\nVendor 包(长期缓存)"]
    AppJs["product-[hash].js\nApp 包(按需加载)"]
    SdkJs["sdk-rs-[h].js\nSDK 包(按需/预加载)"]
    ChunkJs["chunk-[hash]\nCommon 包(自动抽取)"]
  end

子应用和 SDK 采用 lib 模式独立构建,将 react/react-dom 标为 external,由 Shell 的 Import Maps 在运行时提供。这意味着 React 等公共依赖只加载一份,既减小了产物体积,又避免了多实例问题。

Import Maps

生产环境利用浏览器原生 Import Maps 实现模块共享。壳层构建时由 @xingwu/vite-pluginpackage.json 依赖版本自动生成 importmap,无需手写。

graph TD
  CI["CI 构建\nvite build\n生成 SRI"] --> Asset["静态资源发布\n上传+分发\n返回资源 URL"]
  Asset --> Config["配置中心注册\n写入新版本描述符"]
  Config --> Gray["灰度策略\npercentage\nwhitelist"]
  Gray --> Rollback["回滚 = 配置中心切回旧版"]

插件入口从"构建时硬编码"变为"运行时配置",带来了三个关键能力:

  • 灰度发布:不同用户看到同一插件的不同版本,只需在配置中心为不同灰度规则返回不同 entry URL
  • 秒级回滚:回滚无需重新构建,只需将插件描述符指向上一版本的资源 URL + SRI
  • 独立部署:子应用/SDK 可以独立构建发布,壳层无需重新打包

插件配置外置

App 和 SDK 的配置以 JSON 文件存放在 shell/config/ 目录,构建时由 shellConfigVitePlugin 复制到 dist/config/。生产环境中,这些 JSON 文件将以 ConfigMap 的形式从容器平台获取——当增加 App 或 SDK 时,无需修改代码,只需修改配置。

加载时通过 Zod Schema 对 JSON 内容做运行时校验,确保 nameentryroutePrefix 等关键字段类型正确,防止脏配置在下游引发难以定位的错误。


插件沙箱与安全

星坞对不同来源的插件采用分级信任策略:

信任等级来源隔离策略
L1 受信内部 Monorepo公约 + 审计 + 代码审查
L2 半信内部但跨团队公约 + 运行时监控 + SdkContext 受限 API
L3 不信第三方CSP + SRI + iframe 隔离

首版实现覆盖 L1/L2 场景,采用"公约 + 受限上下文 + 运行时审计":

  • 插件只能通过 AppContext / SdkContext 与框架交互;
  • SharedStateBus 和 ConfigCenter 的写入均记录来源插件与调用栈;
  • 远程加载的插件入口支持 SRI 校验。

L3 不信场景的降级策略(iframe 隔离 + postMessage 通信)在架构上预留了扩展点,但暂不实现。


星坞 vs 巨石应用

维度巨石应用星坞框架
构建全量打包,改一行等半天Shell + 按需加载,子应用独立构建
部署单一流水线,牵一发动全身子应用/SDK 独立部署,配置驱动
协作同仓库强耦合,代码冲突频繁Monorepo + 三层边界,团队独立开发
首屏加载全部业务代码只加载壳层 + 当前业务,其余按需拉取
回滚重新构建 + 全量发布配置中心切回旧版 URL,秒级回滚
灰度需要额外基建配置中心原生支持灰度策略
UI 复用复制代码或强依赖公共包SDK 声明式 UI 契约,三种消费方式
调试全应用启动子应用可独立开发,联调模式按需挂载
复杂度低(单应用简单)高(框架本身有学习成本)
一致性天然一致(同一份代码)需要约束(共享 React 实例、Import Maps)
调试链路直接跨应用链路较长,需依赖监控体系

客观来说,星坞引入了框架本身的复杂度和学习成本——生命周期、上下文约束、共享 React 实例、Import Maps 对齐——这些都是巨石应用不需要操心的。但在业务规模达到一定量级后,这些前期投入换来的是后续开发的线性增长而非指数级膨胀。是否采用,取决于团队规模和业务复杂度是否已经到了"不拆不行"的临界点。


小结

  1. 星坞框架的核心价值在于三层插件体系(Shell + App + SDK)将巨石应用拆解为可独立开发、部署、回滚的单元,同时通过 AppContext/SdkContext 受限 API 保证边界清晰
  2. 配置驱动是贯穿始终的设计主线——JSON 外置配置、Zod 运行时校验、ConfigCenter 灰度策略、Import Maps 版本协商——让"增减插件不改代码"成为可能
  3. SDK 的 UI 能力填补了传统微前端"纯逻辑或纯页面"之间的空白,布局权与渲染权分离的设计让组件复用不再需要复制代码
  4. 开发体验上,独立开发 + 联调模式 + 共享 React 实例的方案,让本地开发既有微前端的架构优势,又不失单体应用的调试便利性

如果你也在面临巨石应用的困扰,希望星坞的设计思路能给你一些启发。

Xingwu传送门,欢迎star⭐:xingwu-ops-fe