返回 导航

Vue.js

hangge.com

Vue.js - 使用Mammoth.js实现Word文档转换成HTML详解1(安装配置、基本用法)

作者:hangge | 2025-12-27 10:04
    项目中有时需要把用户上传的 .docxWord)文件直接在前端展示为干净的 HTML,这个功能可以借助 Mammoth.js 来实现。Mammoth.js 是个轻量、专注于从 Word 文档生成语义化 HTML(或纯文本)的好工具。下面我将演示如何在 Vue 2 项目中集成并使用它。

一、安装配置与基本用法

1,基本介绍

(1)Mammoth.js 是一个专注于将 Word 文档(.docx 格式)转换为 HTML 的开源 JavaScript 库,其核心优势在于轻量级架构与高度可配置性。

(2)Mammoth 的目标是通过使用文档中的语义信息并忽略其他细节来生成简单且干净的 HTML。例如,Mammoth 将任何具有标题 1 样式的段落转换为 h1 元素,而不是尝试精确复制标题的样式(字体、文本大小、颜色等)。
提示.docx 使用的结构与 HTML 的结构之间存在很大的不匹配,这意味着对于更复杂的文档来说,转换不太可能完美。如果开发者仅使用样式来对文档进行语义标记,那么 Mammoth 效果最佳。

(3)Mammoth.js 目前支持以下功能:
  • 标题、列表、评论
  • 从自己的 docx 样式到 HTML 的可定制映射。例如,可以通过提供适当的样式映射将 warningHeading 转换为 h1.warning
  • Tables:当前忽略表格本身的格式(例如:边框),但文本的格式与文档其余部分的格式相同。
  • 脚注和尾注、图片、粗体、斜体、下划线、删除线、上标和下标、链接、LineLine breaks
  • 文本框:文本框的内容被视为出现在包含文本框的段落之后的单独段落。

2,安装说明

我们可以在项目执行如下命令安装:
npm install mammoth --save

3,选择本地 DOCX 文件并进行转换

(1)下面样例是本地上传 .docx 并预览转换后的 HTML,同时显示转换警告信息。
<template>
  <div class="docx-converter">
    <h2>DOCX 转 HTML</h2>

    <input
      type="file"
      accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
      @change="onFileChange"
    />

    <div v-if="loading">正在转换……</div>

    <div v-if="messages.length">
      <h4>转换消息</h4>
      <ul>
        <li v-for="(m, idx) in messages" :key="idx">{{ m.type }}: {{ m.message }}</li>
      </ul>
    </div>

    <h4>预览(HTML)</h4>
    <!-- 请务必在生产环境中对 html 进行 XSS 清理(例如用 DOMPurify) -->
    <div class="preview" v-html="html"></div>
  </div>
</template>

<script>
// 推荐导入方式:如果报错请改为 `import mammoth from 'mammoth'` 
// 或 `import * as mammoth from 'mammoth'`
import * as mammoth from "mammoth";

export default {
  name: "DocxConverter",
  data() {
    return {
      html: "",
      messages: [],
      loading: false,
    };
  },
  methods: {
    // 监听文件选择事件
    async onFileChange(event) {
      const file = event.target.files && event.target.files[0];
      if (!file) return;

      if (!file.name.endsWith(".docx")) {
        alert("请上传 .docx 文件");
        return;
      }

      try {
        this.loading = true;
        const arrayBuffer = await file.arrayBuffer();

        // 示例 options:自定义 styleMap、并把图片内联为 base64
        const options = {
          styleMap: [
            // 把 Word 的 "Title" 样式映射为 h1
            "p[style-name='Title'] => h1:fresh",
            // 把强调文本映射为 <em>
            "i => em",
            // 需要的话可以添加更多映射
          ],
          convertImage: mammoth.images.inline(function (element) {
            // element.read("base64") 会返回 promise(图片的 base64)
            return element.read("base64").then(function (imageBuffer) {
              return {
                src: "data:" + element.contentType + ";base64," + imageBuffer,
              };
            });
          }),
        };

        const result = await mammoth.convertToHtml({ arrayBuffer }, options);

        // result.value 是转换后的 HTML 字符串
        // result.messages 是转换过程的警告、错误信息数组
        this.html = result.value;
        this.messages = result.messages || [];
      } catch (err) {
        console.error("转换失败:", err);
        alert("转换失败,请检查控制台错误信息。");
      } finally {
        this.loading = false;
      }
    },
  },
};
</script>

