实际开发中,常见pdf|word|excel等文件的预览和下载

实际开发中,常见pdf|word|excel等文件的预览和下载

背景

实际开发中,大部分文件的预览会以流的方式传输,前端通过Element等UI库提供的上传组件传给后端File类型数据, 后端返回给前端Blob/ArrayBuffer类型数据 , 前端最终借助各种第三方工具或者自定义tool方法, 实现各种类型文件的下载或者预览. 少部分的会以文件地址的方式进行传输, 那么我们直接访问那个文件url即可.

相关类型数据之间的转换

1、File转Blob

ts 复制代码
export function fileToBlob(file: File) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      const arrayBuffer: any = reader.result;
      const blob = new Blob([arrayBuffer], { type: file.type });
      resolve(blob);
    };
    reader.onerror = reject;
    reader.readAsArrayBuffer(file);
  });
}

2、File转ArrayBuffer

ts 复制代码
export function fileToArrayBuffer(file: File) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      const arrayBuffer: any = reader.result;
      resolve(arrayBuffer);
    };
    reader.onerror = reject;
    reader.readAsArrayBuffer(file);
  });
}

3、Blob转ArrayBuffer

ts 复制代码
export function blobToArrayBuffer(blob) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = reject;
    reader.readAsArrayBuffer(blob);
  });
}

4、Blob转File

ts 复制代码
export function blobToFile(blob, fileName, fileType) {
  return new File([blob], fileName, { type: fileType })
}

5、ArrayBuffer转Blob

ts 复制代码
export function arrayBufferToBlob(arrayBuffer, blobType = 'application/octet-stream') {
	const blob = new Blob([arrayBuffer], { type: blobType  });
  	return blob;
}

6、ArrayBuffer转File

ts 复制代码
export function arrayBufferToFile(arrayBuffer, fileName, fileType = 'text/plain') {
	const file= new File([arrayBuffer], fileName, { type: fileType  });
  	return file;
}

根据Blob/File类型生成可预览的Base64地址

有些第三方预览工具不识别Blob/File, 如viewerjsv-viewer 预览图片的时候,是需要图片对应的src的,而不是Blob/File

ts 复制代码
export function createUrlByBlobOrFile(data: any) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      resolve(reader.result);
    };
    reader.onerror = reject;
    reader.readAsDataURL(data);
  });
}

基于Blob类型的各种文件的下载

下载的文件响应类型可打印FIle/Blob对象查看,可执行:downloadFileUtil(fileBlob, fileBlob.type, fileBlob.fileName)

ts 复制代码
export function downloadFileUtil(data: Blob, responseType: string, fileName: any = new Date().valueOf()) {
	  const blob = new Blob([data], { type: responseType });
	  // 创建一个<a></a>标签
	  let a: HTMLAnchorElement | null = document.createElement('a');
	  const blobUrl = window.URL.createObjectURL(blob);
	  a.href = blobUrl;
	  a.download = fileName;
	  a.style.display = 'none';
	  document.body.appendChild(a);
	  a.click();
	  a.remove();
	  // 释放createObjectURL创建的资源
	  window.URL.revokeObjectURL(blobUrl);
}

各种类型文件的预览及其效果

个别预览的第三方插件库,需要使用特定的某些版本,当前指定的版本库都是可用的。

1、当前使用的node版本

2、 业务场景

  • 用户通过上传组件上传附件

用户从本地上传的附件拿到的类型是File, 保存之后, 拿到的就是文件列表项对应的Blob类型。

3、图片类型预览

图片类型预览使用的是v-viewerviewerjs, 可支持的预览图片类型有:jpg, jpeg, png, gif

3.1、安装依赖
bash 复制代码
yarn add v-viewer@^3.0.21 viewerjs@^1.11.7
3.2、ImagePreview.vue

