在实际的前端项目中,经常需要将数据或页面内容导出为Word文档。利用docx和file-saver库,可以实现强大的文档导出功能,满足报表生成、数据导出、文档打印等需求。
- docx:纯JavaScript的Word文档生成库,功能全面,无依赖
- file-saver:简化文件保存操作,兼容性好
安装
bash
# 安装最新版本
npm install docx file-saver
# 或使用yarn
yarn add docx file-saver
# 或使用pnpm
pnpm add docx file-saver
版本兼容性
| 版本 | 特点 | 建议 |
|---|---|---|
| docx 8.x | 传统API,文档丰富 | 已有项目维护 |
| docx 9.x | 新API,直接配置(本文基于此) | 新项目首选 |
| file-saver 2.x | 标准API,兼容性好 | 推荐使用 |
基础示例
vue
<template>
<div>
<button @click="exportSimpleDocument" :disabled="loading">
{{ loading ? "生成中..." : "导出Word文档" }}
</button>
</div>
</template>
<script setup>
import { ref } from "vue";
import { Document, Packer, Paragraph, TextRun } from "docx";
import { saveAs } from "file-saver";
const loading = ref(false);
async function exportSimpleDocument() {
loading.value = true;
try {
// 1. 创建文档对象
const doc = new Document({
sections: [
{
children: [
new Paragraph({
children: [new TextRun("欢迎使用文档导出功能")],
}),
new Paragraph({
children: [new TextRun("这是一个简单的示例文档。")],
}),
],
},
],
});
// 2. 生成Blob
const blob = await Packer.toBlob(doc);
// 3. 保存文件
saveAs(blob, "示例文档.docx");
} catch (error) {
console.error("导出失败:", error);
} finally {
loading.value = false;
}
}
</script>
docx库详解
什么是docx?
docx 是一个纯 JavaScript 库,用于 创建和操作 Microsoft Word (.docx) 文件。它可以在 Node.js 和浏览器环境中运行,不需要安装 Office 软件。
主要特性
- 完整的 Word 文档功能支持
- 纯 JavaScript 实现,无外部依赖
- 支持 Node.js 和浏览器
- 丰富的样式和格式选项
- 表格、图像、列表等元素支持
- TypeScript 友好,有完整的类型提示
markdown
## 浏览器环境注意事项
1. 文件大小限制:浏览器中生成大文档可能导致内存问题
2. 中文支持:必须显式设置中文字体
3. 图片处理:浏览器中需要使用不同的图片加载方式
核心概念架构
文档结构模型
text
文档 (Document)
↓
分段 (Section) [可以有多个]
↓
各种元素 (Paragraph, Table, Image...)
基本工作流程
text
1. 创建文档对象 (Document)
↓
2. 添加分段 (Sections)
↓
3. 向分段添加内容元素
(Paragraphs, Tables, Images...)
↓
4. 配置元素的样式和属性
↓
5. 使用 Packer 转换为文件
↓
6. 保存或下载文件
js
// 导入必要的模块
import {
Document, // 文档
Paragraph, // 段落
TextRun, // 文本块
Packer // 打包器(用于生成文件)
} from "docx";
async function exportDocument() {
try {
// 1. 创建文档对象
const doc = new Document({
// 2. 文档内容分段(可以包含多个分段)
sections: [
{
properties: {}, // 分段属性(页面大小、边距等)
children: [
// 3. 分段中的内容元素
new Paragraph({
// 4. 段落
children: [
// 5. 段落中的文本块
new TextRun("Hello, World!"),
],
}),
],
},
],
});
// 6. 将文档对象转换为文件
const buffer = await Packer.toBuffer(doc); // Node.js 环境
const blob = await Packer.toBlob(doc); // 浏览器环境
} catch (error) {
console.error("导出失败:", error); // 错误提示
}
}
Packer工具对比
在 docx 库中,Packer 类是用于将文档对象序列化为不同格式的工具。toBuffer() 和 toBlob() 是最常用的两个方法,它们用于不同的运行环境。
| 特性 | Packer.toBuffer() |
Packer.toBlob() |
|---|---|---|
| 运行环境 | Node.js | 浏览器 |
| 返回值 | Buffer |
Blob |
| 存储方式 | 二进制缓冲区 | 二进制大对象 |
| 文件操作 | 使用 fs 模块 |
使用 FileSaver 等库 |
| 内存使用 | 直接内存操作 | Blob URL 引用 |
| 典型用途 | 服务器端保存文件 | 客户端下载文件 |
基础组件
1. Document(文档)
文档是最顶层的容器,相当于一个Word文件。
js
const doc = new Document({
creator: "作者名称", // 创建者
title: "文档标题", // 文档标题
description: "文档描述", // 描述
sections: [] // 包含的分段
});
2. Section(分段)
文档由多个分段组成,每个分段可以有不同的页面设置。
js
const section = {
properties: {
page: {
size: {
width: 12240, // A4 宽度 (单位:twips)
height: 15840, // A4 高度
},
margin: {
top: 1440, // 上边距 2.54cm
right: 1440, // 右边距
bottom: 1440, // 下边距
left: 1440, // 左边距
}
}
},
children: [] // 这个分段中的内容
};
单位解释 :docx使用twips作为Word文档的标准单位系统。
| 单位 | 说明 | 换算关系 |
|---|---|---|
twips |
Word标准单位 | 1 twip = 1/20 pt |
pt |
磅 | 1 pt = 20 twips |
inch |
英寸 | 1 inch = 1440 twips |
cm |
厘米 | 1 cm ≈ 567 twips |
常用预设值:
- 单倍行距:240 twips
- 1.5倍行距:360 twips
- 2倍行距:480 twips
- A4宽度:12240 twips (21cm)
- A4高度:15840 twips (29.7cm)
3. Paragraph(段落)
段落是文档的基本内容单元,相当于 Word 中的一个段落。
js
const paragraph = new Paragraph({
children: [ /* 文本块或其他内联元素 */ ],
alignment: "left", // 对齐方式:left, center, right, both
spacing: { // 间距
before: 200, // 段前间距
after: 200, // 段后间距
line: 240 // 行间距(240 = 单倍行距)
},
indent: { // 缩进
firstLine: 720 // 首行缩进 720twips = 0.5英寸
}
});
1). 标题 (Headings)
js
import { HeadingLevel } from "docx";
const headings = [
new Paragraph({
text: "一级标题",
heading: HeadingLevel.HEADING_1,
}),
new Paragraph({
text: "二级标题",
heading: HeadingLevel.HEADING_2,
}),
new Paragraph({
text: "三级标题",
heading: HeadingLevel.HEADING_3,
})
];
2). 列表 (Lists)
列表类型概览
| 列表类型 | 描述 | 配置方式 | 示例 |
|---|---|---|---|
| 有序列表 | 带数字/字母编号 | numbering属性 |
1., 2., 3. |
| 无序列表 | 带项目符号 | bullet属性 |
• 项目1 |
| 多级列表 | 嵌套的列表 | 组合使用 | 1.1., 1.2. |
->无序列表:
js
// 无序列表(项目符号)
const bulletList = [
new Paragraph({
text: "项目1",
bullet: { level: 0 }, // level 表示缩进级别
}),
new Paragraph({
text: "项目2",
bullet: { level: 0 },
}),
];
->有序列表:
有序列表(编号列表)需要两步配置:
- 定义编号格式 (在
Document的numbering.config中) - 应用编号引用 (在
Paragraph的numbering属性中)
js
const doc = new Document({
numbering: {
config: [
// 配置1:数字编号 (1., 2., 3.)
{
reference: "decimal-list", // 唯一引用标识
levels: [{
level: 0, // 列表级别
format: "decimal", // 格式类型
text: "%1.", // 显示模板
alignment: "left", // 对齐方式
style: {
paragraph: {
indent: {
left: 720, // 左缩进 (0.5英寸)
hanging: 360 // 悬挂缩进 (0.25英寸)
}
}
}
}]
},
// 配置2:大写字母编号 (A., B., C.)
{
reference: "upper-letter-list",
levels: [{
level: 0,
format: "upperLetter",
text: "%1.",
alignment: "left"
}]
},
// 配置3:罗马数字编号 (I., II., III.)
{
reference: "roman-list",
levels: [{
level: 0,
format: "upperRoman",
text: "%1.",
alignment: "left"
}]
}
]
},
sections: [{
children: [
// 使用不同格式的有序列表
new Paragraph({
text: "数字列表项",
numbering: {
reference: "decimal-list", // 编号引用名称
level: 0 // 缩进级别
}
}),
new Paragraph({
text: "字母列表项",
numbering: { reference: "upper-letter-list", level: 0 }
}),
new Paragraph({
text: "罗马数字列表项",
numbering: { reference: "roman-list", level: 0 }
})
]
}]
});
instance 属性:
-
在有序列表中可以使用
instance创建独立的编号序列:text一个 reference + level 定义一个编号格式 ┌─────────────────────────────────────┐ │ reference: "demo-list", level: 0 │ │ format: "decimal", text: "%1." │ └─────────────────────────────────────┘ │ ├─ instance: 1 (实例1) │ ├─ 段落1 → 显示 "1." │ ├─ 段落2 → 显示 "2." │ └─ 段落3 → 显示 "3." │ ├─ instance: 2 (实例2) │ ├─ 段落1 → 显示 "1." ← 重新开始! │ ├─ 段落2 → 显示 "2." │ └─ 段落3 → 显示 "3." │ └─ instance: 3 (实例3) ├─ 段落1 → 显示 "1." ← 又重新开始! └─ 段落2 → 显示 "2." -
示例:
jsconst doc = new Document({ // 定义编号格式 numbering: { config: [ { reference: "demo-list", levels: [{ level: 0, format: "decimal", text: "%1." }], }, ], }, sections: [ { properties: {}, children: [ // 实例1 new Paragraph({ text: "实例1-第一项", numbering: { reference: "demo-list", level: 0, instance: 1 }, }), new Paragraph({ text: "实例1-第二项", numbering: { reference: "demo-list", level: 0, instance: 1 }, }), // 实例2(重新开始编号) new Paragraph({ text: "实例2-第一项", numbering: { reference: "demo-list", level: 0, instance: 2 }, }), new Paragraph({ text: "实例2-第二项", numbering: { reference: "demo-list", level: 0, instance: 2 }, // 继续实例2 }), ], }, ], }); // 结果:1., 2., 1., 2. -
重要提示:
- 每个
instance值创建一个独立的编号序列; - 相同的
instance值共享编号计数; - 不同的
instance值各自从1开始编号; - 在docx 9.x版本中,不指定
instance时行为不确定,可能不会自动继续使用前一个实例,因此建议明确指定;`
- 每个
4. TextRun(文本块)
文本块是段落中的具体文本内容,可以设置样式。
js
const textRun = new TextRun({
text: "这是文本内容",
bold: true, // 加粗
italics: false, // 斜体
underline: {}, // 下划线(空对象表示单下划线)
color: "FF0000", // 颜色(十六进制,不带#)
font: "宋体", // 字体
size: 24, // 字体大小(24 = 12pt)
highlight: "yellow" // 高亮
});
5. Tables (表格)
js
import { Table, TableRow, TableCell } from "docx";
const table = new Table({
rows: [
// 表头行
new TableRow({
children: [
new TableCell({
children: [new Paragraph("姓名")],
width: { size: 30, type: "pct" }, // 宽度占30%
}),
new TableCell({
children: [new Paragraph("年龄")],
width: { size: 20, type: "pct" },
}),
],
}),
// 数据行
new TableRow({
children: [
new TableCell({
children: [new Paragraph("张三")],
}),
new TableCell({
children: [new Paragraph("25")],
}),
],
}),
],
width: { size: 100, type: "pct" }, // 表格宽度占100%
});
6. Images(图像)
js
// Node.js环境
import fs from "fs";
const image = new ImageRun({
data: fs.readFileSync("path/to/image.png"),
transformation: {
width: 200,
height: 200,
},
});
// 浏览器环境
const image = new ImageRun({
data: await fetch('image.png').then(res => res.arrayBuffer()),
transformation: {
width: 200,
height: 200,
},
});
样式设置
styles 和 paragraphStyles 是 docx 库中管理文档样式的两个核心配置项,它们有不同的用途和层级关系。
| 方式 | 配置位置 | 作用范围 | 优先级 | 适用场景 |
|---|---|---|---|---|
| 全局默认样式 | Document.styles.default |
整个文档 | 低 | 设置文档基础样式 |
| 自定义段落样式 | Document.styles.paragraphStyles |
特定段落 | 中 | 创建可重用样式 |
| 字符样式 | Document.styles.characterStyles |
文本片段 | 中 | 文本级样式复用 |
| 表格样式 | Document.styles.tableStyles |
表格 | 中 | 表格统一样式 |
| 内联样式 | Paragraph, TextRun 参数 |
单个元素 | 高 | 特定元素样式 |
| 样式继承链 | basedOn, next |
样式间 | - | 样式复用和关联 |
样式层级结构:
Document (文档)
├── styles (文档样式)
│ ├── default (默认样式)
│ ├── characterStyles (字符样式)
│ ├── paragraphStyles (段落样式)
│ └── tableStyles (表格样式)
└── sections (内容分段)
└── Paragraph (段落)
├── style (引用paragraphStyles中的样式)
└── children (内容)
常用样式属性
| 属性 | 说明 | 示例值 |
|---|---|---|
font |
字体 | "宋体", "微软雅黑" |
size |
字号 | 24 (12pt), 28 (14pt) |
bold |
加粗 | true / false |
italics |
斜体 | true / false |
color |
颜色 | "FF0000" (红色) |
underline |
下划线 | {} (单线) |
highlight |
高亮 | "yellow", "green" |
1. 全局默认样式 (styles.default)
这是最基础的样式配置方式,为整个文档设置默认样式。
配置结构:
js
const doc = new Document({
styles: {
default: {
// 文档默认字符样式
document: {
run: {
font: "宋体", // 字体
size: 24, // 字号
color: "000000", // 颜色
},
paragraph: {
spacing: { line: 360 }, // 行距
}
},
// 标题样式
heading1: {
run: {
font: "宋体",
size: 32,
bold: true,
},
paragraph: {
spacing: { before: 240, after: 120 },
}
},
heading2: {
run: {
font: "宋体",
size: 28,
bold: true,
}
},
heading3: {
run: {
font: "宋体",
size: 26,
bold: true,
}
},
// 标题样式
title: {
run: {
font: "宋体",
size: 36,
bold: true,
}
},
// 列表样式
listParagraph: {
run: {
font: "宋体",
size: 24,
}
}
}
},
sections: [{
children: [
// 自动应用 heading1 样式
new Paragraph({
text: "文档标题",
heading: "Heading1"
}),
// 自动应用全局默认样式
new Paragraph({
children: [
new TextRun("正文内容")
]
})
]
}]
});
示例:
js
import { Document, Paragraph, TextRun, Packer } from "docx";
const doc = new Document({
styles: {
default: {
// 设置文档全局样式
document: {
run: {
font: "宋体",
size: 24, // 小四
color: "333333",
},
paragraph: {
spacing: {
line: 360, // 1.5倍行距
after: 100 // 段落间距
},
}
},
// 标题样式
heading1: {
run: {
font: "黑体",
size: 32,
color: "000080", // 深蓝色
bold: true,
},
paragraph: {
spacing: { before: 240, after: 120 },
alignment: "left",
}
},
heading2: {
run: {
font: "黑体",
size: 28,
color: "000080",
bold: true,
}
}
}
},
sections: [{
children: [
// 自动应用 heading1 样式
new Paragraph({
text: "文档标题",
heading: "Heading1"
}),
// 自动应用全局默认样式
new Paragraph({
children: [
new TextRun("正文内容")
]
})
]
}]
});
2. 自定义段落样式 (paragraphStyles)
这是最常用和最灵活的方式,用于定义可重用的段落样式。
配置结构:
js
const doc = new Document({
styles: {
default:{},
paragraphStyles: [
{
id: "样式ID", // 样式ID,必填,用于引用
name: "样式名称", // 样式名称,在Word中显示的名称
basedOn: "Normal", // 基于哪个样式(可选)
next: "Normal", // 按Enter后的样式(可选)
quickFormat: true, // 是否在快速样式库显示
// 字符样式
run: {
font: "宋体",
size: 24,
color: "000000",
bold: false,
italics: false,
underline: {},
},
// 段落样式
paragraph: {
spacing: {
line: 360, // 行间距
before: 0, // 段前间距
after: 100, // 段后间距
},
indent: {
firstLine: 720, // 首行缩进
left: 0, // 左缩进
right: 0, // 右缩进
},
alignment: "left", // 对齐:left, center, right, both
border: {
// 边框
top: { style: "single", size: 4, color: "000000" },
},
shading: {
// 底纹
fill: "F5F5F5",
},
pageBreakBefore: false, // 是否段前分页
keepLines: true, // 保持在同一页
keepNext: true, // 与下段同页
},
},
],
},
});
示例:
js
import { Document, Paragraph } from "docx";
const doc = new Document({
styles: {
paragraphStyles: [
{
id: "ReportTitle",
name: "报告标题",
run: {
font: "黑体",
size: 36,
color: "000080",
bold: true,
},
paragraph: {
alignment: "center",
spacing: { before: 400, after: 300 },
},
},
{
id: "Important",
name: "重点强调",
run: {
color: "FF0000",
bold: true,
highlight: "yellow",
},
paragraph: {
shading: { fill: "FFF8DC" },
},
},
],
},
sections: [
{
children: [
new Paragraph({
text: "年度技术报告",
style: "ReportTitle",
}),
new Paragraph({
text: "⚠️ 注意:此文档为机密文件",
style: "Important",
}),
],
},
],
});
3. 字符样式 (characterStyles)
用于定义可重用的文本片段样式,适用于多个段落中的相同文本样式。
js
const doc = new Document({
styles: {
default:{},
paragraphStyles:[],
characterStyles: [
{
id: "RedBold",
name: "红字加粗",
run: {
color: "FF0000",
bold: true,
}
},
{
id: "Highlight",
name: "高亮",
run: {
highlight: "yellow",
bold: true,
}
},
{
id: "LinkStyle",
name: "链接样式",
run: {
color: "0000FF",
underline: {},
}
}
]
},
sections: [{
children: [
new Paragraph({
children: [
new TextRun({
text: "重要通知:",
style: "RedBold" // 应用字符样式
}),
new TextRun("请及时查看最新消息")
]
})
]
}]
});
4. 内联样式(直接设置)
最高优先级的方式,直接在创建元素时设置样式。
js
import { Paragraph, TextRun } from "docx";
// 1. 在 Paragraph 中直接设置
const paragraph1 = new Paragraph({
text: "这个段落有直接样式",
alignment: "center", // 对齐方式
spacing: {
line: 400, // 行间距
before: 200, // 段前
after: 200 // 段后
},
indent: {
firstLine: 720, // 首行缩进
},
border: {
bottom: { // 下边框
style: "single",
size: 4,
color: "000000"
}
},
shading: { // 背景色
fill: "F0F0F0"
}
});
// 2. 在 TextRun 中直接设置
const paragraph2 = new Paragraph({
children: [
new TextRun({
text: "这段文本",
font: "黑体", // 字体
size: 28, // 字号
bold: true, // 加粗
italics: false, // 斜体
underline: {}, // 下划线
color: "FF0000", // 颜色
highlight: "yellow", // 高亮
shading: { // 文字底纹
fill: "FFCCCC"
},
strike: true, // 删除线
doubleStrike: false, // 双删除线
allCaps: false, // 全部大写
smallCaps: false // 小型大写
})
]
});
5. 样式继承和关联
1. 基于现有样式 (basedOn)
优点:保持样式一致性和便于批量修改。
js
const doc = new Document({
paragraphStyles: [
// 基础样式
{
id: "BaseStyle",
name: "基础样式",
run: {
font: "宋体",
size: 24,
color: "333333",
},
paragraph: {
spacing: { line: 360, after: 100 },
}
},
// 继承基础样式,只修改需要的属性
{
id: "WarningStyle",
name: "警告样式",
basedOn: "BaseStyle", // 继承 BaseStyle
run: {
color: "FF0000", // 只修改颜色
bold: true, // 添加加粗
}
// 其他属性自动从 BaseStyle 继承
},
// 多层继承
{
id: "CriticalWarning",
name: "严重警告",
basedOn: "WarningStyle", // 继承 WarningStyle
run: {
highlight: "yellow", // 添加高亮
size: 28, // 修改字号
},
paragraph: {
shading: { // 添加背景色
fill: "FFEEEE"
}
}
}
]
});
2. 样式链 (next)
js
const doc = new Document({
paragraphStyles: [
{
id: "TitleStyle",
name: "标题样式",
basedOn: "Title",
next: "ChapterIntro", // 按Enter后自动使用 ChapterIntro
// ...
},
{
id: "ChapterIntro",
name: "章节引言",
basedOn: "Normal",
next: "Normal", // 再按Enter回到正文
// ...
}
],
sections: [{
children: [
new Paragraph({
text: "文档标题",
style: "TitleStyle"
}),
// 按Enter后,下一段落会自动使用 ChapterIntro 样式
]
}]
});
6. 混合使用所有方式
实际项目中,通常需要混合使用多种方式:
js
const doc = new Document({
// 1. 全局默认样式
styles: {
default: {
document: {
run: {
font: "宋体",
size: 24,
color: "333333",
},
paragraph: {
spacing: { line: 360 },
}
}
},
// 字符样式
characterStyles: [
{
id: "Keyword",
name: "关键词",
run: {
bold: true,
color: "0000FF",
}
}
]
},
// 2. 自定义段落样式
paragraphStyles: [
{
id: "CompanyReport",
name: "公司报告样式",
basedOn: "Normal",
run: {
font: "微软雅黑",
size: 21,
color: "000000",
},
paragraph: {
spacing: { line: 315, after: 80 },
indent: { firstLine: 420 },
}
}
],
sections: [{
children: [
// 3. 应用自定义段落样式
new Paragraph({
text: "2024年度报告",
style: "CompanyReport"
}),
// 4. 内联样式 + 字符样式
new Paragraph({
children: [
new TextRun({
text: "关键词:",
style: "Keyword" // 字符样式
}),
new TextRun({
text: "数字化转型",
font: "黑体", // 内联样式
size: 26,
bold: true,
color: "FF0000"
})
],
spacing: { line: 400 } // 段落内联样式
})
]
}]
});
样式优先级示例:
js
const doc = new Document({
// 1. 全局默认样式(最低优先级)
styles: {
default: {
document: {
run: {
font: "宋体", // 会被覆盖
size: 24, // 会被覆盖
color: "000000", // 会被覆盖
}
}
}
},
// 2. 段落样式(中等优先级)
paragraphStyles: [
{
id: "MyParaStyle",
name: "段落样式",
run: {
font: "黑体", // 会被TextRun覆盖
size: 28, // 会被TextRun覆盖
color: "0000FF", // 会被TextRun覆盖
}
}
],
sections: [{
children: [
// 3. 段落直接样式 + 引用段落样式
new Paragraph({
text: "示例文本",
style: "MyParaStyle", // 应用段落样式
alignment: "center", // 段落直接样式
spacing: { line: 400 }, // 段落直接样式
// 4. TextRun 内联样式(最高优先级)
children: [
new TextRun({
text: "部分文字",
font: "楷体", // 覆盖段落样式
size: 32, // 覆盖段落样式
color: "FF0000", // 覆盖段落样式
bold: true, // 新增属性
}),
new TextRun("其他文字") // 使用段落样式
]
})
]
}]
});
// 最终效果:
// - "部分文字": 楷体、32号、红色、加粗
// - "其他文字": 黑体、28号、蓝色
// - 整个段落: 居中、1.67倍行距
示例:
创建一份月度工作报告:
js
import {
Document,
Packer,
Paragraph,
TextRun,
HeadingLevel,
Table,
TableRow,
TableCell,
AlignmentType
} from "docx";
async function createMonthlyReport(data) {
// 1. 创建文档
const doc = new Document({
creator: data.creator,
title: `${data.month}月度工作报告`,
description: `${data.year}年${data.month}月工作报告`,
// 样式配置
styles: {
default: {
document: {
run: {
font: "微软雅黑",
size: 24,
color: "333333",
}
}
}
},
sections: [{
properties: {
page: {
size: { width: 12240, height: 15840 }, // A4
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }
}
},
children: [
// 标题
new Paragraph({
text: `${data.month}月度工作报告`,
heading: HeadingLevel.TITLE,
alignment: AlignmentType.CENTER,
spacing: { after: 400 }
}),
// 基本信息
new Paragraph({
children: [
new TextRun({ text: "报告人:", bold: true }),
new TextRun(data.creator)
],
spacing: { after: 100 }
}),
// 工作完成情况
new Paragraph({
text: "一、工作完成情况",
heading: HeadingLevel.HEADING_1,
spacing: { before: 200, after: 100 }
}),
// 任务列表
...data.tasks.map(task =>
new Paragraph({
text: `✅ ${task.description}`,
bullet: { level: 0 },
spacing: { after: 40 }
})
),
// 数据表格
new Table({
rows: [
// 表头
new TableRow({
children: [
new TableCell({
children: [new Paragraph("项目")],
shading: { fill: "F0F0F0" }
}),
new TableCell({
children: [new Paragraph("进度")],
shading: { fill: "F0F0F0" }
}),
new TableCell({
children: [new Paragraph("负责人")],
shading: { fill: "F0F0F0" }
})
]
}),
// 数据行
...data.projects.map(project =>
new TableRow({
children: [
new TableCell({ children: [new Paragraph(project.name)] }),
new TableCell({
children: [new Paragraph({
children: [
new TextRun({
text: `${project.progress}%`,
color: project.progress >= 80 ? "008000" :
project.progress >= 50 ? "FFA500" : "FF0000"
})
]
})]
}),
new TableCell({ children: [new Paragraph(project.owner)] })
]
})
)
],
width: { size: 100, type: "pct" }
})
]
}]
});
// 生成文件
const buffer = await Packer.toBuffer(doc);
return buffer;
}
// 使用示例
const reportData = {
creator: "张三",
month: "1月",
year: "2024",
tasks: [
{ description: "完成项目需求分析" },
{ description: "完成核心功能开发" },
{ description: "完成单元测试" }
],
projects: [
{ name: "项目A", progress: 80, owner: "张三" },
{ name: "项目B", progress: 60, owner: "李四" },
{ name: "项目C", progress: 95, owner: "王五" }
]
};
const docBuffer = await createMonthlyReport(reportData);
优化建议:
1. 样式配置工具
使用工具函数统一管理样式:
js
// styles/config.js
export const STYLE_CONFIG = {
// 颜色配置
colors: {
primary: "#000080", // 主色
secondary: "#800000", // 辅色
success: "#008000", // 成功
warning: "#FFA500", // 警告
danger: "#FF0000", // 危险
info: "#008080", // 信息
light: "#F5F5F5", // 浅色
dark: "#333333", // 深色
},
// 字体配置
fonts: {
chinese: "宋体",
heading: "黑体",
code: "Consolas",
english: "Arial",
},
// 字号配置(单位:磅的2倍)
sizes: {
title: 36, // 18pt
h1: 32, // 16pt
h2: 28, // 14pt
h3: 26, // 13pt
normal: 24, // 12pt (小四)
small: 21, // 10.5pt (五号)
tiny: 18, // 9pt (小五)
},
// 间距配置(单位:twips)
spacing: {
line: {
single: 240, // 单倍
oneHalf: 360, // 1.5倍
double: 480, // 2倍
},
paragraph: {
before: 200,
after: 100,
}
}
};
// 样式工厂函数
export function createStyle(id, name, overrides = {}) {
const baseStyle = {
id,
name,
basedOn: "Normal",
next: "Normal",
quickFormat: true,
run: {
font: STYLE_CONFIG.fonts.chinese,
size: STYLE_CONFIG.sizes.normal,
color: STYLE_CONFIG.colors.dark.replace("#", ""),
},
paragraph: {
spacing: {
line: STYLE_CONFIG.spacing.line.oneHalf,
before: 0,
after: STYLE_CONFIG.spacing.paragraph.after,
}
},
...overrides
};
return baseStyle;
}
// 预设样式
export const PRESET_STYLES = {
// 标题样式
title: (options = {}) => createStyle("TitleStyle", "标题", {
basedOn: "Title",
run: {
font: STYLE_CONFIG.fonts.heading,
size: STYLE_CONFIG.sizes.title,
color: STYLE_CONFIG.colors.primary.replace("#", ""),
bold: true,
},
paragraph: {
alignment: "center",
spacing: { before: 400, after: 300 },
},
...options
}),
// 警告样式
warning: (options = {}) => createStyle("WarningStyle", "警告", {
run: {
color: STYLE_CONFIG.colors.danger.replace("#", ""),
bold: true,
highlight: "yellow",
},
paragraph: {
shading: { fill: "FFF8DC" },
indent: { left: 360, right: 360 },
},
...options
}),
// 代码样式
code: (options = {}) => createStyle("CodeStyle", "代码", {
run: {
font: STYLE_CONFIG.fonts.code,
size: STYLE_CONFIG.sizes.small,
color: STYLE_CONFIG.colors.success.replace("#", ""),
},
paragraph: {
shading: { fill: "F5F5F5" },
indent: { left: 720 },
spacing: { line: 280 },
},
...options
})
};
使用工具函数:
js
import { Document } from "docx";
import { PRESET_STYLES, createStyle } from "./styles/config";
const doc = new Document({
paragraphStyles: [
// 使用预设样式
PRESET_STYLES.title(),
// 使用工厂函数创建自定义样式
createStyle("CustomStyle", "自定义样式", {
run: {
font: "楷体",
size: 26,
italics: true,
},
paragraph: {
alignment: "right",
shading: { fill: "F0F8FF" }
}
}),
// 修改预设样式
PRESET_STYLES.warning({
id: "CriticalWarning",
name: "严重警告",
run: {
size: 28,
underline: {},
}
})
],
// 全局默认样式
styles: {
default: {
document: {
run: {
font: "宋体",
size: 24,
},
paragraph: {
spacing: { line: 360 },
}
}
}
}
});
2.文档生成策略(分块处理)
分块处理的目的 :避免在浏览器中生成大型文档时出现内存溢出、界面卡死、用户无响应等问题。
| 场景 | 需要程度 | 说明 |
|---|---|---|
| 小文档(< 100行) | ⭐ | 完全不需要 |
| 中等文档(100-500行) | ⭐⭐ | 可考虑,但不是必须 |
| 大型报表(500-2000行) | ⭐⭐⭐ | 建议实现 |
| 大数据量(> 2000行) | ⭐⭐⭐⭐⭐ | 必须实现 |
js
// 渐进式生成大型文档
async function generateLargeDocument(data, chunkSize = 50) {
const sections = [];
// 分段处理避免内存溢出
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
const section = await createSection(chunk);
sections.push(section);
}
return new Document({ sections });
}
3. 浏览器环境优化
js
// 添加进度反馈的大型文档生成
async function exportDocumentWithProgress(data, onProgress) {
const totalSteps = data.length;
let completed = 0;
const sections = [];
for (const item of data) {
const section = await createSection(item);
sections.push(section);
completed++;
if (onProgress) {
onProgress(Math.round((completed / totalSteps) * 100));
}
}
const doc = new Document({ sections });
const blob = await Packer.toBlob(doc);
saveAs(blob, "document.docx");
}
4. 错误处理与用户体验
js
// 完整的错误处理示例
async function safeExportDocument(data) {
try {
// 验证数据
if (!data || data.length === 0) {
throw new Error("没有数据可导出");
}
// 检查数据大小(浏览器内存限制)
if (data.length > 1000) {
console.warn("数据量较大,建议分批次导出");
// 可以提示用户或自动分批
}
// 生成文档
const doc = await createDocument(data);
const blob = await Packer.toBlob(doc);
// 检查Blob大小
if (blob.size > 10 * 1024 * 1024) { // 10MB
console.warn("文档较大,下载可能需要时间");
}
// 保存文件
saveAs(blob, `导出文档_${new Date().toISOString().slice(0, 10)}.docx`);
return { success: true, size: blob.size };
} catch (error) {
console.error("文档导出失败:", error);
// 用户友好的错误提示
const errorMessage = {
"没有数据可导出": "请先添加数据再导出",
"NetworkError": "网络连接失败,请检查网络",
"QuotaExceededError": "文档太大,请减少数据量"
}[error.name] || "文档导出失败,请重试";
alert(errorMessage);
return { success: false, error: error.message };
}
}
常见问题及解决方案
问题1:样式不生效
检查清单:
- 样式ID是否正确引用
- 样式优先级是否正确(内联样式 > 段落样式 > 全局样式)
- 样式配置语法是否正确
- 是否拼写错误
问题2:中英文混合字体
js
// 正确的字体设置
{
id: "MixedFont",
name: "混合字体",
run: {
// 分别设置不同字符集的字体
font: {
ascii: "Times New Roman", // 英文字体
eastAsia: "宋体", // 中文字体
cs: "宋体", // 复杂字符
hint: "eastAsia", // 提示使用东亚字体
}
}
}
问题3:编号不正确
现象:有序列表编号混乱
解决方案:
- 明确指定instance:不要依赖默认行为
- 避免中断实例:相同instance的段落放在一起
- 使用不同reference:完全独立的列表使用不同reference
问题4:浏览器内存不足
现象:大文档生成失败
优化策略:
- 分批次生成:将大数据分块处理
- 压缩图片:减少图片大小
- 简化样式:避免过度复杂的样式嵌套
- 提供进度反馈:让用户了解生成进度
项目结构建议
text
project/
├── src/
│ ├── components/
│ │ └── DocExporter.vue # 导出组件
│ ├── utils/
│ │ ├── doc-generator/
│ │ │ ├── config/ # 样式配置
│ │ │ │ ├── styles.js # 样式定义
│ │ │ │ └── templates.js # 文档模板
│ │ │ ├── builders/ # 组件构建器
│ │ │ │ ├── table-builder.js
│ │ │ │ ├── list-builder.js
│ │ │ │ └── style-builder.js
│ │ │ └── index.js # 主入口
│ │ └── file-utils.js # 文件工具
│ └── views/
│ └── ReportView.vue # 报表页面
└── package.json
总结:
- 分层配置 :
- 使用
styles.default设置全局基础样式 - 使用
paragraphStyles定义可重用段落样式 - 使用内联样式处理特殊情况
- 使用
- 样式规划 :
- 先规划样式体系,再开始编码
- 创建样式变量,方便维护
- 使用样式继承减少重复
- 性能优化 :
- 避免过多内联样式
- 复用样式定义
- 使用样式工厂函数
- 兼容性考虑 :
- 指定完整字体链
- 测试不同环境下的显示效果
- 提供样式回退机制
现在你可以在Vue.js项目中轻松实现强大的Word文档导出功能了!
扩展资源