服务端渲染 ECharts 图表

2,334 阅读8分钟

本文是我初学 ECharts 官网中 “服务端渲染” 的实践(Vue3 + express)、理解和总结。官网网址:服务端渲染 - 跨平台方案 - 应用篇 - Handbook - Apache ECharts

普遍情况下,Apache ECharts 会选择在浏览器中动态渲染图表,并且根据用户交互来更新渲染。但是在以下场景里,我们也需要在服务端渲染图表并输出到浏览器中:

  • 需要缩短前端渲染时间,保证首先显示图表
  • 在不支持动态运行脚本的环境下嵌入图表,如Markdown, PDF等

ECharts 提供了两种服务器渲染(server-side rendering,SSR)的方案:SVG 渲染或 Canvas 渲染。

渲染方案渲染结果的形式优点
服务端 SVG 渲染SVG 字符串比 Canvas 图片体积更小;矢量 SVG 图片不会模糊(在实践中发现的确比 Canvas 渲染更清晰);支持初始动画
服务端 Canvas 渲染图片图片形式适用场景更广泛,对不支持 SVG 的场景可以选择

官网建议优先考虑使用 SVG 渲染方案,如果 SVG 不适用,也可以考虑 Canvas 渲染方案。但使用服务端渲染也有一定局限性,特别是不支持交互相关的部分操作。所以,如果有交互需求,可参考服务端渲染 Hydration(本文也有提及和实践)

