【实战分享】js生成word(docx),以及将word转成pdf解决方案分享

news2024/11/17 23:49:25

本文将记录如何用js生成word文件,并在node服务器端将word转换成pdf。记录的代码均是在真实业务场景中使用成功的代码,没有记录中间踩坑的过程。想直接抄答案的家人们可以跳转到1.2 程序编写部分,最终效果图可在1.2 程序编写部分中4. 效果展示模块查看。

如果有更好的解决方案,也欢迎大家在评论区讨论、分享~

本文demo存放地址:github.com/ChicKo1108/…

一、DocxTemplater:使用js生成word

老铁们,话不多说,先上链接:Docxtemplater | Word, Powerpoint, Excel generation using templates in your application | docxtemplater

DocxTemplater是一个基于模板生成最终文件的插件,它通过一个简单的{tag}语法将预设的数据填入模板Word或者Powerpoint中,帮助开发者快速生成最终文件。

由于DocxTemplater是基于{tag}变量替换,得到最终的文件,因此文件的样式是非常可控的。在开发过程中,设计师只需要出一版最终的成品word,开发者将内容替换成对应的{tag}即可(再也不用被设计师追着还原设计稿了!)。

DocxTemplater是一个收费的库,但是它拥有免费的开源版本,对于我所涉及的业务,使用免费版本完全可以解决。

开源版包括的功能包括:

  • {tag}替换
  • 条件判断
  • 循环
  • 图片渲染

除免费版外,它还拥有pro plan(950€ per\year)Entreprise plan(24500€ per\year),具体功能大家可以前往官网查看。(但是白嫖的永远是最香的!)

由于我的业务只涉及生成word和pdf,所以本文只介绍word的相关内容,如果需要处理ppt,大家可前往官网自行学习。

1.1 模板语法

在了解模板语法之前,我们需要先创建一个tempalte.docx模板文件。

变量替换

变量使用{key}标签,在tempalte.docx文件中输入以下模板:

Hello, my name is {name}. 

然后准备以下数据:

let data = {
    name: "千万",
} 

最终我们生成的docx文件将会是:

Hello, my name is 千万. 

条件判断

条件判断使用{#key}开始,使用{/key}结束,最简单的用法是使用Boolean类型数据进行填充。

Hello, my name is {name}.
{#hasAge}I'm {age} years old.{/hasAge}
{#hasWeight}My weight is {weight}.{/hasWeight} 

然后准备以下数据:

let data = {
    name: "千万",
    hasAge: true,
    age: 23,
    hasWeight: false,
} 

最终我们生成的docx文件将会是:

Hello, my name is 千万.
I'm 23 years old. 

除了Boolean类型的数据以外,我们也可以填充其他类型:

type是否显示模块
false / 空数组不显示
非空数组显示,且将循环渲染内部元素
对象显示,且使用对象内部变量替换{tag}
其他真值显示

如果变量填充了数组,其实就是我们下面要介绍的循环语法,在下面一小节中再进行介绍。

在这里简单说一下填充对象的情况:

准备word模板如下:

总价格:{price}
{#product} ${productName}: ${price} {/product} 

准备数据如下:

let data = {
    price: 159,
    product: {
        productName: 'pencel',
        price: 1.2
    }
} 

最终我们生成的docx文件将会是:

总价格:159
pencel:$1.2 

循环

循环的标志与条件判断相同,但对应的变量应使用Array来填充。

{#examScoreList}
    {exam}: {score}
{/examScoreList} 

然后我们填充以下数据:

let data = {
    examScoreList: [
        { exam: 数学, score: 60 },
        { exam: 语文, score: 50 },
        { exam: 英语, score: 40 },
    ],
} 

最终我们生成的docx文件将会是:

 数学: 60
    语文: 50
    英语: 40 

表格循环

值得注意的是,循环不仅仅可以循环一段普通文字,我们也可以对表格进行循环,包括:循环行和循环整个表。如果想要循环渲染多个表格,只需要在表格外面使用循环语法即可,不在此处过多赘述。下面展示循环渲染一个表中的行的写法:

image.png

上图中可以看到,我在表格的第二行中使用了循环语法进行填写,这样我们最终生成的文档中,表头和尾就不会被循环,第二行将会被多次渲染,结果如下:

image.png

图片

图片使用{%image}进行标注即可,对于图片的数据传入需要特殊处理,后面的部分会进行介绍。

总结

根据以上语法,我们就可以准备对应的word模板文件了,大部分场景下应该都可以满足。在准备模板的时候,固定的文案和样式直接保留在文档中即可,包括页眉、页脚,各个段落的行距、间距,文字的字体、大小等。其他需要根据真实数据渲染的值,就用标签标注上。准备好模板文件以后,就可以开始脚本函数的编写了。

PS: 要善用表格进行排版布局!

1.2 程序编写

安装所需库

npm install docxtemplater
npm install docxtemplater-image-module-free // 图片模块,没有图片需求可以不装
npm install pizzip // 处理模板文件用到,且只能使用该库 

客户端生成

1. 获取模板文件的binaryString

function getFileBinaryString(templateFile) { // templateFile是File对象
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = (e) => {
      resolve(e.target.result);
    }
    reader.onerror = reject;
    reader.readAsBinaryString(templateFile);
  });
} 

这里使用到了FileReader类,用于将模板文件转换为binaryString,需要注意浏览器的兼容性。

如果对兼容性有要求,可以是使用pizzip/utils中提供的方法getBinaryContent,但是此库对ts兼容性比较差,因此我在实际代码中使用了FileReader

import PizZipUtils from "pizzip/utils/index.js";

function loadFile(url, callback) { PizZipUtils.getBinaryContent(url, callback); } 

2. 生成最终文件(无需图片)

// generate-doxc.js

import PizZip from 'pizzip';
import DocxTemplater from 'docxtemplater';

function getFileBinaryString(templateFile) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = (e) => {
      resolve(e.target.result);
    }
    reader.onerror = reject;
    reader.readAsBinaryString(templateFile);
  });
}

