背景:随着业务的复杂性提高,前端上传文件变得越来越常见,那么深入了解上传这一行为很有必要;
一、input标签 type="file"
1.1 基本用法:
<input type="file" />
思考🤔,我们是否可以使用 v-model来获取选择的文件呢,hhh, 答案是不行;(注意下面的写法是不允许的)
// v-model cannot be used on file inputs since they are read-only. Use a v-on:change listener instead.vue(59)
<input v-model="filePath" type="file" />
在Vue中,使用v-model指令可以将表单输入和应用程序状态进行双向绑定。然而,<input type="file" />元素是一个特殊情况,因为它的值是只读的,无法直接通过v-model来绑定。如果你想要获取文件路径,你可以使用@change事件监听器来捕获文件选择的变化,然后在事件处理程序中更新Vue实例的数据。
<input type="file" @change="handleFileChange" />
const handleFileChange = (event) => {
console.log(event.target?.files); // FileList {0: File, length: 1}
console.log(event.target?.value); // 'C:\\fakepath\\test.pdf'
};
备注: 为了阻止恶意软件猜测文件路径,该值的字符串表示总是以
C:\fakepath为前缀的文件名,而并不是文件的真实路径
1.2 其他参数
定义input能选择的文件类型,例如:accept=".pdf,.zip,.ofd", 属性接受的是一个字符串(唯一文件类型说明符号)可以是 1、一个以英文句号(“.”)开头的合法的不区分大小写的文件名扩展名。例如:.jpg、.pdf 或 .doc; 2、一个不带扩展名的 MIME 类型字符串
布尔类型,用于表示是否多选
...
1.3 上传文件夹
input 中还存在一个不标准的属性: webkitdirectory ,表示存在兼容性,不过大部分都兼容
<input
ref="uploadDirRef"
type="file"
id="file"
accept=".pdf,.ofd"
:webkitdirectory="true"
:mozdirectory="true"
:odirectory="true"
@change="handleDirChange"
/>
const handleDirChange = (event) => {
console.log(event.target?.files); // FileList {0: File, length: 1}
};
在选择文件目录后,该目录及其整个内容层次结构将包含在所选项目集内。可以使用 webkitEntries (en-US) 属性获取选定的文件系统条目,意思是:文件夹中还有文件夹的多级结构,也能成功选到其中的文件;
使用这种方式存在的问题:
- 会显示浏览器的上传提示弹窗,(比较丑)
- 无法判断空文件夹,上传了空文件夹不会触发change事件,所以响应不好
二、upload组件
基础用法
<template>
<el-upload
v-model:file-list="fileList"
class="upload-demo"
action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
multiple
:on-preview="handlePreview"
:on-remove="handleRemove"
:before-remove="beforeRemove"
:limit="3"
:on-exceed="handleExceed"
>
<el-button type="primary">Click to upload</el-button>
<template #tip>
<div class="el-upload__tip">
jpg/png files with a size less than 500KB.
</div>
</template>
</el-upload>
</template>
拖拽上传
upload组件里面支持拖拽上传,源码 封装了一个 upload-dragger 子组件,主要利用了DragEvent.dataTransfer 这个只读属性;在进行拖放操作时,传输的数据。核心代码如下:
<template>
<div
:class="[ns.b('dragger'), ns.is('dragover', dragover)]"
@drop.prevent="onDrop"
@dragover.prevent="onDragover"
@dragleave.prevent="dragover = false"
>
<slot />
</div>
</template>
const onDrop = (e: DragEvent) => {
if (disabled.value) return
dragover.value = false
e.stopPropagation()
const files = Array.from(e.dataTransfer!.files)
emit('file', files)
}
源码解析
看了一下源码,主要核心代码还是使用的 input标签 type="file",主要加上了一些其他的样式,还有文件列表;
<input
ref="inputRef"
:class="ns.e('input')"
:name="name"
:multiple="multiple"
:accept="accept"
type="file"
@change="handleChange"
@click.stop
/>
三、useFileDialog
在vueUse库中,存在一个处理文件上传的 hooks: useFileDialog
<script setup lang="ts">
import { useFileDialog } from "@vueuse/core";
const { files, open, reset, onChange } = useFileDialog({
accept: "image/*", // 设置类型
directory: true, // 设置 - 选择文件夹
});
onChange((files) => {
/** do something with files */
});
</script>
<template>
<div>
<button type="button" @click="() => open()">Choose file</button>
</div>
</template>
查看源码发现与 核心就是 input标签 type="file",与前面第一条很类似,不过封装思路值得学习;
四、js新api,window.showOpenFilePicker()
js新增了 window.showOpenFilePicker()、 window.showDirectoryPicker()、window.showSaveFilePicker() 等api,兼容性查,慎用;
五、其他细节
1. 中断终止
因为上传是个耗时的任务,用户很可能中途停止,如关闭弹窗、关闭抽屉等,这时候我们需要终止上传;
使用action方式的中断方法,调用组件暴露给外部的api
uploadRef.value?.abort();
使用:http-request=自定义上传请求的,可以使用AbortController中断
const controller = new AbortController()
try {
const r = await fetch('/json', { signal: controller.signal });
} catch (e) {}
// 取消上传
controller.abort()
2. 大文件上传
一般大文件上传需要切片还要考虑断点重传,笔者还未研究这块,暂时给自己留个作业
3. 替换原文件
限制只能上传一个文件,再次选择时需要替换掉原文件,(其实这个交互在组件中是没有的,按照组件的实现应该 x 掉原来的组件,然后重新上传一个, 但是 PM 觉得就要 能继续选择文件,而且覆盖原文件, coder 有啥办法呢, 当然是满足他😌 )
这里使用 el-upload 举例子,而 ant-design 则可以 通过 maxCount 属性限制上传数量。当为 1 时,始终用最新上传的代替当前。
方案1: 设置 limit 和 on-exceed 可以在选中时自动替换上一个文件 (官方文档的例子)
<template>
<el-upload
ref="upload"
class="upload-demo"
action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
:limit="1"
:on-exceed="handleExceed"
:auto-upload="false"
>
<template #trigger>
<el-button type="primary">select file</el-button>
</template>
<el-button class="ml-3" type="success" @click="submitUpload">
upload to server
</el-button>
<template #tip>
<div class="el-upload__tip text-red">
limit 1 file, new file will cover the old file
</div>
</template>
</el-upload>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { genFileId } from 'element-plus'
import type { UploadInstance, UploadProps, UploadRawFile } from 'element-plus'
const upload = ref<UploadInstance>()
const handleExceed: UploadProps['onExceed'] = (files) => {
upload.value!.clearFiles()
const file = files[0] as UploadRawFile
file.uid = genFileId()
upload.value!.handleStart(file)
}
const submitUpload = () => {
upload.value!.submit()
}
</script>
方案2:利用选择文件都会触发change事件
<script setup lang="ts">
const fileList = ref([])
const handleFileChange = (file, list) => {
if (list.length > 1) {
list.splice(0, 1)
}
fileList.value = list
}
</script>
<template>
<el-upload
v-model:file-list="fileList"
class="upload-demo"
action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
multiple
:on-preview="handlePreview"
:on-remove="handleRemove"
:before-remove="beforeRemove"
:on-change="handleFileChange"
>
<el-button type="primary">Click to upload</el-button>
<template #tip>
<div class="el-upload__tip">
jpg/png files with a size less than 500KB.
</div>
</template>
</el-upload>
</template>