一、功能背景
前端获取echarts的base64格式图片,需要在服务端开启无头(headless)浏览器截图。
本文引用了《基于nodejs在服务端生成echarts图片方案》中的ejs模版及相应的静态资源
juejin.cn/post/720548…
二、实现思路
启动web服务,基于ejs动态渲染出html并渲染,利用headless浏览器打开页面截图并返回base64
flowchart LR
A["http请求"] --> B["web服务"]-->C["模版渲染+puppteer截图"]-->D["返回base64"]
三、具体实现步骤
1、利用midway/koa构建web服务
具体创建web服务步骤可参考midway官网,下面提供配置文件。
configuration.ts
@Configuration({
imports: [
koa,
validate,
view,
staticFile,
{
component: info,
enabledEnvironment: ['local'],
},
],
importConfigs: [join(__dirname, './config')],
})
export class MainConfiguration {
@App('koa')
app: koa.Application;
async onReady(applicationContext: IMidwayContainer) {
// add middleware
this.app.useMiddleware([ReportMiddleware]);
// add filter
// this.app.useFilter([NotFoundFilter, DefaultErrorFilter]);
this.app.useFilter([WeatherEmptyDataErrorFilter, ValidateErrorFilter]);
//全局注册poppeteer,无需每次调用时引入。
applicationContext.registerObject('puppeteer', puppeteer);
//全局注册lodash,无需每次调用时引入。
applicationContext.registerObject('lodash', lodash);
}
}
config.default
export default {
// use for cookie sign key, should change to your own and keep security
keys: '1736305632632_7030',
view: {
defaultExtension: '.ejs',
defaultViewEngine: 'ejs',
// cache: false,
mapping: {
'.ejs': 'ejs',
},
rootDir: {
default: path.join(__dirname, '../../view'),
layout: path.join(__dirname, '../../view/layout'),
debug: path.join(__dirname, '../../view/debug'),
},
},
staticFile: {
prefix: '/',
dir: path.join(__dirname, '../../public'),
maxAge: 31536000,
buffer: true,
},
koa: {
port: 7001,
},
} as MidwayConfig;
这里需要额外引入@midwayjs/view-ejs和@midwayjs/static-file。
2、模版渲染+puppteer截图
ejs模版
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style>
* {
margin:0;
padding: 0
}
</style>
</head>
<body>
<div id="container" style="height: <%=chartHeight%>px; width: <%=chartWidth%>px;"></div>
<script type="text/javascript">
var chartConfig = <%- chartConfigStr %>;
chartConfig.animation = false;
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('container'));
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(chartConfig);
</script>
</body>
</html>
renderEharts.service.ts(包含了数据处理、模版渲染、puppteer截图等操作)
// src/service/renderEharts.service.ts
import { Inject, Provide } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
import * as Default_options from '../../public/js/defaultOptions.js';
import { PuppeteerService } from './puppeteer.service';
@Provide()
export class RenderEchartsService {
// 注入PuppeteerService
@Inject()
private puppeteerService: PuppeteerService;
@Inject()
ctx: Context;
// 注入lodash
@Inject('lodash')
private _: any;
/**
* 使用Puppeteer进行测试的异步函数
* @returns 返回页面标题的Promise
*/
/**
* 生成ECharts图表的异步函数
* @param chartsConfig - 包含图表配置的对象
* @returns 返回生成的ECharts图表的Base64编码字符串
*/
async generateEcharts(chartsConfig: any): Promise<string> {
// 图表配置
let chartConfig = chartsConfig.chartConfig;
const chartWidth = chartsConfig.width;
const chartHeight = chartsConfig.height;
const version = chartsConfig.version;
// 模版类型
const tempType = chartsConfig.tempType;
// 整合内置options
const defaultOpt = Default_options[tempType];
if (defaultOpt) {
chartConfig = this._.merge({}, defaultOpt, chartConfig);
}
// 动态设置柱状图是否展示label
const isShowLabel = chartsConfig.showLabel;
if (tempType.includes('bar')) {
chartConfig.series = chartConfig.series.map(serie => {
const newSerics = this._.merge({}, serie, {
label: {
show: !!isShowLabel,
},
});
return newSerics;
});
}
// 动态设置地图的max
if (tempType === 'map') {
const valueData = chartConfig.series[0].data.map(a => {
return a.value;
});
const maxData = valueData.reduce((a, b) => {
return Math.max(a, b);
});
const minData = valueData.reduce((a, b) => {
return Math.min(a, b);
});
chartConfig.visualMap.max = maxData || 100000;
chartConfig.visualMap.min = minData || 0;
}
// // 渲染到请求总用于的body上
await this.ctx.render('echarts.ejs', {
chartConfigStr: JSON.stringify(chartConfig),
chartWidth,
chartHeight,
version,
});
// 获取渲染后的HTML内容
const htmlContent = this.ctx.body as string;
return await this.renderEcharts(version, htmlContent);
}
private async renderEcharts(
version: number,
htmlContent: string
): Promise<string> {
try {
// 选择echarts版本
// 提前加载资源防止后续setContent不生效
const newPage = await this.puppeteerService.newPage();
await newPage.addScriptTag({
url: `http://localhost:7001/charts/echarts${version}.min.js`,
});
await newPage.addScriptTag({
url: 'http://localhost:7001/charts/china.js',
});
// 页面直接渲染body上的内容,并等待无请求后
await newPage.setContent(htmlContent);
// 截图,返回base64编码
return await newPage
.screenshot({ encoding: 'base64', fullPage: true })
.then(data => {
newPage.close();
return `data:image/png;base64,${data}`;
});
} catch (error) {
console.error('Rendering failed, retrying...', error);
// 出现后重启浏览器
await this.puppeteerService.destroy();
await this.puppeteerService.init();
return this.renderEcharts(version, htmlContent);
}
}
}
三、效果展示
调试页面
四、优化点
渲染后无需生成静态的html页面再打开浏览器截图,只需将生成html直接puppteer.setContent即可(需要注意的是,setContent后不会加载js静态资源,需要在前面提前puppteer.addScriptTag进去对应静态资源)。这样做的好处在于能够减少文件读取(fs)操作。
在midway中利用@Singleton()构建单例模式,这样就不用每个请求都打开浏览器,只需服务启动后实例化一个浏览器,在浏览器中开关页面。这样做的好处可大幅提高接口响应速度。
最后可利用pm2自带的cluster模式启动项目,可提高接口QPS。
四、待改进点
1、增加缓存
2、docker镜像方便部署