Proxy & Isolation

Web Host 在沙箱上下文中运行每个子微前端,并通过 Proxy API 把它桥接到宿主。微前端应用和 Web 组件都通过从 @wippy-fe/proxy 导入来访问宿主。

Proxy API 注入与嵌套

Proxy API

Proxy API 是你通往宿主的入口。一个运行时 —— proxy.js —— 负责投递它:它把 API 和当前的 AppConfig 放到页面上,并通过 @wippy-fe/proxy 模块暴露它们。

  • 对于微前端应用(view.page),宿主把 proxy.js 注入页面的 srcdoc。
  • 对于 Web 组件(view.component),运行时已经存在于宿主页面中 —— 组件挂载在宿主 DOM 中,而不是单独的 iframe 里。

你的代码通过 @wippy-fe/proxy 导出的同步 getter 使用它:

import { host, api, on, config } from '@wippy-fe/proxy'

host.navigate('/dashboard')
const data = await api.get('/api/v1/agents')   // api 是一个 axios 实例;await 等的是 HTTP 调用
on('@visibility', (visible) => { /* 暂停或恢复工作 */ })

可移植的 Vue 路由是个例外:@wippy-fe/router 会替你消费 @history 并上报本地导航。不要在它周围再添加手工的路由订阅。

这些 getter 是同步的:host、api、on、config 以及其余部分在你的代码运行的那一刻就已就绪 —— 配置在运行时初始化之前就已到位(见下文),因此没有需要等待的握手。请在你的 Vite 构建中把 @wippy-fe/proxy 标记为 external —— 宿主通过 import map 提供它。完整的 API 面参见 Proxy API。

配置如何到达应用 iframe

当宿主加载一个 view.page 时,它会构建 srcdoc 并在你的应用脚本之前按顺序注入:

<!-- 1. 子端 AppConfig —— 在运行时加载之前同步设置 -->
<script>window.__WIPPY_APP_CONFIG__ = { /* auth, env, theming, hostConfig, context */ }</script>
<!-- 2. 该页面的 CSS 注入标志 -->
<script>window.__WIPPY_PROXY_CONFIG__ = { injections: { css: { themeConfig: true, primevue: true /* … */ } } }</script>
<!-- 3. 运行时(前面是 loading.js) -->
<script src="/.../loading.js"></script>
<script src="/.../proxy.js"></script>

由于配置全局变量在 proxy.js 运行之前就已设置,运行时会同步初始化,@wippy-fe/proxy 的 getter 立即可用 —— 没有握手。页面不直接引用这些脚本;<script data-role="@wippy/scripts"> 占位符会被宿主替换为正确排序的标签。按页面的覆盖以 window.__WIPPY_CONFIG_OVERRIDES__ 形式到达(参见 Proxy API —— 配置覆盖)。

Web 组件看到的是同一批全局变量,因为它运行在宿主页面中,而运行时早在组件的 connectedCallback 触发之前就设置好了它们。

应用与 Web 组件的差异

两者都从 @wippy-fe/proxy 导入同一套 API。差异在于执行上下文以及样式的投递方式:

微前端应用(view.page) Web 组件(view.component)
运行于 自己的 srcdoc iframe 宿主页面 DOM(Shadow DOM)
运行时投递 注入 iframe 的 proxy.js 运行时已存在于宿主页面中
CSS 完整注入流水线(themeConfig、primevue 等)—— 参见 CSS 注入 通过 hostCssKeys 注入 Shadow DOM —— 参见主题化:Web Components

组合与嵌套

子级可以组合。微前端应用或 Web 组件本身也可以承载子级 —— 同样是微前端应用或 Web 组件 —— 而这些子级又能承载自己的子级,深度不限。每一层都使用同一套 @wippy-fe/proxy API。

