LogicFlow流程图 PNG 导出「线上按钮置灰、无反应」故障复盘

LogicFlow流程图 PNG 导出「线上按钮置灰、无反应」故障复盘

涉及模块:src/views/design/(LogicFlow 画图) 触发场景:模板画图 / 生产工艺画图 → 左上角工具栏「导出流程图」按钮 表象:本地 dev 正常,线上部署后点击导出按钮置灰、无下载、控制台打印 CSS scripts from different sources have been filtered out,最终卡住无反应


一、故障现象

环境 表现
本地 dev (vite) 点击导出,按钮短暂置灰,浏览器正常下载 PNG
线上构建包 (vite build) 点击后按钮置灰,控制台输出 CSS scripts from different sources have been filtered out,随后永远不复位(首版),或 20s 后弹出「导出失败,请稍后重试」(加超时兜底之后)

二、根因链条(由外到内)

2.1 按钮永久置灰的直接原因

src/views/design/LFComponents/Control.vue 中:

js 复制代码
const $_exportSnapshot = async () => {
  if (exporting.value) return;
  exporting.value = true;
  try { await exportFlowSnapshotWithFeedback(props.lf, {...}); }
  finally { exporting.value = false; }
};

finally 只有在 await 的 Promise 落地 (resolve/reject)时才会执行。按钮永远不复位 = 该 Promise 永远没落地 = LogicFlow 官方 getSnapshotBlob 内部有一条 Promise 挂起。

2.2 LogicFlow Snapshot 内部有一条无 onerror 的 Promise

node_modules/@logicflow/extension/src/tools/snapshot/index.ts:456-504

js 复制代码
return new Promise((resolve) => {   // ❌ 没有 reject
  img.onload = () => { ... resolve(canvas) }
  // ❌ 没有 img.onerror
  const svg2Img = `data:image/svg+xml;charset=utf-8,${new XMLSerializer().serializeToString(copy)}`
  img.src = svg2Img.replace(/\n/g,'').replace(/\t/g,'').replace(/#/g,'%23')
})

只要 img.onload 不触发,Promise 就永挂。而这条外层 Promise 又被 snapshotBlobgetSnapshotBlob 层层 await,最终传导到我们的按钮。

2.3 为什么 img.onload 在线上不触发?

有两条独立的成因,本项目同时命中了两条

成因 A:useGlobalRules = true 让 SVG 变得脆弱

原始代码在 flow-snapshot-css.js 中:

js 复制代码
snapshot.useGlobalRules = true
snapshot.customCssRules = buildFlowSnapshotCssRules(!!isDark)

useGlobalRules = true 让 Snapshot 把 document.styleSheets(Element Plus + 项目全套 CSS)都拼进 SVG 的 <foreignObject><style> 里。带来的副作用:

  1. 跨域样式表读取抛 SecurityError,触发那条日志(日志本身无害,是信号)。
  2. 全量 CSS 里可能带有 @font-face url(...)background: url(/xxx.svg) 等外部 URL,序列化进 SVG 后,被浏览器按跨域资源处理,光栅化 SVG 时按加载失败对待。
  3. data:image/svg+xml;charset=utf-8,... 后拼上几百 KB 的 CSS,部分浏览器直接拒绝解析长 data URL。
  4. 序列化时只替换了 \n\t#%&<>、中文等特殊字符未 URL-encode,data URL 解析可能出错。
成因 B:start 节点用 <img> 引用小 SVG 图标,被 Vite 内联

src/views/design/registerNode/start/start.vue(旧版):

html 复制代码
<img :src="startDefaultIcon" class="node-icon-image" alt="开始" />
ts 复制代码
import startDefaultIcon from '@/assets/process-flow-icons/start-default.svg';

start-default.svg 只有 827 字节 ,低于 Vite 默认的 build.assetsInlineLimit: 4096(4 KB)。所以:

环境 startDefaultIcon 的值
Dev /src/assets/process-flow-icons/start-default.svg(普通 URL 字符串)
线上构建 data:image/svg+xml;base64,PD94b...(内联 base64 data URL)

于是线上 DOM 里的 img 长这样:

html 复制代码
<foreignObject>
  <div class="node-title node-start">
    <span class="node-icon node-icon--start">
      <img src="data:image/svg+xml;base64,PD94b..." class="node-icon-image" alt="开始" />
    </span>
    ...
  </div>
</foreignObject>

LogicFlow Snapshot 把整块画布序列化时形成的嵌套是:

xml 复制代码
外层 SVG data URL
  └─ <foreignObject>
       └─ <div><span>
            └─ <img src="内层 SVG data URL"> ← 关键

Chromium 把外层 SVG 作为位图光栅化 渲染(<img src="data:image/svg+xml,..."> 的场景)时,<foreignObject> 里再嵌一层 SVG data URL 属于被限制的路径,浏览器既不触发 onload 也不触发 onerror,img 永远处于"pending"状态。撞上 2.2 的无 onerror bug 后 → 永挂。

Dev 时因为 img 是外部 URL(普通 svg 文件),走的分支更宽松,浏览器对这种同源 SVG 图标在光栅化中通常能正常处理,所以本地看不到问题。


三、修复方案(已实施)

修复 1:关闭 useGlobalRules

文件src/views/design/flow-snapshot-css.js

js 复制代码
export function configureFlowSnapshot(lf, isDark) {
  const snapshot = lf?.extension?.snapshot
  if (!snapshot) return
  snapshot.useGlobalRules = false                          // 由 true 改为 false
  snapshot.customCssRules = buildFlowSnapshotCssRules(!!isDark)
}
  • 不再遍历 document.styleSheets,那条控制台日志消失。
  • 只用手写的兜底 CSS(buildFlowSnapshotCssRules),SVG 体积大幅缩水。
  • 完全避免外部字体 / 图标 URL 混进 SVG。

⚠️ 关闭 useGlobalRules 的前提是 buildFlowSnapshotCssRules 必须覆盖所有需要在导出图里正确显示的节点样式。当前已覆盖:node-titlenode-namenode-icon、各类型节点(start/end/component/assignment/decision/startParallel/endParallel/machineLearning/deepLearning)以及徽章、进度条、颜色控制态。新增节点样式时必须同步补进这里(见第五节)。

修复 2:getSnapshotBlob 加超时兜底

文件src/views/design/export-flow-snapshot.js

js 复制代码
function withTimeout(promise, ms, message) {
  let timerId
  const timeout = new Promise((_, reject) => {
    timerId = setTimeout(() => reject(new Error(message)), ms)
  })
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timerId))
}

