Skip to content

代码分层与实现解读 ​

本页面向需要维护 fin-micro 的开发者。先记住一个边界:fin-micro 管理的是应用、页签、导航事务和子应用生命周期;登录接口、菜单后台协议、业务数据和服务端权限仍由接入项目负责。

1. 一次打开页面发生什么 ​

text
项目创建 MicroHost
  └─ 注册 appId、菜单、路由归属和能力白名单
      └─ open/operate 提交页签事务
          └─ bridge(appId) 创建应用级桥
              └─ page(tab.key) 绑定固定页面生命周期
                  ├─ runtime.mount(appId, container) 挂载一个 Wujie 实例
                  └─ MicroTabCache/MicroRouteView 渲染页签内部页面

关闭、刷新或 LRU 淘汰时,顺序反过来:页面 SDK 先失效,宿主释放页签订阅和守卫,runtime 移除 DOM 并等待 destroyApp,最后项目销毁 browser/router/session 连接。异步结果必须检查 key 或 revision,不能用旧页面的结果更新同 tabId 重开的页面。

2. 目录职责 ​

目录作用典型入口
src/sdk框架无关的类型、桥、错误码、会话和权限契约AppBridge、PageSDK、getMicroBridge
src/core宿主状态机:应用注册、页签、导航队列、离页守卫、能力白名单createMicroHost
src/runtimeWujie 的挂载、就绪回报、超时、卸载和资源预算createWujieRuntime
src/vueVue 适配:页签上下文、缓存、路由出口、权限和 session 注入MicroTabCache、MicroRouteView、useMicroPage
src/browserhash、history、sessionStorage 与宿主状态同步connectMicroBrowser
src/iframe精确窗口/origin/channel/session 的受控 RPCcreateIframeRpc
src/layout可选的展示外壳,不创建 host、router 或认证状态HostShell、StandaloneShell
scaffold/host基座示例的认证、菜单解码、平台适配和工作区编排platform/*、router/*
scaffold/vue子应用示例的独立/嵌入启动、路由和业务页面bootstrap.ts、createMicroSession
cli创建 host/vue 项目并检查发布内容cli/index.mjs

根入口 src/index.ts 会把 SDK、core、runtime、Vue 和 browser 全部带入。非 Vue 子应用应从 /sdk 导入;只需要宿主状态时从 /core 导入;需要 Wujie 时从 /runtime 导入。这样可以避免无关的 Vue、Element Plus 或布局代码进入无框架应用。

3. 三个身份必须分开 ​

  • appId 表示加载哪个子应用,宿主注册表中必须唯一;当前一个 appId 只允许一个 Wujie 运行实例。
  • tabId 表示业务页签,例如 order:detail:1001。同一个应用可以有多个详情页,但每个详情必须有不同 tabId。
  • key 表示一次页面生命周期。关闭重开、刷新或同 tabId 更换路由都会生成新 key,用于隔离 SDK、守卫、缓存和迟到异步任务。

route 只是子应用内部路径,activeMenuId 只用于菜单高亮。二者都不能代替 tabId 或权限判断。

4. 核心导航该怎么用 ​

createMicroHost 内部把每次交互放入串行队列。openTab/activateTab/closeTab 是同步底层原语,适合初始化和受控适配器;菜单、按钮和页面业务应使用 host.operate 或页面 SDK,这样才能执行 beforeLeave、权限复核和一次性提交。

页面内使用固定生命周期的 SDK:

ts
const tab = useMicroTab()
const page = useMicroPage(bridge, tab)

await page.openTab({ appId: 'order', tabId: 'order:list', title: '订单', route: '/list' })
await page.refreshCurrent()       // 生成新 key,旧 page 立即失效
await page.back()                 // 关闭当前页并回到宿主记录的来源或邻页

openTab 只允许本应用;跨应用必须使用 navigateTo 或 back(destination),并同时通过宿主的 canNavigate 和 canOpen。committed 只表示页签状态已提交,不表示远程资源或业务接口已经成功。

离页守卫只负责确认和保存,然后返回布尔值;不要在守卫里等待同一宿主的 operate/openTab/closeCurrent,否则会等待自己所在的串行事务。取消和超时只结束导航等待,不会回滚已经发出的保存、支付等服务端副作用。

5. Vue 页面和运行时的关系 ​

MicroTabCache 按 tab.key 建立 KeepAlive 边界,只渲染已经访问过的页签。停用页面仍保留实例;关闭、刷新和 LRU 淘汰才会真正卸载。页面草稿不能只放在组件内存,应由业务层按用户、应用和业务记录持久化。

MicroRouteView 从固定 tab.route 解析并懒加载 Vue Router 页面,给每个缓存页提供独立的 useRoute 上下文。它不会调用宿主 router.push,也不会代替宿主守卫;页面间跳转应使用 PageSDK。路由 meta.permissions 只做前端展示和导航准入,接口仍必须由服务端鉴权。

createWujieRuntime 对同一 appId 只保留一个实例,并给每次挂载分配独立 attemptId。加载超时、初始化未就绪和卸载超时都会隔离旧实例;迟到的 Wujie 回调不能覆盖新一轮挂载。maxMountedApps、maxConcurrentMounts 和 residualSandboxes 用于防止失败清理导致资源无限增长。

6. 宿主通信和凭据边界 ​

宿主只通过显式注册的能力暴露函数,子应用拿到的是 AppBridge,不是宿主对象。每次能力调用都有应用白名单、超时、取消和生命周期复核;handler 应使用 context.appId,不能信任 payload 自报身份。

公开 SessionSnapshot 只包含认证状态、用户展示信息、权限和 revision,不携带 Token。Token 通过 getToken 按需获取,Cookie 模式返回 null;退出、用户切换或凭据更新必须推进 revision,防止旧请求结果提交到新会话。iframe RPC 只转发显式列出的窄能力,不能把完整 AppBridge、路由守卫或组件对象序列化过去。

7. 当前已落地的注册边界优化 ​

本轮在 src/core/index.ts 做了三项收紧:

  1. 创建宿主时校验 appId、应用名称和应用注册表类型,错误更早暴露。
  2. 菜单叶节点的 tabId 在整个宿主注册表内检查唯一,避免两个应用配置相同页签后才在运行时冲突。
  3. host.apps 返回冻结数组;应用对象、菜单、target 和 menuRoutes 也保持冻结,外部不能通过修改公开数组改变运行时注册表。

生产构建在类型检查后校验公开入口、声明引用和模板运行能力;无框架 core 与可选 Vue、布局入口保持独立。测试用例和历史临时验收工程已移除,实际接入仍需验证浏览器与服务端协议。

8. 建议拆成需求的后续工作 ​

这些事项涉及业务选择,当前代码没有擅自实现:

优先级需求需要确认的规则
P0真实浏览器验收与 CIChrome 版本、Wujie 资源入口、CORS/CSP、前进后退和整页刷新场景
P0生产认证协议Cookie 还是 Bearer、刷新方式、CSRF、401 并发失效和注销失败策略
P0菜单来源是否始终使用部署者可信注册表,后台只返回授权;还是要支持动态应用注册
P1业务草稿持久化保存时机、草稿冲突、刷新/退出/LRU 淘汰后的恢复范围
P1跨应用来源恢复刷新后是否需要恢复 back() 来源链;若需要,存储格式和权限复核规则
P1运行时运维是否上报加载耗时、失败次数、残留沙箱和用户可见诊断
P2其他框架适配React 等框架是否需要等价的 tab cache、route context 和 PageSDK 绑定层

在这些规则确定前,不建议新增第二套全局状态机、让后台直接下发可执行 URL,或把 Token 放入公开会话、URL、事件和 sessionStorage。