详解-上传文件:从HTTP上传开始

文件上传是一个非常常见的功能, 透过现象看本质才能让我们更了解文件是如何上传的

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实现一个文件上传,具体步骤为

  1. 给文件文本框添加onChange事件
  2. 点击上传文件时, 触发change事件,拿到要上传的文件
  3. 创建FormData 对象 ,发送键值对数据, 浏览器会自动添加请求头Content-Type: multipart/form-data
  4. 将需要上传的数据通过append 添加到 FromData 对象
  5. 发送网络请求

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. 预览图片

当我们选择完文件,想要看一下文件的内容, 这时候要怎么办呢? 很简单,只需要这几步

  1. 创建一个文件读取器 FileReader 对象
  2. 通过 FileReader 对象的 read方法将文件读取为 base64编码的格式
  3. 由于读取文件是异步进行的, 读取完文件时会调用 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>
相关推荐
m0_748256782 分钟前
SpringBoot 依赖之Spring Web
前端·spring boot·spring
web1350858863531 分钟前
前端node.js
前端·node.js·vim
m0_5127446432 分钟前
极客大挑战2024-web-wp(详细)
android·前端
若川41 分钟前
Taro 源码揭秘:10. Taro 到底是怎样转换成小程序文件的?
前端·javascript·react.js
潜意识起点1 小时前
精通 CSS 阴影效果:从基础到高级应用
前端·css
奋斗吧程序媛1 小时前
删除VSCode上 origin/分支名,但GitLab上实际上不存在的分支
前端·vscode
IT女孩儿1 小时前
JavaScript--WebAPI查缺补漏(二)
开发语言·前端·javascript·html·ecmascript
m0_748256563 小时前
如何解决前端发送数据到后端为空的问题
前端
请叫我飞哥@3 小时前
HTML5适配手机
前端·html·html5
@解忧杂货铺5 小时前
前端vue如何实现数字框中通过鼠标滚轮上下滚动增减数字
前端·javascript·vue.js