一个节点如何承载子级取决于子级的类型:

  • iframe 子级 —— 微前端应用、制品或任意 Wippy HTML —— 通过 <w-iframe>、<w-artifact> 或 html.inject 承载。它们会把运行时(基础 URL、import map、loading.js、proxy.js 和配置)注入子级的 srcdoc,因此子级获得 Proxy API 的方式与顶层应用完全一致。它的代理会经由父级向上桥接到宿主。
  • Web 组件子级不需要这些。渲染它的标签 —— 或用 loadWebComponent / loadByTagName 加载它 —— 它就运行在同一个 DOM 中,直接导入 Proxy API。

无论子级运行在顶层还是嵌套多层,它自己的代码都完全相同:从 @wippy-fe/proxy 导入并使用。没有特殊的嵌套规则。

具体机制参见下文的 <w-iframe>、<w-artifact>和高级 HTML 注入。

内部实现 —— 不要读取或覆盖

proxy.js 会为自身安装以下全局变量。应用和组件代码绝不应读取或赋值它们 —— 请改用 @wippy-fe/proxy。记录它们只是为了让你不要意外破坏:

全局变量 含义
window.$W 异步访问器对象($W.host()、$W.api() 等)。内部使用;受支持的接口是 @wippy-fe/proxy。
window.getWippyApi / window.initWippyApi 异步的"解析实例"函数。内部使用(initWippyApi 已弃用)。
window.__WIPPY_APP_API__ 解析出的代理实例。
window.__WIPPY_APP_CONFIG__ 子端 AppConfig 快照。
window.__WIPPY_PROXY_CONFIG__ / window.__WIPPY_CONFIG_OVERRIDES__ CSS 注入标志和按页面的覆盖。
window.__WIPPY_WEB_COMPONENT_CACHE__ 已加载组件的缓存。

公开的 JavaScript API 由两个入口点构成:initWippyApp(config, rootContainer?) 挂载整个 Web Host(facade 使用的模块嵌入入口;参见 Facade 入口点),而 @wippy-fe/proxy 是子应用和组件使用的同步 API。上表中的一切都是内部实现。

PostMessage 协议(IFrameMessageType)—— 内部传输

这是运行时内部使用的线路协议;应用代码从不发送或接收这些消息 —— @wippy-fe/proxy 会替你处理。

标准的宿主注入路径无需握手即可启动 —— 配置早在 proxy.js 运行之前就已作为 window.__WIPPY_APP_CONFIG__ 同步存在,因此运行时会立即构建其实例。get-config/set-config 交换在这条路径上仍会发生,但只作为非阻塞的重新同步与实时更新通道:在同步实例构建完成之后,iframe 运行时总会发送 get-config,宿主以 set-config 回应,并在此后每次配置更新时重新推送 set-config。嵌套的 <w-iframe> 子级行为相同。你的代码从不等待这些 —— 同步 getter 早已可用。

只有在一种场景下握手才是唯一且阻塞的配置来源:手动的、无 facade 的 iframe 嵌入(iframe.html?waitForCustomConfig),此时不存在预先注入的 window.__WIPPY_APP_CONFIG__,因此初始化会阻塞在第一条 set-config 上,父级必须回应 get-config 请求(参见 Facade 入口点 § 手动 iframe 嵌入)。

每条消息都是形如 { type: '@gen2-chat', action: IFrameMessageType.*, ...payload } 的 JSON 信封。type 字段可通过 APP_CONFIG_IFRAME_EVENT_TYPE 配置,默认为 '@gen2-chat'。

所有消息类型都定义在 IFrameMessageType 枚举中:

