伺服器端渲染

通常,Apache EChartsTM 會在瀏覽器中動態渲染圖表,並在使用者互動後重新渲染。然而,在某些特定情況下,我們也需要在伺服器端渲染圖表。

  • 減少 FCP 時間並確保圖表立即顯示。
  • 將圖表嵌入不支援腳本的環境中,例如 Markdown、PDF。

在這些情況下,ECharts 提供 SVG 和 Canvas 伺服器端渲染 (SSR) 解決方案。

解決方案 渲染結果 優點
伺服器端 SVG 渲染 SVG 字串 比 Canvas 影像小;
向量 SVG 影像不會模糊;
支援初始動畫
伺服器端 Canvas 渲染 影像 影像格式適用於更廣泛的場景,並且對於不支援 SVG 的場景是可選的

一般而言,應優先選擇伺服器端 SVG 渲染解決方案,或者如果 SVG 不適用,則可以考慮 Canvas 渲染解決方案。

伺服器端渲染也有一些限制,特別是一些與互動相關的操作無法支援。因此,如果您有互動需求,您可以參考下面的「使用 Hydration 的伺服器端渲染」。

伺服器端渲染

伺服器端 SVG 渲染

版本更新

  • 5.3.0:引入了新的零依賴基於字串的伺服器端 SVG 渲染解決方案,並支援初始動畫
  • 5.5.0:新增了輕量級客戶端執行時環境,允許在客戶端上進行一些互動,而無需載入完整的 ECharts

我們在 5.3.0 中引入了新的零依賴基於字串的伺服器端 SVG 渲染解決方案。

// Server-side code
const echarts = require('echarts');

// In SSR mode the first container parameter is not required
let chart = echarts.init(null, null, {
  renderer: 'svg', // must use SVG rendering mode
  ssr: true, // enable SSR
  width: 400, // need to specify height and width
  height: 300
});

// use setOption as normal
chart.setOption({
  //...
});

// Output a string
const svgStr = chart.renderToSVGString();

// If chart is no longer useful, consider disposing it to release memory.
chart.dispose();
chart = null;

整體程式碼結構與瀏覽器中的幾乎相同,從 init 開始初始化圖表範例,然後透過 setOption 設定圖表的組態項目。但是,傳遞給 init 的參數將與瀏覽器中使用的不同。

  • 首先,由於 SVG 是在伺服器端以字串為基礎渲染的,我們不需要容器來顯示渲染的內容,因此我們可以將 nullundefined 作為 init 中的第一個 container 參數傳遞。
  • 然後在 init 的第三個參數中,我們需要告訴 ECharts 我們需要透過在顯示中指定 ssr: true 來啟用伺服器端渲染模式。然後 ECharts 將知道它需要停用動畫循環和事件模組。
  • 我們還必須指定圖表的 heightwidth,因此如果您的圖表大小需要對容器做出回應,您可能需要考慮伺服器端渲染是否適合您的場景。

在瀏覽器中,ECharts 會在 setOption 後自動將結果渲染到頁面,然後判斷每一幀是否有需要重新繪製的動畫,但在 Node.js 中,我們在設定 ssr: true 後不會這樣做。相反,我們使用 renderToSVGString 將目前圖表渲染為 SVG 字串,然後可以透過 HTTP 回應將其返回到前端或儲存到本機檔案。

回應用於瀏覽器 (以 Express.js 為例)

res.writeHead(200, {
  'Content-Type': 'application/xml'
});
res.write(svgStr); // svgStr is the result of chart.renderToSVGString()
res.end();

或儲存到本機檔案

fs.writeFile('bar.svg', svgStr, 'utf-8');

伺服器端渲染中的動畫

正如您在上面的範例中所看到的,即使使用伺服器端渲染,ECharts 仍然可以提供動畫效果,這些效果是透過將 CSS 動畫嵌入到輸出的 SVG 字串中實現的。無需額外的 JavaScript 來播放動畫。

然而,CSS 動畫的限制使我們無法在伺服器端渲染中實現更靈活的動畫,例如長條圖競賽動畫、標籤動畫和 lines 系列中的特殊效果動畫。某些系列的動畫,例如 pie,已針對伺服器端渲染進行了特別最佳化。

如果您不想要此動畫,您可以在 setOption 時設定 animation: false 來關閉它。

setOption({
  animation: false
});

伺服器端 Canvas 渲染

如果您希望輸出是影像而不是 SVG 字串,或者您仍然使用較舊的版本,我們建議使用 node-canvas 進行伺服器端渲染,node-canvas 是 Node.js 上的 Canvas 實作,它提供的介面幾乎與瀏覽器中的 Canvas 相同。

這是一個簡單的範例

var echarts = require('echarts');
const { createCanvas } = require('canvas');

// In versions earlier than 5.3.0, you had to register the canvas factory with setCanvasCreator.
// Not necessary since 5.3.0
echarts.setCanvasCreator(() => {
  return createCanvas();
});

const canvas = createCanvas(800, 600);
// ECharts can use the Canvas instance created by node-canvas as a container directly
let chart = echarts.init(canvas);

