node生成word文档

需求
最近有项目需要用到生成word文档,平时经常用的都是通过模板生成,里面变量使用占位符替换,好处是快捷、方便、简单、不需要通过代码调word样式,确定是很多库不支持图片绘制(很多都是付费功能),找一圈,发现一个很有意思的库,正好也满足我们的需求,特此分享一下
依赖

// https://docx.js.org/#/ npm i docx // https://www.npmjs.com/package/download npm i download

【node生成word文档】说明,因为docx绘图只支持文件流,所以要把网络文件下载到本地转成buffer
代码
话不多说,上代码
import * as fs from "fs" import { Document, Packer, Paragraph, TextRun, ImageRun, HeadingLevel, AlignmentType, convertInchesToTwip, Table, TableRow, TableCell, WidthType, VerticalAlign, BorderStyle } from "docx" const download = require('download')// 性别 enum Gender { Male = 'male', Female = 'female' }// 选手 type PlayerSchema = { name: string gender: string idCard?: string birthday?: string weight?: string remark?: string avatar?: string localAvatar?: string level: string } type GroupSchema = { // gender: Gender institution: string leader: string phone: string coach: string doctor: string players: PlayerSchema[] }// 所有数据 interface DataSchema{ [key: string]: GroupSchema }// 表格无边框 const noBoder = { top: { style: BorderStyle.NIL, size: 0, color: 'FFFFFF' }, bottom: { style: BorderStyle.NIL, size: 0, color: 'FFFFFF' }, left: { style: BorderStyle.NIL, size: 0, color: 'FFFFFF' }, right: { style: BorderStyle.NIL, size: 0, color: 'FFFFFF' } }// 删除下载的照片及文件夹 function delStaticFile(groupNames: string[]) { for (let groupName of groupNames) { if (fs.existsSync(groupName)) { const files = fs.readdirSync(groupName) files.map((file: string) => { let curPath = groupName + "/" + file // 删除选手招聘 fs.unlinkSync(curPath) }) fs.rmdirSync(groupName) } } }// 生成word async function generate (data: DataSchema) { const groupNames = Object.keys(data)// 比较粗糙的控制单元格长度逻辑 const longHeaders = ['身份证号', '备注']// 下载远程资源到本地 for (let groupName of groupNames) { if (!fs.existsSync(groupName)) { fs.mkdirSync(groupName) }const players = data[groupName].players for (let player of players) { if (player.avatar) { const avatarArr = player.avatar.split('/') const fileName = `${groupName}/${avatarArr[avatarArr.length - 1]}`if (!fs.existsSync(fileName)) { await download(player.avatar, groupName) } // 下载后的本地的资源路径 player.localAvatar = fileName } } }// 需要多个文件合一 const sections = groupNames.map(groupName => { const info = data[groupName] const { institution, leader, phone, coach, doctor, players } = info // 标头内容 // let headers = ['序号', '照片', '姓名', '性别', '出生年月', '体重', '级别', '备注'] let headers = ['序号', '照片', '姓名', '性别', '身份证号', '级别', '备注']// 表格数据 let tableData: any[][] = [] tableData.push(headers)// 填充选手信息 let index = 1 for (let player of players) { tableData.push([ index.toString(), player.localAvatar || '', player.name, player.gender === Gender.Male ? '男' : '女', player.idCard, // player.birthday, // player.weight, player.level, player.remark, ]) index++ }// 表格渲染 const tableRows = tableData.map(colums => { return new TableRow({ children: colums.map(cell => { return new TableCell({ verticalAlign: VerticalAlign.CENTER, width: { // 设置宽度 dxa长度单位 https://stackoverflow.com/questions/14360183/default-wordml-unit-measurement-pixel-or-point-or-inches size: longHeaders.some(j => cell === j) ? 3000 : 800, type: WidthType.DXA, }, children: cell && colums.findIndex(i => i === cell) === 1 && cell !== '照片' ? [new Paragraph({ alignment: AlignmentType.CENTER, children: [ new ImageRun({ // 将图片转化为buffer data: fs.readFileSync(cell), transformation: { width: 100, height: 129, }, }) ] })]: [new Paragraph({ alignment: AlignmentType.CENTER, children:[ new TextRun(cell || '') ] })] }) }) }) })// 渲染报名表格 const table = new Table({ alignment: AlignmentType.CENTER, rows: tableRows })return { properties: {}, children: [ // new Paragraph({ //style: "wellSpaced", //children: [ //new TextRun({ //text: '附件 4', //color: '999999', //}) //], // }),// 表头信息 new Paragraph({ spacing: { before: 400, after: 400 }, style: "Title", text: `自 由 搏 击 比 赛 报 名 表(${groupName === Gender.Male ? '男子' : '女子'})`, heading: HeadingLevel.TITLE, alignment: AlignmentType.CENTER }),// 队伍信息 new Table({ style: "wellSpaced", alignment: AlignmentType.CENTER, borders: noBoder, rows: [ new TableRow({ children: [ new TableCell({ width: { size: 600, type: WidthType.DXA, }, borders: noBoder, children: [ new Paragraph(`单位: `), ], }), new TableCell({ width: { size: 1800, type: WidthType.DXA, }, borders: noBoder, children: [ new Paragraph(`${institution}`) ], }), new TableCell({ width: { size: 700, type: WidthType.DXA, }, borders: noBoder, children: [ new Paragraph(`领队: `), ], }), new TableCell({ width: { size: 1200, type: WidthType.DXA, }, borders: noBoder, children: [ new Paragraph(`${leader}`) ], }), new TableCell({ width: { size: 1100, type: WidthType.DXA, }, borders: noBoder, children: [ new Paragraph(`联系电话: `), ], }), new TableCell({ width: { size: 1400, type: WidthType.DXA, }, borders: noBoder, children: [ new Paragraph(`${phone}`) ], }), new TableCell({ width: { size: 700, type: WidthType.DXA, }, borders: noBoder, children: [ new Paragraph(`教练: `), ], }), new TableCell({ width: { size: 1300, type: WidthType.DXA, }, borders: noBoder, children: [ new Paragraph(`${coach}`) ], }), new TableCell({ width: { size: 700, type: WidthType.DXA, }, borders: noBoder, children: [ new Paragraph(`队医: `), ], }), new TableCell({ width: { size: 1300, type: WidthType.DXA, }, borders: noBoder, children: [ new Paragraph(`${doctor}`) ], }), ], }), ] }),// 用于段落距离(table无法设置spacing属性) new Paragraph({ spacing: { // 通过调整before值来调整段落渐进 before: 400, }, text: ``, }),// 选手信息 table,// 印章和时间 new Paragraph({ style: "wellSpaced", children: [ new TextRun({ text: '\t\t\t\t报名单位章:\t\t\t\t\t\t', }), new TextRun({ text: '年\t\t' }), new TextRun({ text: '月\t\t' }), new TextRun({ text: '日' }) ] }) ] } })// 创建整个文档 const doc = new Document({ styles: { paragraphStyles: [ { id: "Title", name: "title", basedOn: "Normal", next: "Normal", quickFormat: true, run: { size: 30, bold: true, color: "000000" } }, { id: "wellSpaced", name: "Well Spaced", basedOn: "Normal", quickFormat: true, paragraph: { indent: { left: convertInchesToTwip(0.5), }, spacing: { before: 400, }, }, }, ], }, sections })// 生成word文档 Packer.toBuffer(doc).then((buffer) => { fs.writeFileSync("enrolls.docx", buffer) })// 删除下载的选手照片 delStaticFile(groupNames) }const group: GroupSchema = { institution: '江苏省南京市舜禹集团总部', leader: '王猛(男)', phone: '18861856665', coach: '刘国梁(男)', doctor: '杨永信(女)', players: [ { name: '莱昂纳多迪卡普里奥', gender: Gender.Male, idCard: '320888199001019878', birthday: '1999-01-02', weight: '60kg', avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/13.png', remark: '', level: '60kg' }, { name: '张三', gender: Gender.Male, idCard: '320888199001019878', birthday: '1999-01-02', weight: '60kg', avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/7.png', remark: '', level: '60kg' }, { name: '张三', gender: Gender.Male, idCard: '320888199001019878', birthday: '1999-01-02', weight: '60kg', avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/14.png', remark: '', level: '60kg' }, { name: '张三', gender: Gender.Male, idCard: '320888199001019878', birthday: '1999-01-02', weight: '60kg', avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/14.png', remark: '', level: '60kg' }, { name: '张三', gender: Gender.Male, idCard: '320888199001019878', birthday: '1999-01-02', weight: '60kg', avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/14.png', remark: '', level: '60kg' }, { name: '张三', gender: Gender.Male, idCard: '320888199001019878', birthday: '1999-01-02', weight: '60kg', avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/14.png', remark: '', level: '60kg' }, { name: '张三', gender: Gender.Male, idCard: '320888199001019878', birthday: '1999-01-02', weight: '60kg', avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/14.png', remark: '', level: '60kg' }, { name: '张三', gender: Gender.Male, idCard: '320888199001019878', birthday: '1999-01-02', weight: '60kg', avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/14.png', remark: '', level: '60kg' }, { name: '张三', gender: Gender.Male, birthday: '1999-01-02', weight: '60kg', avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/14.png', idCard: '320888199001019878', remark: '', level: '60kg' }, { name: '张三', gender: Gender.Male, idCard: '320888199001019878', birthday: '1999-01-02', weight: '60kg', avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/14.png', remark: '', level: '60kg' }, { name: '张三', gender: Gender.Male, idCard: '320888199001019878', birthday: '1999-01-02', weight: '60kg', avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/14.png', remark: '', level: '60kg' }, { name: '张三', gender: Gender.Male, idCard: '320888199001019878', birthday: '1999-01-02', weight: '60kg', avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/14.png', remark: '', level: '60kg' } ] }const data: DataSchema = { [Gender.Male]: group, [Gender.Female]: group, }generate(data)

    推荐阅读