export async function generateDocxFile(template, fileData) {
  return new Promise((resolve, reject) => {
    getFileBinaryString(template)
      .then(templateData => {
        const zip = new PizZip(templateData);
        const doc = new DocxTemplater()
          .loadZip(zip)
          .render(fileData); // fileData是我们需要定义好,传给docxtempale的数据。
        const out = doc.getZip().generate({
          type: 'blob',
          mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        });
        resolve(out);
      })
      .catch(reject);
  });
} 

3. 准备数据,生成最终文件

接下来我们准备一个<input type="file" />的文件输入框(你也可以使用网络请求,或者任何方式拿到文件,只要最终获得二进制数据就可以),用来获取模板文件。同时准备好相应的数据,来对模板进行填充。

// App.jsx

import { saveAs } from 'file-saver';
import { generateDocxFile } from './utils/generate-docx';

const fileData = {
  intro: '国际劳动节,又称五一国际劳动节、劳动节、国际示威游行日,是纪念工人和劳工运动的斗争和成果的日子。国际劳动节是一项由国际劳工运动所推动的节日,全世界劳工和工人阶级在一般会在五朔节(5月1日)举行的庆祝节日,而美国和加拿大在9月第一个星期一举行。是世界上80多个国家的劳动节。',
  activities: [
    {
      name: '阿尔及利亚',
      activity: '在阿尔及利亚,5月1日是公共假日,以庆祝劳动节。'
    },
    {
      name: '安哥拉',
      activity: '5月1日在安哥拉被承认为公共假日,称为劳动节。'
    },
    {
      name: '埃及',
      activity: '在埃及,5月1日被称为劳动节,是一个带薪的公共假期。在传统上,埃及总统会主持正式的五一节庆祝活动。'
    },
    {
      name: '加纳',
      activity: '5月1日是加纳的一个节日,属于庆祝全国所有工人。工会和劳工协会以游行的形式来庆祝劳动节。加纳也会举行阅兵式,通常由工会大会秘书长和各地区的区域秘书致辞。来自不同工作地点的工人通过条幅和衣着表明他们的公司。'
    }
  ]
}

function App() {
  const handleFileChange = async (e) => {
    const file = e.target.files[0];
    const out = await generateDocxFile(file, fileData);
    saveAs(out, `${new Date().getTime()}.docx`);
  }

  return (
    <div className="App">
      <input type="file" onChange={handleFileChange} />
    </div>
  )
}

export default App; 

4. 效果展示

模板文件如下:

模板文件.png

生成结果如下:

结果1.png

5. 图片处理

如果需要在模板中使用图片,我们需要安装docxtemplater-image-module-free模块。

引入了此模块后,需要在加载模板文件后,载入image模块,然后异步填入数据。

// generate-docx.js

