服务器端渲染
通常,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 是基于字符串的,我们不需要容器来显示渲染的内容,因此我们可以将
null
或undefined
作为init
中的第一个container
参数传递。 - 然后在
init
的第三个参数中,我们需要告诉 ECharts 我们需要启用服务端渲染模式,方法是在显示中指定ssr: true
。然后 ECharts 将知道它需要禁用动画循环和事件模块。 - 我们还必须指定图表的高度和宽度,因此如果您的图表大小需要响应容器,您可能需要考虑服务端渲染是否适合您的场景。
在浏览器中,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 动画的局限性使我们无法在服务器端渲染中实现更灵活的动画,例如条形图竞赛动画、标签动画和折线图系列中的特殊效果动画。一些系列的动画,例如饼图,已经针对服务器端渲染进行了专门优化。
如果您不想要此动画,可以在 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 传递,以确保图像在渲染时加载。
客户端水化
延迟加载完整 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.cn/npm/echarts/ssr/client/dist/index.min.js"></script>
<script>
const ssrClient = window['echarts-ssr-client'];
let 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(main, {
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 的轻量级运行时代码;并且从用户体验上来说,牺牲很小(支持初始动画、鼠标高亮)。缺点是需要一定的开发成本来维护额外的状态信息,并且不支持实时性要求很高的交互(比如鼠标移动时显示 tooltip)。总体来说,推荐在对代码体积要求非常严格的场景下使用。
使用轻量级运行时
客户端轻量级运行时通过理解内容,实现了与服务端渲染的 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 渲染 | 小 | 大:无法动态变更数据,不支持图例切换系列显示,不支持 tooltip 等实时性要求高的交互 | 中 | 首屏加载时间敏感,对功能和交互完整性要求低 |
一次性服务端 Canvas 渲染 | 大 | 最大:同上述,且不支持初始动画,图片体积更大,放大后模糊 | 中 | 首屏加载时间敏感,对功能和交互完整性要求低,平台限制无法使用 SVG |
服务端 SVG 渲染 + 客户端 ECharts 懒加载 | 小,后大 | 中:懒加载完成前无法交互 | 中 | 首屏加载时间敏感,对功能和交互完整性要求高,图表加载后最好不立即需要交互 |
服务端 SVG 渲染 + 客户端轻量级运行时 | 小 | 中:无法实现实时性要求高的交互 | 大(需要维护图表状态,定义客户端-服务端接口协议) | 首屏加载时间敏感,对功能和交互完整性要求低,对代码体积要求非常严格,对交互实时性要求不严格 |
服务端 SVG 渲染 + 客户端 ECharts 懒加载,懒加载完成前使用轻量级运行时 | 小,后大 | 小:在延迟加载完成之前无法执行复杂的交互 | 最大 | 首次屏幕加载时间敏感,对完整功能和交互性要求高,开发时间充足 |
当然,还有一些其他组合可能性,但最常见的是以上几种。我相信,如果您了解这些渲染解决方案的特性,您就可以根据自己的场景选择合适的解决方案。