使用 HTTP 上传文件,HTTP 请求体共有 4 种编码方式:
- form-data
- x-www-form-urlencoded
- raw
- binary
作用如下:
类型 | Content-Type 值 | 作用 |
---|---|---|
form-data | multipart/form-data | 以 key-value 的形式组织数据,用分隔符 boundary 隔开,可以上传文件 |
x-www-form-urlencoded | application/x-www-from-urlencoded | 以 key-value 的形式组织数据,用&拼接,不能上传文件 |
raw | application/json,text/plain,text/html 等 | 上传文本 |
binary | application/octet-stream | 只能上传一个二进制文件,没有 key-value 对 |
下面挑选 multipart/form-data
和 application/octet-stream
来讲解如何上传文件
使用 form-data 上传文件
几句话概括思路
前端
- 请求体为
FormData
对象 - 浏览器会自行加上
Content-Type:multipart/form-data
请求头,并设置boundary
;不用自行设置此请求头
服务端
- 从请求体拿到
File
对象 - 文件名通过
file.name
获取
示例代码
前端
axios 示例
html
<input type="file" ref="file" /> <button @click="clickFn">上传</button>
javascript
clickFn() {
const value = this.$refs.file.files[0];
const formData = new FormData();
formData.append("file", value, value.name);
axios
.post("/upload", formData)
.then((res) => {
console.log(res);
})
.catch((err) => {
console.error(err);
});
}
fetch 示例
html
<input type="file" ref="file" /> <button @click="uploadFn">fetch上传</button>
javascript
uploadFn() {
const value = this.$refs.file.files[0];
const formData = new FormData();
formData.append("file", value, value.name);
fetch("/upload", {
method: "post",
body: formData,
})
.then((res) => {
console.log(res);
})
.catch((err) => {
console.error(err);
});
}
在 Nodejs 服务端环境上传文件
- 由于在 nodejs 环境中运行,需要安装
form-data
依赖
shell
npm i -S form-data
- 请求体使用
form-data
来发送文件 - 不需要指定
Content-Type
(axios 会根据数据类型自动加上Content-Type
,form-data
库会自动加上Content-Type
的boundary
值)
javascript
const axios = require("axios");
const FormData = require("form-data");
const { promises } = require("fs");
const path = require("path");
const app = new Koa();
const router = new Router();
const koaBody = KoaBody({
multipart: true,
});
const instance = axios.create({
baseURL: "http://localhost:3000",
});
async function main(instance) {
const content = await promises.readFile(
path.resolve(__dirname, "../static/test.xml")
);
const formData = new FormData();
formData.append("file", content, "test.xml"); // file 为前后端约定好的字段名,"test.xml"为发送的文件名(不能省略)
try {
const res = await instance.post("/upload", formData);
const str = res.data.toString();
console.log(str);
} catch (err) {
console.error(err);
}
}
const port = 3000;
app.use(router.routes()).use(router.allowedMethods());
app.listen(port);
console.log(`server started on port ${port}.`);
服务端
这里使用 koa 来实现
从 ctx.request.files
中获取 file
字段(前后端约定好) file.name
获取文件名
javascript
router.post("/upload", koaBody, async (ctx, next) => {
const file = ctx.request.files?.file;
console.log(file.toString("utf-8"));
try {
await promises.copyFile(
file.path,
path.resolve(__dirname, `../uploads_koa/${file.name}`)
);
} catch (err) {
console.error(err);
}
ctx.result = "OK";
ctx.status = 200;
next();
});
运行效果截图
使用 octet-stream 上传文件
几句话概括思路
使用非 get 的请求(需要把二进制流放到请求体中,所以不带请求体的 get method 不能用)
前端:
- 请求头设置
"Content-Type": "application/octet-stream"
- 请求体为
File
对象 - 文件名可以放到 URI 参数中,也可以自定义请求头字段,要注意使用
encodeURIComponent
来对文件名编码
服务端:
- 拿到请求体的流,保存成文件
- 从 URI 参数或者自定义请求头字段中获取文件名,要注意使用
decodeURIComponent
来对文件名解码
示例代码
前端
axios 示例
javascript
async postRequestStream() {
const param = new URLSearchParams();
param.append("requestType", "fileStream");
const file = this.$refs.file.files[0];
const requestData = file;
param.append("custom-filename", encodeURIComponent(file.name)); // 通过URI参数传文件名
try {
const res = await this.instance.post(
`/req?${param.toString()}`,
requestData,
{
headers: {
"Content-Type": "application/octet-stream",
"my-filename": encodeURIComponent(file.name), // 通过自定义请求头字段传文件名
},
data: requestData,
}
);
this.data = res.data;
} catch (err) {
console.error(err);
}
},
fetch 示例
javascript
async postRequestStream() {
const param = new URLSearchParams();
param.append("requestType", "fileStream");
const file = this.$refs.file.files[0];
param.append("custom-filename", encodeURIComponent(file.name)); // 通过URI参数传文件名
try {
const res = await fetch(
`/req?${param.toString()}`,
{
method: "post",
headers: {
"Content-Type": "application/octet-stream",
"my-filename": encodeURIComponent(file.name), // 通过自定义请求头字段传文件名
},
body: file,
}
);
} catch (err) {
console.error(err);
}
},
服务端
koa 示例
需要编写代码从请求体中拿到文件流的内容,做法是:
- 把读取文件流的代码封装成一个
promise
- 监听
ctx.req
的data
事件,在回调函数中把内容保存到缓冲区数组 - 监听
ctx.req
的end
事件,执行promise
的resolve
回调
javascript
async function fileStreamRequestHandler(ctx) {
const chunks = [];
console.log(
"filename: ",
decodeURIComponent(ctx.request.headers["my-filename"])
); // 解析通过URI参数传过来的文件名
console.log(
"url param filename: ",
decodeURIComponent(ctx.query["custom-filename"])
); // 解析通过自定义请求头字段传过来的文件名
const promise = new Promise((resolve, reject) => {
ctx.req.on("data", (chunk) => {
chunks.push(chunk);
});
ctx.req.on("end", () => {
const buffer = Buffer.concat(chunks);
resolve(buffer);
});
ctx.req.on("error", (err) => {
reject(err);
});
}); // 把读取文件流的代码封装成一个promise
try {
const buffer = await promise;
ctx.body = buffer;
ctx.response.set({
"Content-Type": "application/octet-stream",
"Content-Disposition": "",
});
ctx.status = 200;
} catch (err) {
ctx.body = {
msg: "error",
data: err,
};
ctx.status = 500;
}
return;
}
运行效果截图
chrome 浏览器无法预览 application/octet-stream
类型的 body,不过可以根据 请求头的 Content-Length
字段的值看出,已经上传了文件