为了大家更好理解服务端渲染,我先贴出init方法的类型(具体可看官方API文档:Documentation - Apache ECharts

(dom?: HTMLDivElement | HTMLCanvasElement, theme?: Object | string, opts?: {
    devicePixelRatio?: number, // 设备像素比
    renderer?: string, // 渲染模式,支持'canvas'或者'svg'
    useDirtyRect?: boolean, // 是否开启脏矩形渲染
    useCoarsePointer?: boolean, // 是否扩大可点击元素的响应范围 
    pointerSize?: number, // 扩大元素响应范围的像素大小
    ssr?: boolean, // 是否使用服务端渲染
    width?: number|string, // 可显式指定实例宽度,单位为像素
    height?: number|string, // 可显式指定实例高度,单位为像素
    locale?: string // 使用的语言
}) => ECharts

参数解释

  • dom:实例容器,一般是具有宽高的 DIV 元素。只有设置opts.ssr开启了服务端渲染后该参数才是可选。也支持直接使用canvas元素作为容器。
  • theme:应用的主题。
  • opts:附加参数。

服务端 SVG 渲染

我使用的是 5.3.0 里新引入的零依赖的服务端 SVG 字符串渲染方案:

以下是我实践服务端 SVG 渲染的服务端完整代码:

import express from "express";
import echarts from "echarts";
const app = express();
// 服务端 SVG 渲染的整体代码结构
function renderSVGChart() {
    // 在服务端 SVG 渲染模式下第一个参数不需要再传入 DOM 对象
    const chart = echarts.init(null, null, {
        renderer: 'svg',    // 指定使用 SVG 模式
        ssr: true,    // 开启 SSR
        width: 400,   // 需要指明高和宽
        height: 300
    })
    // 正常使用 setOption
    chart.setOption({
        xAxis: {
            type: "category",
            data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
        },
        yAxis: {
            type: "value"
        },
        series: [
            {
                data: [120, 200, 150, 80, 70, 110, 130],
                type: "bar",
            }
        ]
    });
    // 使用 renderToSVGString 将当前的图表渲染成 SVG 字符串
    return chart.renderToSVGString();
}
// 为路径“/” 的 GET 请求定义路由处理程序
app.get('/', (req, res) => {
    // 设置响应头,解决跨域
    res.header('Access-Control-Allow-Origin', '*')
    res.header('Access-Control-Allow-Headers', 'Authorization,X-API-KEY, Origin, X-Requested-With, Content-Type, Accept, Access-Control-Request-Method')
    res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PATCH, PUT, DELETE')
    res.header('Allow', 'GET, POST, PATCH, OPTIONS, PUT, DELETE')
    // 向请求客户端发送 HTTP 响应消息(返回的是 SVG 字符串)
    res.send(renderSVGChart());
})

app.listen(3000, () => {
    console.log('Example app listening on port 3000')
})

可以看出整体使用的代码结构跟在浏览器中使用是一致的:首先init初始化一个图表实例,然后通过setOption设置图表的配置项。但是init传入的参数会跟在浏览器中使用有些出入:

  • 因为服务端会通过字符串拼接的方式来渲染得到 SVG ,并不需要定义容器来展示渲染内容,所以在通过init初始化图表实例时,第一个参数dom传入null或者undefined
  • init的第三个参数中,我们需要显式指定ssr: true来告诉ECharts我们需要开启服务端渲染模式,Echarts会关闭动画循环和事件交互模块。
  • 在服务端渲染中我们也必须要通过widthheight指定图表的高和宽。
// 在服务端 SVG 渲染模式下第一个参数不需要再传入 DOM 对象
const chart = echarts.init(null, null, {
    renderer: 'svg',    // 指定使用 SVG 模式
    ssr: true,    // 开启 SSR
    width: 400,   // 需要指明高和宽
    height: 300
})

初始化完图表实例后,接着调用setOption。在浏览器中我们在setOption完后ECharts就会自动进行渲染并把结果绘制到页面中,后续也会在每一帧判断是否有动画需要进行重绘。在服务端中我们设置了ssr: true故则没有这个过程,取而代之我们使用了renderToSVGString,将当前的图表渲染到 SVG 字符串,进一步通过 HTTP Response 返回给前端。

const chart = echarts.init(...)
chart.setOption({...});
const svgString = chart.renderToSVGString()
app.get('/', (req, res) => {
    // ...
    // 向请求客户端发送 HTTP 响应消息(返回的是 SVG 字符串)
    res.send(svgString);
})

经过以上程序,服务端返回响应后,客户端处理如下(此处我省略了封装axios和封装请求方法的代码,直接展示我发送请求后处理响应):

// vue setup lang="ts"
import { ref } from 'vue'
import { api } from '../api/data'

const chart = ref('')
// 发送请求,请求成功后把响应的数据赋值给变量chart
api.getSVGChart().then(res => {
  chart.value = res.data;
}).catch(err => {
  console.log(err);
})
<!-- 使用 v-html 标签渲染SVG字符串 -->
<div v-html="chart"></div>

image.png


服务端 Canvas 渲染

如果你希望输出的是一张图片而非 SVG 字符串,或者你还在使用更老的版本,官方推荐使用 node-canvas 来实现 ECharts 的服务渲染,node-canvas 是在 NodeJS 上的一套 Canvas 实现,它提供了跟浏览器中 Canvas 几乎一致的接口。

以下是我实践服务端 Canvas 渲染的服务端完整代码:

import express from "express";
import echarts from "echarts";
import { createCanvas } from 'canvas';
const app = express();
// 服务端 Canvas 渲染
function renderCanvasChart() {
    // 创建 Canvas 实例
    const canvas = createCanvas(400, 300);
    // ECharts 可以直接使用 node-canvas 创建的 Canvas 实例作为容器
    const chart = echarts.init(canvas);
    // 正常使用 setOption
    chart.setOption({
        xAxis: {
            type: "category",
            data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
        },
        yAxis: {
            type: "value"
        },
        series: [
            {
                data: [120, 200, 150, 80, 70, 110, 130],
                type: "bar"
            }
        ]
    });
    return canvas;
}
app.get('/', (req, res) => {
    // 解决跨域
    res.header('Access-Control-Allow-Origin', '*')
    res.header('Access-Control-Allow-Headers', 'Authorization,X-API-KEY, Origin, X-Requested-With, Content-Type, Accept, Access-Control-Request-Method')
    res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PATCH, PUT, DELETE')
    res.header('Allow', 'GET, POST, PATCH, OPTIONS, PUT, DELETE')
    res.header("Content-Type", "image/png")
    // 通过响应返回 PNG 图片
    res.send(renderCanvasChart().toBuffer("image/png"));
})

app.listen(3000, () => {
    console.log('Example app listening on port 3000')
})

经过以上程序,服务端返回响应后,客户端处理如下(同样,此处我省略了封装axios和封装请求方法的代码,直接展示我发送请求后处理响应):

// vue setup lange="ts"
import { ref } from 'vue'
import { api } from '../api/data'

const chart = ref('')
const transformBlobToPNG = (data: Blob) => {
  //将Blob类型数据转换为url对象
  return window.URL.createObjectURL(data);
}
api.getCanvasChart().then(res => {
  chart.value = transformBlobToPNG(res.data)
}).catch(err => {
  console.log(err);
})
<!-- 显示PNG图片 -->
<img :src="chart" alt="">

image.png

我们可以对比一下服务端 SVG 渲染和 Canvas 渲染后的结果,SVG 的渲染结果是比 Canvas 清晰一些。


服务端渲染 Hydration

服务端渲染无法支持的功能包括:

  • 动态改变数据
  • 高亮鼠标所在的数据项
  • 点击图例切换系列是否显示
  • 移动鼠标显示提示框
  • 其他交互相关的功能

如果有相关需求,可以考虑先使用服务端渲染快速输出首屏图表,然后等待echarts.js加载完后,重新在客户端渲染同样的图表(称为 Hydration),这样就可以实现正常的交互效果和动态改变数据了。需要注意的是,在客户端渲染的时候,应开启 tooltip: { show: true } 之类的交互组件,并且用 animation: 0 关闭初始动画(初始动画应由服务端渲染结果的 SVG 动画完成)。

我在此实践中先用 SVG 做服务端渲染,再用 Canvas 做客户端渲染的效果。本次实践的服务端代码我就不贴了,与服务端 SVG 渲染的服务端代码一致。以下是我客户端的代码:

import { ref } from 'vue'
import { api } from '../api/data'
import * as echarts from 'echarts';

const chart = ref('')
// 控制显示服务端的 SVG 图片类型图表
const loading = ref(false);

api.getSVGChart().then(res => {
  chart.value = res.data;
}).catch(err => {
  console.log(err);
})

const loadChart = () => {
  loading.value = true;
  // 客户端渲染同样数据的图表,实现交互效果
  let card = document.getElementById('card');
  let myChart = echarts.init(card);
  myChart.setOption({
    xAxis: {
      type: "category",
      data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
    },
    yAxis: {
      type: "value"
    },
    tooltip: {
      // 展现交互效果
      show: true
    },
    series: [
      {
        data: [120, 200, 150, 80, 70, 110, 130],
        type: "bar",
        // 关闭初始动画
        animation: 0
      }
    ]
  })
}
<!-- 服务端渲染 Hydration -->
<!-- 鼠标移入容器后,服务端渲染的 SVG 图片会取消显示, -->
<!-- 取而代之的是客户端已经渲染好的动态图表 -->
<div id="card" @mouseover.once="loadChart">
  <div v-html="chart" v-if="!loading"></div>
</div>

image.png

结语

以上就是我分享的所有内容,感谢大家的观看!欢迎大家在评论区发表自己的见解~