DOCX 预览技术docx-preview和mammoth

3 阅读9分钟

DOCX 预览

2. 核心开源库概述

库名称核心定位实现思路主要优势
docx-preview像素级还原预览直接解析 DOCX 内部结构,渲染为 HTML/CSS高度还原格式,支持分页、页眉页脚
mammoth内容提取与转换提取语义结构,转换为干净的 HTML生成结构化 HTML,便于二次编辑

3. docx-preview 详细解析

3.1 技术原理

docx-preview 采用直接解析 DOCX 文件内部结构的方式,将 Word 文档的 XML 内容转换为浏览器可渲染的 HTML 和 CSS。其核心工作流程包括:

  1. 文件解析:读取 DOCX 文件(本质是 ZIP 压缩包),提取内部的 XML 结构文件
  2. 内容转换:将 Word 的 XML 格式转换为中间表示
  3. 样式映射:将 Word 样式转换为 CSS 样式
  4. 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 核心配置参数

参数名类型默认值说明
classNamestring"docx"自定义 CSS 类名前缀,用于样式定制
inWrapperbooleantrue是否将内容包裹在容器中
ignoreWidthbooleanfalse是否忽略原始页面宽度
ignoreHeightbooleanfalse是否忽略原始页面高度
ignoreFontsbooleanfalse是否忽略字体定义,使用浏览器默认字体
breakPagesbooleantrue是否分页展示,设置为 false 可滚动显示
ignoreLastRenderedPageBreakbooleantrue是否忽略文档中的分页标签
renderHeadersbooleantrue是否渲染页眉内容
renderFootersbooleantrue是否渲染页脚内容
renderFootnotesbooleantrue是否渲染脚注
renderEndnotesbooleantrue是否渲染尾注
renderCommentsbooleanfalse是否渲染批注内容
useBase64URLbooleanfalse图片资源是否使用 base64 URL
useMathMLPolyfillbooleanfalse是否启用公式渲染补丁
experimentalbooleanfalse是否启用实验性功能(如制表符支持)
trimXmlDeclarationbooleantrue是否去除 XML 声明头
debugbooleanfalse是否启用调试模式,输出详细日志

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。其工作流程包括:

  1. 文件解析:读取 DOCX 文件,提取内部 XML 结构
  2. 语义分析:分析文档的语义结构,识别标题、段落、列表等元素
  3. 样式映射:根据配置将 Word 样式映射到 HTML 标签和类名
  4. 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 核心配置参数

参数名类型默认值说明
styleMapstring / arrayWord 样式到 HTML 的映射规则
includeEmbeddedStyleMapbooleantrue是否包含文档内嵌的样式映射
includeDefaultStyleMapbooleantrue是否结合默认样式映射一起生效
convertImagefunctiontrue图片处理策略,默认转为 base64
ignoreEmptyParagraphsbooleantrue是否忽略空段落
idPrefixstring""生成 ID 的前缀(如脚注)
transformDocumentfunctionHTML 渲染前修改文档结构的回调

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-previewmammoth
核心定位格式还原内容提取
输出质量视觉还原度高结构清晰度高
文件大小生成的 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 前端优化策略
  1. 懒加载:只渲染可见区域的内容,滚动时动态加载其他部分
  2. 虚拟滚动:对于长文档,只渲染当前视口内的内容
  3. 图片优化:使用适当的图片格式和压缩算法,实现图片懒加载
  4. 代码分割:将预览功能打包为独立 chunk,按需加载
  5. Web Worker:将文档解析和转换放在 Web Worker 中执行,避免阻塞主线程
6.2.2 后端优化策略(可选)
  1. 预转换:提前将常用文档转换为 HTML 或 PDF 格式
  2. 缓存机制:缓存转换结果,避免重复转换
  3. 分布式处理:使用分布式架构处理大量文档转换请求
  4. 异步处理:对于大文件转换,使用异步队列处理

6.3 最佳实践

  1. 根据场景选择合适的库:格式还原优先选择 docx-preview,内容提取优先选择 mammoth
  2. 合理配置优化性能:根据实际需求调整配置参数,避免不必要的功能开销
  3. 实现优雅降级:对于不支持的特性,提供合理的降级显示方案
  4. 优化用户体验:添加加载状态、错误处理、进度提示等
  5. 考虑跨浏览器兼容性:测试不同浏览器的渲染效果,确保一致的体验
  6. 定期更新依赖:关注库的更新,及时升级以获得更好的性能和兼容性

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 操作步骤
  1. 将上述代码保存为 docx-preview-demo.html
  2. 用现代浏览器打开该文件
  3. 点击"选择文件"按钮,选择一个 .docx 格式的文档
  4. 点击"渲染文档"按钮开始渲染
  5. 等待渲染完成后查看预览效果
  6. 可以点击"清除内容"按钮重新选择文件