枚举成员 线路取值 方向 说明
GetConfig get-config 子 → 宿主 初始握手:子级请求自己的 AppConfig
SetConfig set-config 宿主 → 子 宿主响应 GetConfig 投递 AppConfig
UrlWasUpdatedInParent url-was-updated-in-parent 宿主 → 子 宿主 URL 变化;触发子级的 @history 事件
VisibilityWasUpdatedInParent visibility-was-updated-in-parent 宿主 → 子 iframe 可见性变化;触发子级的 @visibility 事件
TopicWasReceivedInParent topic-was-received-in-parent 宿主 → 子 向已订阅的子级投递 WebSocket 主题事件
CmdRouteChanged cmd-route-changed 子 → 宿主 子级内部路由变化;宿主更新浏览器 URL
CmdTitleChanged cmd-title-changed 子 → 宿主 子级 document.title 变化;宿主更新页面标题
CmdStartChat cmd-start-chat 子 → 宿主 打开新的聊天会话
CmdOpenSession cmd-open-session 子 → 宿主 导航到已有的聊天会话
CmdOpenArtifact cmd-open-artifact 子 → 宿主 在侧边栏或模态框中打开制品
CmdNavigate cmd-navigate 子 → 宿主 SPA 导航请求
CmdShowToast cmd-show-toast 子 → 宿主 显示 toast 通知
CmdShowConfirm cmd-show-confirm 子 → 宿主 显示确认对话框
OnConfirmResult on-confirm-result 宿主 → 子 投递确认对话框结果
CmdSetContext cmd-set-context 子 → 宿主 向聊天会话发送上下文
CmdHandleError cmd-handle-error 子 → 宿主 向宿主上报错误
CmdLogout cmd-logout 子 → 宿主 触发登出
CmdSubscribe cmd-subscribe 子 → 宿主 订阅某个 WebSocket 主题
CmdUnSubscribe cmd-unsubscribe 子 → 宿主 取消订阅某个主题
OnSubscription on-subscription 宿主 → 子 投递订阅事件数据
CmdStateGet cmd-state-get 子 → 宿主 读取一个持久化状态键
CmdStateSet cmd-state-set 子 → 宿主 写入一个持久化状态键
CmdStateRemove cmd-state-remove 子 → 宿主 删除一个持久化状态键
CmdStateClear cmd-state-clear 子 → 宿主 清除该页面的全部状态
CmdStateGetAll cmd-state-get-all 子 → 宿主 读取全部持久化状态
OnStateResult on-state-result 宿主 → 子 投递状态读取结果
OnStateError on-state-error 宿主 → 子 上报状态操作失败
CmdWsSend cmd-ws-send 子 → 宿主 通过宿主连接转发一条 WebSocket 命令
CmdBodySize cmd-body-size 子 → 宿主 为 auto-height 上报 body 尺寸
CmdBridgePost cmd-bridge-post 子 ↔ 父 经由 host.bridge 的发送即忘通道消息
CmdBridgeRequest cmd-bridge-request 子 ↔ 父 经由 host.bridge 的请求/响应通道消息
CmdClaimNavOwner cmd-claim-nav-owner 子 → 宿主 声明导航所有权(nav-owner 模式)
CmdReleaseNavOwner cmd-release-nav-owner 子 → 宿主 释放导航所有权
CmdLayoutSubscribe cmd-layout-subscribe 子 → 宿主 订阅受管布局更新
CmdLayoutUpdatePanel cmd-layout-update-panel 子 → 宿主 修补一条面板定义
CmdLayoutBroadcast cmd-layout-broadcast 子 ↔ 宿主 标签页内布局总线消息
OnLayoutChange on-layout-change 宿主 → 子 完整布局快照更新
OnLayoutPanelChanged on-layout-panel-changed 宿主 → 子 按面板的实时状态增量
OnLayoutBroadcast on-layout-broadcast 宿主 → 子 布局总线广播投递

应用代码从不直接发送或接收这些消息。代理会透明地处理协议,只暴露 @wippy-fe/proxy 这一层 API。

<w-iframe> 自定义元素

<w-iframe> 是内置于 proxy.js 的底层 iframe 原语。它接受原始源 HTML,注入完整的 Wippy 运行时(基础 URL、import map、loading.js、proxy.js、子端配置),并把结果渲染为沙箱化的 srcdoc iframe。