<style scoped>
.preview {
  border: 1px solid #ddd;
  padding: 12px;
  margin-top: 12px;
  max-height: 60vh;
  overflow: auto;
  background: #fff;
}

/* ====== 为标题样式添加样式 ====== */
::v-deep .preview h2,
::v-deep .preview h3,
::v-deep .preview h4 {
  margin-top: 24px;
  margin-bottom: 12px;
}

::v-deep .preview h2 {
  font-size: 1.5rem;
  font-weight: 600;
}

::v-deep .preview h3 {
  font-size: 1.3rem;
  font-weight: 600;
}

::v-deep .preview h4 {
  font-size: 1.2rem;
  font-weight: 600;
}

/* ====== 为 table 添加样式 ====== */
::v-deep .preview table {
  width: 100%;
  border-collapse: collapse;
  table-layout: auto;
  margin: 6px 0;
}

/* 单元格边框与内边距 */
::v-deep .preview table th,
::v-deep .preview table td {
  border: 1px solid #d1d5db; /* 灰色边框,可按需调整 #e5e7eb #cbd5e1 等 */
  padding: 8px 10px;
  vertical-align: middle;
}

/* 表头样式(可选) */
::v-deep .preview table th {
  background: #f9fafb;
  font-weight: 600;
}

/* 表格在窄屏时不要换行太多(可选) */
::v-deep .preview table td {
  word-break: break-word;
}

/* 如果使用 Tailwind 的 prose 可能会有默认的 table 样式,下面加个更高权重以覆盖 */
::v-deep .preview table,
::v-deep .preview table th,
::v-deep .preview table td {
  /* 提高优先级避免被 reset 覆盖 */
  border-color: #d1d5db;
}
</style>

(2)下面进行测试,首先我们准备一个 docx 格式的文档,内容如下:

(3)打开我们的页面,选择该文件。

(4)选择后就会自动将其转换成 html 并显示在页面上。
提示messages 会包含可能的警告(例如样式不支持等),利于调试或给用户提示。

4,从远程 URL 加载 DOCX 并转换

(1)下面样例演示如何从 URL 读取并渲染 DOCX
<template>
  <div class="docx-preview">
    <button @click="loadDocx">加载远程 DOCX</button>
    <div v-if="loading">加载中...</div>
    <div v-if="error" style="color:red">{{ error }}</div>
    <!-- 渲染 HTML -->
    <div v-html="htmlContent" class="preview"></div>
  </div>
</template>

<script>
import mammoth from "mammoth";

export default {
  data() {
    return {
      htmlContent: "",
      loading: false,
      error: ""
    };
  },
  methods: {
    // 加载远程 DOCX 文件
    async loadDocx() {
      this.loading = true;
      this.error = "";
      this.htmlContent = "";
      const url = "http://localhost:8080/data/sample.docx"; // 替换你的远程 URL

      try {
        // 1. 远程获取 DOCX 文件
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error("无法加载 DOCX 文件:" + response.statusText);
        }
        // 2. 转换成 ArrayBuffer(mammoth 必须用它)
        const arrayBuffer = await response.arrayBuffer();
        // 3. 使用 mammoth 转换为 HTML
        const result = await mammoth.convertToHtml(
          { arrayBuffer },
          {
            styleMap: [
              // 你可以在这里配置样式映射
              "p[style-name='Title'] => h1.title",
              "p[style-name='Subtitle'] => h2.subtitle"
            ]
          }
        );
        this.htmlContent = result.value; // HTML 内容
        console.log("转换信息:", result.messages); // 警告等提示
      } catch (err) {
        this.error = err.message || "加载失败";
      } finally {
        this.loading = false;
      }
    }
  }
};
</script>