v-viewerviewerjs 可以通过指令、组件和api三种方式实现预览。 实际开发中,基本上都是使用的是Blob类型,Blob类型转换为Base64地址后, 是不能通过import { api as viewerApi } from 'v-viewer';的方式预览的,尽管api的方式很简单,但它貌似只是支持本地文件URL/服务器文件URL。

通过使用viewer组件,借助img标签可以识别Base64图片路径,从而通过点击img列表,实现图片预览

ts 复制代码
<template>
  <div class="image-preview">
    <viewer :images="props.images" class="v-viewer">
      <img
        v-for="(imgItem, index) in props.images"
        :key="index"
        class="view-img-item"
        :src="imgItem.url"
        :alt="imgItem.name"
        :title="imgItem.name"
      />
    </viewer>
    <div class="auto-close-preview-com">
      <Close class="close-icon" @click="closeImgPreviewFn" />
    </div>
  </div>
</template>

<script lang="ts" setup>
import 'viewerjs/dist/viewer.css';
import { component as Viewer } from 'v-viewer';
import { onMounted } from 'vue';
import { ElMessage } from 'element-plus';


const props = defineProps({
  images: {
    type: Array as any,  // images存储的是Blob转成Base64的数组,类型转换上文createUrlByBlobOrFile可实现
    default: () => [],
  },
});
const emits = defineEmits(['closeImgPreview']);

function closeImgPreviewFn() {
  emits('closeImgPreview');
}
onMounted(() => {
  ElMessage.info('点击图片列表可预览~');
});
</script>

<style lang="css" scoped>
.image-preview {
  position: fixed;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 9998;
  background-color: rgb(0 0 0 / 70%);
  .v-viewer {
    width: 100%;
    height: 100%;
    .view-img-item {
      width: 250px;
      height: 250px;
      margin-right: 20px;
    }
  }
  .auto-close-preview-com {
    position: absolute;
    -webkit-app-region: no-drag;
    background-color: rgb(0 0 0 / 50%);
    border-radius: 50%;
    cursor: pointer;
    height: 80px;
    overflow: hidden;
    right: -40px;
    top: -40px;
    transition: background-color 0.15s;
    width: 80px;
    color: #ffffff;
    .close-icon {
      bottom: 15px;
      left: 15px;
      position: absolute;
      background-position: -260px 0;
      font-size: 0;
      height: 20px;
      line-height: 0;
      width: 20px;
    }
  }
}
</style>
3.3、效果

4、Excel文件的预览

Excel文件预览使用的是xlsx 插件库, 可支持类型有:xls, xlsx

4.1、依赖安装
bash 复制代码
yarn add xlsx@^0.18.5
4.2、ExcelPreview.vue
ts 复制代码
<template>
  <div class="xlsx-preview-box"></div>
</template>

<script lang="ts" setup>
import { onMounted } from 'vue';
// XLSX: 无法预览docx文件, 预览pdf也会乱码  只能预览xlsx文件
import * as XLSX from 'xlsx';

const props = defineProps({
  fileBlob: {
    type: Blob,
    default: () => null,
  },
});

onMounted(() => {
  if (props.fileBlob) {
    const reader = new FileReader();
    // 通过readAsArrayBuffer将blob转换为ArrayBuffer
    reader.readAsArrayBuffer(props.fileBlob);
    reader.onload = (event: any) => {
      // 读取ArrayBuffer数据变成Uint8Array
      const data = new Uint8Array(event.target.result);
      // 这里的data里面的类型和后面的type类型要对应
      const workbook = XLSX.read(data, { type: 'array' });
      const sheetNames = workbook.SheetNames; // 工作表名称
      const worksheet = workbook.Sheets[sheetNames[0]];
      const html = XLSX.utils.sheet_to_html(worksheet);
      document.getElementsByClassName('xlsx-preview-box')[0].innerHTML = html;
    };
  }
});
</script>

