跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
TypeScript大前端

前端使用 docx 库实现 HTML 页面导出 Word 文档

综述由AI生成针对 HTML 页面导出 Word 并支持混合方向排版的需求,利用 docx 库配合自定义解析逻辑实现。通过给 DOM 节点添加 orient 属性区分页面方向,递归遍历子节点生成 Paragraph、Table 等元素,最终打包下载。代码示例展示了 Vue 组件结构与核心转换函数,解决了样式保留与分页控制问题。

奶糖兔发布于 2025/11/30更新于 2026/6/1027 浏览
前端使用 docx 库实现 HTML 页面导出 Word 文档

前言

最近遇到一个需求,需要将页面的 html 导出为 word 文档,并且包含横向和竖向页面,并且可以进行混合方向导出。经过一段时间的实验,发现只有 docx 这个库满足这个要求。在这里记录一下实现思路以及代码。

docx 官网

一、效果展示

页面内容:

在这里插入图片描述

导出样式:

在这里插入图片描述

二、解决思路

  1. 首先在页面上设置哪些部分是需要横向导出,哪些部分是需要竖向导出的,以方便后面进行解析。
  2. 根据页面样式以及各类 html 标签进行解析,然后以 docx 的形式生成,最后导出来。

三、实现代码

1、index.vue

这里 class 中的 section 代表了 docx 中的一节,也就是一个页面。同时 newpage 属性控制了是不是要换一个新页,orient 属性是页面横向纵向的标识(Z 纵向 H 横向)。也可以根据自己的需求自行添加属性,在后面自己进行对应的解析。

<template>
  <div>
    <el-row>
      <el-col :span="24">
        <div>
          <el-button type="primary" @click="exportToWord" style="float: right">导出</el-button>
        </div>
        <div style="overflow-y: auto; height: calc(85vh)" id="export">
          <div class="section" orient="Z">
            <h1 style="text-align: center">这里是标题 1</h1>
          </div>
          <div class="section" orient="Z" newpage="true">
            <h2 style="text-align: center">这里是标题 2</h2>
            <h3 style="text-align: center">这里是标题 3</h3>
          </div>
          <div class="section" orient="Z">
            <p>这里是一段文字内容</p>
          </div>
          <div class="section" orient="Z">
            <el-table :data="tableData" :span-method="arraySpanMethod" border style="width: 100%">
              <el-table-column prop="id" label="ID" width="180" header-align="center" align="left"/>
              <el-table-column prop="name" label="姓名" width="" header-align="center" align="left"/>
              <el-table-column prop="amount1" label="列 1" width="" header-align="center" align="center"/>
              <el-table-column prop="amount2" label="列 2" width="" header-align="center" align="right"/>
              <el-table-column prop="amount3" label="列 3" width="" header-align="center" align="left"/>
            </el-table>
          </div>
          <div class="section" orient="H">
            <p>这里是横向页面内容</p>
          </div>
          <div class="section" orient="Z">
            <p>这里是纵向页面内容</p>
          </div>
        </div>
      </el-col>
    </el-row>
  </div>
</template>
<script lang="ts" setup name="">
// 导出用
import * as htmlDocx from 'html-docx-js-typescript';
import { saveAs } from 'file-saver';
import { exportDocxFromHTML } from '@/utils/exportWord';

// 导出 word
const exportToWord = async () => {
  let contentElement = document.getElementById('export') as HTMLElement;
  // 克隆元素 操作新元素
  let newDiv = contentElement.cloneNode(true) as HTMLElement;
  // 这里可以对 newDiv 进行一些操作...
  exportDocxFromHTML(newDiv, `test.docx`);
};

import type { TableColumnCtx } from 'element-plus';

interface User {
  id: string;
  name: string;
  amount1: string;
  amount2: string;
  amount3: number;
}

interface SpanMethodProps {
  row: User;
  column: TableColumnCtx<User>;
  rowIndex: number;
  columnIndex: number;
}

const tableData: User[] = [
  { id: '12987122', name: 'Tom', amount1: '234', amount2: '3.2', amount3: 10 },
  { id: '12987123', name: 'Tom', amount1: '165', amount2: '4.43', amount3: 12 },
  { id: '12987124', name: 'Tom', amount1: '324', amount2: '1.9', amount3: 9 },
  { id: '12987125', name: 'Tom', amount1: '621', amount2: '2.2', amount3: 17 },
  { id: '12987126', name: 'Tom', amount1: '539', amount2: '4.1', amount3: 15 },
];

const arraySpanMethod = ({ row, column, rowIndex, columnIndex }: SpanMethodProps) => {
  if (rowIndex % 2 === 0) {
    if (columnIndex === 0) {
      return [1, 2];
    } else if (columnIndex === 1) {
      return [0, 0];
    }
  }
};