当你手上有源 HTML,并希望获得 Wippy 微前端应用自动获得的那套运行时行为时,请使用 <w-iframe>:已认证的 API、状态中继、WebSocket 中继、nav-owner 路由以及父子桥接消息。

属性与属性值

属性 / 属性值 必填 默认值 说明
src 否 — 通过代理 api 拉取原始源 HTML 的 URL。
srcdoc 否 — 原始源 HTML。对于较大的字符串也可用 element.srcdoc = html 设置。
base-url 否 由 src 或 document.baseURI 推导 注入的 <base href>,用于解析相对资源。
resource-id 否 元素 id,其次是 src 子端上下文标识符;决定默认的状态与日志作用域。
resource-type 否 page 子端上下文类型:page 或 artifact。
sub-path 否 父级路由 子级初始路由。在 GetConfig 握手中作为 config.context.route 转发。
auto-height 否 false 让 iframe 高度随子级 CmdBodySize 上报调整。
nav-owner 否 false 拦截子级的 CmdRouteChanged,派发 nav-owner-route DOM 事件而不改动宿主 URL。

元素上可接受的 JS 属性:

const frame = document.querySelector('w-iframe')
frame.proxyConfig = { injections: { css: { markdown: false } } }
frame.configOverrides = { customization: { customCSS: ':root { --brand: red }' } }
frame.srcdoc = sourceHtml

事件与方法

事件 detail 说明
loading — 在拉取/处理/渲染开始之前触发。
load — 沙箱 iframe 加载完成后触发。
error 原始错误 拉取、注入或加载失败时触发。
nav-owner-route { path: string, navId?: number } 设置了 nav-owner 时的子级路由变化。事件会冒泡且为 composed。
wippy-message { channel, payload, requestId?, respond?, reject? } 来自子级的桥接消息。
方法 说明
post(channel, payload?) 向子级发送即忘的桥接消息。
request<T>(channel, payload?, { timeoutMs }?) 请求/响应桥接消息;以处理器返回值解析。

Shadow parts:loader、error、frame。

设置 nav-owner 时,默认的路由同步往返会被完全抑制:宿主不会更新自己的地址栏,也不会向子级回传 UrlWasUpdatedInParent。导航所有权完全委托给监听 nav-owner-route 的父级代码。事件 detail 中的 path 是子级传给 host.onRouteChanged(internalRoute, navId?) 的原始内部路由,与传入时完全一致 —— 它不带挂载前缀(这与默认的 CmdRouteChanged 路径不同,后者宿主会加上页面的挂载前缀)。任何前缀处理或路由器映射都由嵌入方父级负责:

const frame = document.querySelector('w-iframe')
frame.addEventListener('nav-owner-route', (event) => {
  const { path, navId } = event.detail
  myRouter.push(path)
})

父子桥接

桥接使用具名通道,因此两侧都不需要处理原始的 postMessage 信封。

父级:

const frame = document.querySelector('w-iframe')

frame.addEventListener('wippy-message', async (event) => {
  const { channel, payload, respond, reject } = event.detail

  if (channel === 'pick-file') {
    try {
      respond({ id: 'file-1', name: 'data.csv' })
    } catch (error) {
      reject(error)
    }
  }
})

frame.post('refresh', { reason: 'parent-click' })
const result = await frame.request('get-selection', undefined, { timeoutMs: 5000 })

子级:

import { host } from '@wippy-fe/proxy'

host.bridge.post('ready', { value: 1 })
const file = await host.bridge.request('pick-file', { accept: '.csv' })

const off = host.bridge.on('refresh', async (payload) => {
  console.log('refresh requested', payload)
  return { ok: true }
})

