服務端渲染
通常情況下,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
引數中傳入null
或undefined
。 - 然後在
init
的第三個引數中,我們需要透過在配置中指定ssr: true
來告訴 ECharts 我們需要啟用服務端渲染模式。這樣 ECharts 就會知道需要停用動畫迴圈和事件模組。 - 我們還必須指定圖表的
height
和width
,所以如果你的圖表大小需要響應容器,你可能需要考慮服務端渲染是否適合你的場景。
在瀏覽器中,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 懶載入,在懶載入完成前使用輕量級執行時 | 小,然後大 | 小:懶載入完成前無法進行復雜互動。 | 最大 | 對首屏載入時間敏感,對完整功能和互動有高要求,開發時間充足。 |
當然,還有一些其他的組合可能性,但最常見的是以上這些。相信如果你理解了這些渲染方案的特點,就可以根據自己的場景選擇合適的方案。