onMounted(async () => {});
</script>
<style lang="scss" scoped></style>

2、exportWord.ts

这个部分是进行了 html 转换成 docx 形式的拼接组合。可以根据理解自行调整样式以及解析过程。

import {
  Document,
  Packer,
  Paragraph,
  TextRun,
  ImageRun,
  ExternalHyperlink,
  WidthType,
  VerticalAlign,
  AlignmentType,
  PageOrientation,
  HeadingLevel,
  Table,
  TableRow,
  TableCell,
  BorderStyle,
} from 'docx';
import { saveAs } from 'file-saver';
import { ElMessageBox, ElMessage } from 'element-plus';

/**
 * 字符串是否为空
 * @param {*} obj
 * @returns
 */
export function isEmpty(obj: any) {
  if (typeof obj === 'undefined' || obj == null || obj === '') {
    return true;
  } else {
    return false;
  }
}

// 定义类型
type DocxElement = Paragraph | Table | TextRun | ImageRun | ExternalHyperlink;

// 保存图片,表格,列表
type ExportOptions = {
  includeImages: boolean;
  includeTables: boolean;
  includeLists: boolean;
};

const includeImages = ref(true);
const includeTables = ref(true);
const includeLists = ref(true);

// 保存样式对象
type StyleOptions = {
  bold: boolean; // 是否加粗
  font: Object; // 字体样式
  size: number; // 字体大小
  id: String | null; // 样式 id
};

// 横向 A4
export const H_properties_A4 = {
  page: {
    size: {
      width: 15840, // A4 横向宽度 (11 英寸)
      height: 12240, // A4 横向高度 (8.5 英寸)
    },
  },
};

// 纵向 A4
export const Z_properties_A4 = {
  page: {
    size: {
      width: 12240, // A4 纵向宽度 (8.5 英寸 * 1440 twip/inch)
      height: 15840, // A4 纵向高度 (11 英寸 * 1440)
    },
    orientation: PageOrientation.LANDSCAPE,
  },
};

// 根据 html 生成 word 文档
export const exportDocxFromHTML = async (htmlDom: any, filename: any) => {
  let sections = [] as any; // 页面数据
  let doms = htmlDom.querySelectorAll('.section');
  try {
    const options: ExportOptions = {
      includeImages: includeImages.value,
      includeTables: includeTables.value,
      includeLists: includeLists.value,
    };
    let preorient = 'Z';
    for (let i = 0; i < doms.length; i++) {
      let dom = doms[i];
      let orient = dom.getAttribute('orient');
      let newpage = dom.getAttribute('newpage');
      if (orient == preorient && newpage != 'true' && sections.length > 0) {
        // 方向一致且不分页,继续从上一个 section 节添加
        // 获取子节点
        let childNodes = dom.childNodes;
        // 递归处理所有节点
        let children = [];
        for (let j = 0; j < childNodes.length; j++) {
          const node = childNodes[j];
          const result = await parseNode(node, options, null);
          children.push(...result);
        }
        if (sections[sections.length - 1].children && children.length > 0) {
          for (let c = 0; c < children.length; c++) {
            let one = children[c];
            sections[sections.length - 1].children.push(one);
          }
        }
      } else {
        // 否则则新开一个 section 节
        // 获取子节点
        let childNodes = dom.childNodes;
        // 递归处理所有节点
        let children = [];
        for (let j = 0; j < childNodes.length; j++) {
          const node = childNodes[j];
          const result = await parseNode(node, options, null);
          children.push(...result);
        }
        let section = {
          properties: orient == 'H' ? H_properties_A4 : Z_properties_A4,
          children: children,
        };
        sections.push(section);
        preorient = orient;
      }
    }
    if (sections.length > 0) {
      // 创建 Word 文档
      const doc = new Document({
        styles: {
          default: {
            heading1: {
              // 宋体 二号
              run: {
                size: 44,
                bold: true,
                italics: true,
                color: '000000',
                font: '宋体',
              },
              paragraph: {
                spacing: {
                  after: 120,
                },
              },
            },
            heading2: {
              // 宋体 小二
              run: {
                size: 36,
                bold: true,
                color: '000000',
                font: '宋体',
              },
              paragraph: {
                spacing: {
                  before: 240,
                  after: 120,
                },
              },
            },
            heading3: {
              // 宋体 四号
              run: {
                size: 28,
                bold: true,
                color: '000000',
                font: '宋体',
              },
              paragraph: {
                spacing: {
                  before: 240,
                  after: 120,
                },
              },
            },
            heading4: {
              // 宋体
              run: {
                size: 24,
                bold: true,
                color: '000000',
                font: '宋体',
              },
              paragraph: {
                spacing: {
                  before: 240,
                  after: 120,
                },
              },
            },
            heading5: {
              run: {
                size: 20,
                bold: true,
                color: '000000',
                font: '宋体',
              },
              paragraph: {
                spacing: {
                  before: 240,
                  after: 120,
                },
              },
            },
          },
          paragraphStyles: [
            {
              id: 'STx4Style', // 样式 ID
              name: '宋体小四号样式', // 可读名称
              run: {
                font: '宋体', // 字体
                size: 24, // 字号
              },
              paragraph: {
                spacing: {
                  line: 360, // 1.5 倍行距 (240*1.5=360)
                }, // indent: { firstLine: 400 }, // 首行缩进 400twips(约 2 字符)
              },
            },
            {
              id: 'THStyle', // 样式 ID
              name: '表头样式', // 可读名称
              run: {
                font: '等线', // 字体
                size: 20.5, // 字号
              },
              paragraph: {
                spacing: {
                  before: 240,
                  after: 120,
                },
              },
            },
            {
              id: 'TDStyle', // 样式 ID
              name: '单元格样式', // 可读名称
              run: {
                font: '等线', // 字体
                size: 20.5, // 字号
              },
            },
          ],
        },
        sections: sections,
      });
      // 生成并下载文档
      await Packer.toBlob(doc).then((blob) => {
        saveAs(blob, filename);
      });
    } else {
      ElMessage.error('导出失败,该页面没有要导出的信息!');
    }
  } catch (error) {
    console.error('导出失败:', error);
    ElMessage.error('导出失败,请联系管理人员!');
  }
};