<style scoped>
button {
  margin-bottom: 16px;
  padding: 8px 16px;
  background: #4caf50;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.preview {
  border: 1px solid #ddd;
  padding: 12px;
  margin-top: 12px;
  max-height: 60vh;
  overflow: auto;
  background: #fff;
}

/* ====== 为标题样式添加样式 ====== */
::v-deep .preview h2,
::v-deep .preview h3,
::v-deep .preview h4 {
  margin-top: 24px;
  margin-bottom: 12px;
}

::v-deep .preview h2 {
  font-size: 1.5rem;
  font-weight: 600;
}

::v-deep .preview h3 {
  font-size: 1.3rem;
  font-weight: 600;
}

::v-deep .preview h4 {
  font-size: 1.2rem;
  font-weight: 600;
}

/* ====== 为 table 添加样式 ====== */
::v-deep .preview table {
  width: 100%;
  border-collapse: collapse;
  table-layout: auto;
  margin: 6px 0;
}

/* 单元格边框与内边距 */
::v-deep .preview table th,
::v-deep .preview table td {
  border: 1px solid #d1d5db; /* 灰色边框,可按需调整 #e5e7eb #cbd5e1 等 */
  padding: 8px 10px;
  vertical-align: middle;
}

/* 表头样式(可选) */
::v-deep .preview table th {
  background: #f9fafb;
  font-weight: 600;
}

/* 表格在窄屏时不要换行太多(可选) */
::v-deep .preview table td {
  word-break: break-word;
}

/* 如果使用 Tailwind 的 prose 可能会有默认的 table 样式,下面加个更高权重以覆盖 */
::v-deep .preview table,
::v-deep .preview table th,
::v-deep .preview table td {
  /* 提高优先级避免被 reset 覆盖 */
  border-color: #d1d5db;
}
</style>

(2)打开页面,点击“价值远程 DOCX”按钮就可以从设置的 URL 地址读取 DOCX 并转换为 HTML 显示了。

附:自定义 styleMap 实现样式映射

1,基本介绍

(1)styleMap 是一组映射规则,用来把 Word 中使用的样式(Style)映射成我们想要的 HTML 标签或带类名的元素。
注意styleMap 只能作用于使用了 Word 样式(Style)的文本。对那些在 Word 中直接用字体面板手动改字号/颜色的“直接格式”无效
(2)styleMap 优点如下:
  • 把语义(Heading/Quote/Caption)保留下来,而不是直接把视觉样式(字号/颜色)硬编码到 HTML
  • 前端可以用统一 CSS 控制外观,保证页面风格一致。
  • 能减少 HTML 中的“内联样式噪音”,生成更干净、可维护的输出。

2,样例代码

    下面是一份比较全面的 styleMap 映射样例,覆盖标题、正文、引用、代码、表格、图注、列表与字符样式,并对没有使用样式的文档提供补救(粗体/斜体等)
<template>
  <div class="docx-converter">
    <h2>StyleMap样例</h2>

    <input type="file" 
      accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
      @change="onFileChange" />

    <div v-if="loading">正在转换……</div>

    <div v-if="messages.length">
      <h4>转换消息</h4>
      <ul>
        <li v-for="(m, idx) in messages" :key="idx">{{ m.type }}: {{ m.message }}</li>
      </ul>
    </div>

    <h4>预览(HTML)</h4>
    <!-- 请务必在生产环境中对 html 进行 XSS 清理(例如用 DOMPurify) -->
    <div class="preview" v-html="html"></div>
  </div>
</template>

<script>
// 推荐导入方式:如果报错请改为 `import mammoth from 'mammoth'` 
// 或 `import * as mammoth from 'mammoth'`
import * as mammoth from "mammoth";