const result = await withTimeout(
  lf.getSnapshotBlob(backgroundColor, 'png', { ... }),
  20000,
  '导出超时',
)

作用:防挂死安全阀 。即便未来 LogicFlow 内部再冒出别的挂起路径,20s 后强制 reject,exporting.value 会被 finally 复位,按钮恢复可点,并弹出「导出失败,请稍后重试」。

修复 3(根治):start 节点改为内联 SVG

文件src/views/design/registerNode/start/start.vue

html 复制代码
<template>
  <div class="node-title node-start" ref="myNode">
    <span class="node-icon node-icon--start">
      <svg
        class="node-icon-image"
        viewBox="0 0 1024 1024"
        xmlns="http://www.w3.org/2000/svg"
        aria-hidden="true"
      >
        <path d="M512 1024C229.257143 1024 ..." fill="#507BEE" />
        <path d="M378.285714 283.428571l323.428572 230.628572L384 740.571429l-5.714286-457.142858z" fill="#FF8432" />
      </svg>
    </span>
    <span class="node-name"><span></span></span>
  </div>
</template>

去掉了 import startDefaultIcon from '.../start-default.svg'。SVG 内容直接内联到模板里,成为外层画布 SVG 的一部分,从根本上消除「外层 SVG data URL 里嵌 img 再嵌内层 SVG data URL」的嵌套结构,也不再依赖 Vite 是否内联该资源。


四、验证清单

上线前后都要复测:

  • 模板画图(/process-templates/draw/:id)→ 点击导出,能下载 PNG,图内 start 节点显示"蓝圆 + 橙色三角"图标。
  • 生产工艺画图(/process-templates/draw/:id?source=product)→ 同上。
  • 大流程图(>50 节点)→ 能导出,且导出图完整无截断。
  • 暗色主题下导出,背景色与节点颜色对得上。
  • 浏览器控制台不再 出现 CSS scripts from different sources have been filtered out
  • 网络面板不再有 .../src/assets/process-flow-icons/start-default.svg 请求(因为已内联)。
  • 断网状态下点击导出:应该在 20s 内弹出「导出失败,请稍后重试」,按钮恢复可点,不能永久置灰。

五、后续开发注意事项(重要)

5.1 新增 / 修改 LogicFlow 自定义节点时

规则 1:节点 template 里禁止用 <img src> 引用小图标

原因:Vite 会把小图标(<4 KB)内联成 data URL。放在 <foreignObject> 里就会触发本次故障链。

正确姿势(按优先级):

  1. 内联 <svg> 到 template(本次 start 节点的做法,最稳)
  2. <el-icon><SomeIcon /></el-icon>(Element Plus 内置图标,也是纯 inline SVG,如 end 节点)
  3. <svg><use xlink:href="#icon-xxx" /></svg> iconfont 符号 (如 component/assignment/machineLearning 节点) ⚠️ iconfont 的 symbol 定义在主文档里,导出到独立 SVG 后 #icon-xxx 找不到 → 图标会丢(但不会挂)。若某图标必须出现在导出图里,改用姿势 1 或 2。

规则 2:节点如需背景图 / 图案 ,用 CSS background-color / linear-gradient / clip-path不要background-image: url(...)。若必须用背景图,用 base64 直接写死在 CSS 里。

规则 3:新节点的关键视觉样式必须补进 src/views/design/flow-snapshot-css.jsbuildFlowSnapshotCssRules

