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 中:
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:
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 又被 snapshotBlob → getSnapshotBlob 层层 await,最终传导到我们的按钮。
2.3 为什么 img.onload 在线上不触发?
有两条独立的成因,本项目同时命中了两条:
成因 A:useGlobalRules = true 让 SVG 变得脆弱
原始代码在 flow-snapshot-css.js 中:
snapshot.useGlobalRules = true
snapshot.customCssRules = buildFlowSnapshotCssRules(!!isDark)
useGlobalRules = true 让 Snapshot 把 document.styleSheets(Element Plus + 项目全套 CSS)都拼进 SVG 的 <foreignObject><style> 里。带来的副作用:
- 跨域样式表读取抛
SecurityError,触发那条日志(日志本身无害,是信号)。 - 全量 CSS 里可能带有
@font-face url(...)、background: url(/xxx.svg)等外部 URL,序列化进 SVG 后,被浏览器按跨域资源处理,光栅化 SVG 时按加载失败对待。 data:image/svg+xml;charset=utf-8,...后拼上几百 KB 的 CSS,部分浏览器直接拒绝解析长 data URL。- 序列化时只替换了
\n、\t、#,%、&、<、>、中文等特殊字符未 URL-encode,data URL 解析可能出错。
成因 B:start 节点用 <img> 引用小 SVG 图标,被 Vite 内联
src/views/design/registerNode/start/start.vue(旧版):
<img :src="startDefaultIcon" class="node-icon-image" alt="开始" />
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 长这样:
<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 把整块画布序列化时形成的嵌套是:
外层 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
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-title、node-name、node-icon、各类型节点(start/end/component/assignment/decision/startParallel/endParallel/machineLearning/deepLearning)以及徽章、进度条、颜色控制态。新增节点样式时必须同步补进这里(见第五节)。
修复 2:getSnapshotBlob 加超时兜底
文件:src/views/design/export-flow-snapshot.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
<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> 里就会触发本次故障链。
正确姿势(按优先级):
- 内联
<svg>到 template(本次 start 节点的做法,最稳) <el-icon><SomeIcon /></el-icon>(Element Plus 内置图标,也是纯 inline SVG,如 end 节点)<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.js 的 buildFlowSnapshotCssRules。
因为已经关闭 useGlobalRules,Snapshot 不会自动带上项目和 Element Plus 的样式表。任何写在 .vue <style scoped> 或 nodeVisual.scss 里的样式,如果决定节点的最终视觉(背景色、圆角、边框、图标颜色、异形 clip-path、徽章、进度条等),都必须原样重新在 buildFlowSnapshotCssRules 里写一份,导出图才能正确呈现。
模板:
// flow-snapshot-css.js 里追加
.node-xxx {
border-radius: 8px;
background: ${p.nodeXxxBg};
/* 其他关键样式... */
}
.node-icon--xxx { color: #xxx; }
如果新样式引用了主题色,在 flow-diagram-theme.js 的 getFlowDiagramPalette 里加一项,然后在 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的选项:lf.getSnapshotBlob(backgroundColor, 'png', { fileType: 'png', backgroundColor, // 底色,默认取主题 canvasBg padding: 40, // 图形四周留白 partial: false, // false = 完整画布,true = 当前视口 })
5.3 排查线上导出问题的通用步骤
- 看按钮是否复位。永久置灰 = 内部 Promise 永挂;20s 后复位 = 走到了
withTimeout兜底,说明 LogicFlow 内部还是挂了,进入 3。 - 看控制台是否有
CSS scripts from different sources have been filtered out。有 = 有人误开了useGlobalRules,或引入了新的跨域样式表。 - 看网络面板在点导出的瞬间是否有意外的 4xx / cors error 请求(尤其是字体、图标、iconfont sprite)。
- 看 DOM:F12 → Elements → 找到
<foreignObject>下的节点 → 看每个<img>的src:- 若是长长的
data:image/svg+xml;base64,...→ 高度怀疑撞上本次故障,改回内联 SVG。 - 若是外链
http(s)://→ 检查是否跨域、是否 CORS。
- 若是长长的
- 直接把
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 | useGlobalRules 由 true 改为 false,注释说明原因 |
src/views/design/export-flow-snapshot.js | 新增 withTimeout 工具函数,为 getSnapshotBlob 包 20s 超时 |
src/views/design/registerNode/start/start.vue | 移除 import startDefaultIcon,<img> 改为内联 <svg> |
其他文件(Control.vue、design/index.vue、process-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 体积和外部资源引用,加超时兜底防将来其他挂起路径。