服務端渲染

通常情況下,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 是基於字串的,我們不需要一個容器來顯示渲染的內容,所以我們可以在 init 的第一個 container 引數中傳入 nullundefined
  • 然後在 init 的第三個引數中,我們需要透過在配置中指定 ssr: true 來告訴 ECharts 我們需要啟用服務端渲染模式。這樣 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 仍然可以提供動畫效果,這是透過在輸出的 SVG 字串中嵌入 CSS 動畫來實現的。不需要額外的 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 = canvas.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,服務端渲染方案可以在渲染圖表的同時做到以下幾點:

  • 支援初始動畫(即圖表首次渲染時播放的動畫)。
  • 高亮樣式(即滑鼠移到柱狀圖的柱子上時的高亮效果)。

但有些功能是服務端渲染無法支援的:

  • 動態改變資料
  • 點選圖例切換系列是否顯示
  • 移動滑鼠顯示提示框(tooltip)
  • 其他與互動相關的功能

如果你有這類需求,可以考慮使用服務端渲染來快速輸出首屏圖表,然後等待 echarts.js 載入完成後,在客戶端重新渲染同一個圖表,這樣就可以實現正常的互動效果和動態改變資料。注意,在客戶端渲染時,你應該開啟像 tooltip: { show: true } 這樣的互動元件,並用 animation: 0 關閉初始動畫(初始動畫應該由服務端渲染結果的 SVG 動畫完成)。

正如我們所見,從使用者體驗的角度來看,幾乎沒有二次渲染的過程,整個切換效果非常無縫。你也可以像上面的例子那樣,在載入 echarts.js 的過程中使用像 pace-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 上編輯此頁

Oviliaplainheartpissangballoon72