一、前言
笔者在之前常规版本迭代的时候遇到一个文件预览的小需求,我们前端的vue工程里没有现成的组件可以使用。经过参考别人对类似需求的实现,以及结合我们系统里前端基础设施和组件的一般写法,封装了一个非常简易的文件预览组件,在此分享出来。因为笔者不是专职的前端工程师,因此有谬误的地方很欢迎读者指正,不吝赐教。
二、组件封装
需求是需要在这个对话框的列表后面加一个文件预览的功能。本页面之前已经有文件上传和下载的功能,因此在只需要在文件操作的地方加一个预览按钮调起一个对话框将文件信息解析出来即可。

按照项目中已有的下载附件的方法,父组件传入列表行里的文件的路径,文件名,展示名,以及控制对话框是否展示的属性。(下面的代码涉及到业务相关的信息都已隐去)
props:{
attachDialogVisible: {
type: Boolean,
default: false
},
//附件路径
attachPath: {
type: String,
default: "",
required: true
},
//附件名
attachFile: {
type: String,
default: "",
required: true
},
//展示名
showName: {
type: String,
default: "",
required: true
},
}
类似excel,word之类的文件,可以用一些开源的js库进行解析。考虑到有些文件类型是浏览器可以直接打开的,为了不在浏览器面前班门弄斧,点击预览的时候判断如果是类似pdf,jpg,png之类的文件通过inline方式下载,直接在浏览器里打开。
template主要是放一个对话框,excel解析的内容放在el-table中,word解析的内容可以渲染成v-html(注意安全问题)
这部分前端代码类似下面这样
template部分:
<template>
<div >
<!-- 预览附件 -->
<el-dialog
:visible="attachDialogVisible"
:width="'820px'"
:close-on-click-modal="false"
@close="onClose()"
>
<!-- 表格 -->
<div v-if="excelVisible">
<el-table :data="tableData"
style="width: 100%"
height="450"
size="mini">
<el-table-column :prop="item"
:label="item"
width="140"
v-for="(item,index) in checkboxTableColumn"
:key="'column'+index">
</el-table-column>
</el-table>
<el-pagination v-if="excelData != 0"
@size-change="handleSizeChange"
@current-change="CurrentChange"
:current-page="currentPage"
:page-sizes="[10, 20, 30,40,50,100,200,500]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
<!-- 文档 -->
<div v-if="wordVisible">
<div v-html="wordData" ></div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="onClose()">取消</el-button>
</span>
</el-dialog>
</div>
</template>
js部分(省略一些数据操作的方法):
methods: {
preViewFile: function () {
let val=this.attachFile.toLowerCase()
if(val.endsWith("xlsx")||val.endsWith("xls")||val.endsWith("doc")||val.endsWith("docx")){
this.downloadAndParseAttach()
}else if(val.endsWith("pdf")||val.endsWith("jpg")||val.endsWith("jpeg")||val.endsWith("png")){
//调用父组件关闭对话框
this.$emit('update:attachDialogVisible', false)
this.browserOpenFile()
}else{
this.$message.warning('不支持预览的文件类型 !');
this.$emit('update:attachDialogVisible', false)
}
},
browserOpenFile: function () {
var form = $("<form class='hidden' target='_blank' method='post' action='" + this.appCode + "/xxxx/browserOpenFile'></form>");
form.append(this.input('attachPath', this.attachPath)).append(this.input('attachFile', this.attachFile)).append(this.input('showName', this.showName));
$(document.body).append(form);
form.submit().remove();
},
downloadAndParseAttach: function () {
//这里先下载文件,excel可以用xlxs库对excel进行解析,word文件可以用mammoth库解析。把解析结果呈现到template的对话框中。网上demo很多不赘述。
},
}
对应的后端代码的关键点在于指定对应文件类型的MediaType和把Content-Disposition设置为inline
//用于在浏览器中直接打开的文件
@RequestMapping("/browserOpenFile")
public void browserOpenFile(String attachPath, String attachFile, String showName, HttpServletResponse response) throws IOException {
log.info("download: {}, {}, {}", attachPath, attachFile, showName);
Set<String> supportExtensions=new HashSet<>(Arrays.asList("pdf","jpg","jpeg","png"));
String fileExtension=attachFile.toLowerCase().split("\\.")[1];
if(!supportExtensions.contains(fileExtension)){
throw new BizException("文件:"+attachFile+"为不支持浏览器预览的文件格式!");
}
String basePath = "/xxxxxxxxxxxxxxx";
String parent = basePath + attachPath;
File file = new File(parent, attachFile);
File tmpFile = downloadFile(file.getPath());
String type="";
switch (fileExtension){
case "pdf":
type= MediaType.APPLICATION_PDF_VALUE;
break;
case "jpg":
case "jpeg":
type= MediaType.IMAGE_JPEG_VALUE;
break;
case "png":
type=MediaType.IMAGE_PNG_VALUE;
break;
default:
type=MediaType.APPLICATION_OCTET_STREAM_VALUE;
}
browserOpen(tmpFile,type, response);
if(tmpFile != null && tmpFile.exists()){
tmpFile.delete();
}
}
public static long browserOpen(File file, String type, HttpServletResponse response) throws IOException {
response.reset();
if (file.exists()) {
response.setContentType(type);
response.setHeader("Content-Disposition", "inline");
return fileForResponse(file, response);
} else {
response.setContentType(MediaType.TEXT_PLAIN_VALUE);
return fileNotFound(file, response);
}
}
三、渲染数据的问题
如果预览文件组件在主页面的列表上使用,那么在v-if中用父组件传值的attachDialogVisible控制,把preViewFile函数放在created里面调用,每次重新点开都能渲染传入新的文件地址名称触发preViewFile函数进行文件下载,重新渲染组件。但是这个组件使用在打开的对话框的列表里面,如下图所示。实验发现按照之前的套路,点开对话框点击不同的行并没有触发preViewFile方法。

为了解决这个问题,想到用watch方式,比如监听attachFile变化触发preViewFile,这样子可以实现传入的文件名不同就可以重新下载文件解析数据。这个对于excel和word弹出对话框预览数据的方式没问题(因为文件名不变第二次点开还是展示之前的内容),但是对于浏览器弹窗的情况譬如预览pdf的时候,第一次点击正常但是第二次点击因为attachFile并没有变化就不会弹窗。于是又尝试监听attachDialogVisible变量来触发文件解析。实践证明这样子是可行的,不会出现第二次点击浏览器不弹窗的情况。
watch: {
//attachDialogVisible改变时就刷新数据
attachDialogVisible(value, oldValue) {
if(value){
this.preViewFile()
}
}
}
四、拓展
这是笔者第一次在实践中使用watch语法,对其原理也产生了好奇心。以下引用自vue的官方文档,深入响应式原理部分。这里其实简要阐述了watch的原理。vue会对 watch 每个属性创建一个 watcher,并在数据变化时通知到对应的watcher重新渲染组件。
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有
的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。
Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及
更低版本浏览器的原因。
这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在
property 被访问和修改时通知变更。这里需要注意的是不同浏览器在控制台打印数据对
象时对 getter/setter 的格式化并不同,所以建议安装 vue-devtools 来获取对检查数据更
加友好的用户界面。
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据
property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联
的组件重新渲染。

五,小结
笔者作为前端方面的菜鸟,日常写到前端代码总是磕磕绊绊。不过近来接触多了前端工程化的各种技术,也对其越来越感兴趣,所以也想多关注一下这方面的知识,逼着自己做点总结和输出。水平有限,欢迎指正。
lian

391

被折叠的 条评论
为什么被折叠?



