用2种方式实现文件上传

使用 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-dataapplication/octet-stream 来讲解如何上传文件

使用 form-data 上传文件

几句话概括思路

前端
  1. 请求体为 FormData 对象
  2. 浏览器会自行加上 Content-Type:multipart/form-data 请求头,并设置 boundary;不用自行设置此请求头
服务端
  1. 从请求体拿到 File 对象
  2. 文件名通过 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 服务端环境上传文件

  1. 由于在 nodejs 环境中运行,需要安装 form-data 依赖
shell 复制代码
npm i -S form-data
  1. 请求体使用 form-data 来发送文件
  2. 不需要指定 Content-Type(axios 会根据数据类型自动加上 Content-Typeform-data 库会自动加上 Content-Typeboundary 值)
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 不能用)

前端:

  1. 请求头设置 "Content-Type": "application/octet-stream"
  2. 请求体为 File 对象
  3. 文件名可以放到 URI 参数中,也可以自定义请求头字段,要注意使用 encodeURIComponent 来对文件名编码

服务端:

  1. 拿到请求体的流,保存成文件
  2. 从 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 示例

需要编写代码从请求体中拿到文件流的内容,做法是:

  1. 把读取文件流的代码封装成一个 promise
  2. 监听 ctx.reqdata 事件,在回调函数中把内容保存到缓冲区数组
  3. 监听 ctx.reqend 事件,执行 promiseresolve 回调
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 字段的值看出,已经上传了文件

参考资料

docs.huihoo.com/rfc/RFC1867...

相关推荐
前端老鹰2 分钟前
JavaScript Intl.RelativeTimeFormat:自动生成 “3 分钟前” 的国际化工具
前端·javascript
梦想CAD控件2 分钟前
(在线CAD插件)网页CAD实现图纸表格智能提取
前端·javascript·全栈
sorryhc23 分钟前
【AI解读源码系列】ant design mobile——Space间距
前端·javascript·react.js
页面仔Dony1 小时前
绝对路径与相对路径的区别及作用
前端·javascript
林太白1 小时前
Zustand状态库(简洁、强大、易用的React状态管理工具)
前端·javascript·react.js
YuJie2 小时前
vue3 无缝滚动
前端·javascript·vue.js
小野鲜2 小时前
前端打开新的独立标签页面,并且指定标签页的大小,管理新标签页面的打开和关闭(包含源码和使用文档)
前端·javascript
十五_在努力2 小时前
参透 JavaScript —— 解析浅拷贝、深拷贝及手写实现
前端·javascript
王六岁2 小时前
JavaScript值和引用详解:从栈堆内存到面试实战
javascript·面试
ikonan4 小时前
译:Chrome DevTools 实用技巧和窍门清单
前端·javascript