文件上传
原理概述
原理就是根据 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 ,而且没有兼容问题,所有浏览器都支持,就是体验很差,导致页面刷新,页面其他数据丢失。
<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- 获得文件的扩展名,重命名文件
// 导入 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 对象,这样就拿到了接口的数据
<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>