详解-上传文件:从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>
相关推荐
用户47949283569156 小时前
Safari 中文输入法的诡异 Bug:为什么输入 @ 会变成 @@? ## 开头 做 @ 提及功能的时候,测试同学用 Safari 测出了个奇怪的问题
前端·javascript·浏览器
没有故事、有酒6 小时前
Ajax介绍
前端·ajax·okhttp
朝新_6 小时前
【SpringMVC】详解用户登录前后端交互流程:AJAX 异步通信与 Session 机制实战
前端·笔记·spring·ajax·交互·javaee
裴嘉靖6 小时前
Vue 生成 PDF 完整教程
前端·vue.js·pdf
毕设小屋vx ylw2824266 小时前
Java开发、Java Web应用、前端技术及Vue项目
java·前端·vue.js
冴羽7 小时前
今日苹果 App Store 前端源码泄露,赶紧 fork 一份看看
前端·javascript·typescript
蒜香拿铁7 小时前
Angular【router路由】
前端·javascript·angular.js
brzhang8 小时前
读懂 MiniMax Agent 的设计逻辑,然后我复刻了一个MiniMax Agent
前端·后端·架构
西洼工作室8 小时前
高效管理搜索历史:Vue持久化实践
前端·javascript·vue.js
广州华水科技8 小时前
北斗形变监测传感器在水库安全中的应用及技术优势分析
前端