本文讲解实现文件上传的前后端逻辑,是大文件上传实现专栏的第一篇文章,为专栏后面的其他进阶功能做铺垫。
本文最终实现:后端上传接口,前端交互(点击选择文件、拖拽选择文件、上传文件以及上传进度条显示)。
值得注意的是:我们接口处理文件并不是用的multer
(解析FormData), 而是以流的方式接收和处理。
后端部分
后端部分,用 Express.js
来做。需求是:要接收文件 并 以指定文件名 保存到特定文件夹下面。
先创建项目和安装依赖:
# 创建项目
mkdir server-by-express
cd server-by-express
npm init -y
# 安装将用到的依赖
npm i express fs-extra cors
新建项目之后,在vscode中打开,新建一个index.js文件,键入如下代码:
const express = require("express");
const cors = require("cors");
const fs = require("fs-extra");
const app = express();
app.use(cors());
app.get("/", (req, res) => {
res.json({ message: "Hello World" });
});
app.listen(8080, () => {
console.log("Server is running on port 3000");
});
在项目根目录下执行 node index.js
跑起来看下
测试一下没问题
下面开始业务相关代码:
先确保文件上传的目录存在
// 临时文件夹,文件将会被上传到这个文件夹中
const TEMP_DIR = path.resolve(__dirname, "temp");
// 确保临时文件夹存在
fs.ensureDir(TEMP_DIR);
然后定义一个辅助函数,用来将可读流流入到可写流:
/**
* 数据从可读流流向可写流
* @param {ReadableStream} rs 可读流
* @param {WritableStream} ws 可写流
* @returns 返回一个Promise,当流结束时,Promise会被resolve
*/
function pipeStream(rs, ws) {
return new Promise((resolve, reject) => {
rs.pipe(ws).on("finish", resolve).on("error", reject);
});
}
接下来实现上传文件接口:
app.post("/upload/:filename", async (req, res) => {
// 获取文件名
const { filename } = req.params;
// 拼接文件路径
const filePath = path.resolve(TEMP_DIR, filename);
// 创建可读流
const ws = fs.createWriteStream(filePath);
// 将可读流中的数据写入到可写流中,完成文件上传
await pipeStream(req, ws);
res.json({ success: true });
});
注意这里以流的形式接收文件,而不是借助 multer
处理 FormData
数据。
为了热更新,代码改完及时生效,安装下 npm i nodemon
, 然后在package.json
添加一条script
然后执行 npm run start
重启下,后面修改了就会自动重启了。
下面用 postman 来测试下:
用 post 请求,body 里面选择二进制文件,测试没问题,后端接口就完成了。
前端部分
先看看最终效果:
准备工作
用vite
初始化项目,选择 vue
, 用 javascript
就好, 项目名为 client-by-vue
# 创建和启动项目
npm create vite@latest
cd client-by-vue
npm i
npm run dev
先把无关的文件删除掉,保持简单。
打开网址看看,没问题。
把后面会用到依赖安装了,分别是element-plus
和 axios
, element-plus
安装按照官网指南全量安装,包括图标:
npm i element-plus @element-plus/icons-vue axios
试一下组件库引入:
好了,以上就是前端需要准备的部分,下面开始开发文件上传功能
样式
先把样式写出来:画一个文件选择框和一个按钮,鼠标悬浮和边框变色
<template>
<div id="upload-container">
<el-icon :size="30"><Files /></el-icon>
</div>
<el-button>开始上传</el-button>
</template>
<script setup></script>
<style scoped>
#upload-container {
height: 200px;
line-height: 200px;
text-align: center;
border: 1px dashed #ccc;
margin-bottom: 20px;
cursor: pointer;
}
#upload-container:hover,
#upload-container.is-dragover {
border: 1px dashed #0037ff;
color: #0037ff;
}
</style>
实现拖拽选择文件功能
拖拽获取到文件,需要用到drop
事件,在 event.dataTransfer
读取到,同时要阻止浏览器默认事件; isDragover
用于控制拖拽的高亮(注意看上面的css样式):
<div
id="upload-container"
:class="{ 'is-dragover': isDragover }"
@drop="handleDrop"
@dragover.prevent
@dragenter.prevent="isDragover = true"
@dragleave.prevent="isDragover = false"
>
<el-icon :size="30"><Files /></el-icon>
</div>
const { isDragover } = toRefs(
reactive({
isDragover: false,
})
);
const handleDrop = (e) => {
e.preventDefault();
const { files } = e.dataTransfer;
console.log("files: ", files);
};
效果是这样的:
这样就拿到了拖拽的File
文件对象了,可以用来上传。
实现点击选择文件功能
除了拖拽获取到文件,还有点击选择文件交互,也来实现一下:利用input:type=file
的点击事件,可以打开选择文件的弹框。
<div
id="upload-container"
...
@click="handleClick"
...
>
<el-icon :size="30"><Files /></el-icon>
</div>
const handleClick = () => {
const input = document.createElement("input");
input.type = "file";
input.addEventListener("change", (e) => {
const { files } = e.target;
console.log("files: ", files);
});
input.click();
};
效果同样可以拿到文件对象
文件展示
例子以视频和图片讲解,来展示一下拿到视频文件和图片文件吧。
思路上就是用一个响应式对象,选完文件,就存一下 url(URL.createObjectURL(file)
)和原文件对象,template 中根据类型用img
或video
进行展示
// 新增一个 selectedFile 用来存放文件信息
const { isDragover, selectedFile } = toRefs(
reactive({
isDragover: false,
selectedFile: { url: "", file: null },
})
);
const handleDrop = (e) => {
e.preventDefault();
const { files } = e.dataTransfer;
if (files.length === 0) return;
// 限制只能上传图片或视频
const isMedia =
files[0].type.indexOf("image") > -1 || files[0].type.indexOf("video") > -1;
if (!isMedia) {
return ElMessage.warning("只能上传图片或视频");
}
// 拿到文件之后,存放到 selectedFile 中, url 为文件的本地路径,file 为文件对象
selectedFile.value = {
url: URL.createObjectURL(files[0]),
file: files[0],
};
};
const handleClick = () => {
const input = document.createElement("input");
input.type = "file";
input.addEventListener("change", (e) => {
const { files } = e.target;
if (files.length === 0) return;
// 限制只能上传图片或视频
const isMedia =
files[0].type.indexOf("image") > -1 ||
files[0].type.indexOf("video") > -1;
if (!isMedia) {
return ElMessage.warning("只能上传图片或视频");
}
// 拿到文件之后,存放到 selectedFile 中, url 为文件的本地路径,file 为文件对象
selectedFile.value = {
url: URL.createObjectURL(files[0]),
file: files[0],
};
});
input.click();
};
<div
id="upload-container"
...
>
<template v-if="selectedFile.url">
<img
v-if="selectedFile.file.type.indexOf('image') > -1"
:src="selectedFile.url"
/>
<video
controls
v-if="selectedFile.file.type.indexOf('video') > -1"
:src="selectedFile.url"
></video>
</template>
<el-icon v-else :size="30"><Files /></el-icon>
</div>
效果是这样的:
文件上传
好了,选择文件功能搞定,接下来简单封装一下axios
, 用来发送上传文件请求。
src 目录下面新建一个 axiosInstance.js 文件,内容如下:
// axios 设置baseURL 响应拦截器
import axios from "axios";
const axiosInstance = axios.create({
baseURL: "http://localhost:8080",
});
axiosInstance.interceptors.response.use(
(response) => {
if (response.data && response.data.success) {
return response.data;
}
},
(error) => {
console.log("error: ", error);
return Promise.reject(error);
}
);
export default axiosInstance;
在 App.js 文件中引入 axiosInstance.js 来发送请求:
import axiosInstance from "./axiosInstance";
...
const handleUpload = () => {
if (!selectedFile.value.file) {
return ElMessage.warning("请先选择文件");
}
const file = selectedFile.value.file;
axiosInstance
.post(`/upload/${file.name}`, file, {
headers: {
"Content-Type": "application/octet-stream",
},
})
.then((res) => {
ElMessage.success("上传成功");
})
.catch((err) => {
ElMessage.error("上传失败");
});
}
<!-- 按钮添加上点击事件 -->
<el-button @click="handleUpload">开始上传</el-button>
开始发送请求,注意请求头设置 application/octet-stream
!
看看上传效果:
上传进度展示
上面演示的都是小文件,本地上传速度很快,接下来试一下大一点的文件,我找了一个1.5G的视频:
在本地没有网络延迟的情况下,虽然上传成功了,但是有明显的等待感,网络请求一直 pending, 体验不是很好,下面加个显示上传进度来优化一下:
// 添加一个对象,用来保存进度信息
const {
...
progressInfo
} = toRefs(
reactive({
...
progressInfo: {
name: "",
percent: 0,
},
})
);
const handleUpload = () => {
...
const file = selectedFile.value.file;
axiosInstance
.post(`/upload/${file.name}`, file, {
headers: {
"Content-Type": "application/octet-stream",
},
// 这里更新进度信息
onUploadProgress(progressEvent) {
progressInfo.value = {
name: file.name,
percent: Math.round(
(progressEvent.loaded * 100) / progressEvent.total
),
};
},
})
...
};
<div class="progress" v-if="progressInfo.percent > 0">
<span>{{ progressInfo.name }}</span>
<el-progress :percentage="progressInfo.percent"></el-progress>
</div>
通过axios
发送请求配置项的 onUploadProgress
回调可以实时更新上传的进度。
最终就达到了上面展示的效果:
总结
本文带大家起项目,先开发后端上传接口,然后大篇幅演示实现拖拽选择文件、点击选择文件、上传文件以及上传进度展示等前端交互。
代码可以跟着上面的讲解敲一遍加深印象,同时我放到gitee上面,有需要自取。
如果有帮助也望能顺手点个赞和收藏,这是我继续更文的重要动力来源哦~