~/ ?.log $
返回文章列表
11 min read
更新于 2026年3月6日

我不知道的 dumi(02)— 跨框架组件渲染:React 组件如何跑在 Vue 文档里

假设团队维护一个 React 组件库,文档用 dumi 构建。某天产品线提出需求:需要在 Vue 技术栈的项目文档中,嵌入这些 React 组件的实时预览。

一、一个看似矛盾的需求

假设团队维护一个 React 组件库,文档用 dumi 构建。某天产品线提出需求:需要在 Vue 技术栈的项目文档中,嵌入这些 React 组件的实时预览。

这听起来像是要在同一个页面里同时运行两套框架的渲染引擎——React 有自己的虚拟 DOM 和协调机制,Vue 也有自己的响应式系统和模板编译器。它们对 DOM 的控制方式完全不同,直接混用几乎必然冲突。

但 dumi 2.0 确实支持跨框架组件渲染。它是怎么做到的?

问题的关键在于——不是让两个框架”共享” DOM,而是给每个框架一个独立的 DOM 子树


二、适配器模式:用 Web Components 当”翻译官”

dumi 的跨框架渲染基于一个核心思路:用 Web Components 作为框架之间的桥接层

Web Components 是浏览器原生的组件化标准,不依赖任何框架。通过 customElements.define() 注册的自定义元素,可以在任何 HTML 中使用——无论宿主环境是 Vue、React 还是纯 HTML。

dumi 的做法是将 React 组件封装为 Web Components:

class ReactAdapter extends HTMLElement {
  connectedCallback() {
    const shadowRoot = this.attachShadow({ mode: 'open' });
    const mountPoint = document.createElement('div');
    shadowRoot.appendChild(mountPoint);

    const root = ReactDOM.createRoot(mountPoint);
    root.render(React.createElement(ReactComponent, this.getProps()));
  }

  disconnectedCallback() {
    // 清理 React 渲染树,避免内存泄漏
  }

  getProps() {
    return JSON.parse(this.getAttribute('props') || '{}');
  }
}

customElements.define('react-button', ReactAdapter);

注册完成后,在 Vue 文档中就可以直接使用:

<template>
  <div>
    <h2>React Button 组件预览</h2>
    <react-button :props="JSON.stringify({ type: 'primary', label: '点击' })" />
  </div>
</template>

说白了,Web Components 在这里扮演的角色就像一个”翻译官”——外部看到的是一个标准的 HTML 自定义元素,内部运行的是 React 渲染引擎。宿主框架(Vue)不需要知道这个元素内部是怎么实现的,只需要像使用普通 HTML 元素一样使用它。


三、隔离的三个维度:CSS、状态、事件

让两个框架在同一个页面共存,最大的挑战不是”启动两个渲染引擎”——而是隔离。如果 React 组件的样式影响了 Vue 的布局,或者 React 的事件冒泡到 Vue 的事件处理器中,页面就会出问题。

dumi 在三个维度做了隔离:

CSS 隔离:Shadow DOM

上面的代码中,this.attachShadow({ mode: 'open' }) 创建了一个 Shadow DOM。Shadow DOM 是浏览器提供的原生样式隔离机制——Shadow DOM 内部的样式不会泄露到外部,外部的样式也不会影响内部

这意味着 React 组件的 CSS(无论是 CSS Modules、styled-components 还是普通全局样式)都被封印在 Shadow DOM 内部,不会和 Vue 文档的样式产生冲突。

但 Shadow DOM 也有一个常见陷阱——外部的 CSS 变量(Custom Properties)可以穿透 Shadow DOM 边界。这在主题定制场景下是优点(可以统一控制主题色),但也可能导致意外的样式影响。

/* 宿主页面 */
:root {
  --primary-color: blue;
}

/* Shadow DOM 内部的 React 组件 */
.button {
  color: var(--primary-color);
} /* 会读取到宿主的 blue */

状态隔离:独立作用域

dumi 的组件预览使用 react-live 库提供的 LiveProvider 组件。每个预览实例都有自己独立的 scope——注入的变量(如 ReactuseState)在不同预览之间互不干扰。

更关键的是,dumi 对预览代码的执行使用了 new Function() 而非直接 eval()new Function() 创建的函数在全局作用域而非当前作用域中执行,这在一定程度上避免了预览代码意外访问到 dumi 内部变量。

事件隔离:Shadow DOM 的事件边界

Shadow DOM 有一个重要的事件行为:Shadow DOM 内部触发的事件,在冒泡出 Shadow Root 时会被”重定向”(retarget)——事件的 target 会被替换为 Shadow Host 元素本身

// Shadow DOM 内部的 React 按钮被点击
// 事件冒泡到 Shadow Root 时:
//   event.target → <react-button>(而不是内部的 <button>)
// Vue 的事件处理器看到的是自定义元素被点击,不会感知到内部的 React DOM 结构

这个重定向机制天然地隔离了两个框架的事件系统——Vue 的事件委托不会意外捕获到 React 内部元素的事件细节。


四、iframe 沙箱:更强的隔离方案

