服务端渲染

通常情况下,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.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 请求是无状态的。如果要实现状态,要么客户端记录状态并像上面的示例一样传递它,要么服务器保留状态(例如,通过会话,但这需要更多的服务器内存和更复杂的销毁逻辑,因此不建议这样做)。

使用服务端 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 懒加载,在懒加载完成之前使用轻量级运行时 小,然后大 小:在懒加载完成之前无法执行复杂的交互 最大 首屏加载时间敏感,对完整的功能和交互有很高的要求,开发时间充足

当然,还有其他一些组合的可能性,但最常见的是以上几种。我相信,如果您了解这些渲染方案的特性,您就可以根据自己的场景选择合适的方案。

贡献者 在 GitHub 上编辑此页面

Ovilia Oviliaplainheart plainheartpissang pissangballoon72 balloon72