// 将图片处理为base64,给模板使用
function convertImgToBase64(url, outputFormat) { 
  return new Promise((resolve, reject) => {
    let canvas = document.createElement(
      'CANVAS',
    );
    const ctx = canvas.getContext('2d'),
      img = new Image();
    img.crossOrigin = 'Anonymous';
    img.onload = function () {
      canvas.height = img.height;
      canvas.width = img.width;
      ctx.drawImage(img, 0, 0);
      var dataURL = canvas.toDataURL(outputFormat || 'image/png');
      canvas = null;
      resolve(dataURL);
    };
    img.onerror = function (e) {
      reject(e);
    };
    img.src = url;
  });
}

const imageOpts = { // 图片的配置
  centered: false,
  getImage: function (tagValue, tagName) { // 将图片转成base64
    return new Promise((resolve) => {
      if (typeof tagValue === 'string' && base64Regex.test(tagValue)) {
        return resolve(tagValue);
      } else {
        convertImgToBase64(tagValue).then((base64) => {
          return resolve(base64Parser(base64));
        });
      }
    });
  },
  // 设置图片宽高,可以根据tagName为每一张图片设置不同宽高
  getSize: function (img, tagValue, tagName) {
    // img是图片Buffer,tagValue是图片初始值,tagName是图片在模板中定义的标签key值
    return [150, 150]; // [宽, 高]
  }
};

