文件上传是一个非常常见的功能, 透过现象看本质才能让我们更了解文件是如何上传的
1.使用REST Client实现模拟文件上传
REST Client可以让我们实现各种http请求, 让我们从简单的开始, 使用REST CLient模拟一个get请求
- 首先我们在VSCode 插件市场上下载REST CLient插件
- 创建一个后缀名为http的文件,如 upload.http
1.1 使用REST CLient发送一个get请求
在upload.http文件中编写请求格式, 这里简单发送一个get请求,获取掘金的一篇文章
http
# 请求方法 请求路径 HTTP协议版本号
GET /post/7237020208648634429 HTTP/1.1
# 请求地址
Host: juejin.cn
可以看到, 我们点击Send Request 之后, 就可以看到服务器给我们返回的数据了
1.2 使用REST CLient模拟上传一个文件
- 接下来我们就用REST CLient模拟上传一个文件
http
# 请求方法 请求路径 HTTP协议版本号
POST /uploadFile HTTP/1.1
# 请求地址
Host: localhost:8080
# 这里的Content-type 表示后面的请求体用的是什么格式, 这里的multipart/form-data表示请求体是由多部分组成的, multipart/form-data 需要分隔符boundary; 也就是说请求体里面的每一部分都是由分隔符分隔开的
Content-Type: multipart/form-data; boundary=myboundary
# 需要在上面空一行,表示请求头结束,下面就是请求体了, 每个字段以 --myboundary开始
--myboundary
# Content-Disposition: form-data;固定写法 name属性表示要上传的字段的名称,同等于Input里面的name属性, filename属性值表示要上传文件的名称,如果上传的不是文件,则filename属性值可以省略
Content-Disposition: form-data; name="userName";
# 需要在上面空一行,然后再写该字段对应的数据, 例如: zhangsan
zhangsan
--myboundary
Content-Disposition: form-data; name="myUploadFile"; filename="test.jpg"
# 该字段是文件数据, 还需要告诉服务器该文件的文件类型
Content-Type: image/jpeg
# 这里是文件的二进制数据,在REST Client中可以通过 < 文件路径读出文件的二进制数据
< ./test.jpg
--myboundary--
# --myboundary-- 表示请求体结束
这样子我们就完成了文件的上传了,关于服务端,我们用node简单写了一个接收文件的接口
js
const express = require('express')
const cors = require('cors')
const multer = require('multer')
const app = express()
app.use(cors())
app.use(express.json())
//保存上传的文件到这个路径下
const upload = multer({ dest: './uploads/imgs' })
// 接收文件的字段名为 myUploadFile
app.post('/uploadFile', upload.single('myUploadFile'), (req, res) => {
console.log(req.file)
res.send(JSON.stringify({
code: 200,
msg: 'success'
}))
})
app.listen(8080, () => {
console.log('server is running at http://localhost:8080')
})
我们可以看到,上传文件的几步重要步骤
1. 上传文件只需要拿到需要上传的文件
2. 使用Content-Type: multipart/form-data 的形式传输该文件
3. 上传文件的二进制数据到服务端
1.3 关于multipart/form-data
一般我们上传文件得时候, Content-type
得值都是multipart/form-data
, 为什么我们要使用Content-type呢? 这要从Content-Type的作用说起
Content-Type作用是为了告诉别人我携带的数据是什么格式
举个例子,我们使用express 返回一个文本, 设置ContentType,以不同形式解析该数据,得到的结果是不同的
当Content-type 的值为text/plain的时候, 浏览器解析出来的结果是这样的
js
app.get('/get', (req, res) => {
res.set('Content-Type', 'text/plain')
res.sendfile(`${__dirname}/uploads/html/hello.txt`)
})
当Content-type 的值为text/html;charset=utf-8的时候, 浏览器解析出来的结果是这样的
js
app.get('/get', (req, res) => {
res.set('Content-Type', 'text/html;charset=utf-8')
res.sendfile(`${__dirname}/uploads/html/hello.txt`)
})
可以看到, 尽管我们返回的文件后缀是 .txt , 但是他还是以 html的形式去解析, 所以我们得出一个结论
不管返回的文件后缀名是什么,浏览器是根据 Content-type
设定的值去解析的
enctype:规定了form表单在发送到服务器时候编码方式,它有如下的三个值。
application/x-www-form-urlencoded
:默认的编码方式。但是在用文本的传输和MP3等大型文件的时候,使用这种编码就显得 效率低下,该编码方式把form数据转换成一个字串(name1=value1&name2=value2...),然后把这个字串append到url后面,用?分割,加载这个新的url。。text/plain
:纯文体的传输。空格转换为 "+" 加号,但不对特殊字符编码。multipart/form-data
:multipart,顾名思义,多部分。使用该编码方式会将表单进行分割成每个控件(以--boundary为分隔符
,将分割控件数据分隔开,在最后以----boundary----结尾
)。每个部分必须加上Content-Disposition(form-data)
,对于上传文件还会设置Content-Type。该编码方式如文章1.2所示
我们在上传文件的时候,为什么不是使用application/x-www-form-urlencoded
:默认的编码方式呢?
multipart/form-data
最初由《RFC 1867: Form-based File Upload in HTML》文档提出。
text
The encoding type application/x-www-form-urlencoded is inefficient
for sending large quantities of binary data or text containing
non-ASCII characters. Thus, a new media type,multipart/form-data,
is proposed as a way of efficiently sendingthe values associated
with a filled-out form from client to server.
1867文档中写了为什么要新增一个类型,而不使用旧有的application/x-www-form-urlencoded
:因为此类型不适合用于传输大型二进制数据或者包含非ASCII字符的数据 。平常我们使用这个类型都是把表单数据使用url编码后传送给后端,二进制文件当然没办法一起编码进去了。所以multipart/form-data
就诞生了,专门用于有效的传输文件。
2. 使用Axios实现文件传
通过刚才讲的, 我们基本明白了文件上传的原理, 那么现在我们来使用Vue + Axios实现一个文件上传,具体步骤为
- 给文件文本框添加onChange事件
- 点击上传文件时, 触发change事件,拿到要上传的文件
- 创建FormData 对象 ,发送键值对数据, 浏览器会自动添加请求头
Content-Type: multipart/form-data
- 将需要上传的数据通过append 添加到 FromData 对象
- 发送网络请求
2.1 关于FormData
FormData 对象用以将数据编译成键值对,以便用XMLHttpRequest
来发送数据。其主要用于发送表单数据,但亦可用于发送带键数据 (keyed data),而独立于表单使用 。使用FormData() 构造函数
,浏览器会自动识别并添加请求头 "Content-Type: multipart/form-data",且参数依然像是表单提交时的那种键值对,此外 FormData() 构造函数 new 时可以直接传入 form 表单的 dom 节点。
js
<template>
<div class="swapper">
<div class="upload_icon">+</div>
<!-- 1. 添加change事件 -->
<input type="file" @change="handleFileChange" class="upload_input">
<button type="submit" @click="handleClick">提交</button>
</div>
</template>
<script setup>
import axios from 'axios';
import { ref } from 'vue'
const file = ref(null)
const containerDom = ref(null)
const handleFileChange = async(e) => {
// 2. e.target.files 是文件数组, 我们取下标问0, 拿到文件对象
file.value = e.target.files[0];
}
const handleClick = async () => {
upLoadFile(file.value)
}
const upLoadFile = async (file,maxSize = 10* 1024 * 1024) => {
if(file.size > maxSize) {
alert('文件过大')
return
}
// 3. 创建FormData对象
const formData = new FormData()
// 4. 给FormData对象添加键值对, 字段名为 myUploadFile , 值为 文件二进制数据
formData.append('myUploadFile',file)
// 5. 发送表单数据上传文件
await axios.post('http://localhost.charlesproxy.com:8080/uploadFile',formData)
}
</script>
<style scoped>
.swapper{
margin: 100px auto;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.upload_input {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
display: block;
opacity: 0;
cursor: pointer;
z-index: 10;
}
button{
margin-top: 20px;
}
</style>
这里有同学就想问了, 我们通过change拿到的文件对象为什么可以直接 append 到 FormData 对象中, 不用转换为 二进制吗, 其实 我们通过change拿到的文件对象就是一个二进制文件, 是可以直接上传的 ,详细看这里File - Web API 接口参考 | MDN (mozilla.org)
3. 预览图片
当我们选择完文件,想要看一下文件的内容, 这时候要怎么办呢? 很简单,只需要这几步
- 创建一个文件读取器
FileReader
对象 - 通过
FileReader
对象的 read方法将文件读取为 base64编码的格式 - 由于读取文件是异步进行的, 读取完文件时会调用
FileReader
对象的 onLoad方法,在这里将读取完的结果赋值给元素的url就可以了
js
<template>
<div class="swapper">
<div class="upload_container" ref="containerDom">
<div class="upload_icon">+</div>
<input type="file" @change="handleFileChange" class="upload_input">
</div>
<button type="submit" @click="handleClick">提交</button>
</div>
</template>
<script setup>
import axios from 'axios';
import { ref } from 'vue'
const file = ref(null)
const containerDom = ref(null)
const handleFileChange = async (e) => {
console.log(e.target.files)
file.value = e.target.files[0];
preView()
}
const preView = () => {
// 1. 创建一个文件读取器 `FileReader` 对象
const reader = new FileReader()
// 2. 通过 `FileReader` 对象的 read方法将文件读取为 base64编码的格式
reader.readAsDataURL(file.value)
// 3. 由于读取文件是异步进行的, 读取完文件时会调用 `FileReader` 对象的
// onLoad方法,在这里将读取完的结果赋值给元素的url就可以了
reader.onload = (e) => {
containerDom.value.style.backgroundImage = `url(${e.target.result})`
}
}
const handleClick = () => {
upLoadFile(file.value)
}
const upLoadFile = async (file, maxSize = 10 * 1024 * 1024) => {
if (file.size > maxSize) {
alert('文件过大')
return
}
const formData = new FormData()
formData.append('myUploadFile', file)
await axios.post('http://localhost.charlesproxy.com:8080/uploadFile', formData)
}
</script>
<style scoped>
.swapper {
/* display: flex; */
margin: 100px auto;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.upload_container {
margin: auto;
display: flex;
align-items: center;
justify-content: center;
width: 100px;
height: 100px;
border: 1px solid #ccc;
position: relative;
}
.upload_input {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
display: block;
opacity: 0;
cursor: pointer;
z-index: 10;
}
button {
margin-top: 20px;
}
</style>