四种方式实现在导出word中添加「图表」
前言
为什么要说说如何在 word 中插入「图表」呢?因为前端熟知的第三方库Docx不支持插入「图表」!!!
Docx这个库提供了优雅的声明式 API,让我们可以使用 JS/TS 轻松生成 「.docx」 文件。此外,它还同时支持 Node.js 和浏览器。
为了解决这个问题,我想通过这篇文章聊一聊,希望大家不仅能实现前后端导出 word 功能,还能实现在导出的 word 中插入「图表」!本文主讲如何在导出的 word 中插入「图表」,经过漫长的技术实现,分步从前端到后端,从问题衍生到技术优化。主体分为以下四个部分
- React 通过echarts的 APIgetDataURL实现
- Node.js 通过puppeteerheadless 浏览器快照功能实现
- Node.js 通过canvas实现(推荐)
- Golang 通过模板渲染text/template实现 word 导出和插入「图表」功能(强烈推荐)
Docx 的常用 api
Docx 这个库为开发者提供了许多类,用于创建 Word 中的对应元素,这里我们简单介绍几个常见的类:
- Document:用于创建新的 Word 文档;
- Sections:用于创建区块,一个区块包括一个或多个段落;
- Paragraph:用于创建新的段落;
- Text Run:用于创建文本,支持设置加粗、斜体和下划线样式;
- Image Run:用于创建图片,支持浮动和内联的定位;
- Tables:用于创建表格,支持设置表格每一行和每个表格单元的内容。
React 通过 echarts 的 getDataURL 实现
Docx 不能插入「图表」,值得庆幸的是图片还是可以插入的。 Image Run 可以通过 Data URL 和 buffer 插入图片,于是我联想到了 echarts 的「getDataURL」能够获取到图表的 Data Url。按照下面的方法就能实现「图表」图片的 Data URL 生成了。
/**
* @description:通过echarts的实例api生成图表Data URL
* @param {option} echarts图表的option配置
* @return {string} Data URL
*/
const getChartDataURL = (option = {}) => {
const container = document.createElement("div")
container.id = "container"
container.style.display = "none"
container.style.width = "500px"
container.style.height = "500px"
document.body.appendChild(container)
const instance = echarts.init(container)
// 绘制图表
// 刚开始绘制不出来图表的原因如下:
// 如果有动画效果的话,生成的图片会是在有动画效果出来以前的样子,就是说数据还没渲染上去,因此导出的图片没有数据。
// https://blog.csdn.net/qq_35239421/article/details/106526147
instance.setOption(option)
const dataUrl = instance.getDataURL({ type: "png" })
document.body.removeChild(container)
return dataUrl
}
完整项目参考代码戳这里,代码可直接运行查看,下同!
Node.js 通过 puppeteer headless 浏览器快照功能实现
echarts 只能在浏览器使用(当然你也可以使用在后端封装好的 echarts 库,我没有这样去实现,可在评论区给出你们实现的链接哈!),该怎么办啊!后端还有 headless 浏览器呢。通过 puppeteer 的快照功能实现如下:
/** * 生成echarts图片 *
* @param {*} option echarts 选项 *
* @returns 图片buffer
* */
async function getEchartsChart(option = {}) {
// 启动浏览器
const browser = await puppeteer.launch({
args: ["--no-sandbox"],
})
// 创建空白页面
const page = await browser.newPage()
try {
// 定义网页模板
const content = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>chart</title>
</head>
<body>
<div id="container" style="width: 500px;height:500px;" />
</body>
</html>`
// 设置网页源码
await page.setContent(content)
// 添加script标签和属性
await page.addScriptTag({
path: path.resolve(
__dirname,
"./node_modules/echarts/dist/echarts.min.js"
),
})
// 网页中执行js代码
await page.evaluate(option => {
const myChart = window.echarts.init(document.getElementById("container"))
myChart.setOption(option)
}, option)
// 网页中获取id为container的元素
let elem = await page.$("#container")
// 截图元素快照
let buffer = await elem.screenshot({
type: "png",
// path: path.resolve(__dirname, './123.png') // 快照的生成路径
})
return buffer
} catch (err) {
console.log(`render echarts chart error: ${err}`)
return null
} finally {
// 关闭网页
await page.close()
// 关闭浏览器
await browser.close()
}
}
Node.js 通过 canvas 实现
每次都要通过 headless 浏览器去获取图表的快照,一个请求对应一个 headless 浏览器,这样既不高效,又非常耗费性能。通过独立的 canvas 库设置为 echarts 的容器就能很好的优化了,实现方式如下:
const canvas = require("canvas")
const echarts = require("echarts")
const fs = require("fs")
/** * 生成echarts图片 *
* @param {*} option echarts 选项 *
* @returns 图片buffer
* */
async function getBufferByCanvas(option = {}) {
//创建一个canvas实例
let ctx = canvas.createCanvas(500, 500)
//将canvas实例设置为echarts容器
echarts.setCanvasCreator(() => ctx)
//使用canvas实例为容器创建echarts实例
let chart = echarts.init(ctx)
//设置图标实例配置项
chart.setOption(option)
return chart.getDom().toBuffer()
}
Golang 通过模板渲染 实现 word 导出和插入「图表」功能
- 为什么要用 Go 呢?因为我们项目后端就用的 Go,Node.js 同样也能这样实现导出和插入图表。还有一个原因是:Go 是真的快啊!
- 每次插入「图表」感觉都是插入的图片,这样导出的 word 意义在哪儿?图表只能看,不能修改和交互(→_→)。好了,我看到了你鄙视的小眼神,接下来的模板渲染方法帮你解决这些所有的疑问。
「.docx」的特性
.docx 的 word 文档其实就是一个 zip 压缩包(xml 文件的集合),我们通过把example.docx
重命名为example.zip
,然后使用 ZIP 压缩/解压软件(mac 可以使用 unzip)解压出来,你就可以看到如下文件:
[Content_Types].xml
:该文件用于定义里面每一个 XML 文件的内容类型;_rels
:该目录下一般会有一个 「.rels」 后缀的文件,它里面保存了这个目录下各个 Part 之间的关系。_rels
目录不止一个,它实际上是有层级的;docProps
:该目录下的 XML 文件用于保存 docx 文件的属性;word
:该目录下包含了 Word 文档中的内容、字体、样式或主题等信息。
word
文件夹是我们需要关注并修改为我们的模板文件,可以看到文件夹目录如下:
charts
是图表集合document.xml
是整个文档的 xml 显示
模板渲染实现 word 导出
- 一个请求创建一个
结果模板文件夹
- 把
源模板文件夹
中需要插入数据的地方修改为相应的模板语法
,在 Go 中就是这样的模板语法{{.Title}}
,通过把源模板文件夹
结合数据渲染出最终的文件替换结果模板文件夹
中的同名文件。比如这里的/word/charts/chart1.xml
文件(其他 xml 文件同理)
<c:cat>
<c:strRef>
<c:f>Sheet1!$A$2:$A$5</c:f>
<c:strCache>
<c:ptCount val="{{.ChartData | len | print}}" />
{{/* 遍历数据ChartData: []Chart{{"小明", 100}, {"小花", 88}, {"小红", 66}} */}}
{{range $index, $element := .ChartData}}
<c:pt idx="{{$index | print}}">
{{ /* name */}}
<c:v>{{$element.Key}}</c:v>
</c:pt>
{{end}}
</c:strCache>
</c:strRef>
</c:cat>
<c:val>
<c:numRef>
<c:f>Sheet1!$B$2:$B$5</c:f>
<c:numCache>
<c:formatCode>General</c:formatCode>
<c:ptCount val="{{.ChartData | len | print}}"/>
{{range $index, $element := .ChartData}}
<c:pt idx="{{$index | print}}">
{{/* value */}}
<c:v>{{$element.Value}}</c:v>
</c:pt>
{{end}}
</c:numCache>
</c:numRef>
</c:val>
优缺点
-
优点:
- 这里的图表就可以交互和修改了!
- 性能也很好,没有依赖其他第三方库。
- 可以根据相应的 xml 修改 word 样式(当然 Docx 也可以)
- 缺点:每个请求都要生成一个解压后的模板文件夹,如果请求过多,文件夹占用的内存较大。