用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...

相关推荐
Dread_lxy8 分钟前
vue 依赖注入(Provide、Inject )和混入(mixins)
前端·javascript·vue.js
奔跑草-1 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
羡与1 小时前
echarts-gl 3D柱状图配置
前端·javascript·echarts
前端郭德纲1 小时前
浏览器是加载ES6模块的?
javascript·算法
JerryXZR2 小时前
JavaScript核心编程 - 原型链 作用域 与 执行上下文
开发语言·javascript·原型模式
帅帅哥的兜兜2 小时前
CSS:导航栏三角箭头
javascript·css3
渗透测试老鸟-九青2 小时前
通过投毒Bingbot索引挖掘必应中的存储型XSS
服务器·前端·javascript·安全·web安全·缓存·xss
龙猫蓝图2 小时前
vue el-date-picker 日期选择器禁用失效问题
前端·javascript·vue.js
夜色呦2 小时前
掌握ECMAScript模块化:构建高效JavaScript应用
前端·javascript·ecmascript
peachSoda72 小时前
随手记:简单实现纯前端文件导出(XLSX)
前端·javascript·vue.js