Skip to content

文件上传

原理概述

原理就是根据 http 协议的规范和定义,完成请求消息体的封装和消息体的解析,然后将二进制内容保存到文件。

要上传一个文件,需要把form标签的enctype设置为multipart/form-data,同时method必须为post方法。

multipart/form-data表示什么

multipart互联网上的混合资源,就是资源由多种元素组成,form-data表示可以使用HTML Forms 和 POST 方法上传文件

http 请求的消息体

  • 请求头:

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryDCntfiXcSkPhS4PN表示本次请求要上传文件,其中boundary表示分隔符,如果要上传多个表单项,就要使用boundary分割,每个表单项由---XXX开始,以---XXX结尾。

  • 消息体- Form Data 部分

每一个表单项又由Content-Type和Content-Disposition组成。

Content-Disposition: form-data 为固定值,表示一个表单元素,name 表示表单元素的 名称,回车换行后面就是name的值,如果是上传文件就是文件的二进制内容。

Content-Type:表示当前的内容的 MIME 类型,是图片还是文本还是二进制数据。

解析

客户端发送请求到服务器后,服务器会收到请求的消息体,然后对消息体进行解析,解析出哪是普通表单哪些是附件。

可能大家马上能想到通过正则或者字符串处理分割出内容,不过这样是行不通的,二进制buffer转化为string,对字符串进行截取后,其索引和字符串是不一致的,所以结果就不会正确,除非上传的就是字符串。

不过一般情况下不需要自行解析,目前已经有很成熟的三方库可以使用。

最原始的文件上传

使用 form 表单上传文件

在 ie时代,如果实现一个无刷新的文件上传那可是费老劲了,大部分都是用 iframe 来实现局部刷新或者使用 flash 插件来搞定,在那个时代 ie 就是最好用的浏览器(别无选择)。

这种方式上传文件,不需要 js ,而且没有兼容问题,所有浏览器都支持,就是体验很差,导致页面刷新,页面其他数据丢失。

html
<form method="post" action="http://localhost:8100" enctype="multipart/form-data">
   选择文件:<input type="file" name="f1"/> input 必须设置 name 属性,否则数据无法发送		<br/>
	  <br/>
  标题:<input type="text" name="title"/>
    <br/>
    <br/>
    <br/>
    <button type="submit" id="btn-0">上 传</button>
</form>

文件上传接口

服务端文件的保存基于现有的库koa-body结合 koa2实现服务端文件的保存和数据的返回。

在项目开发中,文件上传本身和业务无关,代码基本上都可通用。

在这里我们使用koa-body库来实现解析和文件的保存。

koa-body 会自动保存文件到系统临时目录下,也可以指定保存的文件路径。

然后在后续中间件内得到已保存的文件的信息,再做二次处理。

  • ctx.request.files.f1得到文件信息,f1为input file 标签的 name
  • 获得文件的扩展名,重命名文件
ts
// 导入 koa
import Koa from "koa";

import koaStatic from "koa-static";
import koaBody from "koa-body";
import path from "node:path";
import fs from "node:fs";
import type { File } from "formidable";

// 实例化 koa
const app = new Koa();

const port = "8100";
const uploadHost = `http://localhost:${port}/uploads/`;

app.use(
  koaBody({
    formidable: {
      //设置文件的默认保存目录,不设置则保存在系统临时目录下  os
      uploadDir: path.resolve(__dirname, "../static/uploads")
    },
    multipart: true // 开启文件上传,默认是关闭
  })
);

//开启静态文件访问
app.use(koaStatic(path.resolve(__dirname, "../static")));

//文件二次处理,修改名称
app.use(ctx => {
  // 得到文件对象
  let file = ctx.request.files?.f1 as unknown as File;
  let path = file.filepath;
  let originalFilename = file.originalFilename;
  // 获得拓展名
  let ext = originalFilename?.split(".").pop();
  let nextPath = path + "." + ext;
  if (file.size > 0 && path) {
    // 重命名文件
    fs.renameSync(path, `${nextPath}`);
  }
  ctx.body = `{
    "fileUrl":"${uploadHost}${nextPath.slice(nextPath.lastIndexOf("/") + 1)}"
  }`;
  console.log(path);
});

// 启动
app.listen(8100, () => {
  console.log("Server is running at http://localhost:8100");
});

局部刷新 - iframe

ie 时代的上传文件局部刷新,借助 iframe 实现。

  • 局部刷新

页面内放一个隐藏的 iframe,或者使用 js 动态创建,指定 form 表单的 target 属性值为iframe标签 的 name 属性值,这样 form 表单的 shubmit 行为的跳转就会在 iframe 内完成,整体页面不会刷新。

  • 拿到接口数据

然后为 iframe 添加load事件,得到 iframe 的页面内容,将结果转换为 JSON 对象,这样就拿到了接口的数据

html
<iframe id="temp-iframe" name="temp-iframe" src="" style="display:none;"></iframe>
<form method="post" target="temp-iframe" action="http://localhost:8100" enctype="multipart/form-data">
    选择文件(可多选):
  <input type="file" name="f1" id="f1" multiple /><br /> input 必须设置 name 属性,否则数据无法发送<br />
  <br />
  标题:<input type="text" name="title" /><br /><br /><br /><button type="submit" id="btn-0">上 传</button>
</form>
<script>
  var iframe = document.getElementById('temp-iframe');
  iframe.addEventListener('load', function () {
    var result = iframe.contentWindow.document.body.innerText;
    //接口数据转换为 JSON 对象var obj = JSON.parse(result);
    if (obj && obj.fileUrl.length) {
      alert('上传成功');
    }
    console.log(obj);
  });
</script>

Released under the MIT License.