<style lang="css">
.xlsx-preview-box {
  width: 100%;
  height: 100%;
  overflow: auto;
  table {
    width: 100%;
    border-spacing: 0;
    tr {
      height: 40px;
      font-size: 14px;
      color: #666666;
      line-height: 14px;
      font-weight: 400;
    }
    tr:first-child {
      background-color: #ececec !important;
      height: 60px;
      font-size: 16px;
      color: #666666;
      font-weight: 700;
    }
    td {
      min-width: 80px;
      text-align: center;
      border: 1px solid #cccccc;
    }
    tr:nth-child(2n) {
      background-color: #fafafa;
    }

    tr:nth-child(2n + 1) {
      background-color: #ffffff;
    }
  }
}
</style>
4.3、预览效果

5、word文件的预览

word文件预览使用的是docx-preview 插件库, 可支持类型有:doc, docx

5.1、依赖安装
bash 复制代码
yarn add docx-preview@0.3.0

docx-preview 需要是0.3.0版本,最新的0.3.3版本会报docx-preview类型错误。且最新的版本解析的blob文件类型和0.3.0版本不一致,最新版本还会预览失败:报(Can't find end of central directory : is this a zip file ? If it is, see)。

5.2、WordPreview.vue
ts 复制代码
<template>
  <div ref="wordPreviewRef" class="word-preview"></div>
</template>

<script lang="ts" setup>
import { ref, nextTick } from 'vue';
// docx-preview 需要是0.3.0版本,最新的0.3.3版本会报docx-preview类型错误
// 且最新的版本解析的blob类型和0.3.0版本不一致
// 最新版本还会预览失败:报(Can't find end of central directory : is this a zip file ? If it is, see)
import { renderAsync } from 'docx-preview';

const props = defineProps<{
  wordBlob: any;
}>();

const wordPreviewRef = ref({});

nextTick(() => {
  renderAsync(
    props.wordBlob, // blob 的type: application/vnd.openxmlformats-officedocument.wordprocessingml.document
    wordPreviewRef.value as HTMLElement, // HTMLElement 渲染文档内容的元素,
  );
});
</script>

<style lang="scss" scoped>
.word-preview {
  width: 100%;
  height: 100%;
  overflow: auto;
}
</style>
5.3、预览效果

6、pdf文件的预览

pdf文件预览使用的是pdfjs-dist 插件库, 可支持类型有:pdf

6.1、依赖安装
bash 复制代码
yarn add pdfjs-dist@2.16.105

pdfjs-dist 底层是pdfjs。不建议使用打包后的mjs类型的版本包。因为不支持线上环境对GlobalWorkerOptions.workerSrc的支持。具体的是:本地可以引入node_module路径,但是正式环境没这个路径;如果把对应的pdf.worker.min.mjs放到assets下,会报错:Failed to resolve module specifier '@/assets/pdfjs/pdf.worker.min.mjs; 如果放到public下,会报错Failed to load module script, public目录文件不会被编译,浏览器无法识别mjs文件

6.2、PdfPreview.vue
ts 复制代码
<template>
  <div class="pdf-preview">
    <!-- block: 避免一个视图显示多个canvas页 -->
    <canvas
      v-for="pageIndex in pdfPages"
      :id="`pdf-canvas-` + pageIndex"
      ref="pdfPreviewRef"
      :key="pageIndex"
      style="display: block"
    ></canvas>
  </div>
</template>

<script lang="ts" setup>
import { ref, onMounted, nextTick, reactive } from 'vue';

// import 'pdfjs-dist/web/pdf_viewer.css';
// 4.5.136版本
// import * as pdfjsLib from 'pdfjs-dist'; // /legacy/build/pdf.js
// import * as pdfjsViewer from 'pdfjs-dist/web/pdf_viewer.js';
import 'pdfjs-dist/web/pdf_viewer.css';
import * as pdfjsLib from 'pdfjs-dist';
import { blobToArrayBuffer } from '@/utils/tools';

const props = defineProps<{
  pdfBlob: any;
}>();

const pdfPreviewRef = ref({});
// pdf页数
const pdfPages = ref(0);
// pdf缩放比例
const pdfScale = ref(2.5); // 可以控制canvas的宽高
// pdf文档流,
// 这个不能使用ref,使用ref会报错: Cannot read from private field
let pdfDoc = reactive<any>({});

const renderPdf = (num) => {
  pdfDoc.getPage(num).then((page) => {
    const canvasId = `pdf-canvas-${num}`;
    const canvas: any = document.getElementById(canvasId);
    const ctx = canvas?.getContext('2d');
    const dpr = window.devicePixelRatio || 1;
    const bsr =
      ctx.webkitBackingStorePixelRatio ||
      ctx.mozBackingStorePixelRatio ||
      ctx.msBackingStorePixelRatio ||
      ctx.oBackingStorePixelRatio ||
      ctx.backingStorePixelRatio ||
      1;
    const ratio = dpr / bsr;
    const viewport = page.getViewport({ scale: pdfScale.value });
    canvas.width = viewport.width * ratio;
    canvas.height = viewport.height * ratio;
    canvas.style.width = `${viewport.width}px`;
    canvas.style.height = `${viewport.height}px`;
    ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
    const renderContext = {
      canvasContext: ctx,
      viewport: viewport,
    };
    page.render(renderContext);
    if (num < pdfPages.value) {
      renderPdf(num + 1);
    }
  });
};

// 获取pdf文档流与pdf文件的页数
const loadFile = async () => {
  //  string | URL | TypedArray | ArrayBuffer | DocumentInitParameters
  const pdfArrayBuffer: any = await blobToArrayBuffer(props.pdfBlob);
  const loadingTask = pdfjsLib.getDocument(pdfArrayBuffer);
  loadingTask.promise.then((pdf) => {
    pdfDoc = pdf; // 获取pdf文档流
    pdfPages.value = pdf.numPages; // 获取pdf文件的页数
    nextTick(() => {
      renderPdf(1);
    });
  });
};

onMounted(async () => {
  // 正式环境找不到node_modules
  // pdfjsLib.GlobalWorkerOptions.workerSrc =
  //   '../../../node_modules/pdfjs-dist/build/pdf.worker.min.mjs';
  // 放在assets下: Failed to resolve module specifier '@/assets/pdfjs/pdf.worker.min.mjs
  // pdfjsLib.GlobalWorkerOptions.workerSrc = '@/assets/pdfjs/pdf.worker.min.mjs';
  // const baseurl = window.location.origin + window.location.pathname; // 本地路径
  // ${baseurl}pdfjs/pdf.worker.min.mjs 静态服务访问的返回的是流
  // pdfjsLib.GlobalWorkerOptions.workerSrc = `${baseurl}pdfjs/pdf.worker.min.mjs`; // Failed to load module script
  // public/pdfjs/pdf.worker.js: 将'../../../node_modules/pdfjs-dist/build/pdf.worker.js';复制到public目录下
  pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js';  // "pdfjs/"不能写成"/pdfjs/", 前者是相对路径, 后者是绝对路径(相对线上环境服务器)
  loadFile();
});
</script>
<style lang="scss" scoped>
.pdf-preview {
  width: 100%;
  height: 100%;
  overflow: auto;
}
</style>
6.3、预览效果

7、json/xml文件的预览

vue-json-viewer支持jsonxml文件的预览

7.1、依赖安装
bash 复制代码
yarn add vue-json-viewer@^3.0.4
7.2、全局引入
7.3、JsonViewer组件的使用

fileData存储的是后端接口返回的json字符串

ts 复制代码
<json-viewer v-else-if="preState.fileType === 'Json'" :value="preState.fileData" />
7.4、预览效果


8、bim文件的预览

geobim文件的预览使用的是@xbim/viewer插件库,当前使用的方式支持BlobUrl两种方式

8.1、依赖安装
bash 复制代码
yarn add @xbim/viewer@^2.1.0-pre202305041434
8.2、GeoBimPreview.vue

该组件接收的是url, 但是loadGeoBim处理兼容了Blob

ts 复制代码
<template>
  <canvas id="bim-canvas" style="width: 100%; height: 100%"></canvas>
</template>

<script lang="ts" setup>
import { watch, nextTick } from 'vue';
import { Grid, NavigationCube, Viewer, ViewType } from '@xbim/viewer';

const props = defineProps({
  dwgUrl: {
    type: String,
    default: () => '',
  },
});
let viewer;
const setViewerOptions = () => {
  viewer.background = [26, 51, 76, 255];
  viewer.highlightingColour = [0, 0, 225, 200];
  viewer.brightness = -0.5;
  viewer.hoverPickColour = [0, 0, 225, 200];
};
const setViewerPlugin = () => {
  const cube = new NavigationCube();
  cube.ratio = 0.05;
  // eslint-disable-next-line no-multi-assign
  cube.passiveAlpha = cube.activeAlpha = 0.85;
  viewer.addPlugin(new Grid());
  viewer.addPlugin(cube);
};
const token = localStorage.getItem('TOKEN') as string;
const headers = {
  Authorization: `Bearer ${JSON.parse(token).access_token}`,
};
const loadGeoBim = (dwgUrl) => {
  const check = Viewer.check();
  if (check.noErrors) {
    nextTick(() => {
      viewer = new Viewer('bim-canvas');
      setViewerOptions();
      setViewerPlugin();
      viewer.on('loaded', function () {
        viewer.show(ViewType.DEFAULT, undefined, undefined, false);
        viewer.start();
      });
      // 前置管理、任务管理、数据管理里访问的数据是四库的后端接口返回的文件流,服务管理里访问的是可视化系统后台接口返回的文件地址
      // node_modules\.vite\deps\@xbim_viewer.js  修复bim的左右键
      fetch(dwgUrl, { headers })
        .then((responce) => responce.arrayBuffer())
        .then((arrayBuffer) => {
          const blob = new Blob([arrayBuffer], { type: 'application/octet-stream' });
          viewer.load(blob);
        })
        .catch((err) => {
          viewer.load(dwgUrl);
        });
    });
  }
};
watch(
  () => props.dwgUrl,
  (dwgUrl) => {
    loadGeoBim(dwgUrl);
  },
  {
    immediate: true,
    deep: true,
  },
);
</script>
8.3、预览效果
相关推荐
幽络源小助理1 小时前
HTML5 + Bootstrap5 网站底部代码分享与解析
前端·html·html5·网站底部代码
请叫我飞哥@1 小时前
HTML5 动画效果:淡入淡出(Fade In/Out)详解
前端·html·html5
计算机毕设指导62 小时前
基于Springboot的医院资源管理系统【附源码】
java·前端·spring boot·后端·mysql·spring·tomcat
浪遏2 小时前
Langchain.js | StructedOutputParser | 疯狂输出(二)
前端·javascript·aigc
一 乐2 小时前
考研助手|基于SSM+vue的考研助手系统的设计与实现(源码+数据库+文档)
前端·数据库·vue.js·后端·考研·考研助手
还需studystudy2 小时前
Vue——使用html2pdf插件,下载pdf文档到本地
前端·vue.js·pdf
mit6.8242 小时前
[Qt] 信号和槽(2) | 多对多 | disconnect | 结合lambda | sum
linux·前端·c++·qt·学习
A雄2 小时前
2025新春烟花代码(一)HTML5夜景放烟花绽放动画效果
前端·html·html5
冴羽2 小时前
Solid.js 最新官方文档翻译(22)—— 部署
前端·javascript·react.js
xulihang2 小时前
在浏览器中扫描DotCode码
前端·javascript·图像识别