export default {
  name: "DocxConverter",
  data() {
    return {
      html: "",
      messages: [],
      loading: false,
    };
  },
  methods: {
    // 监听文件选择事件
    async onFileChange(event) {
      const file = event.target.files && event.target.files[0];
      if (!file) return;

      if (!file.name.endsWith(".docx")) {
        alert("请上传 .docx 文件");
        return;
      }

      try {
        this.loading = true;
        const arrayBuffer = await file.arrayBuffer();

        // 自定义 styleMap
        const styleMap = [
          // 段落级样式(Paragraph)
          "p[style-name='Title'] => h1.doc-title:fresh",
          "p[style-name='Subtitle'] => h2.doc-subtitle:fresh",
          "p[style-name='Heading 1'] => h2.heading-1:fresh",
          "p[style-name='Heading 2'] => h3.heading-2:fresh",
          "p[style-name='Body Text'] => p.body-text:fresh",
          "p[style-name='列出段落1'] => p.body-text:fresh",
          "p[style-name='Quote'] => blockquote.doc-quote:fresh",
          "p[style-name='Caption'] => figcaption.doc-caption:fresh",
          "p[style-name='Code'] => pre > code.doc-code:fresh",

          // 列表(如果文档里用样式做了列表项)
          "p[style-name='List Paragraph'] => ul > li:fresh",

          // 表格样式
          "table[style-name='Table Grid'] => table.doc-table",
          "table[style-name='Light Shading'] => table.doc-table--shaded",

          // 运行级(字符样式)
          "r[style-name='Strong'] => strong",
          "r[style-name='Emphasis'] => em",
          "r[style-name='Monospace'] => code",

          // 捕获常见直接格式(补救:当文档没有使用样式)
          "b => strong",
          "i => em",
          "u => u",
          "strike => del"
        ];

        // 示例 options:自定义 styleMap、并把图片内联为 base64
        const options = {
          styleMap,
          convertImage: mammoth.images.inline(function (element) {
            // element.read("base64") 会返回 promise(图片的 base64)
            return element.read("base64").then(function (imageBuffer) {
              return {
                src: "data:" + element.contentType + ";base64," + imageBuffer,
              };
            });
          }),
        };

        const result = await mammoth.convertToHtml({ arrayBuffer }, options);

        // result.value 是转换后的 HTML 字符串
        // result.messages 是转换过程的警告、错误信息数组
        this.html = result.value;
        this.messages = result.messages || [];
      } catch (err) {
        console.error("转换失败:", err);
        alert("转换失败,请检查控制台错误信息。");
      } finally {
        this.loading = false;
      }
    },
  },
};
</script>

<style scoped>
.preview {
  border: 1px solid #ddd;
  padding: 12px;
  margin-top: 12px;
  max-height: 60vh;
  overflow: auto;
  background: #fff;
}

::v-deep .preview .doc-title {
  font-size: 28px;
  font-weight: 700;
  margin: 0 0 12px;
}

::v-deep .preview .doc-subtitle {
  font-size: 18px;
  color: #666;
  margin: 0 0 8px;
}

::v-deep .preview .heading-1 {
  font-size: 22px;
  margin: 10px 0;
}

::v-deep .preview .heading-2 {
  font-size: 18px;
  margin: 8px 0;
}

::v-deep .preview .lead,
.body-text {
  font-size: 14px;
  line-height: 1.7;
  margin: 8px 0;
}

::v-deep .preview .doc-quote {
  border-left: 4px solid #eee;
  padding-left: 12px;
  color: #555;
  margin: 10px 0;
}

::v-deep .preview .doc-code {
  background: #f6f6f6;
  padding: 10px;
  border-radius: 4px;
  font-family: monospace;
}

::v-deep .preview .doc-table {
  border-collapse: collapse;
  width: 100%;
}

::v-deep .preview .doc-table th,
::v-deep .preview .doc-table td {
  border: 1px solid #ddd;
  padding: 6px 8px;
}

</style>
评论

全部评论(0)

回到顶部