当 GrapesJS 遇上 Ant Design:一场跨文档样式注入的踩坑之旅

最近在开发一个低代码可视化编辑器时,踩了一个挺有意思的坑 —— Ant Design 组件的样式莫名其妙地"消失"了。折腾了好一阵,最终发现是 CSS-in-JS 在跨 iframe 场景下的一个隐蔽问题。记录一下排查过程,希望对遇到类似问题的朋友有所帮助。

问题现象

我们的编辑器基于 GrapesJS 构建,左侧是功能导航树,中间是 iframe 画布,右侧是组件属性面板(traits)。技术栈是 React + Ant Design。

诡异的是:

  • 左侧导航树:样式正常 ✅
  • 中间 iframe 画布里的组件:样式正常 ✅
  • 右侧属性面板里的 Ant Design 组件:样式全丢

按钮没有颜色,输入框没有边框,下拉菜单一片空白… 就像 CSS 从没加载过一样。

排查思路

第一反应:是不是 CSS 没打包进去?

打开 DevTools 检查 <head> 标签,发现 Ant Design 的样式确实存在。那为什么没生效?

仔细一看,样式是有,但位置不对 —— 样式被注入到了 iframe 的 head 里,而不是主文档

为什么会这样?

这就要说到之前为了解决另一个问题做的一个"骚操作"了。

GrapesJS 的画布是一个 iframe,我们需要在里面渲染 Ant Design 组件。但 Ant Design 5.x 使用 CSS-in-JS(通过 @ant-design/cssinjs),它默认把样式注入到主文档的 <head> 中。这导致 iframe 里的组件没有样式。

当时的解决方案是 Monkey-patch

// canvas:frame:load 事件中
const originalCreateElement = document.createElement.bind(document);

document.createElement = function(tagName) {
  if (tagName === 'style') {
    // 强制在 iframe 的 document 中创建 style 元素
    return iframeDoc.createElement(tagName);
  }
  return originalCreateElement(tagName);
};

确实,这让 iframe 里的组件有了样式。但副作用来了 —— 所有 document.createElement('style') 调用都被重定向到 iframe 了,包括属性面板里的组件。

这就是 traits 面板样式丢失的根本原因:样式被创建在 iframe document 中,但 traits 面板在主文档中,跨文档的样式自然不会生效。

解决方案

思路:区分渲染上下文

关键是让 monkey-patch 能够"智能"地判断:这个 style 元素是给 iframe 用的,还是给主文档用的。

我们的做法是利用 @ant-design/cssinjsStyleProvider 组件。它有一个 container 属性,指定样式应该注入到哪个 DOM 节点。

// iframe 组件渲染
<StyleProvider container={iframeHead}>
  <Button>我在 iframe 里</Button>
</StyleProvider>

// 主文档组件渲染  
<StyleProvider container={document.head}>
  <Button>我在主文档里</Button>
</StyleProvider>

虽然 StyleProvider 控制了"往哪里插入",但 [createElement(‘style’)](file:///d:/code/dorms_1.0/platform/JeecgBoot/code-canvas-server/server/src/views/UIDesigner.tsx#179-199) 在哪个 document 中调用,还是会影响元素的归属。关键洞察是:

如果元素在 iframe document 中创建,插入到主文档会导致跨文档 DOM 操作失败(或行为异常)。

最终实现

我们在渲染组件前,把当前的 container 存到全局变量中:

// mountReactComponent 中
window.__CURRENT_STYLE_CONTAINER__ = container;
root.render(<StyleProvider container={container}>...</StyleProvider>);

然后修改 monkey-patch 的判断逻辑:

document.createElement = function(tagName) {
  if (tagName === 'style') {
    const iframeHead = window.__ANTD_CSSINJS_CONTAINER__;
    const currentContainer = window.__CURRENT_STYLE_CONTAINER__;
    
    // 只有当容器明确是 iframe head 时,才使用 iframe document
    if (currentContainer === iframeHead) {
      return iframeDoc.createElement(tagName);
    }
    // 其他情况(traits、未设置容器等)使用原始方法
  }
  return originalCreateElement(tagName);
};

这个逻辑很简洁:

  • container === iframeHead → 使用 iframe document 创建
  • 其他情况 → 使用主文档创建

相比之前"无差别拦截"的粗暴方式,现在只在明确需要的时候才介入。

踩坑小结

  1. Monkey-patch 要慎用:它是全局的,影响范围往往超出预期
  2. 跨 iframe 的 CSS-in-JS 确实麻烦:样式的创建和插入是两个独立步骤,需要分别处理
  3. 调试思路:样式不生效时,先确认样式规则存在,再检查它在正确的文档/容器中

适用场景

如果你在做类似的事情:

  • 基于 GrapesJS / Craft.js 等可视化编辑器
  • 使用 React + Ant Design(或其他 CSS-in-JS 库)
  • 需要在 iframe 画布和主文档中同时渲染组件

希望这篇文章能帮你少走点弯路。


写于 2025 年底,代码基于 React 19 + Ant Design 6.x + GrapesJS

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