// 递归转换 DOM 节点为 docx 元素
export const parseNode = async (
  node: Node,
  options: ExportOptions,
  style: any
): Promise<DocxElement[]> => {
  const elements: DocxElement[] = [];
  // 1、处理文本节点
  if (node.nodeType === Node.TEXT_NODE) {
    const text = node.textContent?.trim();
    if (!isEmpty(text)) {
      const parent = node.parentElement;
      if (style == null) {
        let child = new TextRun({ text: text });
        elements.push(child);
      } else {
        const isBold = style.bold ? true : parent?.tagName === 'STRONG' || parent?.tagName === 'B';
        const Font = style.font ? style.font : '宋体';
        const Size = style.size ? style.size : 24;
        if (!isEmpty(style.id)) {
          let child = new TextRun({ text: text, style: style.id });
          elements.push(child);
        } else {
          let child = new TextRun({ text: text, bold: isBold, font: Font, size: Size });
          elements.push(child);
        }
      }
    }
    return elements;
  }
  // 2、处理元素节点
  if (node.nodeType === Node.ELEMENT_NODE) {
    const element = node as HTMLElement;
    const tagName = element.tagName.toUpperCase();
    const childNodes = element.childNodes;
    // 递归处理子节点
    let childElements: DocxElement[] = [];
    for (let i = 0; i < childNodes.length; i++) {
      const child = childNodes[i];
      if (tagName == 'A') {
        if (style == null) {
          style = { id: 'Hyperlink' };
        } else {
          style.id = 'Hyperlink';
        }
      }
      const childResult = await parseNode(child, options, style);
      childElements = childElements.concat(childResult);
    }
    // 根据标签类型创建不同的 docx 元素
    switch (tagName) {
      case 'H1':
        return [
          newParagraph({
            heading: HeadingLevel.HEADING_1,
            alignment: AlignmentType.CENTER,
            children: childElements.filter((e) => e instanceof TextRun) as TextRun[],
          }),
        ];
      case 'H2':
        return [
          newParagraph({
            heading: HeadingLevel.HEADING_2,
            alignment: AlignmentType.CENTER,
            children: childElements.filter((e) => e instanceof TextRun) as TextRun[],
          }),
        ];
      case 'H3':
        return [
          newParagraph({
            heading: HeadingLevel.HEADING_3,
            alignment: AlignmentType.LEFT,
            children: childElements.filter((e) => e instanceof TextRun) as TextRun[],
          }),
        ];
      case 'H4':
        return [
          newParagraph({
            heading: HeadingLevel.HEADING_4,
            alignment: AlignmentType.LEFT,
            children: childElements.filter((e) => e instanceof TextRun) as TextRun[],
          }),
        ];
      case 'H5':
        return [
          newParagraph({
            heading: HeadingLevel.HEADING_5,
            alignment: AlignmentType.LEFT,
            children: childElements.filter((e) => e instanceof TextRun) as TextRun[],
          }),
        ];
      case 'P':
        return [
          newParagraph({
            children: childElements.filter((e) => e instanceof TextRun) as TextRun[],
            style: 'STx4Style', // 应用样式 ID
          }),
        ];
      case 'BR':
        return [newTextRun({ text: '', break: 1 })];
      case 'A':
        const href = element.getAttribute('href');
        if (href) {
          return [
            newParagraph({
              children: [
                newExternalHyperlink({
                  children: childElements.filter((e) => e instanceof TextRun) as TextRun[],
                  link: href,
                }),
              ],
            }),
          ];
        } else {
          return childElements.filter((e) => e instanceof TextRun) as TextRun[];
        }
      case 'TABLE':
        return getTable(element, options);
      default:
        return childElements;
    }
  }
  return elements;
};