Web Components + Shadow DOM 能处理大多数场景,但有些情况需要更强的隔离。比如:

(1) 组件依赖全局变量(如 window.CONFIGdocument.title

(2) 组件会修改全局样式(如 Ant Design 的 ConfigProvider 全局注入)

(3) 组件使用了 Portal,渲染目标是 document.body

这些场景下,Shadow DOM 的隔离不够——需要 JavaScript 执行环境级别的隔离。dumi 提供了 iframe 模式:

````jsx iframe
import React, { useState } from 'react';

export default () =&gt; {
  const [count, setCount] = useState(0);
  return &lt;button onClick={() =&gt; setCount(c =&gt; c + 1)}&gt;Count: {count}&lt;/button&gt;;
};
``` 

标记 `iframe` 后,dumi 会为这个代码块生成一个独立的 iframe 页面。iframe 拥有完全独立的 `window`、`document`、CSS 上下文——比 Shadow DOM 的隔离更彻底,但性能开销也更大(每个 iframe 都是一个独立的浏览文档)。

换句话说,dumi 提供了两级隔离方案:**Shadow DOM 是"轻量隔离",iframe 是"完全隔离"**。选择哪个取决于组件的侵入性——如果组件只修改自己的 DOM 子树,Shadow DOM 足够;如果组件会"逃逸"到全局环境,就需要 iframe。

---

## 五、跨框架通信:postMessage 桥接

隔离之后,下一个问题是通信。如果 Vue 文档中的 React 组件需要响应宿主页面的操作(比如切换主题、传递配置),怎么办?

对于 Web Components 方案,通信相对简单——通过自定义元素的属性(attributes)传递数据:

```javascript
// Vue 侧:修改属性
document.querySelector('react-button').setAttribute('props', JSON.stringify({ theme: 'dark' }));

// React 适配器侧:监听属性变化
class ReactAdapter extends HTMLElement {
  static get observedAttributes() {
    return ['props'];
  }

  attributeChangedCallback(name, oldVal, newVal) {
    if (name === 'props') {
      this.rerender(JSON.parse(newVal));
    }
  }
}
```

对于 iframe 方案,通信必须通过 `window.postMessage`:

```javascript
// 宿主页面 → iframe
const iframe = document.querySelector('iframe.dumi-preview');
iframe.contentWindow.postMessage({ type: 'theme-change', theme: 'dark' }, '*');

// iframe 内部监听
window.addEventListener('message', (event) => {
  if (event.data.type === 'theme-change') {
    setTheme(event.data.theme);
  }
});
```

`postMessage` 是浏览器为跨域/跨文档通信设计的标准 API。它是异步的、序列化的(数据会经过结构化克隆),不会创建引用共享——天然适合隔离环境之间的通信。

---

## 六、性能与取舍

跨框架渲染的代价是什么?

**(1)** **额外的运行时体积**:页面需要同时加载 React 和 Vue 的运行时。React 的 `react` + `react-dom` 约 40KB(gzip),Vue 3 约 30KB(gzip)。对文档站点来说可以接受,但对生产应用需要慎重考虑。

**(2)** **首屏渲染开销**:Web Components 的注册和 Shadow DOM 创建有额外开销。如果页面有大量跨框架组件预览,建议使用懒加载——只在组件进入视口时才初始化渲染。

**(3)** **调试复杂度**:Shadow DOM 内部的元素在 DevTools 中需要展开 Shadow Root 才能看到。iframe 的调试更复杂,需要切换到对应 iframe 的执行上下文。

在实际项目中,跨框架渲染主要用于**文档展示场景**——让一个框架的组件库能在另一个框架的文档中展示实时预览。如果是生产应用中需要混用框架(如 React 应用中嵌入 Vue 组件),通常有更轻量的方案(如 Module Federation 或直接用 Web Components 封装)。

---

## 七、总结

dumi 的跨框架组件渲染,核心思路是**用 Web Components 做桥接,用 Shadow DOM / iframe 做隔离,用属性传递 / postMessage 做通信**。

如果你只记住一句话:**跨框架渲染不是让两个框架共享 DOM,而是给每个框架一个隔离的 DOM 子树,然后用标准的浏览器 API 桥接它们。** Web Components 就是那个"标准 API"——它不属于任何框架,所以它可以连接所有框架。

---

**延伸阅读:**

- [MDN — Web Components](https://developer.mozilla.org/en-US/docs/Web/API/Web_Components)
- [MDN — Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM)
- [dumi 官方文档 — 跨框架支持](https://d.umijs.org/)
- [react-live — GitHub](https://github.com/FormidableLabs/react-live)

---

**本系列其他文章:**

- 上一篇:[从 Markdown 到文档站点:dumi 的 SSG 流程与约定式路由](/posts/dumi-01/)

**相关主题:**

- 如果你对 React 组件的渲染机制感兴趣,可以看:[我不知道的 React 系列](/series/React/)
share.ts

// 觉得这篇文章有帮助?

// 欢迎分享给更多人

export const  subscribe  =  "/rss.xml" ;