// setOption as normal
chart.setOption({
  //...
});

const buffer = renderChart().toBuffer('image/png');

// If chart is no longer useful, consider disposing it to release memory.
chart.dispose();
chart = null;

// Output the PNG image via Response
res.writeHead(200, {
  'Content-Type': 'image/png'
});
res.write(buffer);
res.end();

影像載入

node-canvas 提供了一個 Image 實作來載入影像。如果您在程式碼中使用影像,我們可以使用 5.3.0 中引入的 setPlatformAPI 介面來調整它們。

echarts.setPlatformAPI({
  // Same with the old setCanvasCreator
  createCanvas() {
    return createCanvas();
  },
  loadImage(src, onload, onerror) {
    const img = new Image();
    // must be bound to this context.
    img.onload = onload.bind(img);
    img.onerror = onerror.bind(img);
    img.src = src;
    return img;
  }
});

如果您使用來自遠端的影像,我們建議您透過 http 請求預先提取影像以取得 base64,然後再將其作為影像的 URL 傳遞,以確保在渲染時載入影像。

客戶端 Hydration

延遲載入完整的 ECharts

使用最新版本的 ECharts,伺服器端渲染解決方案可以執行以下操作以及渲染圖表

  • 支援初始動畫(即首次渲染圖表時播放的動畫)
  • 突出顯示樣式(即當滑鼠移動到長條圖中的長條上方時的突出顯示效果)

但有些功能無法透過伺服器端渲染支援

  • 動態變更資料
  • 按一下圖例以切換是否顯示系列
  • 移動滑鼠以顯示工具提示
  • 其他與互動相關的功能

如果您有此類需求,您可以考慮使用伺服器端渲染快速輸出第一個畫面圖表,然後等待 echarts.js 完成載入並在客戶端重新渲染相同的圖表,以便您可以實現正常的互動效果並動態變更資料。請注意,在客戶端渲染時,您應該開啟互動式元件,例如 tooltip: { show: true },並使用 animation: 0 關閉初始動畫(初始動畫應由伺服器端渲染結果的 SVG 動畫完成)。

正如我們所看到的,從使用者體驗的角度來看,幾乎沒有二次渲染過程,並且整個切換效果非常無縫。您也可以使用類似 pace-js 的程式庫來在載入 echarts.js 期間顯示載入進度列,如上述範例所示,以解決 ECharts 完全載入之前沒有互動回饋的問題。

將伺服器端渲染與客戶端渲染結合使用,並在客戶端上延遲載入 echarts.js 是適合需要快速渲染第一個畫面然後需要支援互動的場景的好解決方案。但是,載入 echarts.js 需要一些時間,並且在完全載入之前,沒有互動回饋,在這種情況下,可能會向使用者顯示「載入中」文字。這是針對需要快速渲染第一個畫面然後需要支援互動的場景的常用建議解決方案。

輕量級客戶端執行時環境

解決方案 A 提供了一種實現完整互動的方式,但在某些情況下,我們不需要複雜的互動,我們只希望能夠在伺服器端渲染的基礎上在客戶端執行一些簡單的互動,例如:按一下圖例以切換是否顯示系列。在這種情況下,我們是否可以避免在客戶端上載入至少數百 KB 的 ECharts 程式碼?

從 v5.5.0 版本開始,如果圖表只需要以下效果和互動,則可以透過伺服器端 SVG 渲染 + 客戶端輕量級執行時環境來實現

  • 初始圖表動畫 (實作原理:伺服器渲染的 SVG 帶有 CSS 動畫)
  • 突出顯示樣式 (實作原理:伺服器渲染的 SVG 帶有 CSS 動畫)
  • 動態變更資料 (實作原理:輕量級執行時環境向伺服器請求二次渲染)
  • 按一下圖例以切換是否顯示系列 (實作原理:輕量級執行時環境向伺服器請求二次渲染)
<div id="chart-container" style="width:800px;height:600px"></div>

<script src="https://cdn.jsdelivr.net/npm/echarts/ssr/client/dist/index.min.js"></script>
<script>
const ssrClient = window['echarts-ssr-client'];

const isSeriesShown = {
  a: true,
  b: true
};

function updateChart(svgStr) {
  const container = document.getElementById('chart-container');
  container.innerHTML = svgStr;

  // Use the lightweight runtime to give the chart interactive capabilities
  ssrClient.hydrate(container, {
    on: {
      click: (params) => {
        if (params.ssrType === 'legend') {
          // Click the legend element, request the server for secondary rendering
          isSeriesShown[params.seriesName] = !isSeriesShown[params.seriesName];
          fetch('...?series=' + JSON.stringify(isSeriesShown))
            .then(res => res.text())
            .then(svgStr => {
              updateChart(svgStr);
            });
        }
      }
    }
  });
}

// Get the SVG string rendered by the server through an AJAX request
fetch('...')
  .then(res => res.text())
  .then(svgStr => {
    updateChart(svgStr);
  });
</script>

伺服器端根據客戶端傳遞的有關每個系列是否顯示 (isSeriesShown) 的資訊執行二次渲染,並傳回新的 SVG 字串。伺服器端程式碼與上方相同,將不再重複。

