From 4beadc6161d4381a998053baf705a65e186da367 Mon Sep 17 00:00:00 2001 From: HouYunFei <1844025705@qq.com> Date: Thu, 16 Jul 2026 15:11:48 +0800 Subject: [PATCH] feat(markdown-node): improve rendering performance and user interaction by caching parsed Markdown and optimizing DOM updates --- CHANGELOG.md | 6 ++ plugins/canvas/markdown/package.json | 2 +- plugins/canvas/markdown/src/index.tsx | 96 ++++++++++++++----- web/src/pages/canvas/project.tsx | 130 +++++++++++++++----------- 4 files changed, 155 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53c9c70..5eb8350 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,15 @@ ## Unreleased +## v0.8.1 - 2026-07-16 + + [新增] 插件 SDK 扩展:AI 生成能力、面板控制能力。 + [优化] 3D 全景(1.1.0)支持上传与 AI 生成并升级查看器; + [优化] HTML 节点(1.2.0)迁移到统一交互开关; ++ [优化] 便利贴(1.1.0)可拖动移动、自选颜色、移除资源角标与衍生功能; ++ [优化] SVG 节点(1.1.0)透明背景融入画布、可拖动、去除默认值; ++ [优化] 画布节点渲染性能:memo 化节点回调,交互时不再全量重渲染; ++ [修复] 修复 Markdown 节点在点击/移动视角时重复渲染导致图片反复请求的问题; + [修复] 修复插件版本号显示不更新,插件面板新增可升级绿点提醒。 ## v0.8.0 - 2026-07-15 diff --git a/plugins/canvas/markdown/package.json b/plugins/canvas/markdown/package.json index ea54308..8684ade 100644 --- a/plugins/canvas/markdown/package.json +++ b/plugins/canvas/markdown/package.json @@ -1,6 +1,6 @@ { "name": "canvas-plugin-markdown", - "version": "1.0.0", + "version": "1.1.0", "private": true, "type": "module", "description": "Infinite Canvas Markdown 节点插件", diff --git a/plugins/canvas/markdown/src/index.tsx b/plugins/canvas/markdown/src/index.tsx index e437fea..c6dca7a 100644 --- a/plugins/canvas/markdown/src/index.tsx +++ b/plugins/canvas/markdown/src/index.tsx @@ -1,6 +1,10 @@ // Markdown 节点:编辑与渲染 Markdown。marked 从 CDN 按需加载,不打进插件体积。 +// 移动/交互:走宿主统一的「交互 ⇄ 移动」开关(interactionToggle)——默认「移动」态整块可拖, +// 切「交互」态可滚动/选择;编辑态强制可交互。 +// 防闪烁:解析结果按源码模块级缓存,且只在 HTML 真正变化时写入 DOM—— +// 画布任何重渲染都不会重新解析或重载 Markdown 里的图片。 // styles.css 由 esbuild 以 text 方式打进 bundle,通过 plugin.css 自动注入。 -import { definePlugin, useEffect, useMemo, useState } from "@infinite-canvas/plugin-sdk"; +import { definePlugin, useEffect, useRef, useState } from "@infinite-canvas/plugin-sdk"; import type { CanvasNodeContentProps } from "@infinite-canvas/plugin-sdk"; import css from "./styles.css"; @@ -15,44 +19,76 @@ function loadMarked(): Promise { return markedPromise; } -function MarkdownContent({ ctx }: CanvasNodeContentProps) { - const [editing, setEditing] = useState(false); - // marked 就绪状态:已缓存则首帧即为 true,避免重渲染/重挂载时闪成空白 - const [markedReady, setMarkedReady] = useState(Boolean(marked)); - const value = ctx.node.metadata?.content || ""; +const PLACEHOLDER = "*选中节点,点上方工具条的 ✎ 编辑 Markdown*"; +// 解析结果按源码缓存(模块级):同一段 Markdown 只解析一次,重复渲染/重挂载都命中缓存, +// 返回的是同一个字符串引用,配合下方「值不变不写 DOM」彻底避免图片被重新请求。 +const htmlCache = new Map(); +function renderMarkdown(source: string): string { + if (!marked) return ""; + const key = source || PLACEHOLDER; + let out = htmlCache.get(key); + if (out === undefined) { + out = marked.parse(key); + htmlCache.set(key, out); + } + return out; +} + +// 预览态:用 ref 手动写 innerHTML,且仅在 html 真正变化时写入。 +// 这样宿主的任何重渲染(点击、移动视角、选中态变化等)都不会触碰已渲染的 DOM,图片不会重新加载。 +function MarkdownPreview({ ctx }: CanvasNodeContentProps) { + const [, force] = useState(0); + const ref = useRef(null); + const lastHtml = useRef(null); + + // marked 未就绪时按需加载,加载完触发一次重渲染填充内容 useEffect(() => { if (marked) return; let alive = true; - loadMarked().then(() => alive && setMarkedReady(true)); + loadMarked().then(() => alive && force((n) => n + 1)); return () => { alive = false; }; }, []); - // marked 就绪后同步计算 HTML(按 value 记忆),不再用异步 setState 填充,消除闪烁 - const html = useMemo(() => (marked ? marked.parse(value || "*双击右上角按钮编辑 Markdown*") : ""), [value, markedReady]); + const source = (ctx.node.metadata?.content as string | undefined) || ""; + const html = renderMarkdown(source); - const toggle = { position: "absolute", right: 8, top: 8, zIndex: 20, width: 32, height: 32, display: "grid", placeItems: "center", borderRadius: 8, border: `1px solid ${ctx.theme.node.stroke}`, background: `${ctx.theme.toolbar.panel}dd`, color: ctx.theme.node.text, cursor: "pointer" } as const; + useEffect(() => { + const el = ref.current; + if (!el || lastHtml.current === html) return; // 内容没变:不动 DOM,已加载的图片保持原样 + el.innerHTML = html; + lastHtml.current = html; + }, [html]); + return
e.stopPropagation()} style={{ height: "100%", width: "100%", color: ctx.theme.node.text }} />; +} + +function MarkdownEditor({ ctx }: CanvasNodeContentProps) { + const value = (ctx.node.metadata?.content as string | undefined) || ""; return ( -
e.stopPropagation()} style={{ position: "relative", height: "100%", width: "100%", display: "flex", flexDirection: "column" }}> - - {editing ? ( -