React + Mermaid 圖表渲染消失問題剖析及 4 種代碼級修復方案

Mermaid 是一個流行的庫,它可以將文本圖表(例如 graph LR; A-->B;)轉換為 SVG 圖表。

在靜態 HTML 頁面中,Mermaid 會查找 <pre class="mermaid"> 代碼塊,并在頁面加載時將它們替換為渲染后的圖表。

它甚至會添加一個特殊的 data-processed 屬性來標記已轉換的塊。

然而,在 React 應用中,這可能會導致一個意外的 bug:

你的 Mermaid 圖表最初顯示正常,但一旦 React 重新渲染(例如狀態變化后),圖表就會消失,取而代之的是原始的 Mermaid 代碼。

“Mermaid 圖表在項目首次加載時渲染得非常完美,但如果我修改圖表標記,它就會以純文本形式渲染,而不是圖表。”

為什么會這樣?我們又該如何解決呢?

Mermaid 渲染的工作原理

在底層,Mermaid 通過掃描 DOM 來查找帶有 class="mermaid" 的元素,并解析它們的文本。

例如,<pre class="mermaid">graph LR A-->B</pre> 將被替換為一個內聯 SVG,顯示該流程圖。

在頁面加載時(或手動觸發時),Mermaid 的運行函數會遍歷文檔,將每個代碼塊轉換為 SVG,并為這些元素添加 data-processed 標簽,以避免再次渲染。

這就是 Mermaid 的正常生命周期:

  • 初始加載: Mermaid(通常通過 mermaid.init()、mermaid.contentLoaded() 或 mermaid.run())會找到所有 <pre class="mermaid">…</pre> 塊,并將它們替換為 <svg> 圖表。
  • 標記: 轉換圖表后,Mermaid 會添加 data-processed="true" 屬性,這樣它就知道不用再處理它了。
  • 重新渲染: 如果你后來添加更多 .mermaid 塊并再次調用 mermaid.run(),它會跳過任何已標記的塊。

data-processed 機制是性能的關鍵:它防止 Mermaid 在每次調用時重新解析每個圖表。

但在 React 中,這個機制適得其反。

React 的虛擬 DOM 與 Mermaid 的沖突

React 使用自己的 虛擬 DOM 來高效更新 UI。

在 React 組件中,你返回的 JSX 描述了 UI 應該 是什么樣子。

React 然后將這個虛擬樹與實際 DOM 進行比較,并進行最小化更新。

關鍵在于,React 會覆蓋它“擁有”的任何 DOM 部分

正如 React 文檔所解釋的,“React 會自動更新 DOM 以匹配你的渲染輸出”。

換句話說,如果 Mermaid 直接修改了真實 DOM(通過插入 <svg>),React 并不知道;

下次 React 渲染該組件時,它會用渲染函數指定的內容替換那個 <svg>——在我們的案例中,很可能是原始的 <pre class="mermaid">…</pre> 文本。

換一種說法,Mermaid 操作的是 真實 DOM,而 React 維護的是 虛擬 DOM 并將其與真實 DOM 協調。

當兩者沖突時,React 會獲勝。

可以這么說:“Mermaid 直接與瀏覽器的真實 DOM 交互,這與 React 的虛擬 DOM 方法形成對比。”

具體來說,在 React 應用中的典型序列是:

  1. 初始掛載: React 渲染組件,包含 <div class="mermaid">…圖表代碼…</div>。然后我們觸發 mermaid.contentLoaded() 或類似函數,Mermaid 將其轉換為 DOM 中的 <svg>。
  2. 狀態更新: 某些東西變化了(props 或狀態),React 重新運行組件的渲染函數。如果該函數仍然返回原始的 <div class="mermaid">…圖表代碼…</div>,React 會用原始文本元素覆蓋 SVG,因為那是虛擬 DOM 指定的內容
  3. 圖表消失,原始文本重新出現。

這種交互就是 Mermaid 圖表在更新時“消失”的原因:React 本質上抹除了 Mermaid 的工作。

要修復這個問題,我們需要將 Mermaid 的真實 DOM 渲染與 React 的渲染周期橋接起來。

策略 #1:移除 data-processed并重新運行 Mermaid

一個常見且直接的修復方法是 清除 data-processed 標志,并在圖表數據變化時再次調用 Mermaid 的渲染

由于 Mermaid 不會重新渲染已標記的塊,我們首先移除該屬性,讓它視之為新塊。

