实际开发中,常见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, 如
viewerjs
、v-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-viewer
和viewerjs
, 可支持的预览图片类型有:jpg
,jpeg
,png
,gif
3.1、安装依赖
bash
yarn add v-viewer@^3.0.21 viewerjs@^1.11.7
3.2、ImagePreview.vue
v-viewer
和viewerjs
可以通过指令、组件和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
插件库, 可支持类型有:
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
支持json
和xml
文件的预览
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
插件库,当前使用的方式支持Blob
和Url
两种方式
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>