host.bridge.on() 返回一个取消订阅函数(() => void)。一个通道对应一个活动处理器。 若同一通道注册了多个处理器,最近注册的那个胜出,并处理该通道上所有到达的消息 —— 包括发送即忘的 post() 和 request()。on() 不是叠加式的:更早的处理器会被遮蔽(而非移除),在更新的处理器存在期间不会运行,并且代理会在重复注册时输出一条 console.warn。若最新的处理器取消订阅,该通道上此前的处理器会重新激活。如果你需要多个彼此独立的监听器,请使用不同的通道名。

若省略 options.timeoutMs,host.bridge.request()(以及父级侧的 frame.request())默认使用 10 秒(10000 毫秒)期限。超时时返回的 Promise 会以一个 Error 拒绝,其消息为 Bridge request <id> timed out after <ms>ms。若请求的通道对方没有注册处理器,则会立即以 No handler registered for channel "<channel>" 拒绝,而不会等到期限结束。

<w-artifact> 自定义元素

<w-artifact> 解析制品或页面的元数据与内容,然后在内部把基于 iframe 的类型委托给 <w-iframe>。它负责内容类型检测(HTML、Markdown、Web 页面包、ESM 包、直接标签组件),并提供比原始 <w-iframe> 更高层的 API。

属性

属性 必填 取值 默认值 说明
id 是 制品 / 页面 UUID — 内容标识符。
type 否 artifact | page artifact 决定调用的 REST 端点:/api/v1/artifact/<id>/content 或 /api/public/pages/content/<id>。
auto-height 否 布尔标志 false 转发给内部 <w-iframe> 以进行 CmdBodySize 高度同步。
url 否 任意 URL — 直接从该 URL 拉取内容;忽略 id/type。
sub-path 否 路径字符串 — 作为子级初始路由转发给内部 <w-iframe>。
nav-owner 否 布尔标志 false 转发给内部 <w-iframe>;子级路由变化会派发 nav-owner-route。

事件

事件 触发时机 detail
loading 拉取开始之前 —
load iframe 加载完成后 —
error 拉取或渲染失败 原始错误
nav-owner-route nav-owner 子级路由变化 { path: string, navId?: number }
wippy-message 来自嵌套 iframe 的桥接消息 { channel, payload, requestId?, respond?, reject? }

CSS 状态与 parts

该元素会设置 status 属性(loading、ready、error)并暴露 shadow parts:

w-artifact[status="loading"] { opacity: 0.5; }
w-artifact[status="error"]   { border: 1px solid var(--p-danger-color); }

w-artifact::part(loader) { font-size: 1rem; }
w-artifact::part(frame)  { border: 0; }

<w-iframe>、<w-artifact> 与原始 <iframe> 的对比

特性 <w-iframe> <w-artifact> 原始 <iframe>
注入 Wippy 运行时 是 是(经由 <w-iframe>) 否
解析制品/页面元数据 否 是 否
已认证的内容拉取 是(原始 HTML) 是(完整解析器) 否
状态中继 是 是 否
WebSocket 中继 是 是 否
父子桥接 是 是(转发) 否
nav-owner 支持 是 是 否
内容类型检测 否 是 否
CSS shadow parts loader、error、frame loader、error、frame —
status 属性 是 是 否

当你拥有 Wippy 制品 UUID 或页面 ID,并希望由平台处理全部解析时,使用 <w-artifact>。当你已经拥有源 HTML 并想要直接注入运行时时,使用 <w-iframe>。只有对完全外部、不需要 Wippy API 的内容才使用原始 <iframe>。

高级 HTML 注入

对于需要执行源 HTML 到 srcdoc 转换但不挂载元素的场景,代理暴露了 html.inject(...):

import { html } from '@wippy-fe/proxy'

const processed = await html.inject(sourceHtml, {
  baseUrl: 'https://example.com/app/',
  resourceId: 'child-id',
  resourceType: 'page',
  route: '/initial',
})

同一个函数也可以通过 instance.html.inject、$W.html 以及 import { html } from '@wippy-fe/proxy' 访问。常规挂载请优先使用 <w-iframe>;只有在构建自定义托管基础设施时才使用 html.inject(...)。