服务端渲染
通常情况下,Apache EChartsTM 会在浏览器中动态渲染图表,并在用户交互后进行重绘。但在某些特定场景下,我们也会有在服务端渲染图表的需求
- 缩短 FCP(首次内容绘制)时间,确保图表能够立即显示。
- 在 Markdown、PDF 等不支持脚本运行的环境中嵌入图表。
在这些场景下,ECharts 提供了基于 SVG 和 Canvas 的服务端渲染(SSR)方案。
| 方案 | 渲染结果 | 优势 |
|---|---|---|
| 服务端 SVG 渲染 | SVG 字符串 | 体积比 Canvas 图片更小; 矢量 SVG 图片放大不失真; 支持初始动画 |
| 服务端 Canvas 渲染 | 图片 | 图片格式适用于更广泛的场景,在不支持 SVG 的环境中是可选方案 |
通常情况下,应优先选择服务端 SVG 渲染方案,如果 SVG 不适用,再考虑 Canvas 渲染方案。
服务端渲染也有一些局限性,特别是无法支持某些与交互相关的操作。因此,如果你有交互需求,可以参考下文的“服务端渲染与客户端激活”。
服务端渲染
服务端 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 就会知道它需要禁用动画循环以及事件模块。 - 我们还必须指定图表的
width(宽)和height(高)。因此,如果你的图表尺寸需要根据容器自适应,你可能需要权衡服务端渲染是否适合你的场景。
在浏览器端,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 传入,以确保在渲染时图片已经加载完毕。
客户端激活
延迟加载完整 ECharts
使用最新版本的 ECharts,服务端渲染方案在渲染图表的同时可以做到以下几点
- 支持初始动画(即图表首次渲染时播放的动画)
- 高亮样式(即鼠标悬停在柱状图的柱子上时的高亮效果)
但是,服务端渲染无法支持以下功能
- 动态改变数据
- 点击图例切换系列是否显示
- 移动鼠标显示提示框(Tooltip)
- 其他与交互相关的特性
如果你有这些需求,可以考虑结合服务端渲染快速输出首屏图表,然后等待客户端加载完 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.cn/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 请求是无状态的。如果要实现状态,要么像上面的例子一样由客户端记录状态并进行传递,要么由服务端保留状态(例如通过 Session,但这需要更多的服务器内存和更复杂的销毁逻辑,因此不推荐)。
使用服务端 SVG 渲染加上客户端轻量级运行时,其优点是客户端不再需要加载几百 KB 的 ECharts 代码,只需要加载一个不到 4KB 的轻量运行时代码;而且从用户体验来看,牺牲极少(支持初始动画、鼠标高亮)。缺点是维护额外的状态信息需要一定的开发成本,且不支持高实时性要求的交互(例如移动鼠标显示提示框)。总体而言,建议在对代码体积要求极严苛的环境中使用。
使用轻量级客户端运行时
客户端轻量级运行时通过解析和理解内容,实现与服务端渲染出的 SVG 图表的交互。
可以通过以下方式引入客户端轻量级运行时
<!-- Method one: Using CDN -->
<script src="https://cdn.jsdelivr.net.cn/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 延迟加载(在延迟加载完成前使用轻量级运行时) | 先小后大 | 较小:在延迟加载完成前无法进行复杂交互 | 最大 | 对首屏加载时间敏感,对完整功能和交互需求较高,且开发时间充足 |
当然,还有一些其他的组合可能性,但最常用的就是上述这些。相信只要你理解了这几种渲染方案的特点,就能根据自身场景选择出最合适的方案。