因为已经关闭 useGlobalRules,Snapshot 不会 自动带上项目和 Element Plus 的样式表。任何写在 .vue <style scoped>nodeVisual.scss 里的样式,如果决定节点的最终视觉(背景色、圆角、边框、图标颜色、异形 clip-path、徽章、进度条等),都必须原样重新在 buildFlowSnapshotCssRules 里写一份,导出图才能正确呈现。

模板:

js 复制代码
// flow-snapshot-css.js 里追加
.node-xxx {
  border-radius: 8px;
  background: ${p.nodeXxxBg};
  /* 其他关键样式... */
}
.node-icon--xxx { color: #xxx; }

如果新样式引用了主题色,在 flow-diagram-theme.jsgetFlowDiagramPalette 里加一项,然后在 CSS 兜底里读它,保证暗色模式也 OK。

5.2 修改导出逻辑时

  • 不要 删除 withTimeout 兜底。LogicFlow Snapshot 内部还有多处 Promise 缺 onerror,把它当作强制安全阀留着。

  • 不要useGlobalRules 再改回 true,除非确认 LogicFlow 官方已修复上文提到的 img.onerror 缺失问题(可看 node_modules/@logicflow/extension/src/tools/snapshot/index.ts 是否补上)。

  • 想让导出结果换背景色 / 边距,改 exportFlowSnapshot 里传给 getSnapshotBlob 的选项:

    js 复制代码
    lf.getSnapshotBlob(backgroundColor, 'png', {
      fileType: 'png',
      backgroundColor,   // 底色,默认取主题 canvasBg
      padding: 40,       // 图形四周留白
      partial: false,    // false = 完整画布,true = 当前视口
    })

5.3 排查线上导出问题的通用步骤

  1. 看按钮是否复位 。永久置灰 = 内部 Promise 永挂;20s 后复位 = 走到了 withTimeout 兜底,说明 LogicFlow 内部还是挂了,进入 3。
  2. 看控制台是否有 CSS scripts from different sources have been filtered out 。有 = 有人误开了 useGlobalRules,或引入了新的跨域样式表。
  3. 看网络面板在点导出的瞬间是否有意外的 4xx / cors error 请求(尤其是字体、图标、iconfont sprite)。
  4. 看 DOM :F12 → Elements → 找到 <foreignObject> 下的节点 → 看每个 <img>src
    • 若是长长的 data:image/svg+xml;base64,... → 高度怀疑撞上本次故障,改回内联 SVG。
    • 若是外链 http(s):// → 检查是否跨域、是否 CORS。
  5. 直接把 getSnapshotBlob 拿出来单跑await lf.getSnapshotBlob('#fff', 'png', { padding: 40 })。看它是否 resolve。若不 resolve,用 DevTools → Sources → 打断点在 snapshot/index.ts:456 的 img.onload / img.src 处,人肉检查 img.complete、img.currentSrc、Console 里手工触发 img.decode().catch(console.log) 看具体错误。

六、涉及文件清单

文件 改动
src/views/design/flow-snapshot-css.js useGlobalRulestrue 改为 false,注释说明原因
src/views/design/export-flow-snapshot.js 新增 withTimeout 工具函数,为 getSnapshotBlob 包 20s 超时
src/views/design/registerNode/start/start.vue 移除 import startDefaultIcon<img> 改为内联 <svg>

其他文件(Control.vuedesign/index.vueprocess-templates/draw/index.vue 等)未改动。


七、一句话总结

线上 <img> 拿到的是 Vite 自动内联的 data URL,被 LogicFlow Snapshot 序列化成外层 SVG data URL 后形成"SVG 里嵌 img 再嵌 SVG data URL"的嵌套,Chromium 光栅化拒绝加载但又不报错,撞上 LogicFlow 官方 Promise 缺 onerror 的实现漏洞,永远挂起。修法是:节点里不用 <img> 引用小 SVG,用内联 SVG 或 el-icon;同时关掉 useGlobalRules 减少 SVG 体积和外部资源引用,加超时兜底防将来其他挂起路径。

相关推荐
梨想橙汁1 小时前
Vite 优化、踩坑汇总 + Webpack 迁移 Vite 实战
前端·webpack·vite
10share1 小时前
给 JSX 组件也来一份 Vue 式 scoped:jsx-scoped 让样式隔离不再靠自觉
前端·vue.js
小聪7081 小时前
elpis-core 领域模型驱动设计解析
前端
北城笑笑1 小时前
Vue 104 ,AI + ECharts + Word:大模型数据可视化报告生成实战(前端导出图文并茂 Word 文档)
前端·vue.js·word·echarts
创新技术阁1 小时前
FastapiAdmin 实战:二次开发前的准备(环境配置与项目启动)
前端·后端·fastapi
李少兄1 小时前
JavaScript 运算符完全指南
java·开发语言·javascript
志尊宝1 小时前
Vue3 零基础每日笔记(010):watchEffect——用到谁就自动听谁的“懒人侦听器“
前端·vue.js·笔记
狗哥哥2 小时前
从“看对方向”到“做出行动”:投资决策卡
前端
拖孩2 小时前
一个人 + AI 做的小程序,一个月赚了 36 块
前端·后端·微信小程序