四种方式实现在导出word中添加「图表」

2,659 阅读6分钟

前言

为什么要说说如何在 word 中插入「图表」呢?因为前端熟知的第三方库Docx不支持插入「图表」!!!

Docx这个库提供了优雅的声明式 API,让我们可以使用 JS/TS 轻松生成 「.docx」 文件。此外,它还同时支持 Node.js 和浏览器。

docx-chart.jpg

为了解决这个问题,我想通过这篇文章聊一聊,希望大家不仅能实现前后端导出 word 功能,还能实现在导出的 word 中插入「图表」!本文主讲如何在导出的 word 中插入「图表」,经过漫长的技术实现,分步从前端到后端,从问题衍生到技术优化。主体分为以下四个部分

  1. React 通过echarts的 APIgetDataURL实现

  2. Node.js 通过puppeteerheadless 浏览器快照功能实现

  3. Node.js 通过canvas实现(推荐)

  4. 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,然后解压出来,你就可以看到如下文件:

1.jpg

  • [Content_Types].xml:该文件用于定义里面每一个 XML 文件的内容类型;
  • _rels:该目录下一般会有一个 「.rels」 后缀的文件,它里面保存了这个目录下各个 Part 之间的关系。_rels 目录不止一个,它实际上是有层级的;
  • docProps:该目录下的 XML 文件用于保存 docx 文件的属性;
  • word:该目录下包含了 Word 文档中的内容、字体、样式或主题等信息。

word文件夹是我们需要关注并修改为我们的模板文件,可以看到文件夹目录如下:

2.jpg

  • charts是图表集合
  • document.xml是整个文档的 xml 显示

模板渲染实现 word 导出

我们以下面的 word 文档为例

6.jpg

  1. 一个请求创建一个结果模板文件夹

3.jpg

  1. 源模板文件夹中需要插入数据的地方修改为相应的模板语法,在 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>
  1. 最后把结果模板文件夹的集合压缩为 zip 包,然后重命名为.docx 这就是一个根据数据生成的 word 文档啦!可以直接返回给前端了。最终渲染的 word 文档如下

5.jpg

优缺点

  • 优点:
    • 这里的图表就可以交互和修改了!
    • 性能也很好,没有依赖其他第三方库。
    • 可以根据相应的 xml 修改 word 样式(当然 Docx 也可以)
  • 缺点:
    • 每个请求都要生成一个解压后的模板文件夹,如果请求过多,文件夹占用的内存较大。

完整项目参考代码戳这里

参考