關於狀態記錄:與純客戶端渲染相比,開發人員需要記錄和維護一些額外資訊(例如本範例中是否顯示每個系列)。這是不可避免的,因為 HTTP 請求是無狀態的。如果您想實作狀態,則客戶端會記錄狀態並像上面的範例一樣傳遞它,或者伺服器會保留狀態(例如,透過會話,但它需要更多的伺服器記憶體和更複雜的銷毀邏輯,因此不建議使用)。

使用伺服器端 SVG 渲染加上客戶端輕量級執行時環境,優點是客戶端不再需要載入數百 KB 的 ECharts 程式碼,只需要載入一個小於 4KB 的輕量級執行時程式碼;從使用者體驗來看,犧牲也很少(支援初始動畫、滑鼠突出顯示)。缺點是它需要一定的開發成本來維護額外的狀態資訊,並且它不支援具有高即時要求的互動(例如,在移動滑鼠時顯示工具提示)。總體而言,建議在對程式碼量有非常嚴格要求的環境中使用它

使用輕量級執行時環境

客戶端輕量級執行時環境可以透過了解內容來與伺服器端渲染的 SVG 圖表進行互動。

客戶端輕量級執行時環境可以透過以下方式導入

<!-- Method one: Using CDN -->
<script src="https://cdn.jsdelivr.net/npm/echarts/ssr/client/dist/index.min.js"></script>
<!-- Method two: Using NPM -->
<script src="node_modules/echarts/ssr/client/dist/index.js"></script>

API

全域變數 window['echarts-ssr-client'] 中提供以下 API

hydrate(dom: HTMLElement, options: ECSSRClientOptions)

  • dom: 圖表容器,其內容應在呼叫此方法之前設定為伺服器端渲染的 SVG 圖表
  • options: 配置項
ECSSRClientOptions
on?: {
  mouseover?: (params: ECSSRClientEventParams) => void,
  mouseout?: (params: ECSSRClientEventParams) => void,
  click?: (params: ECSSRClientEventParams) => void
}

如同 圖表滑鼠事件,此處的事件適用於圖表項目(例如,長條圖的長條、折線圖的資料項目等),而非圖表容器。

ECSSRClientEventParams
{
  type: 'mouseover' | 'mouseout' | 'click';
  ssrType: 'legend' | 'chart';
  seriesIndex?: number;
  dataIndex?: number;
  event: Event;
}
  • type: 事件類型
  • ssrType: 事件物件類型,legend 代表圖例資料,chart 代表圖表資料物件
  • seriesIndex: 系列索引
  • dataIndex: 資料索引
  • event: 原生事件物件

範例

請參閱上方「輕量級客戶端執行時」章節。

總結

以上,我們介紹了幾種不同的渲染解決方案,包括

  • 客戶端渲染
  • 伺服器端 SVG 渲染
  • 伺服器端 Canvas 渲染
  • 客戶端輕量級執行時渲染

這四種渲染方法可以組合使用。讓我們總結一下它們各自適用的場景

渲染解決方案 載入量 功能和互動的損失 相對開發工作量 建議情境
客戶端渲染 最大 最小 首屏載入時間不敏感,且對完整功能和互動有高度需求
客戶端渲染(按需部分套件導入 大:未包含的套件無法使用相應的功能 首屏載入時間不敏感,對程式碼大小沒有嚴格要求但希望盡可能小,僅使用 ECharts 功能的一小部分,沒有伺服器資源
一次性伺服器端 SVG 渲染 大:無法動態更改資料,不支援圖例切換系列顯示,不支援工具提示和其他對即時性要求高的互動 首屏載入時間敏感,對完整功能和互動的要求較低
一次性伺服器端 Canvas 渲染 最大:與上述相同,且不支援初始動畫,圖片體積較大,放大時模糊 首屏載入時間敏感,對完整功能和互動的要求較低,平台限制無法使用 SVG
伺服器端 SVG 渲染加上客戶端 ECharts 延遲載入 小,然後大 中:延遲載入完成前無法互動 首屏載入時間敏感,對完整功能和互動有高度需求,圖表最好在載入後不需要立即互動
伺服器端 SVG 渲染加上客戶端輕量級執行時 中:無法實現對即時性要求高的互動 大(需要維護圖表狀態,定義客戶端-伺服器介面協定) 首屏載入時間敏感,對完整功能和互動的要求較低,對程式碼大小要求非常嚴格,對互動即時性要求不高
伺服器端 SVG 渲染加上客戶端 ECharts 延遲載入,在延遲載入完成前使用輕量級執行時 小,然後大 小:在延遲載入完成前無法執行複雜的互動 最大 首屏載入時間敏感,對完整功能和互動有高度需求,開發時間充足

當然,還有其他一些組合的可能性,但最常見的是以上這些。我相信如果您了解這些渲染解決方案的特性,就可以根據自己的情境選擇合適的解決方案。

貢獻者 在 GitHub 上編輯此頁面

Ovilia Oviliaplainheart plainheartpissang pissangballoon72 balloon72