# 文件上传原理
原理: 完成消息体封装和消息体的额解析,然后将二进制内容保存到文件。
人话: 把 form
标签的 enctype
设置为 multipart/form-data
,同时 method
必须为 post
方法
# 单文件上传和上传进度
使用 form 表单上传文件,原来都是用 form 表单,但是现在一般使用 input
+ xhr
来实现比较好
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>
# 多文件上传和上传进度
html5
只需要一个标签加个属性就搞定了,file 标签开启 multiple
HTML
<input type="file" name="f1" multiple />
NODE
//二次处理文件,修改名称
app.use(ctx => {
var files = ctx.request.files.f1; // 多文件, 得到上传文件的数组
var result = [];
//遍历处理
files &&
files.forEach(item => {
var path = item.path;
var fname = item.name; //原文件名称
var nextPath = path + fname;
if (item.size > 0 && path) {
//得到扩展名
var extArr = fname.split(".");
var ext = extArr[extArr.length - 1];
var nextPath = path + "." + ext;
//重命名文件
fs.renameSync(path, nextPath);
//文件可访问路径放入数组
result.push(uploadHost + nextPath.slice(nextPath.lastIndexOf("/") + 1));
}
});
//输出 json 结果
ctx.body = `{
"fileUrl":${JSON.stringify(result)}
}`;
});
局部刷新的做法:
页面内放一个隐藏的 iframe
,或者使用 js 动态创建,指定 form
表单的 target
属性值为 iframe
标签 的 name
属性值,这样 form
表单的 submit
行为的跳转就会在 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>
无刷新上传,借助 XHR
<div>
选择文件(可多选):
<input type="file" id="f1" multiple /><br /><br />
<button type="button" id="btn-submit">上 传</button>
</div>
function submitUpload() {
//获得文件列表,注意这里不是数组,而是对象
var fileList = document.getElementById("f1").files;
if (!fileList.length) {
alert("请选择文件");
return;
}
var fd = new FormData(); //构造FormData对象
fd.append("title", document.getElementById("title").value);
//多文件上传需要遍历添加到 fromdata 对象
for (var i = 0; i < fileList.length; i++) {
fd.append("f1", fileList[i]); //支持多文件上传
}
var xhr = new XMLHttpRequest(); //创建对象
xhr.open("POST", "http://localhost:8100/", true);
xhr.send(fd); //发送时 Content-Type默认就是: multipart/form-data;
xhr.onreadystatechange = function() {
console.log("state change", xhr.readyState);
if (this.readyState == 4 && this.status == 200) {
var obj = JSON.parse(xhr.responseText); //返回值
if (obj.fileUrl.length) {
alert("上传成功");
}
}
};
}
//绑定提交事件
document.getElementById("btn-submit").addEventListener("click", submitUpload);
监控上传进度
- 添加显示进度的标签
div.progress
- 编写处理进度的监听函数,
xhr.onprogress
还有xhr.upload.onprogress
- 通过监控
event.lengthComputable
是否变化,event.loaded
表示发送了多少字节,event.total
表示文件总大小计算出进度,然后来控制样式。
xhr.onprogress = updateProgress;
xhr.upload.onprogress = updateProgress;
function updateProgress(event) {
console.log(event);
if (event.lengthComputable) {
var completedPercent = ((event.loaded / event.total) * 100).toFixed(2);
progressSpan.style.width = completedPercent + "%";
progressSpan.innerHTML = completedPercent + "%";
if (completedPercent > 90) {
//进度条变色
progressSpan.classList.add("green");
}
console.log("已上传", completedPercent);
}
}
PS: xhr.upload.onprogress
要写在 xhr.send
方法前面
# 拖拽上传
- 首先定义一个允许拖放文件的区域
drop
事件一定要阻止事件的默认行为e.preventDefault()
,不然浏览器会直接打开文件- 为拖拽区域绑定事件,鼠标在拖拽区域上
dragover
,鼠标离开拖拽区域dragleave
,在拖拽区域上释放文件drop
drop
事件内获取文件信息e.dataTransfer.files
- 组装 formData,xhr 发送 ajax
<div class="drop-box" id="drop-box">
拖动文件到这里,开始上传
</div>
<button type="button" id="btn-submit">上 传</button>
<script>
var box = document.getElementById("drop-box");
//禁用浏览器的拖放默认行为
document.addEventListener("drop", function(e) {
console.log("document drog");
e.preventDefault();
});
//设置拖拽事件
function openDropEvent() {
box.addEventListener("dragover", function(e) {
console.log("elemenet dragover");
box.classList.add("over");
e.preventDefault();
});
box.addEventListener("dragleave", function(e) {
console.log("elemenet dragleave");
box.classList.remove("over");
e.preventDefault();
});
box.addEventListener(
"drop",
function(e) {
e.preventDefault(); //取消浏览器默认拖拽效果
var fileList = e.dataTransfer.files; //获取拖拽中的文件对象
var len = fileList.length; //用来获取文件的长度(其实是获得文件数量)
//检测是否是拖拽文件到页面的操作
if (!len) {
box.classList.remove("over");
return;
}
box.classList.add("over");
window.willUploadFileList = fileList;
},
false
);
}
openDropEvent();
function submitUpload() {
var fileList = window.willUploadFileList || [];
if (!fileList.length) {
alert("请选择文件");
return;
}
var fd = new FormData(); //构造FormData对象
for (var i = 0; i < fileList.length; i++) {
fd.append("f1", fileList[i]); //支持多文件上传
}
var xhr = new XMLHttpRequest(); //创建对象
xhr.open("POST", "http://localhost:8100/", true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
var obj = JSON.parse(xhr.responseText); //返回值
if (obj.fileUrl.length) {
alert("上传成功");
}
}
};
xhr.send(fd); //发送
}
//绑定提交事件
document.getElementById("btn-submit").addEventListener("click", submitUpload);
</script>
# 大文件分片上传
file 继承了 blob ,表示原始数据和二进制数据,提供 slice
方法进行截取
大概是一个这样的过程
- 把大文件进行分段 比如 2M 一片,发送到服务器携带一个标志,可以暂时用当前的时间戳,用于标识一个完整的文件
- 服务端保存各段文件
- 浏览器端所有分片上传完成,发送给服务端一个合并文件的请求
- 服务端根据文件标识、类型、各分片顺序进行文件合并
- 删除分片文件
分片如何做到,其实就像是操作字符串一样
var start = 0,
end = 0;
while (true) {
end += chunkSize;
var blob = file.slice(start, end);
start += chunkSize;
if (!blob.size) {
//截取的数据为空 则结束
//拆分结束
break;
}
chunks.push(blob); //保存分段数据
}
前端过程,尽量会手写最好
function submitUpload() {
var chunkSize = 2 * 1024 * 1024; //分片大小 2M
var file = document.getElementById("f1").files[0];
var chunks = [], //保存分片数据
token = +new Date(), //时间戳
name = file.name,
chunkCount = 0,
sendChunkCount = 0;
//拆分文件 像操作字符串一样
if (file.size > chunkSize) {
//拆分文件
var start = 0,
end = 0;
while (true) {
end += chunkSize;
var blob = file.slice(start, end);
start += chunkSize;
if (!blob.size) {
//截取的数据为空 则结束
//拆分结束
break;
}
chunks.push(blob); //保存分段数据
}
} else {
chunks.push(file.slice(0));
}
chunkCount = chunks.length; //分片的个数
//没有做并发限制,较大文件导致并发过多,tcp 链接被占光 ,需要做下并发控制,比如只有4个在请求在发送
for (var i = 0; i < chunkCount; i++) {
var fd = new FormData(); //构造FormData对象
fd.append("token", token);
fd.append("f1", chunks[i]);
fd.append("index", i);
xhrSend(fd, function() {
sendChunkCount += 1;
if (sendChunkCount === chunkCount) {
//上传完成,发送合并请求
console.log("上传完成,发送合并请求");
var formD = new FormData();
formD.append("type", "merge");
formD.append("token", token);
formD.append("chunkCount", chunkCount);
formD.append("filename", name);
xhrSend(formD);
}
});
}
}
function xhrSend(fd, cb) {
var xhr = new XMLHttpRequest(); //创建对象
xhr.open("POST", "http://localhost:8100/", true);
xhr.onreadystatechange = function() {
console.log("state change", xhr.readyState);
if (xhr.readyState == 4) {
console.log(xhr.responseText);
cb && cb();
}
};
xhr.send(fd); //发送
}
//绑定提交事件
document.getElementById("btn-submit").addEventListener("click", submitUpload);
# 大文件断点上传
大致过程:
在分片上传前提下
- 为每个分段生成 hash 值,使用
spark-md5
库为每个文件分片打一个哈希 - 将上传成功的分段信息保存到本地,(为了简单,其实要用 hash,比如我这里记录文件索引的形式记录已上传还是未上传)
- 重新上传时,都会拿当前上传的分段和本地分段 hash 值的对比,如果相同的话则跳过,继续下一个分段的上传
//获得本地缓存的数据
function getUploadedFromStorage() {
return JSON.parse(localStorage.getItem(saveChunkKey) || "{}");
}
//写入缓存
function setUploadedToStorage(index) {
var obj = getUploadedFromStorage();
obj[index] = true;
localStorage.setItem(saveChunkKey, JSON.stringify(obj));
}
//分段对比
var uploadedInfo = getUploadedFromStorage(); //获得已上传的分段信息
for (var i = 0; i < chunkCount; i++) {
console.log("index", i, uploadedInfo[i] ? "已上传过" : "未上传");
if (uploadedInfo[i]) {
//对比分段
sendChunkCount = i + 1; //记录已上传的索引
continue; //如果已上传则跳过
}
var fd = new FormData(); //构造FormData对象
fd.append("token", token);
fd.append("f1", chunks[i]);
fd.append("index", i);
(function(index) {
xhrSend(fd, function() {
sendChunkCount += 1;
//将成功信息保存到本地
setUploadedToStorage(index);
if (sendChunkCount === chunkCount) {
console.log("上传完成,发送合并请求");
var formD = new FormData();
formD.append("type", "merge");
formD.append("token", token);
formD.append("chunkCount", chunkCount);
formD.append("filename", name);
xhrSend(formD);
}
});
})(i);
}
# url小知识
// http://localhost:8080/users?userName=张三
// 转译中文search
encodeURIComponent('张三') = '转移码'
// 反转译
decodeURIComponent('转移码') = '张三'
// 访问url客户端自动下载
Headers: {
'Content-Disposition': `attachment;filename=${encodeURI(file.name)}`
},
# 对接cos云服务
export let resolveData: { time: number; list: UploadResolveType[] } = reactive({
time: 0,
list: []
})
let Bucket = ''
let Region = ''
let fileKey = ''
let filemd5 = ''
let fileType = ''
let realFileKey = ''
// fileReader 同步
function uploadFile(file: File) {
return new Promise((resolve) => {
const reader = new FileReader()
reader.readAsBinaryString(file)
reader.onload = (e: ProgressEvent<FileReader>) => {
resolve(e.target!.result)
}
}).catch((e) => {
console.error(e)
})
}
const getCosInstance = async (
file: File,
filedata: UploadResolveType,
source: number
) => {
const spark = new SparkMD5()
const result: any = await uploadFile(file)!
spark.appendBinary(result)
filemd5 = spark.end()
fileType = getExtendName(file.name)
const res = await $http.getUploadAuthorization({
fileNames: JSON.stringify([filemd5 + '_' + file.size + '.' + fileType]),
source
})
if (res.data) {
const credentials: any = res.data?.fileList[0]
fileKey =
(source === SOURCE_TYPE.RESOURCE ? COS_SOURCE_PATH : EBOOK_SOURCE_PATH) +
'/' +
credentials.newFileName
Bucket = credentials.bucket
Region = credentials.region
realFileKey = credentials.objectKey
filedata.oid = credentials.oid
filedata.fileKey = fileKey
return new COS({
getAuthorization: (options, callback) => {
const action = options.Scope[0].action.replace('name/cos:', '')
const sign = credentials.authMap[action]
callback(sign)
}
})
} else {
Message.error('credentials invalid')
throw new Error('credentials invalid')
}
}
export async function upload(
file: File,
uid: number,
source: number = SOURCE_TYPE.RESOURCE
): Promise<any> {
const filedata: UploadResolveType = {
file,
uid: uid,
data: {},
options: {},
precent: 0,
taskId: '',
isFinished: false,
md5Name: '',
oid: '',
fileKey: ''
}
const cos = await getCosInstance(file, filedata, source)
return new Promise((resolve) => {
resolveData.time = Math.random() * Date.now()
if (cos) {
cos.uploadFile(
{
Bucket,
Region,
Key: realFileKey,
Body: file,
SliceSize: 5 * SIZE_COMPANY,
Headers: {
'Content-Disposition': `attachment;filename=${encodeURI(file.name)}`
},
onTaskReady: function (taskId) {
/* 非必须 */
filedata.taskId = taskId
},
onProgress: function (progressData) {
/* 非必须 */
if (progressData.percent !== filedata.precent) {
const currentIndex = resolveData.list.findIndex(
(item) => item.uid === filedata.uid
)
resolveData.list[currentIndex].precent = progressData.percent
resolveData.list[currentIndex].isFinished =
Number(filedata.precent) === 1
}
}
},
(err, data) => {
console.log(err)
filedata.data = data
filedata.md5Name = filemd5 + '_' + file.size + '.' + fileType
}
)
resolveData.list.push(filedata)
resolve(resolveData)
}
})
}
export function resumeResolveData() {
resolveData = reactive({
time: 0,
list: []
})
}
← JS 小知识点 Typescript 笔记 →