// 获取一个表格
export const getTable = async (element: any, options: ExportOptions) => {
  if (!options.includeTables) {
    return [];
  } else {
    const rows = Array.from(element.rows);
    const tableRows = rows.map((row: any) => {
      const cells = Array.from(row.cells);
      const tableCells = cells.map(async (cell: any, index: any) => {
        let textAlign = cell.style.textAlign; // 居中/居左
        let width = (cell.style.width + '').replace('%', ''); // 宽度
        let classlist = Array.from(cell.classList);
        if (classlist && classlist.length > 0) {
          if (classlist.indexOf('is-left') > -1) {
            textAlign = 'left';
          } else if (classlist.indexOf('is-center') > -1) {
            textAlign = 'center';
          } else if (classlist.indexOf('is-right') > -1) {
            textAlign = 'right';
          }
        }
        const cellChildren = [];
        for (let i = 0; i < cell.childNodes.length; i++) {
          let childNode = cell.childNodes[i];
          if (cell.tagName == 'TH') {
            const styleoption: StyleOptions = {
              bold: true,
              font: '等线',
              size: 21,
              id: null,
            };
            const result = await parseNode(childNode, options, styleoption);
            cellChildren.push(
              newParagraph({
                alignment:
                  textAlign == 'center'
                    ? AlignmentType.CENTER
                    : textAlign == 'right'
                    ? AlignmentType.RIGHT
                    : AlignmentType.LEFT, // 水平居中/居右/居左
                children: result,
                style: 'THStyle',
              })
            );
          } else {
            const styleoption: StyleOptions = {
              bold: false,
              font: '等线',
              size: 21,
              id: null,
            };
            const result = await parseNode(childNode, options, styleoption);
            cellChildren.push(
              newParagraph({
                alignment:
                  textAlign == 'center'
                    ? AlignmentType.CENTER
                    : textAlign == 'right'
                    ? AlignmentType.RIGHT
                    : AlignmentType.LEFT, // 水平居中/居右/居左
                children: result,
                style: 'TDStyle',
              })
            );
          }
        }
        // 动态判断是否合并
        return newTableCell({
          rowSpan: cell.rowSpan,
          columnSpan: cell.colSpan,
          verticalAlign: VerticalAlign.CENTER,
          verticalMerge: cell.rowSpan > 1 ? 'restart' : undefined,
          width: {
            size: parseFloat(width), // 设置第一列宽度为 250
            type: WidthType.PERCENTAGE, //WidthType.DXA, // 单位为 twip (1/20 of a point)
          },
          children: cellChildren.filter((e) => e instanceof Paragraph) as Paragraph[],
        });
      });
      return Promise.all(tableCells).then((cells) => {
        return newTableRow({ children: cells });
      });
    });
    return Promise.all(tableRows).then((rows) => {
      return [
        newTable({
          rows: rows,
          width: {
            size: 100,
            type: WidthType.PERCENTAGE,
          },
        }),
      ];
    });
  }
};

目录

  1. 前言
  2. 一、效果展示
  3. 二、解决思路
  4. 三、实现代码
  5. 1、index.vue
  6. 2、exportWord.ts
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Windows 10 安装 OpenJDK 8 指南
  • 时间序列预测架构演进:从传统深度学习到大模型时代
  • C++ 测试与调试实战:保障代码质量与稳定性
  • WorkBuddy 安装使用完全指南:腾讯 AI 桌面智能体工作台
  • 算力虚拟化开源分析:以 Flex:ai 为例谈工程交付边界
  • 双指针专题:复写零问题模拟
  • 数据结构:选择排序算法详解(直接、树形、堆排序)
  • OpenClaw 集成飞书搭建 AI 机器人指南
  • STL 容器适配器 stack 与 queue 底层模拟及算法实战
  • SpringBoot 整合 Langchain4j RAG 技术深度使用解析
  • AI 编程零基础入门:从原理认知到工程化实战体系
  • Java AI 插件安装及开发使用指南
  • 本地部署大语言模型:实用工具与操作指南
  • 递归经典实战:汉诺塔、链表操作与快速幂详解
  • 基于 Uniapp 与 SSM 的供销 APP 购物商城系统设计与实现
  • Web 可访问性最佳实践:构建包容性的数字体验
  • C++ queue 类源码实现与逻辑详解
  • Jupyter Notebook 安装教程:Python 3.10 版本
  • Flutter eip55 库在鸿蒙系统的适配与以太坊地址校验实战
  • AI 绘画提示词参数设置与生成效果实战指南

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online