Appearance
代码分层与实现解读
本页面向需要维护 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/runtime | Wujie 的挂载、就绪回报、超时、卸载和资源预算 | createWujieRuntime |
src/vue | Vue 适配:页签上下文、缓存、路由出口、权限和 session 注入 | MicroTabCache、MicroRouteView、useMicroPage |
src/browser | hash、history、sessionStorage 与宿主状态同步 | connectMicroBrowser |
src/iframe | 精确窗口/origin/channel/session 的受控 RPC | createIframeRpc |
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 做了三项收紧:
- 创建宿主时校验
appId、应用名称和应用注册表类型,错误更早暴露。 - 菜单叶节点的
tabId在整个宿主注册表内检查唯一,避免两个应用配置相同页签后才在运行时冲突。 host.apps返回冻结数组;应用对象、菜单、target 和 menuRoutes 也保持冻结,外部不能通过修改公开数组改变运行时注册表。
生产构建在类型检查后校验公开入口、声明引用和模板运行能力;无框架 core 与可选 Vue、布局入口保持独立。测试用例和历史临时验收工程已移除,实际接入仍需验证浏览器与服务端协议。
8. 建议拆成需求的后续工作
这些事项涉及业务选择,当前代码没有擅自实现:
| 优先级 | 需求 | 需要确认的规则 |
|---|---|---|
| P0 | 真实浏览器验收与 CI | Chrome 版本、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。