文件上传是 Web 项目的常用功能。相信大家在开发过程中或多或少都遇到过相关的需求。
在今天的这篇文章中,我总结了一些场景和解决方案,希望能帮助你彻底掌握文件上传相关的问题。
我们的目标
首先,我们来明确一下文件上传的具体功能。
根据上传目标,有3种任务:
- 上传单个文件
- 同时上传多个文件
- 上传整个文件夹
根据用户操作,有:
- 选择要上传的文件
- 将文件拖到框中然后上传
- 从剪贴板上传
从性能的角度来看,我们可能需要:
- 压缩文件后上传
- 将大文件分成块然后上传
另外,有时我们可能不会在客户端浏览器上传文件,而是通过服务器上传到另一台服务器。
我们将依次讨论这些。
准备工作
在开始编程工作之前,我们还是需要了解一些背景知识。
- 首先,在上传文件时,我们使用最流行的 HTTP 库 Axios。在实际开发中,我们一般不直接使用 XMLHttpRequest,使用 Axios 符合真实的开发模式。
- 当我们在前端讨论上传文件的时候,要想全面了解相关原理,就必须了解相关的后端代码。这里我们使用 Koa 来实现我们的服务器。
- 最后希望大家对formdata有一个简单的了解,我们使用这种数据格式来上传文件。
上传单个文件
传单个文件的需要太常见了。例如,当我们注册微信后,我们可能需要上传更改一个头像。
文件上传功能需要客户端和服务器的配合。在我们的项目中,用户在客户端选择一个文件,然后上传到服务器;服务器保存文件并返回它的 URL。
这是项目预览:
上面的 Gif 展示了文件上传的完整过程:
- 用户在浏览器中选择文件
- 用户点击上传按钮
- 上传的文件放在服务器的uploadFiles文件夹中
- 然后服务器返回一个URL,就是上传文件的地址
- 用户可以通过这个 URL 访问资源
编码
这个项目的所有代码都保存在 GitHub 上:
你可以将其克隆到你的计算机:
# clone it
$ git clone git@github.com:BytefishMedium/FileUploading.git
# and install npm package
$ cd FileUloading
$ npm install
所有与单个文件上传相关的代码都放在 1-SingleFile 文件夹中。
- client.html 与客户端代码相关。
- server.js 与服务器端代码相关
要运行服务器,你可以转到该文件夹并运行以下命令:
$ node server.js
然后,我们可以在任何浏览器上打开 client.html。
具体操作我已经在上面的gif中展示过了。你可以先自己尝试一下,然后继续阅读。
客户端代码
嗯,把大象放进冰箱需要多少步骤?
只需三步:
- 打开冰箱门
- 把大象放进去
- 关上门。
上传文件也是如此,我们只需要三个步骤:
- 让用户选择要上传的文件
- 阅读此文件
- 使用axios上传文件
在 HTML 中,我们可以使用 input 元素。只需将此元素的类型设置为文件,然后该元素即可用于选择文件。
<input id="fileInput" type="file"/>
用户选择文件后,该文件的元数据将存储在此输入元素的 files 属性中。
const uploadFileEle = document.getElementById("fileInput")
console.log(uploadFileEle.files[0]);
最后,我们使用 Axios 的 post 方法来上传文件。但是在上传文件之前,我们还需要把这个文件打包成FormData格式。
let file = fileElement.files[0];
let formData = new FormData();
formData.set('file', file);
axios.post("http://localhost:3001/upload-single-file", formData)
.then(res => {
console.log(res)
})
提示:FormData 是一种键值类型的数据格式。这是一个例子:
好了,这些就是文件上传相关的知识点了,更完整的代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src=" https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
</head>
<body>
<input type="file" id="fileInput">
<button id="uploadButton">upload</button>
<script>
document.getElementById("uploadButton").onclick = () => {
let fileElement = document.getElementById('fileInput')
// check if user had selected a file
if (fileElement.files.length === 0) {
alert('please choose a file')
return
}
let file = fileElement.files[0]
let formData = new FormData();
formData.set('file', file);
axios.post("http://localhost:3001/upload-single-file", formData, {
onUploadProgress: progressEvent => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
console.log(`upload process: ${percentCompleted}%`);
}
})
.then(res => {
console.log(res.data)
console.log(res.data.url)
})
}
</script>
</body>
</html>
这段代码其实就是为了实现我们之前说的三个步骤:
只是我们增加了两个额外的功能:
- 一是上传按钮。当用户点击上传按钮时,我们开始执行上传逻辑。
- 然后我们再做一个判断,确保用户真的选择了一个文件。
然后,当axios上传文件时,它可以让我们监控文件上传的进度。
我们知道 HTTP 是建立在 TCP 之上的。如果一个HTTP报文比较大,可能会分解成多个不同的TCP报文在网络中传输。
如果你需要写一个进度条来向用户展示上传的进度,你可以使用这个 API。
axios.post("http://localhost:3001/upload-single-file", formData, {
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
console.log(`upload process: ${percentCompleted}%`);
},
});
progressEvent.loaded 表示上传成功的字节数,progressEvent.total 表示文件的总字节数。
好的,这是我们的客户端代码。
服务器端代码
要启动服务器,我们可以使用 Koa。这是一个使用 Koa 的小型服务器:
const path = require("path");
const Koa = require("koa");
const Router = require("@koa/router");
const app = new Koa();
const router = new Router();
const PORT = 3000;
router.get("/", async (ctx) => {
ctx.body = "Hello friends!";
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(PORT, () => {
console.log(`app starting at port ${PORT}`);
});
view rawkoa.server.v1.js hosted with ❤ by GitHub
这是最基本的 Koa 演示。由于本文重点介绍文件上传,所以我就不详细解释了。如果你不熟悉这个,你可以阅读官方文档。
- Koa:https://github.com/koajs/koa
- Koa-router:https://github.com/koajs/router
我们的客户端使用FormData的格式上传文件,那么,我们的服务器也需要解析FormData。而 Koa-multer 是一个帮助我们解析 FormData 数据的中间件:
const path = require("path");
const Koa = require("koa");
const Router = require("@koa/router");
const multer = require("@koa/multer");
const cors = require("@koa/cors");
const app = new Koa();
const router = new Router();
const PORT = 3000;
const upload = multer();
router.get("/", async (ctx) => {
ctx.body = "Hello friends!";
});
// add a route for uploading single files
router.post("/upload-single-file", upload.single("file"), (ctx) => {
console.log("ctx.request.file", ctx.request.file);
ctx.body = `file ${ctx.request.file.filename} has saved on the server`;
});
app.use(cors());
app.use(router.routes()).use(router.allowedMethods());
app.listen(PORT, () => {
console.log(`app starting at port ${PORT}`);
});
关于 Koa-multer,你可以阅读他们的官方文档:
- Koa-multer:https://github.com/koajs/multer
- Multer:https://github.com/expressjs/multer
关键代码是uoload.single('file'),这行代码可以帮助我们解析FormData的数据,然后,把对应的信息放到ctx.request.file中。
其实,此时我们的服务器已经可以接收到客户端上传的文件了,但是,接收到文件后并没有存储到磁盘中。
如果我们想让 Koa-multer 为我们将文件保存到磁盘,我们可以添加以下配置:
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, UPLOAD_DIR);
},
filename: function (req, file, cb) {
cb(null, `${file.originalname}`);
},
});
const upload = multer({ storage: storage });
完整的代码是 server.js,你可以直接在代码仓库中阅读。
当前的流程图如下所示:
无论如何,我们应该自己尝试一下。
上传多个文件
有了上面的基础,我们编写上传多个文件的代码就简单多了。
首先,我们需要修改 input 元素并为其添加 multiple 属性。
<input type="file" id="fileInput" multiple>
这是告诉浏览器现在这个输入元素应该允许用户同时选择多个文件。
然后用户选择多个文件后,数据会放在fileElement.files中。我们在构造formdata的时候,需要遍历这个列表,把所有的文件都放到formdata中。
let files = Array.from(fileElement.files);
let formData = new FormData();
files.forEach((file) => {
formData.append("file", file);
});
那么上传文件的代码就不需要修改了。
这是完整的代码:
该文件位于项目中的 2-MultipleFiles/client.html 上。
同时,我们还需要调整服务端的代码。
首先,我们需要添加对应的路由/upload-multiple-files
, 然后使用 upload.fields([{ name: “file” }]) 中间件来处理多个文件。之后request中的FormData数据会被放到ctx.files.file中。
演示:
上传目录
现在让我们看一下上传目录的步骤。
与之前类似,我们需要将 input 元素的属性设置为:
<input type="file" id="fileInput" webkitdirectory>
那么在上传目录的时候,input元素的files对象会有webkitRlativePath属性,我们也将它们添加到formdata中。
这里需要注意的是,当文件名中包含\时,koa-multer可能会报错。要解决此问题,我们需要将 \ 替换为 @ 符号。
formData.append('file', file, file.webkitRelativePath.replace(/\//g, "@"));
那么我们还需要修改对应的服务器代码:
演示:
总结
我们依次分析了上传单个文件、多个文件、目录的过程。其实很简单,只要3步:
- 使用输入元素让用户选择文件
- 读取文件并构造FormData
- 使用 Axios 上传 FormData
所有代码都在 GitHub 上,大家可以自己试试。如果您有任何问题,可以发表评论。
由于文章篇幅关系,剩下的文件上传会在后面的文章中介绍