引言
“前端导出Word”听起来像是需要后端协助的工作,但其实纯前端完全可以独立完成!无论是简单的表格数据,还是带有复杂格式、图片、页眉页脚的报告,前端都有成熟的方案。
本文将以医疗系统中“导出会诊单为Word文档”的需求为例,带你彻底搞懂前端导出Word的三种主流方案:纯文本替换、利用HTML转换、以及操作OOXML。你会惊讶于前端的强大能力。
需求与技术选型
- 需求: 将多条会诊记录,按患者分组,导出为格式规范的
.docx文件。 - 方案对比:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 1. 纯文本替换 | 实现最简单 | 格式控制弱,易出错 | 格式固定、简单的文档 |
| 2. HTML转换 | 利用已有CSS,开发快 | 兼容性存疑,格式可能失真 | 格式简单的文档或内部使用 |
| 3. 操作OOXML | 功能最强大,格式精准 | 复杂度最高,需学习成本 | 复杂、高要求的企业级文档 |
一、实现思路与流程 (方案一:纯文本替换)
这种方案的核心是:准备一个Word模板文件,将其另存为XML或直接修改为.docx,然后在代码中替换预定义的占位符。
其核心流程如下图所示:
graph TD
A[用户点击导出Word按钮] --> B[前端Fetch请求模板文件]
B --> C{获取模板成功?}
C -- 失败 --> D[提示错误并终止]
C -- 成功 --> E[将模板内容读为文本字符串]
E --> F[遍历待导出的数据]
F --> G[使用数据值替换模板中的占位符]
G --> H[合并所有替换后的内容]
H --> I[将最终字符串转换为Blob对象]
I --> J[使用FileSaver触发浏览器下载]
J --> K[完成导出]
二、代码实战与详解
1. 准备模板文件
- 在Microsoft Word中设计好会诊单的样式。
- 在需要动态填充内容的地方,填入唯一的占位符,例如:
{name},{consultationNo},{diagnosisOpinion}等。 - 将文件另存为
XML格式,然后将文件后缀.xml改为.docx。或者,更简单的方法:直接将.docx文件的后缀改为.zip,解压后找到word/document.xml文件,用文本编辑器打开,在其中找到你要替换的文字,改为{placeholder},然后再打包回去并改回.docx后缀。这里我们采用第一种简单方法,保存为template.docx,并放入项目的public/templates/目录下。
2. 核心代码实现
<script setup lang="ts">
import { saveAs } from 'file-saver'; // 强大的前端文件下载库
// 导出Word文档函数
async function exportToWord() {
try {
// 1. 定义模板路径
const templateUrl = '/templates/template.docx';
console.log('正在加载模板文件...');
// 2. 使用Fetch API获取模板文件
const response = await fetch(templateUrl);
if (!response.ok) {
throw new Error(`模板加载失败: ${response.status}`);
}
// 3. 将响应流读取为文本
// 注意:这里假设模板是纯文本或XML,但对于真正的.docx文件(二进制),应使用 `await response.blob()` 或 `arrayBuffer()`
const templateText = await response.text();
console.log('模板加载成功!');
// 4. 按患者分组数据
const patientGroups = groupConsultationsByPatient(printParams.value);
// 5. 遍历每个患者的所有会诊单
for (const [patientKey, consultations] of Object.entries(patientGroups)) {
if (consultations.length === 0) continue;
// 获取患者基本信息
const firstConsultation = consultations[0];
const patientInfo = {
consultationNo: firstConsultation.consultationNo || '',
name: firstConsultation.name || '',
// ... 其他基本信息字段
};
let combinedContent = '';
// 6. 处理每份会诊单
for (let i = 0; i < consultations.length; i++) {
const item = consultations[i];
// 复制一份模板文本
let consultationContent = templateText;
// 7. 创建数据对象,映射所有占位符
const data: any = {
...patientInfo,
diagnosisOpinion: item.diagnosisOpinion || '无',
abScanDiagnosis: item.abScanDiagnosis || '无',
// ... 其他需要替换的字段
};
// 8. 🎯 核心步骤:循环替换所有占位符
for (const [key, value] of Object.entries(data)) {
// 创建正则表达式,全局搜索占位符,如 {name}
const regex = new RegExp(`{${key}}`, 'g');
// 执行替换操作
consultationContent = consultationContent.replace(regex, value as string);
}
combinedContent += consultationContent;
// 添加分页符 (ASCII码为12)
if (i < consultations.length - 1) {
combinedContent += '\f';
}
}
// 9. 生成Blob并触发下载
// 注意: 这里类型是Word的MIME类型
const blob = new Blob([combinedContent], {
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
});
// 10. 使用file-saver保存文件
saveAs(blob, `${patientInfo.consultationNo}-${patientInfo.name}-会诊单.docx`);
ElMessage.success('导出成功!');
}
} catch (error: any) {
console.error('导出失败:', error);
ElMessage.error(`导出失败: ${error.message}`);
}
}
// 按患者姓名分组数据的工具函数
function groupConsultationsByPatient(consultations: any[]) {
const groups: Record<string, any[]> = {};
consultations.forEach((consultation) => {
const key = consultation.name;
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(consultation);
});
return groups;
}
</script>
关键注释:
fetch(templateUrl): 通过网络请求获取模板文件。对于生产环境,最好将模板文件放在public目录或CDN上。RegExp与replace: 这是实现替换的核心逻辑。使用正则表达式进行全局(g)替换,确保所有相同的占位符都被替换。Blob: 二进制大对象,允许我们在前端直接操作并在内存中生成文件内容。saveAs(blob, filename):file-saver库的核心方法,模拟了一个文件下载动作。
三、进阶方案:功能强大的docxtemplater
方案一虽然简单,但缺点也很明显:模板制作麻烦,格式控制弱。对于更严肃的项目,推荐使用方案三的增强版——docxtemplater。
docxtemplater的工作原理:
它不需要你将Word存为XML,而是直接操作.docx文件(本质是一个ZIP包)。它解压文件,找到内部的document.xml,然后用你的数据替换其中的占位符,最后再重新打包成.docx。它支持循环、条件判断、图片插入等复杂功能。
代码示例:
import Docxtemplater from 'docxtemplater';
import PizZip from 'pizzip';
import { saveAs } from 'file-saver';
// 1. 获取二进制模板
const response = await fetch('/templates/advanced-template.docx');
const arrayBuffer = await response.arrayBuffer();
// 2. 加载Zip包和模板
const zip = new PizZip(arrayBuffer);
const doc = new Docxtemplater().loadZip(zip);
// 3. 设置数据 (支持嵌套结构和循环)
doc.setData({
name: "张三",
consultations: [
{ diagnosis: "诊断意见1" },
{ diagnosis: "诊断意见2" }
]
});
// 4. 渲染文档
doc.render();
// 5. 生成并导出Blob
const out = doc.getZip().generate({ type: 'blob' });
saveAs(out, 'advanced-report.docx');
四、总结与选择建议
| 特性 | 纯文本替换 | docxtemplater (推荐) | HTML转换 |
|---|---|---|---|
| 复杂度 | 低 | 中 | 低 |
| 功能 | 弱 | 极强 (循环、条件、图片) | 中 |
| 格式保真 | 差 | 高 (完美保留原格式) | 一般 |
| 模板制作 | 麻烦 | 简单 (直接在Word里设计) | 简单 |
选择建议:
- 如果你的需求只是导出一份格式固定、非常简单的文档,纯文本替换可以快速上手。
- 如果你需要导出格式复杂、要求高、且数据动态的企业级文档(如合同、报告、通知),docxtemplater是不二之选。它虽然需要引入额外的库,但带来的灵活性和可靠性是巨大的。
- HTML转换方案可用于快速将网页内容导出,但对复杂Word格式的支持有限。