DOCX 预览
2. 核心开源库概述
| 库名称 | 核心定位 | 实现思路 | 主要优势 |
|---|---|---|---|
| docx-preview | 像素级还原预览 | 直接解析 DOCX 内部结构,渲染为 HTML/CSS | 高度还原格式,支持分页、页眉页脚 |
| mammoth | 内容提取与转换 | 提取语义结构,转换为干净的 HTML | 生成结构化 HTML,便于二次编辑 |
3. docx-preview 详细解析
3.1 技术原理
docx-preview 采用直接解析 DOCX 文件内部结构的方式,将 Word 文档的 XML 内容转换为浏览器可渲染的 HTML 和 CSS。其核心工作流程包括:
- 文件解析:读取 DOCX 文件(本质是 ZIP 压缩包),提取内部的 XML 结构文件
- 内容转换:将 Word 的 XML 格式转换为中间表示
- 样式映射:将 Word 样式转换为 CSS 样式
- HTML 渲染:生成最终的 HTML 结构并渲染到页面中
3.2 适用场景
- 在线文档预览器开发
- 需要高度还原 Word 格式的场景
- 文档管理系统的预览功能
- 在线教育平台的课件预览
3.3 安装与基本使用
3.3.1 安装方式
npm install docx-preview
3.3.2 基本用法
import { renderAsync } from 'docx-preview';
// 核心渲染函数
renderAsync(
docBlob, // DOCX 文件的 Blob 对象
document.getElementById('preview'), // 渲染容器
null, // 可选的回调函数
{ // 配置选项
className: 'docx', // 自定义 CSS 类名前缀
inWrapper: true, // 是否包裹内容
breakPages: true, // 是否分页展示
renderHeaders: true, // 渲染页眉
renderFooters: true // 渲染页脚
}
).then(() => {
console.log('文档渲染完成');
});
3.4 核心配置参数
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
className | string | "docx" | 自定义 CSS 类名前缀,用于样式定制 |
inWrapper | boolean | true | 是否将内容包裹在容器中 |
ignoreWidth | boolean | false | 是否忽略原始页面宽度 |
ignoreHeight | boolean | false | 是否忽略原始页面高度 |
ignoreFonts | boolean | false | 是否忽略字体定义,使用浏览器默认字体 |
breakPages | boolean | true | 是否分页展示,设置为 false 可滚动显示 |
ignoreLastRenderedPageBreak | boolean | true | 是否忽略文档中的分页标签 |
renderHeaders | boolean | true | 是否渲染页眉内容 |
renderFooters | boolean | true | 是否渲染页脚内容 |
renderFootnotes | boolean | true | 是否渲染脚注 |
renderEndnotes | boolean | true | 是否渲染尾注 |
renderComments | boolean | false | 是否渲染批注内容 |
useBase64URL | boolean | false | 图片资源是否使用 base64 URL |
useMathMLPolyfill | boolean | false | 是否启用公式渲染补丁 |
experimental | boolean | false | 是否启用实验性功能(如制表符支持) |
trimXmlDeclaration | boolean | true | 是否去除 XML 声明头 |
debug | boolean | false | 是否启用调试模式,输出详细日志 |
3.5 进阶用法
3.5.1 自定义样式
// 渲染前添加自定义样式
const style = document.createElement('style');
style.textContent = `
.docx {
max-width: 100%;
margin: 0 auto;
}
.docx-page {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
`;
document.head.appendChild(style);
// 然后调用 renderAsync 渲染文档
3.5.2 监听渲染进度
// 使用第三个参数(回调函数)监听进度
renderAsync(
docBlob,
container,
(progress) => {
console.log(`渲染进度:${Math.round(progress * 100)}%`);
// 可以更新进度条等UI元素
},
options
);
3.6 潜在问题与解决方案
| 问题 | 解决方案 |
|---|---|
| 复杂文档渲染时间长 | 1. 实现分页加载;2. 优化文档结构;3. 考虑服务端预渲染 |
| 个别嵌入元素显示不完整 | 1. 检查元素类型是否受支持;2. 考虑降级显示方案 |
| 字体渲染不一致 | 1. 嵌入 Web 字体;2. 使用 ignoreFonts: true 忽略自定义字体 |
| 大文件内存占用高 | 1. 实现文件分片处理;2. 优化渲染策略,只渲染可见区域 |
4. mammoth 详细解析
4.1 技术原理
mammoth 采用"语义提取"的思路,它的核心目标不是还原 Word 文档的视觉样式,而是提取其语义结构并转换为干净的 HTML。其工作流程包括:
- 文件解析:读取 DOCX 文件,提取内部 XML 结构
- 语义分析:分析文档的语义结构,识别标题、段落、列表等元素
- 样式映射:根据配置将 Word 样式映射到 HTML 标签和类名
- HTML 生成:生成结构化的 HTML 输出
4.2 适用场景
- 内容管理系统的文档导入功能
- 富文本编辑器的 Word 内容粘贴
- 文档内容的二次加工和处理
- 移动设备上的轻量级文档阅读
- 搜索引擎友好的文档展示
4.3 安装与基本使用
4.3.1 安装方式
npm install mammoth
4.3.2 基本用法
import mammoth from 'mammoth';
// 将 ArrayBuffer 转换为 HTML
mammoth.convertToHtml({ arrayBuffer: docxBuffer })
.then(result => {
// result.value 包含生成的 HTML
document.getElementById('content').innerHTML = result.value;
// result.messages 包含转换过程中的信息和警告
console.log('转换警告:', result.messages);
})
.catch(error => {
console.error('转换失败:', error);
});
4.4 核心配置参数
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
styleMap | string / array | 无 | Word 样式到 HTML 的映射规则 |
includeEmbeddedStyleMap | boolean | true | 是否包含文档内嵌的样式映射 |
includeDefaultStyleMap | boolean | true | 是否结合默认样式映射一起生效 |
convertImage | function | true | 图片处理策略,默认转为 base64 |
ignoreEmptyParagraphs | boolean | true | 是否忽略空段落 |
idPrefix | string | "" | 生成 ID 的前缀(如脚注) |
transformDocument | function | 无 | HTML 渲染前修改文档结构的回调 |
4.5 进阶用法
4.5.1 自定义样式映射
const options = {
styleMap: [
// 将 Word 中的"注意事项"样式映射为 HTML 的 div.warning
"p[style-name='注意事项'] => div.warning:fresh",
// 将"提示"样式映射为 div.tip
"p[style-name='提示'] => div.tip:fresh",
// 将标题样式映射为对应的 HTML 标题标签
"p[style-name='标题 1'] => h1",
"p[style-name='标题 2'] => h2",
"p[style-name='标题 3'] => h3"
]
};
mammoth.convertToHtml({ arrayBuffer: docxBuffer }, options)
.then(result => {
document.getElementById('content').innerHTML = result.value;
});
4.5.2 图片优化处理
const options = {
convertImage: mammoth.images.imgElement(image => {
// 将图片转换为 Blob URL,避免 HTML 过于臃肿
return image.readAsArrayBuffer().then(buffer => {
const blob = new Blob([buffer], { type: image.contentType });
return {
src: URL.createObjectURL(blob),
alt: "文档图片",
// 可以添加其他属性
class: "doc-image"
};
});
})
};
4.6 潜在问题与解决方案
| 问题 | 解决方案 |
|---|---|
| 样式映射不准确 | 1. 调整 styleMap 配置;2. 查看转换日志优化映射规则 |
| 图片处理性能问题 | 1. 使用 Blob URL 替代 base64;2. 实现图片懒加载 |
| 复杂表格转换效果差 | 1. 简化表格结构;2. 自定义表格转换逻辑 |
| 特殊字符显示异常 | 1. 确保正确的字符编码;2. 预处理特殊字符 |
5. 选型指南
5.1 场景对比
| 目标场景 | 推荐方案 | 理由说明 |
|---|---|---|
| 在线文档预览器 | docx-preview | 高度还原 Word 格式,支持分页、样式、页眉页脚 |
| 内容管理系统 | mammoth | 生成结构化 HTML,利于编辑和再加工 |
| 移动设备轻量级阅读 | mammoth | 生成的 HTML 体积小,加载速度快 |
| 文档管理系统预览 | docx-preview | 提供接近原生 Word 的阅读体验 |
| 富文本编辑器导入 | mammoth | 生成干净的 HTML,便于编辑和格式化 |
5.2 技术对比
| 对比维度 | docx-preview | mammoth |
|---|---|---|
| 核心定位 | 格式还原 | 内容提取 |
| 输出质量 | 视觉还原度高 | 结构清晰度高 |
| 文件大小 | 生成的 HTML 较大 | 生成的 HTML 较小 |
| 渲染性能 | 复杂文档渲染较慢 | 转换速度快 |
| 二次编辑友好度 | 较低 | 较高 |
| 自定义程度 | 样式自定义为主 | 结构和样式均可深度自定义 |
6. 技术拓展
6.1 与同类技术对比
6.1.1 前端方案对比
| 技术方案 | 优势 | 劣势 |
|---|---|---|
| docx-preview | 高度还原格式,纯前端实现 | 生成文件大,渲染性能一般 |
| mammoth | 结构清晰,适合二次编辑 | 视觉还原度较低 |
| Microsoft Office Online | 官方方案,兼容性好 | 依赖外部服务,需要公网访问 |
| Google Docs Viewer | 免费使用,支持多种格式 | 依赖外部服务,自定义程度低 |
6.1.2 后端方案对比
| 技术方案 | 优势 | 劣势 |
|---|---|---|
| LibreOffice + PDF.js | 支持格式多,转换质量高 | 需要后端服务,部署复杂 |
| Aspose.Words | 专业级转换,功能强大 | 商业软件,成本高 |
| Pandoc | 开源免费,支持多种格式 | 命令行工具,集成复杂 |
6.2 性能优化方案
6.2.1 前端优化策略
- 懒加载:只渲染可见区域的内容,滚动时动态加载其他部分
- 虚拟滚动:对于长文档,只渲染当前视口内的内容
- 图片优化:使用适当的图片格式和压缩算法,实现图片懒加载
- 代码分割:将预览功能打包为独立 chunk,按需加载
- Web Worker:将文档解析和转换放在 Web Worker 中执行,避免阻塞主线程
6.2.2 后端优化策略(可选)
- 预转换:提前将常用文档转换为 HTML 或 PDF 格式
- 缓存机制:缓存转换结果,避免重复转换
- 分布式处理:使用分布式架构处理大量文档转换请求
- 异步处理:对于大文件转换,使用异步队列处理
6.3 最佳实践
- 根据场景选择合适的库:格式还原优先选择 docx-preview,内容提取优先选择 mammoth
- 合理配置优化性能:根据实际需求调整配置参数,避免不必要的功能开销
- 实现优雅降级:对于不支持的特性,提供合理的降级显示方案
- 优化用户体验:添加加载状态、错误处理、进度提示等
- 考虑跨浏览器兼容性:测试不同浏览器的渲染效果,确保一致的体验
- 定期更新依赖:关注库的更新,及时升级以获得更好的性能和兼容性
7. Demo 开发
7.1 Demo-1: docx-preview 在线预览器
7.1.1 功能说明
一个完整的 Word 文档在线预览器,支持上传 .docx 文件并实时预览,提供接近原生 Word 的阅读体验。
7.1.2 环境依赖
- 现代浏览器(支持 ES6 模块)
- 无需其他外部依赖
7.1.3 完整代码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>docx-preview 在线预览器</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
h1 {
text-align: center;
margin-bottom: 30px;
color: #2c3e50;
}
.upload-section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
text-align: center;
}
#fileInput {
margin: 10px 0;
padding: 10px;
}
.btn {
background-color: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin: 0 10px;
}
.btn:hover {
background-color: #2980b9;
}
.btn:disabled {
background-color: #bdc3c7;
cursor: not-allowed;
}
.status {
margin-top: 15px;
font-weight: bold;
}
.preview-section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
overflow: auto;
}
#preview {
min-height: 600px;
border: 1px solid #ddd;
padding: 20px;
background-color: #fff;
border-radius: 4px;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
font-size: 18px;
color: #7f8c8d;
}
.error {
color: #e74c3c;
text-align: center;
padding: 20px;
}
/* 自定义 docx-preview 样式 */
.docx {
max-width: 100%;
}
.docx-page {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
padding: 20px;
background-color: white;
}
</style>
</head>
<body>
<div class="container">
<h1>📄 DOCX 在线预览器</h1>
<div class="upload-section">
<h2>上传文档</h2>
<input type="file" id="fileInput" accept=".docx" />
<br>
<button class="btn" id="renderBtn" disabled>渲染文档</button>
<button class="btn" id="clearBtn" disabled>清除内容</button>
<div class="status" id="status">请选择一个 .docx 文件</div>
</div>
<div class="preview-section">
<h2>预览区域</h2>
<div id="preview" class="loading">请上传 .docx 文件开始预览</div>
</div>
</div>
<script type="module">
// 导入 docx-preview
import { renderAsync } from 'https://cdn.jsdelivr.net/npm/docx-preview@0.3.6/+esm';
// 获取 DOM 元素
const fileInput = document.getElementById('fileInput');
const renderBtn = document.getElementById('renderBtn');
const clearBtn = document.getElementById('clearBtn');
const status = document.getElementById('status');
const preview = document.getElementById('preview');
let currentFile = null;
// 文件选择事件处理
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
if (file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || file.name.endsWith('.docx')) {
currentFile = file;
renderBtn.disabled = false;
clearBtn.disabled = false;
status.textContent = `已选择文件: ${file.name}`;
status.style.color = '#27ae60';
} else {
status.textContent = '请选择 .docx 格式的文件';
status.style.color = '#e74c3c';
currentFile = null;
renderBtn.disabled = true;
}
}
});
// 渲染按钮事件处理
renderBtn.addEventListener('click', async () => {
if (!currentFile) return;
try {
status.textContent = '正在渲染文档...';
status.style.color = '#3498db';
preview.innerHTML = '<div class="loading">正在渲染文档...</div>';
// 调用 docx-preview 渲染文档
await renderAsync(
currentFile, // 文件 Blob 对象
preview, // 渲染容器
(progress) => {
// 显示渲染进度
status.textContent = `渲染进度: ${Math.round(progress * 100)}%`;
},
{
className: 'docx', // 自定义 CSS 类名
inWrapper: true, // 包裹内容
breakPages: true, // 分页展示
renderHeaders: true, // 渲染页眉
renderFooters: true, // 渲染页脚
renderComments: false, // 不渲染批注
useBase64URL: false, // 不使用 base64 URL
experimental: true // 启用实验性功能
}
);
status.textContent = '文档渲染完成';
status.style.color = '#27ae60';
} catch (error) {
console.error('渲染失败:', error);
status.textContent = '文档渲染失败,请检查文件格式';
status.style.color = '#e74c3c';
preview.innerHTML = `<div class="error">渲染失败: ${error.message}</div>`;
}
});
// 清除按钮事件处理
clearBtn.addEventListener('click', () => {
preview.innerHTML = '<div class="loading">请上传 .docx 文件开始预览</div>';
fileInput.value = '';
currentFile = null;
renderBtn.disabled = true;
clearBtn.disabled = true;
status.textContent = '请选择一个 .docx 文件';
status.style.color = '#333';
});
</script>
</body>
</html>
7.1.4 操作步骤
- 将上述代码保存为
docx-preview-demo.html - 用现代浏览器打开该文件
- 点击"选择文件"按钮,选择一个
.docx格式的文档 - 点击"渲染文档"按钮开始渲染
- 等待渲染完成后查看预览效果
- 可以点击"清除内容"按钮重新选择文件
7.1.5 预期效果
- 上传文件前:预览区域显示"请上传 .docx 文件开始预览"
- 选择文件后:显示文件名,"渲染文档"按钮变为可用
- 渲染过程中:显示渲染进度百分比
- 渲染成功:在预览区域显示格式化的 Word 文档,支持分页、页眉页脚
- 渲染失败:显示错误信息
7.1.6 调试技巧与常见问题
-
渲染无反应
- 检查浏览器控制台是否有错误信息
- 确保使用的是支持 ES6 模块的现代浏览器
- 确认文件格式正确(必须是
.docx格式)
-
样式渲染异常
- 检查是否有 CSS 冲突
- 尝试调整
ignoreFonts、ignoreWidth等配置参数 - 查看控制台是否有资源加载错误
-
大文件渲染慢
- 尝试使用较小的测试文件
- 考虑调整
breakPages: false改为滚动显示 - 实现分页加载或虚拟滚动
7.2 Demo-2: mammoth 内容提取器
7.2.1 功能说明
一个基于 mammoth 的 Word 内容提取器,将 Word 文档转换为结构化的 HTML,适合用于内容管理系统或富文本编辑器的导入功能。
7.2.2 环境依赖
- 现代浏览器
- 无需其他外部依赖
7.2.3 完整代码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>mammoth 内容提取器</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
h1 {
text-align: center;
margin-bottom: 30px;
color: #2c3e50;
}
.upload-section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
text-align: center;
}
#fileInput {
margin: 10px 0;
padding: 10px;
}
.btn {
background-color: #27ae60;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin: 0 10px;
}
.btn:hover {
background-color: #229954;
}
.btn:disabled {
background-color: #bdc3c7;
cursor: not-allowed;
}
.status {
margin-top: 15px;
font-weight: bold;
}
.content-section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
#content {
min-height: 400px;
border: 1px solid #ddd;
padding: 20px;
background-color: #fff;
border-radius: 4px;
white-space: pre-wrap;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
font-size: 18px;
color: #7f8c8d;
}
.error {
color: #e74c3c;
text-align: center;
padding: 20px;
}
.options-section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.options-section h3 {
margin-bottom: 15px;
color: #34495e;
}
.option-group {
margin-bottom: 10px;
}
.html-output {
background: #f8f9fa;
padding: 15px;
border-radius: 4px;
margin-top: 20px;
max-height: 300px;
overflow: auto;
font-family: monospace;
font-size: 14px;
}
.html-output h4 {
margin-bottom: 10px;
color: #34495e;
}
/* 自定义样式映射的效果 */
.warning {
background-color: #fff3cd;
border: 1px solid #ffeeba;
border-radius: 4px;
padding: 10px;
margin: 10px 0;
color: #856404;
}
.tip {
background-color: #d1ecf1;
border: 1px solid #bee5eb;
border-radius: 4px;
padding: 10px;
margin: 10px 0;
color: #0c5460;
}
.doc-image {
max-width: 100%;
height: auto;
margin: 10px 0;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<div class="container">
<h1>📄 Word 内容提取器</h1>
<div class="upload-section">
<h2>上传文档</h2>
<input type="file" id="fileInput" accept=".docx" />
<br>
<button class="btn" id="extractBtn" disabled>提取内容</button>
<button class="btn" id="clearBtn" disabled>清除内容</button>
<div class="status" id="status">请选择一个 .docx 文件</div>
</div>
<div class="options-section">
<h3>转换选项</h3>
<div class="option-group">
<label><input type="checkbox" id="ignoreEmptyParagraphs" checked> 忽略空段落</label>
</div>
<div class="option-group">
<label><input type="checkbox" id="useBlobUrl" checked> 使用 Blob URL 处理图片</label>
</div>
</div>
<div class="content-section">
<h2>提取结果</h2>
<div id="content" class="loading">请上传 .docx 文件开始提取</div>
<div class="html-output">
<h4>生成的 HTML 代码</h4>
<pre id="htmlCode"></pre>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/mammoth@1.9.1/mammoth.browser.min.js"></script>
<script>
// 获取 DOM 元素
const fileInput = document.getElementById('fileInput');
const extractBtn = document.getElementById('extractBtn');
const clearBtn = document.getElementById('clearBtn');
const status = document.getElementById('status');
const content = document.getElementById('content');
const htmlCode = document.getElementById('htmlCode');
const ignoreEmptyParagraphs = document.getElementById('ignoreEmptyParagraphs');
const useBlobUrl = document.getElementById('useBlobUrl');
let currentFile = null;
// 文件选择事件处理
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
if (file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || file.name.endsWith('.docx')) {
currentFile = file;
extractBtn.disabled = false;
clearBtn.disabled = false;
status.textContent = `已选择文件: ${file.name}`;
status.style.color = '#27ae60';
} else {
status.textContent = '请选择 .docx 格式的文件';
status.style.color = '#e74c3c';
currentFile = null;
extractBtn.disabled = true;
}
}
});
// 提取按钮事件处理
extractBtn.addEventListener('click', () => {
if (!currentFile) return;
status.textContent = '正在提取内容...';
status.style.color = '#3498db';
content.innerHTML = '<div class="loading">正在提取内容...</div>';
htmlCode.textContent = '';
const reader = new FileReader();
reader.onload = function(event) {
const arrayBuffer = event.target.result;
// 构建转换选项
const options = {
ignoreEmptyParagraphs: ignoreEmptyParagraphs.checked,
styleMap: [
"p[style-name='注意事项'] => div.warning",
"p[style-name='提示'] => div.tip",
"p[style-name='标题 1'] => h1",
"p[style-name='标题 2'] => h2",
"p[style-name='标题 3'] => h3"
]
};
// 配置图片处理
if (useBlobUrl.checked) {
options.convertImage = mammoth.images.imgElement(function(image) {
return image.readAsArrayBuffer().then(function(buffer) {
const blob = new Blob([buffer], { type: image.contentType });
return {
src: URL.createObjectURL(blob),
alt: "文档图片",
class: "doc-image"
};
});
});
}
// 调用 mammoth 转换文档
mammoth.convertToHtml({ arrayBuffer }, options)
.then(function(result) {
// 显示转换结果
content.innerHTML = result.value;
htmlCode.textContent = result.value;
status.textContent = `内容提取完成,共 ${result.messages.length} 条提示`;
status.style.color = '#27ae60';
// 输出转换提示
console.log('转换提示:', result.messages);
})
.catch(function(error) {
console.error('提取失败:', error);
status.textContent = '内容提取失败,请检查文件格式';
status.style.color = '#e74c3c';
content.innerHTML = `<div class="error">提取失败: ${error.message}</div>`;
});
};
// 读取文件为 ArrayBuffer
reader.readAsArrayBuffer(currentFile);
});
// 清除按钮事件处理
clearBtn.addEventListener('click', () => {
content.innerHTML = '<div class="loading">请上传 .docx 文件开始提取</div>';
htmlCode.textContent = '';
fileInput.value = '';
currentFile = null;
extractBtn.disabled = true;
clearBtn.disabled = true;
status.textContent = '请选择一个 .docx 文件';
status.style.color = '#333';
});
</script>
</body>
</html>
7.2.4 操作步骤
- 将上述代码保存为
mammoth-demo.html - 用现代浏览器打开该文件
- 点击"选择文件"按钮,选择一个
.docx格式的文档 - 根据需要调整转换选项(忽略空段落、使用 Blob URL 处理图片)
- 点击"提取内容"按钮开始转换
- 查看转换后的内容和生成的 HTML 代码
- 可以点击"清除内容"按钮重新选择文件
7.2.5 预期效果
- 上传文件前:内容区域显示"请上传 .docx 文件开始提取"
- 选择文件后:显示文件名,"提取内容"按钮变为可用
- 转换过程中:显示"正在提取内容..."
- 转换成功:在内容区域显示结构化的 HTML,下方显示生成的 HTML 代码
- 转换失败:显示错误信息
7.2.6 调试技巧与常见问题
-
转换结果样式不符合预期
- 调整 styleMap 配置,优化样式映射规则
- 检查 Word 文档中的样式名称是否与配置匹配
- 使用浏览器开发者工具查看生成的 HTML 结构
-
图片不显示
- 确保图片处理选项正确配置
- 检查浏览器控制台是否有图片加载错误
- 尝试使用不同的图片格式和大小
-
转换速度慢
- 尝试使用较小的测试文件
- 关闭不必要的转换选项
- 优化图片处理逻辑
7.3 Demo-3: 结合使用两个库的完整解决方案
7.3.1 功能说明
一个结合 docx-preview 和 mammoth 的完整解决方案,提供"预览"和"提取"两种模式,用户可以根据需要选择不同的处理方式。
7.3.2 环境依赖
- 现代浏览器(支持 ES6 模块)
- 无需其他外部依赖
7.3.3 完整代码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DOCX 处理完整解决方案</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
h1 {
text-align: center;
margin-bottom: 30px;
color: #2c3e50;
}
.header {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
text-align: center;
}
.mode-selector {
margin: 20px 0;
}
.mode-btn {
background-color: #3498db;
color: white;
border: none;
padding: 12px 24px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin: 0 10px;
transition: all 0.3s ease;
}
.mode-btn:hover {
background-color: #2980b9;
transform: translateY(-2px);
}
.mode-btn.active {
background-color: #27ae60;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.upload-section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
text-align: center;
}
#fileInput {
margin: 10px 0;
padding: 10px;
}
.btn {
background-color: #e67e22;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin: 0 10px;
}
.btn:hover {
background-color: #d35400;
}
.btn:disabled {
background-color: #bdc3c7;
cursor: not-allowed;
}
.status {
margin-top: 15px;
font-weight: bold;
}
.result-section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
#result {
min-height: 500px;
border: 1px solid #ddd;
padding: 20px;
background-color: #fff;
border-radius: 4px;
overflow: auto;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
font-size: 18px;
color: #7f8c8d;
}
.error {
color: #e74c3c;
text-align: center;
padding: 20px;
}
/* 自定义 docx-preview 样式 */
.docx {
max-width: 100%;
}
.docx-page {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
padding: 20px;
background-color: white;
}
/* mammoth 自定义样式 */
.warning {
background-color: #fff3cd;
border: 1px solid #ffeeba;
border-radius: 4px;
padding: 10px;
margin: 10px 0;
color: #856404;
}
.tip {
background-color: #d1ecf1;
border: 1px solid #bee5eb;
border-radius: 4px;
padding: 10px;
margin: 10px 0;
color: #0c5460;
}
.doc-image {
max-width: 100%;
height: auto;
margin: 10px 0;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.html-output {
background: #f8f9fa;
padding: 15px;
border-radius: 4px;
margin-top: 20px;
max-height: 300px;
overflow: auto;
font-family: monospace;
font-size: 14px;
}
.html-output h4 {
margin-bottom: 10px;
color: #34495e;
}
.hidden {
display: none;
}
.info-section {
background: #e3f2fd;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
border-left: 4px solid #2196f3;
}
.info-section h3 {
margin-bottom: 10px;
color: #1565c0;
}
.info-section ul {
margin-left: 20px;
}
</style>
</head>
<body>
<div class="container">
<h1>📄 DOCX 处理完整解决方案</h1>
<div class="info-section">
<h3>功能介绍</h3>
<ul>
<li><strong>预览模式</strong>:使用 docx-preview 实现像素级还原的 Word 文档预览</li>
<li><strong>提取模式</strong>:使用 mammoth 将 Word 文档转换为结构化 HTML,便于二次编辑</li>
<li>支持上传本地 .docx 文件进行处理</li>
<li>提供完整的错误处理和状态反馈</li>
</ul>
</div>
<div class="header">
<h2>选择处理模式</h2>
<div class="mode-selector">
<button class="mode-btn active" data-mode="preview">📖 预览模式</button>
<button class="mode-btn" data-mode="extract">🔧 提取模式</button>
</div>
</div>
<div class="upload-section">
<h2>上传文档</h2>
<input type="file" id="fileInput" accept=".docx" />
<br>
<button class="btn" id="processBtn" disabled>处理文档</button>
<button class="btn" id="clearBtn" disabled>清除内容</button>
<div class="status" id="status">请选择一个 .docx 文件</div>
</div>
<div class="result-section">
<h2>处理结果</h2>
<div id="result" class="loading">请上传 .docx 文件开始处理</div>
<div class="html-output hidden">
<h4>生成的 HTML 代码</h4>
<pre id="htmlCode"></pre>
</div>
</div>
</div>
<script type="module">
// 动态导入所需库
let docxPreview = null;
let mammoth = null;
// 获取 DOM 元素
const fileInput = document.getElementById('fileInput');
const processBtn = document.getElementById('processBtn');
const clearBtn = document.getElementById('clearBtn');
const status = document.getElementById('status');
const result = document.getElementById('result');
const htmlCode = document.getElementById('htmlCode');
const htmlOutput = document.querySelector('.html-output');
const modeBtns = document.querySelectorAll('.mode-btn');
let currentFile = null;
let currentMode = 'preview';
// 初始化库
async function initLibraries() {
try {
// 动态导入 docx-preview
const { renderAsync } = await import('https://cdn.jsdelivr.net/npm/docx-preview@0.3.6/+esm');
docxPreview = renderAsync;
// 动态加载 mammoth
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/mammoth@1.9.1/mammoth.browser.min.js';
script.onload = () => {
mammoth = window.mammoth;
console.log('所有库加载完成');
};
document.head.appendChild(script);
} catch (error) {
console.error('库加载失败:', error);
status.textContent = '库加载失败,请刷新页面重试';
status.style.color = '#e74c3c';
}
}
// 初始化
initLibraries();
// 模式切换事件
modeBtns.forEach(btn => {
btn.addEventListener('click', () => {
// 更新 active 状态
modeBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// 更新当前模式
currentMode = btn.dataset.mode;
// 切换 HTML 输出区域显示
if (currentMode === 'extract') {
htmlOutput.classList.remove('hidden');
} else {
htmlOutput.classList.add('hidden');
}
// 清除当前结果
clearContent();
});
});
// 文件选择事件处理
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
if (file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || file.name.endsWith('.docx')) {
currentFile = file;
processBtn.disabled = false;
clearBtn.disabled = false;
status.textContent = `已选择文件: ${file.name}`;
status.style.color = '#27ae60';
} else {
status.textContent = '请选择 .docx 格式的文件';
status.style.color = '#e74c3c';
currentFile = null;
processBtn.disabled = true;
}
}
});
// 处理按钮事件处理
processBtn.addEventListener('click', async () => {
if (!currentFile) return;
try {
status.textContent = `正在${currentMode === 'preview' ? '预览' : '提取'}文档...`;
status.style.color = '#3498db';
result.innerHTML = `<div class="loading">正在${currentMode === 'preview' ? '预览' : '提取'}文档...</div>`;
if (currentMode === 'preview') {
// 使用 docx-preview 预览文档
await processPreview();
} else {
// 使用 mammoth 提取内容
await processExtract();
}
} catch (error) {
console.error('处理失败:', error);
status.textContent = '文档处理失败,请检查文件格式';
status.style.color = '#e74c3c';
result.innerHTML = `<div class="error">处理失败: ${error.message}</div>`;
}
});
// 预览模式处理
async function processPreview() {
if (!docxPreview) {
throw new Error('docx-preview 库未加载完成');
}
await docxPreview(
currentFile, // 文件 Blob 对象
result, // 渲染容器
(progress) => {
// 显示渲染进度
status.textContent = `预览进度: ${Math.round(progress * 100)}%`;
},
{
className: 'docx', // 自定义 CSS 类名
inWrapper: true, // 包裹内容
breakPages: true, // 分页展示
renderHeaders: true, // 渲染页眉
renderFooters: true, // 渲染页脚
renderComments: false, // 不渲染批注
useBase64URL: false, // 不使用 base64 URL
experimental: true // 启用实验性功能
}
);
status.textContent = '文档预览完成';
status.style.color = '#27ae60';
}
// 提取模式处理
async function processExtract() {
if (!mammoth) {
// 动态加载 mammoth
await new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/mammoth@1.9.1/mammoth.browser.min.js';
script.onload = () => {
mammoth = window.mammoth;
resolve();
};
script.onerror = reject;
document.head.appendChild(script);
});
}
// 读取文件为 ArrayBuffer
const arrayBuffer = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => resolve(event.target.result);
reader.onerror = reject;
reader.readAsArrayBuffer(currentFile);
});
// 配置选项
const options = {
ignoreEmptyParagraphs: true,
styleMap: [
"p[style-name='注意事项'] => div.warning",
"p[style-name='提示'] => div.tip",
"p[style-name='标题 1'] => h1",
"p[style-name='标题 2'] => h2",
"p[style-name='标题 3'] => h3"
],
convertImage: mammoth.images.imgElement(function(image) {
return image.readAsArrayBuffer().then(function(buffer) {
const blob = new Blob([buffer], { type: image.contentType });
return {
src: URL.createObjectURL(blob),
alt: "文档图片",
class: "doc-image"
};
});
})
};
// 转换文档
const mammothResult = await mammoth.convertToHtml({ arrayBuffer }, options);
// 显示结果
result.innerHTML = mammothResult.value;
htmlCode.textContent = mammothResult.value;
status.textContent = `内容提取完成,共 ${mammothResult.messages.length} 条提示`;
status.style.color = '#27ae60';
}
// 清除按钮事件处理
clearBtn.addEventListener('click', clearContent);
// 清除内容函数
function clearContent() {
result.innerHTML = '<div class="loading">请上传 .docx 文件开始处理</div>';
htmlCode.textContent = '';
htmlOutput.classList.add('hidden');
if (currentMode === 'extract') {
htmlOutput.classList.remove('hidden');
}
fileInput.value = '';
currentFile = null;
processBtn.disabled = true;
clearBtn.disabled = true;
status.textContent = '请选择一个 .docx 文件';
status.style.color = '#333';
}
</script>
</body>
</html>
7.3.4 操作步骤
- 将上述代码保存为
docx-combined-demo.html - 用现代浏览器打开该文件
- 选择处理模式:
- 预览模式:使用 docx-preview 实现像素级还原的 Word 文档预览
- 提取模式:使用 mammoth 将 Word 文档转换为结构化 HTML
- 点击"选择文件"按钮,选择一个
.docx格式的文档 - 点击"处理文档"按钮开始处理
- 查看处理结果
- 可以点击"清除内容"按钮重新选择文件
7.3.5 预期效果
- 预览模式:在结果区域显示接近原生 Word 格式的文档预览,支持分页、页眉页脚
- 提取模式:在结果区域显示结构化的 HTML 内容,下方显示生成的 HTML 代码,便于二次编辑
- 提供完整的状态反馈,包括文件选择、处理进度、处理结果等
- 支持动态切换处理模式,无需重新加载页面
7.3.6 调试技巧与常见问题
-
库加载失败
- 检查网络连接是否正常
- 确保浏览器支持 ES6 模块
- 查看浏览器控制台是否有加载错误
-
处理结果不符合预期
- 检查选择的处理模式是否正确
- 确认文件格式为
.docx(注意区分.doc和.docx) - 查看浏览器控制台的错误信息
-
性能优化建议
- 对于大文件,建议先使用较小的测试文件进行调试
- 预览模式下可以尝试关闭分页(修改代码中的
breakPages: false) - 提取模式下可以关闭图片处理以提高速度