Vue3实现前端生成Word并下载的全过程

 更新时间:2026年01月05日 08:40:50   作者:外啫啫  
本文介绍了前端实现Word文件导出功能的技术方案,通过使用docxtemplater、JSZip等库,支持单个文件导出和批量压缩包下载功,同时总结了常见问题及解决方法,需要的朋友可以参考下

1. 前情

在后台管理平台开发过程中,有这样一个需求:从后端获取到数据后,前端生成指定模板内容的word文件并下载到本地。当导出的文件过多时,为了减少响应时间,也加上了进行压缩下载部分。使用过程中可根据实际需求作出调整。

2. 准备

依赖

npm install docxtemplater
 
npm install jszip
 
npm install pizzip
 
npm install file-saver
 
npm install jszip-utils
 
npm install angular-expressions

模板文件

需要按导出样式准备一个模板文件,放到 public 目录下。模板中占位符包含以下部分:

  • 单一变量:{变量名}。直接显示改变量的值。
  • 遍历:{#变量名}内容{/变量名}。会对该数据进行遍历展示
  • 显示/隐藏:{#变量名}内容{/变量名}。其中为true的时候显示,false则不显示。
  • 图片:{%变量名}。变量值为base64格式。
  • if-else:{#变量名}A{/变量名}{^变量名}B{/变量名}。其中值为true的时候显示“A”,为false显示“B”;
  • 复选框:{#变量名}选中{/变量名}{^变量名}非选中{/变量名} 以上部分是我在实际需求中使用到的,其他使用可参考:docxtemplater.com/docs/
let obj={
    name:'张三',
    age:12,
    hobby:['basketball','swimming'],
    url:'xxxxxxxxxxxxxx.png',
    isPic:false
}

这里定一个名为 obj 的对象,接下来按照下面的模板生成:

3. 实现

docFiles.js

import { imageHandle } from '../common/image'
import { saveAs } from 'file-saver';
import JsZip from 'jszip'
import PizZip from "pizzip";
import docxtemplater from "docxtemplater";
import JSZipUtils from "jszip-utils";
import expressions from "angular-expressions";
 
let promises: any[] = [];

下载单个文件

 
/**导出单个word文件
 * @param {object} opts 配置项
 */
export const exportDocx = (opts) => {
  let {
    //模板文件路径(必填)
    tempDocxpath,
    //模板文件数据(必填)
    data,
    //导出文件名
    fileName,
    //是否包含图片
    imageable,
    //导出成功后的回调
    onSuccess,
    //导出失败后的回调
    onError,
  } = Object.assign({
    imageable: false,
    fileName:'新建文件1',
  }, opts)
 
  const promise = new Promise((resolver, reject) => {
    JSZipUtils.getBinaryContent(tempDocxpath, (error, content) => {
      if (error) {
        throw error;
      }
      expressions.filters.size = function (input, width, height) {
        return {
          data: input,
          size: [width, height],
        };
      }
 
      //创建一个PiZip示例,内容为模板的内容
      const zip = new PizZip(content);
 
      //创建并加载docxtemplater实例对象
      let doc = new docxtemplater();
 
      if (imageable) {
        let opts = imageHandle()
        doc.attachModule(new ImageModule(opts));
      }
 
      doc.loadZip(zip);
      doc.setData(data);
 
      try {
        //用模板变量的值替换所有模板变量
        doc.render();
      } catch (error) {
        const e = {
          "message": error.message,
          "name": error.name,
          "stack": error.stack,
          "properties": error.properties
        };
        console.log({ error: e });
        throw error;
      }
 
      //生成一个代表docxtemplater对象的zip文件(在内存中表示)
      const out = doc.getZip().generate({
        type: "blob",
        mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      });
      resolver(out);
    })
  })
 
  Promise.all([promise]).then((out) => {
    if(onSuccess) onSuccess()
    saveAs(out, fileName);
  }).catch(() => {
    if(onError) onError()
  })
}

压缩包形式批量下载

/**多级目录的形式导出文件
 * @param {object} opts 配置项
 */
export const exportFile_MultiLevelDirectory = (opts) => {
  let {
    //模板文件路径
    tempDocxpath,
    //模板文件数据
    data,
    //一级目录名
    zipName,
    //下级目录对应数据路径
    path,
    //下级目录名称类型
    subFolderNameType,
    //文件名称类型
    fileNameType,
    //是否包含图片
    imageable,
    //导出成功后的回调
    onSuccess,
    //导出失败后的回调
    onError,
  } = Object.assign({
    imageable: false,
    zipName:'新建文件1'
  }, opts)
 
  const zips = new JsZip();
  //创建一级目录的压缩包
  const folder = zips.folder(zipName);
 
  let creatOpts = {
    tempDocxpath,
    data,
    path,
    fileNameType,
    subFolderNameType,
    imageable,
    folder,
    zips,
  }
  create_Directory(creatOpts)
  Promise.all(promises).then(() => {
    zips.generateAsync({ type: "blob" }).then(content => {
      //生成二进制流
      if(onSuccess) onSuccess()
      saveAs(content, zipName);
    }).catch(() => {
      if(onError) onError()
    })
  })
}
 
/**创建下级目录
 * @param {object} opts 配置项
 */
function create_Directory(opts) {
  let { 
     tempDocxpath,
     data,
     path,
     folder,
     subFolderNameType,
     fileNameType,
     imageable,
     zips
  } = opts
 
  //遍历数据,依次形成下级目录/文件
  data.forEach((item, index) => {
    if (item[path] || item[path]?.length) {
     item[path].forEach((item2, index2) => {
        //创建下级目录
        let subFolderName = item2.folderName
        let subFolder = folder.folder(subFolderName);
 
        let creatOpts = {
          tempDocxpath,
          data: item[path],
          fileNameType,
          subFolderNameType,
          imageable,
          zips,
          folder: subFolder
        }
        create_Directory(creatOpts)
      })
    }else {
      const fileName = item.fileName
      const promise = new Promise((resolver, reject) => {
        JSZipUtils.getBinaryContent(tempDocxpath, (error, content) => {
          if (error) {
            throw error;
          }
          expressions.filters.size = function (input, width, height) {
            return {
              data: input,
              size: [width, height],
            };
          };
          const zip = new PizZip(content);
          let doc = new docxtemplater();
          if (imageable) {
            let opts = imageHandle()
            doc.attachModule(new ImageModule(opts));
          }
          doc.loadZip(zip);
 
          doc.setData(item);
          try {
            doc.render();
          } catch (error) {
            const e = {
              "message": error.message,
              "name": error.name,
              "stack": error.stack,
              "properties": error.properties
            };
            console.log({ error: e });
            throw error;
          }
          const out = doc.getZip().generate({
            type: "blob",
            mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
          });
          folder.file(fileName, out, { binary: true });
          resolver('success');
        });
      })
      promises.push(promise);
    }
  }
}

注意:导出多个word文件时需注意文档命名问题,避免因名称重复产生的文件覆盖问题。

image.js

/**
 * 图片处理选项配置生成函数。
 * @returns {Object} 返回一个包含图片处理选项的对象。
 */
export const imageHandle = () => {
  // 图片处理
  let opts = {}
 
  opts = {
    //图像是否居中
    centered: false
  };
  opts.getImage = (chartId) => {
    // 将base64的数据转为ArrayBuffer
    return base64DataURLToArrayBuffer(chartId);
  }
  opts.getSize = function (img, tagValue, tagName) {
    //自定义指定图像大小
    return [500, 400];
  }
  return opts
}
 
/**
 * 将base64格式的数据转为ArrayBuffer
 * @param {Object} dataURL base64格式的数据
 */
export function base64DataURLToArrayBuffer(dataURL) {
  const base64Regex = /^data:image\/(png|jpg|jpeg|svg|svg\+xml);base64,/;
  if (!base64Regex.test(dataURL)) {
    return false;
  }
  const stringBase64 = dataURL.replace(base64Regex, "");
  let binaryString;
  if (typeof window !== "undefined") {
    binaryString = window.atob(stringBase64);
  } else {
    binaryString = new Buffer(stringBase64, "base64").toString("binary");
  }
  const len = binaryString.length;
  const bytes = new Uint8Array(len);
  for (let i = 0; i < len; i++) {
    const ascii = binaryString.charCodeAt(i);
    bytes[i] = ascii;
  }
  return bytes.buffer;
}

应用页面文件

注释部分为批量导出

<template>
    <div>
        <n-button type="primary" @click="downWord">导出word</el-button>
    </div>
</template>
<script setup>
    let obj={
        name:'张三',
        images:[]
    }
    
    /*let objList=[
        {
            folderName:'济南市',
            children:[
                {
                    fileName:'张村申请合同',
                    name:'张村',
                    count:72
                },
                {
                    fileName:'大王村申请合同',
                    name:'大王村',
                    count:56
                },
            ]
        },
        {
            folderName:'德州市',
            children:[
                {
                    fileName:'高家村申请合同',
                    name:'高家村',
                    count:10
                },
            ]
        },
        {
            fileName:'居户里村申请合同',
            name:'居户里村',
            count:74
        },
    ]*/
 
    function downWord(){
        let opts={
            tempDocxpath:'/moBan.docx',
            data:obj,
            fileName:'申请表'
        }
        exportDocx(opts)
        /*let opts={
            tempDocxpath:'/moBan.docx',
            data:objList,
            zipName:'合同申请'
        }*/
        //exportFile_MultiLevelDirectory(opts)
    }
</script>

4. 问题

(1)End of data reached (data length = 0, asked index = 4). Corrupted zip ?

原因:模板文件为空文件;

排查:检查模板文件引入是否正确

(2)导出的文件中图片显示undefined

原因:图片路径转换成base64后的格式不对

(3)文件数量不对

排查:是不是文件名重复覆盖导致数量不对

以上就是Vue3实现前端生成Word并下载的全过程的详细内容,更多关于Vue3前端生成Word并下载的资料请关注脚本之家其它相关文章!

相关文章

  • Vue中避免滥用this去读取data中数据

    Vue中避免滥用this去读取data中数据

    这篇文章主要介绍了Vue中避免滥用this去读取data中数据的的相关资料,帮助大家更好的理解和学习使用vue框架,感兴趣的朋友可以了解下
    2021-03-03
  • VUE页面中加载外部HTML的示例代码

    VUE页面中加载外部HTML的示例代码

    本篇文章主要介绍了VUE页面中加载外部HTML的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-09-09
  • ruoyi-vue3 集成aj-captcha实现滑块、文字点选验证码功能

    ruoyi-vue3 集成aj-captcha实现滑块、文字点选验证码功能

    这篇文章主要介绍了 ruoyi-vue3 集成aj-captcha实现滑块、文字点选验证码,本文基于后端RuoYi-Vue 3.8.7 和 前端 RuoYi-Vue3 3.8.7,集成以AJ-Captcha文字点选验证码为例,不需要键盘手动输入,极大优化了传统验证码用户体验不佳的问题,感兴趣的朋友一起看看吧
    2023-12-12
  • 构建Vue大型应用的10个最佳实践(小结)

    构建Vue大型应用的10个最佳实践(小结)

    这篇文章主要介绍了构建Vue大型应用的10个最佳实践(小结),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-11-11
  • Vue 组件间的样式冲突污染

    Vue 组件间的样式冲突污染

    本篇文章主要介绍了Vue 组件间的样式冲突污染,当多个样式出现时,就会导致样式冲突,本文介绍了具体解决方法
    2017-08-08
  • Vue+element自定义指令如何实现表格横向拖拽

    Vue+element自定义指令如何实现表格横向拖拽

    这篇文章主要介绍了Vue+element自定义指令如何实现表格横向拖拽,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-10-10
  • Vue使用三种方法刷新页面

    Vue使用三种方法刷新页面

    这篇文章说明了如何使用Vue去刷新当前页面的多种方法实例,有完成的代码提供参考,希望对你有所帮助
    2021-06-06
  • Vue表单验证 trigger:'blur'OR trigger:'change'区别解析

    Vue表单验证 trigger:'blur'OR trigger:'change&ap

    利用 elementUI 实现表单元素校验时,出现下拉框内容选中后校验不消失的异常校验情形,这篇文章主要介绍了Vue表单验证 trigger:‘blur‘ OR trigger:‘change‘ 区别,需要的朋友可以参考下
    2023-09-09
  • Vue3使用MD5加密实战案例(清晰明了)

    Vue3使用MD5加密实战案例(清晰明了)

    MD5是一种信息摘要算法(对称加密),一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值,用来确保信息传输完整一致性,这篇文章主要给大家介绍了关于Vue3使用MD5加密的相关资料,需要的朋友可以参考下
    2023-05-05
  • Vue3之toRefs和toRef在reactive中的一些应用方式

    Vue3之toRefs和toRef在reactive中的一些应用方式

    这篇文章主要介绍了Vue3之toRefs和toRef在reactive中的一些应用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-03-03

最新评论