export async function generateDocxFile(template, fileData) {
  return new Promise((resolve, reject) => {
    getFileBinaryString(template)
      .then(templateData => {
        const zip = new PizZip(templateData);
        const doc = new DocxTemplater()
          .loadZip(zip)
          .attachModule(new ImageModule(imageOpts)) // 载入模块
          .compile();
        // 异步填充数据
        doc.resolveData(fileData)
          .then(() => {
            doc.render();
            const out = doc.getZip().generate({
              type: 'blob',
              mimeType:
                'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            });
            docxLists.push({ file: out, fileName });
            resolve();
          });
      })
      .catch(reject);
  });
} 

对于有图片的文档生成,需要异步载入数据,且图片数据需要处理为base64,上述代码给出了处理图片的一种解决方案,如果大家有更高效的方法也可以自行使用。

node服务器端生成

该库同样支持在node中使用,其思想与在浏览器端基本一致,在node端可以直接使用buffer,下面贴出官方给出的代码示例。

const PizZip = require("pizzip"); 
const Docxtemplater = require("docxtemplater"); 
const fs = require("fs");
const path = require("path"); // Load the docx file as binary content

const content = fs.readFileSync( path.resolve(__dirname, "input.docx"), "binary" );
const zip = new PizZip(content);
const doc = new Docxtemplater(zip,
{
    paragraphLoop: true,
    linebreaks: true,
}); // Render the document (Replace {first_name} by John, {last_name} by Doe, ...)

doc.render({
    first_name: "John",
    last_name: "Doe",
    phone: "0652455478",
    description: "New Website",
});

const buf = doc.getZip().generate({
    type: "nodebuffer",
    // compression: DEFLATE adds a compression step.
    // For a 50MB output document, expect 500ms additional CPU time 
    compression: "DEFLATE",
});

// buf is a nodejs Buffer, you can either write it to a file or res.send it with express for example.
fs.writeFileSync(path.resolve(__dirname, "output.docx"), buf); 

1.3 总结

docxTemplater是一个通过模板文件生成word的库,它能最大程度的保证最终生成的word的样式的完整和还原。代码搭建好后,对于类似的业务,开发者们只需要编写更多的模板文件,并且把精力集中在对数据的处理上即可。

配合e-charts或其他图表库,也可以让我们实现报表文件的生成。

此外,对于pizzip这个库,它本身是对jszip库的一个升级,拥有对zip文件的操作能力,可以直接解压或者生成zip包,我们可以直接通过此库对批量生成的文件进行打包处理,打包主要用的api如下:

const zip = new pizZip();
zip.file(fileName, fileBuffer); // 生成的文件名 以及 文件的 arrayBuffer
zip.generate({ type: 'blob' }), `documents.zip`); // 生成zip文件 

二、使用libre office将word转换成pdf

在进行此部分业务时,原本想在前端把所有的工作都做好,但是没有找到在客户端就直接转换的方法。因此,此部分在服务器端进行解决。

首先需要在机器上安装libre office软件,具体方法可以自行搜索。

安装好后,项目中安装libreoffice-convert库,这个库对libre office的转换方法进行了封装,直接调用其中方法就好:

const path = require('path');
const fs = require('fs');
const libre = require('libreoffice-convert');

async function docx2pdf(docxBuf, outputPath) {
  libre.convert(docxBuf, '.pdf', undefined, (err, outputBuf) => {
      if (err) {
        console.log(`Error converting file: ${err}`);
      }
      fs.writeFileSync(outputPath, outputBuf);
   });
}

const inputBuf = fs.readFileSync(path.join(__dirname, 'sample.docx'));
let outputPath = path.join(__dirname, 'sample.pdf');

docx2pdf(inputBuf, outputPath); 

如果是zip文件,同样可以安装jsZip或者pizZip进行解压、打包等处理。这里更推荐使用jsZip,因为文档更加丰富,且对ts支持更好。

三、结束语

这是我第二次遇到此类业务,所以本着学习、记录、分享的心态,将内容分享到平台上。在开发过程中遇到了很多“坑”,并没有在本文中记录。本文主要还是以记录最终成功的代码为主,把内容分享给其他有同样需求的家人们。毕竟轮子已经这么完善了,当然要好好利用啦!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/409684.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【解决前端报错】Bad Request: Required request parameter ‘id‘ for method parameter type Long is not present

后端查询列表接口返回的对象里包含Long id,前端获取到这个id,执行通过Long id删除操作。这时删除操作报错400&#xff0c;大意是没找着Long类型的id. swagger相关接口截图&#xff1a; Long类型的在swagger显示是integer64 &#xff0c; integer是integer32. 这是前端请求后…

微信公众号 - 实现 H5 网页在微信内置浏览器中下载文件,可预览和下载 office 文件(doc / xls / ppt / pdf 等)适用于任何前端技术栈网站,兼容安卓和苹果系统!

前言 网上的教程都是让你写页面 “引导” 右上角三个点里,让用户自己去浏览器打开,其实这样用户体验并不好。 本文实现了 最新微信公众号 H5 网页(微信内置浏览器中),预览下载 office 文件,安卓和苹果全都支持! 您可以直接复制代码,移植到自己项目中去,任何前端项目(…

全网超详细的【Axure】Axure RP 9的下载、安装、中文字体、授权

文章目录1. 文章引言2. 下载Axure93. 安装Axure94. Axure9中文5. Axure9授权1. 文章引言 最近在学习原型图&#xff0c;针对画原型图的工具&#xff0c;反复对比墨刀、Axure、xiaopiu后&#xff0c;最终选择了Axure。 接下来&#xff0c;我便从Axure RP 9的下载、安装、中文字…

VUE实现微信扫码登录

获取access_token时序图&#xff1a; public中index.html引入 <script src"https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"></script> 微信登录操作 new WxLogin({// 以下操作把请求到的二维码嵌入到id为"weixin"的标签中i…

走进Vue【三】vue-router详解

目录&#x1f31f;前言&#x1f31f;路由&#x1f31f;什么是前端路由&#xff1f;&#x1f31f;前端路由优点缺点&#x1f31f;vue-router&#x1f31f;安装&#x1f31f;路由初体验1.路由组件router-linkrouter-view2.步骤1. 定义路由组件2. 定义路由3. 创建 router 实例4. 挂…

VUE3 子传父 父传子 双向传递

组件参数传递 父传子 father.vue <template > <!-- 父传子实现 2.向vue页面中的子组件传递该属性 :传给子组件的名字&#xff08;自定义&#xff09;“对应定义在父组件的属性名” --><Header :openpagevaria"openpagevaria" ></Header&g…

使用vue-cli-plugin-electron-builder创建electron+vue项目

文章目录一、nvm环境二、安装vue-cli、yarn三、使用vue项目管理器创建项目四、使用vue项目管理器安装插件五、进入my-electron-vue目录&#xff0c;启动electron六、安装VueDevtools&#xff0c;解决Vue Devtools failed to install: Error: net::ERR_CONNECTION_TIMED_OUT——…

npm install xxxx --legacy-peer-deps命令是什么?

本文分享自华为云社区《npm install xxxx --legacy-peer-deps命令是什么&#xff1f;为什么可以解决下载时候产生的依赖冲突呢&#xff1f;》&#xff0c;作者&#xff1a; gentle_zhou 。 在日常使用命令npm install / npm install XX下载依赖的操作中&#xff0c;我经常会遇…

npm 报错“A complete log of this run can be found in:”解决方法

npm 启动项目 npm run serve/dev的时候报了个错&#xff1a;再次记录一下 ! code ELIFECYCLE npm ERR! errno 1 npm ERR! new0.1.0 serve: vue-cli-service serve npm ERR! Exit status 1 npm ERR! npm ERR! Failed at the new0.1.0 serve script. npm ERR! This is probably n…

【CSS 文字渐变、背景渐变实现方式】

CSS 文字渐变&#xff0c;背景渐变的几种实现方式 在我们日常页面开发当中&#xff0c;使用合适的css渐变效果能让我们的界面更加美观&#xff0c;赏心悦目。 一、CSS 文字渐变&#xff1a; 首先&#xff0c;文字渐变实际上是通过背景渐变裁剪成文字的前景色&#xff0c;然后…

【微信小程序】初入微信小程序

大家好我依旧是山鱼&#x1f41f;&#xff0c;对于初入小程序的同学来说本篇依旧是你的不二选择&#xff0c;它详细的介绍了小程序及小程序&开发者工具的使用&#xff0c;大家快快学起来吧&#xff01; 目录 一&#xff0c;小程序简介 1.1 什么是微信小程序 1.2小程序与…

前端实现一个名言生成器

The sand accumulates to form a pagoda✨ 写在前面✨ JS是什么&#xff1f;✨ 名言生成器✨ 页面搭建✨ 功能实现✨ 写在前面 在上周我们通过HTML、CSS实现了一个简单的‘我的相册‘页面的搭建&#xff0c;很多伙伴呢跟我说难道前端就只能做一些页面搭建的工作吗&#xff1f;…

Vue3 京东到家项目实战第一篇(首页及登录功能开发) 进阶式掌握vue3完整知识体系

目录 项目首页开发 项目准备✌️ 样式开发&#x1f44d; 防抖&#x1f44a; 底部横条✌️ 登陆注册功能开发 样式编写 &#x1f450; 路由守卫实现基础登录校验功能☝️ 使用 axios 发送登录 Mock 请求&#x1f448; 请求函数的封装&#x1f64c; 通过代码拆分增加逻…

html基本标签

目录 1&#xff0c;标题标签h1-h6 2&#xff0c;段落标签p 3&#xff0c;换行标签br 4&#xff0c;水平线标签hr 5&#xff0c;图片标签img及路径详解(绝对/相对路径) 6&#xff0c;超文本链接标签a 7&#xff0c;超本文链接之锚点 8&#xff0c;div标签 9&#xff0c…

JS-获取网页滑动距离,并实时监听

介绍 本文主要介绍通过JS获取网页滑动距离&#xff0c;并实时监听的方法。分析document.body.scrollTop、document.documentElement.scrollTop、window.pageYOffset 三者之间的区别。 一、获取网页滑动距离 JS方法&#xff1a; console.log(网页被卷去的高&#xff1a;, doc…

05-ES6语法:解构赋值

本文我们介绍ES6中解构赋值。ES6的解构赋值语法是一种JS表达式。解构赋值语法是一种JS表达式&#xff0c;通过解构赋值&#xff0c;可以将属性/值从对象/数组中取出&#xff0c;赋值给其他变量。 下面我们具体来看看解构赋值是什么&#xff1f;以及是如何使用的&#xff1f; …

Bootstrap——制作个人简历网页、工具类【边框(添加、删除、颜色、圆角)、清除浮动、颜色(文本、链接、背景)、display属性、浮动、定位、文本对齐】

制作个人简历网页 代码&#xff1a; <!DOCTYPE html> <html><head><meta charset"UTF-8"><title></title><meta name"viewport"content"widthdevice-width,initial-scale1,minimum-scale1,maximum-scale1,u…

移动端H5网页开发必备知识

简介 工欲善其事必先利其器&#xff0c;今天这篇文章主要讲解移动端H5开发必备的一些基础知识以及移动端适配和布局方案。如果已经看过这篇文章了或者已经掌握了移动端基础的话可以看笔者写的 移动端H5网页开发常见问题汇总 移动端开发必备知识-Hybrid App 像素 像素即一个…

在Vue中获取DOM元素的实际宽高

最近使用 D3.js 开发可视化图表&#xff0c;因为移动端做了 rem 适配&#xff0c;所以需要动态计算获取图表容器的宽高&#xff0c;其中涉及到一些原生DOM API的使用&#xff0c;避免遗忘这里总结一下。 一、获取元素 在 Vue 中可以使用 ref 来获取一个真实的 DOM 元素。 为了…

【Vue】Vue 项目前端响应式布局及框架搭建

文章目录Vue 项目前端响应式布局及框架搭建一、项目基本结构二、项目分辨率响应式创建1、flexible.js2、引用 flexible 插件3、修改 flexible 默认配置4、cssrem 插件 (px -> rem)三、项目页面框架搭建1、设置背景图2、设置标题文字3、页面主体部分&#xff08;1&#xff09…