7.1.5 预期效果
  • 上传文件前:预览区域显示"请上传 .docx 文件开始预览"
  • 选择文件后:显示文件名,"渲染文档"按钮变为可用
  • 渲染过程中:显示渲染进度百分比
  • 渲染成功:在预览区域显示格式化的 Word 文档,支持分页、页眉页脚
  • 渲染失败:显示错误信息
7.1.6 调试技巧与常见问题
  1. 渲染无反应

    • 检查浏览器控制台是否有错误信息
    • 确保使用的是支持 ES6 模块的现代浏览器
    • 确认文件格式正确(必须是 .docx 格式)
  2. 样式渲染异常

    • 检查是否有 CSS 冲突
    • 尝试调整 ignoreFontsignoreWidth 等配置参数
    • 查看控制台是否有资源加载错误
  3. 大文件渲染慢

    • 尝试使用较小的测试文件
    • 考虑调整 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 操作步骤
  1. 将上述代码保存为 mammoth-demo.html
  2. 用现代浏览器打开该文件
  3. 点击"选择文件"按钮,选择一个 .docx 格式的文档
  4. 根据需要调整转换选项(忽略空段落、使用 Blob URL 处理图片)
  5. 点击"提取内容"按钮开始转换
  6. 查看转换后的内容和生成的 HTML 代码
  7. 可以点击"清除内容"按钮重新选择文件
7.2.5 预期效果
  • 上传文件前:内容区域显示"请上传 .docx 文件开始提取"
  • 选择文件后:显示文件名,"提取内容"按钮变为可用
  • 转换过程中:显示"正在提取内容..."
  • 转换成功:在内容区域显示结构化的 HTML,下方显示生成的 HTML 代码
  • 转换失败:显示错误信息
7.2.6 调试技巧与常见问题
  1. 转换结果样式不符合预期

    • 调整 styleMap 配置,优化样式映射规则
    • 检查 Word 文档中的样式名称是否与配置匹配
    • 使用浏览器开发者工具查看生成的 HTML 结构
  2. 图片不显示

    • 确保图片处理选项正确配置
    • 检查浏览器控制台是否有图片加载错误
    • 尝试使用不同的图片格式和大小
  3. 转换速度慢

    • 尝试使用较小的测试文件
    • 关闭不必要的转换选项
    • 优化图片处理逻辑

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 操作步骤
  1. 将上述代码保存为 docx-combined-demo.html
  2. 用现代浏览器打开该文件
  3. 选择处理模式:
    • 预览模式:使用 docx-preview 实现像素级还原的 Word 文档预览
    • 提取模式:使用 mammoth 将 Word 文档转换为结构化 HTML
  4. 点击"选择文件"按钮,选择一个 .docx 格式的文档
  5. 点击"处理文档"按钮开始处理
  6. 查看处理结果
  7. 可以点击"清除内容"按钮重新选择文件
7.3.5 预期效果
  • 预览模式:在结果区域显示接近原生 Word 格式的文档预览,支持分页、页眉页脚
  • 提取模式:在结果区域显示结构化的 HTML 内容,下方显示生成的 HTML 代码,便于二次编辑
  • 提供完整的状态反馈,包括文件选择、处理进度、处理结果等
  • 支持动态切换处理模式,无需重新加载页面
7.3.6 调试技巧与常见问题
  1. 库加载失败

    • 检查网络连接是否正常
    • 确保浏览器支持 ES6 模块
    • 查看浏览器控制台是否有加载错误
  2. 处理结果不符合预期

    • 检查选择的处理模式是否正确
    • 确认文件格式为 .docx(注意区分 .doc.docx
    • 查看浏览器控制台的错误信息
  3. 性能优化建议

    • 对于大文件,建议先使用较小的测试文件进行调试
    • 预览模式下可以尝试关闭分页(修改代码中的 breakPages: false
    • 提取模式下可以关闭图片处理以提高速度