例如:

import React, { useEffect } from 'react';
import mermaid from 'mermaid';/** 方法 1:移除 `data-processed` 并調用 contentLoaded。* 這確保 Mermaid 會重新掃描元素。*/
function MermaidChart({ chartDefinition }) {useEffect(() => {// 找到容器并移除 Mermaid 的標記屬性const element = document.getElementById('mermaid-container');element?.removeAttribute('data-processed');// 重新運行 Mermaid 以重新渲染圖表mermaid.contentLoaded();}, [chartDefinition]); // 每當 chartDefinition 變化時運行 effect// 在容器中渲染圖表代碼return (<div id="mermaid-container" className="mermaid">{chartDefinition}</div>);
}

在這個代碼片段中,每次 chartDefinition prop 變化時,我們獲取圖表的 DOM 元素(#mermaid-container),移除其 data-processed 屬性,并調用 mermaid.contentLoaded()。

這會“欺騙” Mermaid,讓它再次查看該元素并重繪圖表。

一篇 StackOverflow 回答總結了這種方法:“你需要在組件狀態更新后移除該屬性并重新調用 mermaid.contentLoaded()。”

這個 hack 在 React 更改底層文本時讓 Mermaid 更新圖表。

注意事項: 確保代碼中的 ID 或類與你的目標匹配。還要注意 mermaid.contentLoaded() 會嘗試重新渲染頁面上的 所有 Mermaid 塊,而不僅僅是一個,因此如果你有許多圖表,這個方法可能會比較耗資源。對于少量圖表來說,它很合適。

策略 #2:使用 mermaid.render()手動生成 SVG

另一種方法是完全繞過 Mermaid 的自動掃描,并 使用 mermaid.render() 手動生成 SVG 代碼

而不是讓 Mermaid 自己修改 DOM,你用圖表文本調用 API,并獲取 SVG 字符串作為返回。

然后,你可以將該字符串注入組件中,例如使用 dangerouslySetInnerHTML。

這樣,React 保持對 DOM 的控制(它看到的是你設置在狀態中的 <svg>),從而完全避免 data-processed 問題。

以下是一個使用現代 Mermaid API(返回 Promise)的 React 示例:

import React, { useState, useEffect } from 'react';
import mermaid from 'mermaid';/** 方法 2:使用 mermaid.render() 獲取 SVG 并注入它。* 這顯式生成圖表代碼。*/
function MermaidChart({ chartDefinition }) {const [svgCode, setSvgCode] = useState('');useEffect(() => {let isMounted = true; // 避免在卸載組件時更新狀態async function renderChart() {try {await mermaid.parse(chartDefinition); // 可選:驗證圖表const { svg } = await mermaid.render('uniqueChartId', chartDefinition);if (isMounted) {setSvgCode(svg);}} catch (error) {console.error('渲染 Mermaid 圖表出錯:', error);}}renderChart();return () => {isMounted = false;};}, [chartDefinition]);// 直接將 SVG 字符串渲染到 DOM 中return <div dangerouslySetInnerHTML={{ __html: svgCode }} />;
}

工作原理: 每次 chartDefinition 變化時,我們解析并渲染它。mermaid.render('uniqueChartId', chartDefinition) 返回一個包含 svg 字段的對象(SVG 標記)。然后我們將該 SVG 存入 React 狀態。組件輸出一個 <div>,其 HTML 設置為 SVG。因為 React 直接渲染 SVG 標記,所以沒有被擦除的風險——React 擁有該 SVG 節點。

這種模式在實踐中被多位作者展示。

例如,一篇教程在 React 應用中使用 mermaid.render("theGraph", definition, (svgCode) => { output.innerHTML = svgCode; })。

Tuanhuy 博客也在 useLayoutEffect 中使用 const { svg } = await mermaid.render("id", graphText); 來設置狀態變量。

關鍵點是:自己使用 Mermaid API,而不是依賴自動掃描。

注意: 如果使用此方法,確保 mermaid.render 的第一個參數(這里是 'uniqueChartId')對每個圖表都是唯一的,因為 Mermaid 用它來標識 SVG 元素。在 React 中,如果你渲染多個圖表,可以使用 ref 或 UUID。

策略 #3:使用 useLayoutEffect進行同步渲染

React 的 useEffect 鉤子在組件更新瀏覽器后運行(繪制后)。

相反,useLayoutEffect 在 React 應用 DOM 更新后但瀏覽器重繪前運行。

這種時機在庫(如 Mermaid)需要立即作用于 DOM 時更安全。

因為 Mermaid 期望真實 DOM 在繪制前就位,使用 useLayoutEffect 可以避免閃爍。

在實踐中,你可以在 useLayoutEffect 中調用 mermaid.contentLoaded()。

例如:

import React, { useLayoutEffect } from 'react';
import mermaid from 'mermaid';/** 方法 3:使用 useLayoutEffect 初始化和渲染。* 這確保 Mermaid 在 React 更新 DOM 后運行。*/
function MermaidChart({ chartDefinition }) {// 一次性初始化 MermaiduseLayoutEffect(() => {mermaid.initialize({ startOnLoad: false });}, []);// 每當圖表變化時重新運行 MermaiduseLayoutEffect(() => {// 當 React 用新 chartDefinition 更新 DOM 時,重新運行 Mermaidmermaid.contentLoaded();}, [chartDefinition]);return <div className="mermaid">{chartDefinition}</div>;
}

在這個示例中,第二個 useLayoutEffect 的依賴數組包含 chartDefinition,因此它會在 React 將新圖表文本放入 DOM 后立即運行。

使用 useLayoutEffect(而非 useEffect)確保我們甚至不會短暫看到原始文本。

可以這么說:“Mermaid 基于真實 DOM 渲染,因此必須在頁面渲染后發生,所以我們使用 useLayoutEffect 鉤子來渲染。”

通過在 useLayoutEffect 中運行 mermaid.contentLoaded(),Mermaid 會看到更新的 <div> 并繪制圖表。

在后續 React 更新中,第一個 effect(空依賴)不會重新運行初始化,而第二個 effect 會根據需要重新運行渲染。

另一個變體是將 useLayoutEffect 與其中的 mermaid.render() 結合(如 Tuanhuy 示例)。

本質是 useLayoutEffect 讓 Mermaid 有機會在瀏覽器繪制前繪制,從而實現更平滑的更新。

策略 #4:使用 MutationObserver 觀察 DOM 變化(高級)

作為可選或高級方法,你可以使用 MutationObserver API 來監視 DOM 變化,并在新圖表代碼出現時觸發 Mermaid。

這更復雜,但適用于 Mermaid 塊由某些渲染器深層插入的系統。

思路是觀察容器元素,當添加新子節點時,在它們上調用 Mermaid。

例如:

import React, { useEffect } from 'react';
import mermaid from 'mermaid';/** 方法 4:使用 MutationObserver 檢測新 Mermaid 塊。*/
function MermaidWrapper() {useEffect(() => {const observer = new MutationObserver((mutationsList) => {for (const mutation of mutationsList) {if (mutation.addedNodes.length > 0) {// 添加了新內容;重新掃描 Mermaid 圖表document.querySelectorAll('div.mermaid').forEach(el => {el.removeAttribute('data-processed');});mermaid.contentLoaded();break;}}});// 觀察整個文檔或特定容器observer.observe(document.body, { childList: true, subtree: true });return () => observer.disconnect();}, []);// ... 你的應用動態插入 <div class="mermaid"> 塊 ...return <ContentWithMermaid />;
}

這里我們監視 document.body(或某個包裝元素)的任何新子節點。

當出現 addedNodes 時,我們假設可能有新 Mermaid 圖表。

我們然后清除所有 .mermaid div 的 data-processed,并調用 mermaid.contentLoaded()。

這確保即使動態插入的圖表也能被渲染。

謹慎使用: MutationObserver 對于大多數應用來說可能是多余的,如果誤用可能會影響性能。但如果你的應用渲染流程難以僅用鉤子攔截,這是一個選項。

結論

總之,React 中的 Mermaid 圖表在重新渲染時消失是因為 React 的虛擬 DOM 用原始文本替換了 Mermaid 注入的 SVG。

要修復這個問題,我們需要在 React 更新后顯式重新觸發 Mermaid。

常見解決方案包括:

  • 清除 data-processed 并重新運行: 移除標記并在 React effect 中調用 mermaid.contentLoaded()(或 mermaid.run())。
  • 使用 mermaid.render() 用 API 生成 SVG 并讓 React 渲染它(如上所示)。
  • 使用 useLayoutEffect 在布局 effect 中調用 Mermaid,讓它在重繪前看到更新的 DOM。
  • (高級)MutationObserver: 監視 DOM 變化并根據需要觸發 Mermaid。

每種方法都有權衡。

直接清除 data-processed 對于簡單案例來說快速且容易,而 mermaid.render() 提供更多控制,但需要手動注入 HTML。

使用 React effect(useLayoutEffect)可以優雅地將 Mermaid 集成到 React 生命周期中。

借助這些策略,你可以讓 Mermaid 圖表在 React UI 更新時保持活躍。

參考資料:

更多細節請參閱 Mermaid 文檔和社區關于將 Mermaid 與 React 集成的帖子。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/91673.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/91673.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/91673.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

[Element]修改el-pagination背景色

[Element]修改el-pagination背景色 代碼 <el-pagination:current-page.sync"queryParams.current":page-size.sync"queryParams.size":page-sizes"[10, 20, 50, 100]"layout"prev, pager, next, jumper, sizes":total"queryP…

Docker 可用鏡像列表

Docker 鏡像源列表&#xff08;7月15日更新-長期&免費&#xff09;_dockerhub國內鏡像源列表-CSDN博客

低代碼可視化工作流的系統設計與實現路徑研究

一、背景分析在數字化轉型不斷深化的背景下&#xff0c;企業業務流程呈現出高度定制化與動態調整的趨勢&#xff0c;傳統信息系統在開發周期、實施成本與擴展能力上的局限性日益凸顯&#xff0c;已難以支撐快速響應和敏捷迭代的實際需求。面向這一現實挑戰&#xff0c;基于 BPM…

mac mlx大模型框架的安裝和使用

mlx是apple平臺的大模型推理框架&#xff0c;對mac m1系列處理器支持較好。 這里記錄mlx安裝和運行示例。 1 安裝mlx框架 conda create -n mlx python3.12 conda activate mlx pip install mlx-lm 2 運行mlx測試例 以下是測試程序&#xff0c;使用方法和hf、vllm等推理框架基…

JAVA 使用Apache POI合并Word文檔并保留批注的實現

一、需求背景 在實際工作中&#xff0c;我們經常需要將多個Word文檔合并成一個文件。但當文檔中包含批注&#xff08;Comments&#xff09;時&#xff0c;傳統的復制粘貼會導致批注丟失或引用錯亂。本文將介紹如何通過Java和Apache POI庫實現保留批注及引用關系的文檔合并功能。…

Linux的服務管理工具:`systemd`(`systemctl`)和`SysVinit ` 筆記250718

Linux的服務管理工具:systemd(systemctl)和SysVinit 筆記250718 Linux的服務管理工具 Linux 的服務管理工具隨著發行版和初始化系統的發展而演變。以下是主要的服務管理工具及其對應的初始化系統&#xff1a; 1. systemd (現代主流標準) 初始化系統&#xff1a; 是絕大多數…

Couchbase 可觀測性最佳實踐

Couchbase 介紹 Couchbase 是一個開源的分布式 NoSQL 數據庫&#xff0c;專為高性能和高可擴展性設計&#xff0c;適用于實時數據處理的企業應用。它結合鍵值存儲和文檔數據庫的優勢&#xff0c;支持 JSON 文檔存儲&#xff0c;并通過 N1QL&#xff08;類 SQL 查詢語言&#x…

構建基于MCP的LLM聊天機器人客戶端開發指南

引言 在當今人工智能技術快速發展的時代&#xff0c;大型語言模型(LLM)已成為構建智能應用的核心組件。MCP(Modular Conversational Platform)作為一個強大的對話平臺&#xff0c;為開發者提供了將LLM能力與自定義工具集成的標準化方式。本文將詳細介紹如何使用Python開發一個…

接口測試的原則、用例與流程詳解

&#x1f345; 點擊文末小卡片&#xff0c;免費獲取軟件測試全套資料&#xff0c;資料在手&#xff0c;漲薪更快 一、接口的介紹軟件測試中&#xff0c;常說的接口有兩種&#xff1a;圖形用戶接口&#xff08;GUI&#xff0c;人與程序的接口&#xff09;、應用程序編程接口&am…

ubuntu 22.02 帶外進單用戶拯救系統

不停地按 F7 &#xff0c;然后進到 menu &#xff0c;選擇 ubuntu &#xff0c;然后按下 ESC &#xff0c;然后瞬間會刷一個 ubuntu 的選項&#xff08;默認是在第一的位置&#xff0c;直接快速按下 e&#xff09;即可進入單用戶模式。 找到類似 linux /boot/vmlinuz-xxx rootU…

Java-75 深入淺出 RPC Dubbo Java SPI機制詳解:從JDK到Dubbo的插件式擴展

點一下關注吧&#xff01;&#xff01;&#xff01;非常感謝&#xff01;&#xff01;持續更新&#xff01;&#xff01;&#xff01; &#x1f680; AI篇持續更新中&#xff01;&#xff08;長期更新&#xff09; AI煉丹日志-30-新發布【1T 萬億】參數量大模型&#xff01;Kim…

【橘子分布式】gRPC(編程篇-上)

一、簡介 我們之前學習了grpc的一些理論知識&#xff0c;現在我們開始正式進入編程環節。 我們的項目結構和之前的thrift結構還是一樣的&#xff0c;一個common,一個client&#xff0c;一個server。只不過在grpc這里common它一般叫做api模塊。還是放置一些公共的實體類&#x…

IOS 18下openURL 失效問題

突然有一天有玩家反饋說應用打開外部連接打不開了&#xff0c;于是查了一下&#xff0c;報錯&#xff1a;BUG IN CLIENT OF UIKIT: The caller of UIApplication.openURL(_:) needs to migrate to the non-deprecated UIApplication.open(_:options:completionHandler:). Force…

前端面試題(React 與 Vue)

目錄 一、React 函數組件 Fiber架構 組件重新渲染 組件通信 為什么不能在if中使用hook useEffect與useLayoutEffect區別 性能優化hooks 受控組件與非受控組件 redux與zustand區別 二、Vue vue2與vue3區別 生命周期 computed與watch區別 v-if與v-show區別 v-mod…

大模型格式

目錄 大模型格式&#xff1a; ollma 可以加載gguf ChatGPT 說&#xff1a; &#x1f50d; 什么是 GGUF&#xff1f; 大模型格式&#xff1a; Ollama 模型格式只能運行已打包成 .gguf 格式的模型&#xff0c;或通過其 Modelfile 方式構建 ModelScope 模型格式大多使用 Hug…

數據結構 棧(1)

1. 棧的概念和結構之前幾篇我們分別講解了順序表和單鏈表的內容&#xff0c;今天我們又來學習一個新的關于數據結構的內容--- 棧 。棧&#xff1a;棧也屬于線性表 , 但它是一種特殊的線性表&#xff0c;其只允許在固定的一端進行插入和刪除元素操作。進行數據插入和刪除操作的一…

【Android代碼】繪本翻頁時通過AI識別,自動通過手機/pad朗讀繪本

核心功能&#xff1a; 打開攝像頭&#xff08;可支持外接攝像頭&#xff09;檢測翻頁&#xff08;后續考慮添加圖像差異算法&#xff09;拍照后用 識圖自動用 TextToSpeech 朗讀文字內容 &#x1f4cc; 說明&#xff1a;使用了 CameraX&#xff08;Android Jetpack&#xff09;…

園區IPv6規劃與部署

?今天我將圍繞“園區IPv6規劃與部署”這一主題&#xff0c;結合行業趨勢、技術難點和實際案例&#xff0c;與大家分享一套可落地的規劃方法論。?在開始前&#xff0c;我想先問大家一個問題&#xff1a;?如果現在讓你給一個新建園區設計網絡&#xff0c;你會優先考慮IPv4還是…

mingw11.2+opencv4.12 cmake contrib編譯

第一次Configure之后&#xff0c;會出現不少錯誤&#xff0c;主要是因為文件沒辦法正常下載引起的,因為之前編譯過vs2022 ,緩存里面有應該下載的文件了&#xff0c;所以這次沒有錯誤&#xff0c;如果你第一次Configure有下載錯誤&#xff0c;可以下載以下的文件飛書 Docs Link:…

免費MCP服務:Excel CSV 轉 JSON MCP by WTSolutions 文檔

簡介 Excel 轉 JSON MCP&#xff08;模型上下文協議&#xff09;提供了一個標準化接口&#xff0c;用于通過模型上下文協議將 Excel 和 CSV 數據轉換為 JSON 格式。此 MCP 實現提供了兩個專門用于數據轉換的工具&#xff1a; excel_to_json_mcp_from_data&#xff1a;轉換制表…