者:Pseudo
转发链接:https://segmentfault.com/a/1190000023434864
文件上传相信很多朋友都有遇到过,那或许你也遇到过当上传大文件时,上传时间较长,且经常失败的困扰,并且失败后,又得重新上传很是烦人。那我们先了解下失败的原因吧!
前面小编也整理过关于文件上传的详细原理和文件上传技巧:
手把手教你前端的各种文件上传攻略和大文件断点续传
一文了解文件上传全过程(1.8w字深度解析)「前端进阶必备」
据我了解大概有以下原因:
基于以上原因,聪明的人们就想到了,将文件拆分多个小文件,依次上传,不就解决以上1,2问题嘛,这便是分片上传。 网络波动这个实在不可控,也许一阵大风刮来,就断网了呢。那这样好了,既然断网无法控制,那我可以控制只上传以经上传的文件内容,不就好了,这样大大加快了重新上传的速度。所以便有了“断点续传”一说。此时,人群中有人插了一嘴,有些文件我已经上传一遍了,为啥还要在上传,能不能不浪费我流量和时间。喔...这个嘛,简单,每次上传时判断下是否存在这个文件,若存在就不重新上传便可,于是又有了“秒传”一说。从此这"三兄弟" 便自行CP,统治了整个文件界。”
注意文中的代码并非实际代码,请移步至github查看最新代码
github: https://github.com/pseudo-god/vue-simple-upload
原生INPUT样式较丑,这里通过样式叠加的方式,放一个Button.
<div class="btns">
<el-button-group>
<el-button :disabled="changeDisabled">
<i class="el-icon-upload2 el-icon--left" size="mini"></i>选择文件
<input
v-if="!changeDisabled"
type="file"
:multiple="multiple"
class="select-file-input"
:accept="accept"
@change="handleFileChange"
/>
</el-button>
<el-button :disabled="uploadDisabled" @click="handleUpload()"><i class="el-icon-upload el-icon--left" size="mini"></i>上传</el-button>
<el-button :disabled="pauseDisabled" @click="handlePause"><i class="el-icon-video-pause el-icon--left" size="mini"></i>暂停</el-button>
<el-button :disabled="resumeDisabled" @click="handleResume"><i class="el-icon-video-play el-icon--left" size="mini"></i>恢复</el-button>
<el-button :disabled="clearDisabled" @click="clearFiles"><i class="el-icon-video-play el-icon--left" size="mini"></i>清空</el-button>
</el-button-group>
<slot
//data 数据
var chunkSize=10 * 1024 * 1024; // 切片大小
var fileIndex=0; // 当前正在被遍历的文件下标
data: ()=> ({
container: {
files: null
},
tempFilesArr: [], // 存储files信息
cancels: [], // 存储要取消的请求
tempThreads: 3,
// 默认状态
status: Status.wait
}),
一个稍微好看的UI就出来了。
选择文件过程中,需要对外暴露出几个钩子,熟悉elementUi的同学应该很眼熟,这几个钩子基本与其一致。onExceed:文件超出个数限制时的钩子、beforeUpload:文件上传之前
fileIndex 这个很重要,因为是多文件上传,所以定位当前正在被上传的文件就很重要,基本都靠它
handleFileChange(e) {
const files=e.target.files;
if (!files) return;
Object.assign(this.$data, this.$options.data()); // 重置data所有数据
fileIndex=0; // 重置文件下标
this.container.files=files;
// 判断文件选择的个数
if (this.limit && this.container.files.length > this.limit) {
this.onExceed && this.onExceed(files);
return;
}
// 因filelist不可编辑,故拷贝filelist 对象
var index=0; // 所选文件的下标,主要用于剔除文件后,原文件list与临时文件list不对应的情况
for (const key in this.container.files) {
if (this.container.files.hasOwnProperty(key)) {
const file=this.container.files[key];
if (this.beforeUpload) {
const before=this.beforeUpload(file);
if (before) {
this.pushTempFile(file, index);
}
}
if (!this.beforeUpload) {
this.pushTempFile(file, index);
}
index++;
}
}
},
// 存入 tempFilesArr,为了上面的钩子,所以将代码做了拆分
pushTempFile(file, index) {
// 额外的初始值
const obj={
status: fileStatus.wait,
chunkList: [],
uploadProgress: 0,
hashProgress: 0,
index
};
for (const k in file) {
obj[k]=file[k];
}
console.log('pushTempFile -> obj', obj);
this.tempFilesArr.push(obj);
}
createFileChunk(file, size=chunkSize) {
const fileChunkList=[];
var count=0;
while (count < file.size) {
fileChunkList.push({
file: file.slice(count, count + size)
});
count +=size;
}
return fileChunkList;
}
async handleUpload(resume) {
if (!this.container.files) return;
this.status=Status.uploading;
const filesArr=this.container.files;
var tempFilesArr=this.tempFilesArr;
for (let i=0; i < tempFilesArr.length; i++) {
fileIndex=i;
//创建切片
const fileChunkList=this.createFileChunk(
filesArr[tempFilesArr[i].index]
);
tempFilesArr[i].fileHash='xxxx'; // 先不用看这个,后面会讲,占个位置
tempFilesArr[i].chunkList=fileChunkList.map(({ file }, index)=> ({
fileHash: tempFilesArr[i].hash,
fileName: tempFilesArr[i].name,
index,
hash: tempFilesArr[i].hash + '-' + index,
chunk: file,
size: file.size,
uploaded: false,
progress: 0, // 每个块的上传进度
status: 'wait' // 上传状态,用作进度状态显示
}));
//上传切片
await this.uploadChunks(this.tempFilesArr[i]);
}
}
async uploadChunks(data) {
var chunkData=data.chunkList;
const requestDataList=chunkData
.map(({ fileHash, chunk, fileName, index })=> {
const formData=new FormData();
formData.append('md5', fileHash);
formData.append('file', chunk);
formData.append('fileName', index); // 文件名使用切片的下标
return { formData, index, fileName };
});
try {
await this.sendRequest(requestDataList, chunkData);
} catch (error) {
// 上传有被reject的
this.$message.error('亲 上传失败了,考虑重试下呦' + error);
return;
}
// 合并切片
const isUpload=chunkData.some(item=> item.uploaded===false);
console.log('created -> isUpload', isUpload);
if (isUpload) {
alert('存在失败的切片');
} else {
// 执行合并
await this.mergeRequest(data);
}
}
// 并发处理
sendRequest(forms, chunkData) {
var finished=0;
const total=forms.length;
const that=this;
const retryArr=[]; // 数组存储每个文件hash请求的重试次数,做累加 比如[1,0,2],就是第0个文件切片报错1次,第2个报错2次
return new Promise((resolve, reject)=> {
const handler=()=> {
if (forms.length) {
// 出栈
const formInfo=forms.shift();
const formData=formInfo.formData;
const index=formInfo.index;
instance.post('fileChunk', formData, {
onUploadProgress: that.createProgresshandler(chunkData[index]),
cancelToken: new CancelToken(c=> this.cancels.push(c)),
timeout: 0
}).then(res=> {
console.log('handler -> res', res);
// 更改状态
chunkData[index].uploaded=true;
chunkData[index].status='success';
finished++;
handler();
})
.catch(e=> {
// 若暂停,则禁止重试
if (this.status===Status.pause) return;
if (typeof retryArr[index] !=='number') {
retryArr[index]=0;
}
// 更新状态
chunkData[index].status='warning';
// 累加错误次数
retryArr[index]++;
// 重试3次
if (retryArr[index] >=this.chunkRetry) {
return reject('重试失败', retryArr);
}
this.tempThreads++; // 释放当前占用的通道
// 将失败的重新加入队列
forms.push(formInfo);
handler();
});
}
if (finished >=total) {
resolve('done');
}
};
// 控制并发
for (let i=0; i < this.tempThreads; i++) {
handler();
}
});
}
// 切片上传进度
createProgresshandler(item) {
return p=> {
item.progress=parseInt(String((p.loaded / p.total) * 100));
this.fileProgress();
};
}
其实就是算一个文件的MD5值,MD5在整个项目中用到的地方也就几点。
本项目主要使用worker处理,性能及速度都会有很大提升.
由于是多文件,所以HASH的计算进度也要体现在每个文件上,所以这里使用全局变量fileIndex来定位当前正在被上传的文件
// 生成文件 hash(web-worker)
calculateHash(fileChunkList) {
return new Promise(resolve=> {
this.container.worker=new Worker('./hash.js');
this.container.worker.postMessage({ fileChunkList });
this.container.worker.onmessage=e=> {
const { percentage, hash }=e.data;
if (this.tempFilesArr[fileIndex]) {
this.tempFilesArr[fileIndex].hashProgress=Number(
percentage.toFixed(0)
);
}
if (hash) {
resolve(hash);
}
};
});
}
因使用worker,所以我们不能直接使用NPM包方式使用MD5。需要单独去下载spark-md5.js文件,并引入
//hash.js
self.importScripts("/spark-md5.min.js"); // 导入脚本
// 生成文件 hash
self.onmessage=e=> {
const { fileChunkList }=e.data;
const spark=new self.SparkMD5.ArrayBuffer();
let percentage=0;
let count=0;
const loadNext=index=> {
const reader=new FileReader();
reader.readAsArrayBuffer(fileChunkList[index].file);
reader.onload=e=> {
count++;
spark.append(e.target.result);
if (count===fileChunkList.length) {
self.postMessage({
percentage: 100,
hash: spark.end()
});
self.close();
} else {
percentage +=100 / fileChunkList.length;
self.postMessage({
percentage
});
loadNext(count);
}
};
};
loadNext(0);
};
当我们的切片全部上传完毕后,就需要进行文件的合并,这里我们只需要请求接口即可
mergeRequest(data) {
const obj={
md5: data.fileHash,
fileName: data.name,
fileChunkNum: data.chunkList.length
};
instance.post('fileChunk/merge', obj,
{
timeout: 0
})
.then((res)=> {
this.$message.success('上传成功');
});
}
Done: 至此一个分片上传的功能便已完成
顾名思义,就是从那断的就从那开始,明确思路就很简单了。一般有2种方式,一种为服务器端返回,告知我从那开始,还有一种是浏览器端自行处理。2种方案各有优缺点。本项目使用第二种。
思路:已文件HASH为key值,每个切片上传成功后,记录下来便可。若需要续传时,直接跳过记录中已存在的便可。本项目将使用Localstorage进行存储,这里我已提前封装好addChunkStorage、getChunkStorage方法。
存储在Stroage的数据
在切片上传的axios成功回调中,存储已上传成功的切片
instance.post('fileChunk', formData, )
.then(res=> {
// 存储已上传的切片下标
+ this.addChunkStorage(chunkData[index].fileHash, index);
handler();
})
在切片上传前,先看下localstorage中是否存在已上传的切片,并修改uploaded
async handleUpload(resume) {
+ const getChunkStorage=this.getChunkStorage(tempFilesArr[i].hash);
tempFilesArr[i].chunkList=fileChunkList.map(({ file }, index)=> ({
+ uploaded: getChunkStorage && getChunkStorage.includes(index), // 标识:是否已完成上传
+ progress: getChunkStorage && getChunkStorage.includes(index) ? 100 : 0,
+ status: getChunkStorage && getChunkStorage.includes(index)? 'success'
+ : 'wait' // 上传状态,用作进度状态显示
}));
}
构造切片数据时,过滤掉uploaded为true的
async uploadChunks(data) {
var chunkData=data.chunkList;
const requestDataList=chunkData
+ .filter(({ uploaded })=> !uploaded)
.map(({ fileHash, chunk, fileName, index })=> {
const formData=new FormData();
formData.append('md5', fileHash);
formData.append('file', chunk);
formData.append('fileName', index); // 文件名使用切片的下标
return { formData, index, fileName };
})
}
随着上传文件的增多,相应的垃圾文件也会增多,比如有些时候上传一半就不再继续,或上传失败,碎片文件就会增多。解决方案我目前想了2种
以上2种方案似乎都有点问题,极有可能造成前后端因时间差,引发切片上传异常的问题,后面想到合适的解决方案再来更新吧。
Done: 续传到这里也就完成了。
这算是最简单的,只是听起来很厉害的样子。原理:计算整个文件的HASH,在执行上传操作前,向服务端发送请求,传递MD5值,后端进行文件检索。若服务器中已存在该文件,便不进行后续的任何操作,上传也便直接结束。大家一看就明白
async handleUpload(resume) {
if (!this.container.files) return;
const filesArr=this.container.files;
var tempFilesArr=this.tempFilesArr;
for (let i=0; i < tempFilesArr.length; i++) {
const fileChunkList=this.createFileChunk(
filesArr[tempFilesArr[i].index]
);
// hash校验,是否为秒传
+ tempFilesArr[i].hash=await this.calculateHash(fileChunkList);
+ const verifyRes=await this.verifyUpload(
+ tempFilesArr[i].name,
+ tempFilesArr[i].hash
+ );
+ if (verifyRes.data.presence) {
+ tempFilesArr[i].status=fileStatus.secondPass;
+ tempFilesArr[i].uploadProgress=100;
+ } else {
console.log('开始上传切片文件----》', tempFilesArr[i].name);
await this.uploadChunks(this.tempFilesArr[i]);
}
}
}
// 文件上传之前的校验: 校验文件是否已存在
verifyUpload(fileName, fileHash) {
return new Promise(resolve=> {
const obj={
md5: fileHash,
fileName,
...this.uploadArguments //传递其他参数
};
instance
.post('fileChunk/presence', obj)
.then(res=> {
resolve(res.data);
})
.catch(err=> {
console.log('verifyUpload -> err', err);
});
});
}
Done: 秒传到这里也就完成了。
文章好像有点长了,具体代码逻辑就先不贴了,除非有人留言要求,嘻嘻,有时间再更新
请前往 https://github.com/pseudo-god... 查看
下周应该会更新处理
1年多没写PHP了,抽空我会慢慢补上来
组件已经运行一段时间了,期间也测试出几个问题,本来以为没BUG的,看起来BUG都挺严重
BUG-1:当同时上传多个内容相同但是文件名称不同的文件时,出现上传失败的问题。
预期结果:第一个上传成功后,后面相同的文文件应该直接秒传
实际结果:第一个上传成功后,其余相同的文件都失败,错误信息,块数不对。
原因:当第一个文件块上传完毕后,便立即进行了下一个文件的循环,导致无法及时获取文件是否已秒传的状态,从而导致失败。
解决方案:在当前文件分片上传完毕并且请求合并接口完毕后,再进行下一次循环。
将子方法都改为同步方式,mergeRequest 和 uploadChunks 方法
BUG-2: 当每次选择相同的文件并触发beforeUpload方法时,若第二次也选择了相同的文件,beforeUpload方法失效,从而导致整个流程失效。
原因:之前每次选择文件时,没有清空上次所选input文件的数据,相同数据的情况下,是不会触发input的change事件。
解决方案:每次点击input时,清空数据即可。我顺带优化了下其他的代码,具体看提交记录吧。
<input
v-if="!changeDisabled"
type="file"
:multiple="multiple"
class="select-file-input"
:accept="accept"
+ οnclick="f.outerHTML=f.outerHTML"
@change="handleFileChange"/>
重写了暂停和恢复的功能,实际上,主要是增加了暂停和恢复的状态
之前的处理逻辑太简单粗暴,存在诸多问题。现在将状态定位在每一个文件之上,这样恢复上传时,直接跳过即可
写了一大堆,其实以上代码你直接复制也无法使用,这里我将此封装了一个组件。大家可以去github下载文件,里面有使用案例 ,若有用记得随手给个star,谢谢!
偷个懒,具体封装组件的代码就不列出来了,大家直接去下载文件查看,若有不明白的,可留言。
代码地址:https://github.com/pseudo-god/vue-simple-upload
接口文档地址 https://docs.apipost.cn/view/0e19f16d4470ed6b#287746
作者:Pseudo
转发链接:https://segmentfault.com/a/1190000023434864
、建立站点后,在文件夹上右键新建一个文件,改名为音乐制作网页,然后双击进入网页,首先,插入表格,17行,2列,表格宽度和表格粗细都为0,确定。选中表格,下边的对齐方式为,居中对齐
2、选中第一个格,按住Ctrl键再选中第二个格,右键,表格,合并单元格,点击插入,图像,选择建的站点下的素材,确定
3、用刚才的方法合并第二行单元格,填写导航栏文字,选择拆分,找到对应代码位置,填写空格代码 在文字前后都添加空格,使导航栏文字间隙均匀,下边背景颜色改为紫色
4、编写下一行文字,背景颜色为绿色,金曲列表文字,下边,HTML,格式为标题5,歌曲下载文字,下边,HTML,格式为标题3,找到代码中金曲列表文字对应位置,添加空格代码
5、在歌曲下载文字后面插入,图像,选择下载图标图片,点击图片,选择连接指向你的歌曲MP3文件
6、在每个格中加入歌名,每个都要插入,布局对象,Div标签,然后添加歌曲名称
7、右半部分,前两行合并,插入布局对象,添加文字那女孩对我说,HTML格式改为标题2,然后将下边剩余所有行合并,插入布局,添加歌词
8、选择歌词,将HTML的格式改为标题5
9、点击代码,找到歌词位置,复制空格,在每一行歌词前面招贴空格,刷新一下,使歌词居中一些
10、按F12预览
小伙伴们,有没有看懂呢,看不懂可以去看视频呦!
HTML 使用超级链接与网络上的另一个文档相连。几乎可以在所有的网页中找到链接。点击链接可以从一张页面跳转到另一张页面。
HTML 链接
如何在HTML文档中创建链接。
(可以在本页底端找到更多实例)
HTML 超链接(链接)
HTML使用标签 <a>来设置超文本链接。
超链接可以是一个字,一个词,或者一组词,也可以是一幅图像,您可以点击这些内容来跳转到新的文档或者当前文档中的某个部分。
当您把鼠标指针移动到网页中的某个链接上时,箭头会变为一只小手。
在标签<a> 中使用了href属性来描述链接的地址。
默认情况下,链接将以以下形式出现在浏览器中:
一个未访问过的链接显示为蓝色字体并带有下划线。
访问过的链接显示为紫色并带有下划线。
点击链接时,链接显示为红色并带有下划线。
注意:如果为这些超链接设置了 CSS 样式,展示样式会根据 CSS 的设定而显示。
HTML 链接语法
链接的 HTML 代码很简单。它类似这样::
<a href="url">链接文本</a>
href 属性描述了链接的目标。.
实例
<a href="http://www.runoob.com/">访问菜鸟教程</a>
上面这行代码显示为:: 访问菜鸟教程
点击这个超链接会把用户带到菜鸟教程的首页。
提示: "链接文本" 不必一定是文本。图片或其他 HTML 元素都可以成为链接。
HTML 链接 - target 属性
使用 target 属性,你可以定义被链接的文档在何处显示。
下面的这行会在新窗口打开文档:
实例
<ahref="http://www.runoob.com/"target="_blank">访问菜鸟教程!</a>
HTML 链接- id 属性
id属性可用于创建在一个HTML文档书签标记。
提示: 书签是不以任何特殊的方式显示,在HTML文档中是不显示的,所以对于读者来说是隐藏的。
实例
在HTML文档中插入ID:
<a id="tips">有用的提示部分</a>
在HTML文档中创建一个链接到"有用的提示部分(id="tips")":
<a href="#tips">访问有用的提示部分</a>
或者,从另一个页面创建一个链接到"有用的提示部分(id="tips")":
<a href="http://www.runoob.com/html/html-links.html#tips">
访问有用的提示部分</a>
基本的注意事项 - 有用的提示
注释: 请始终将正斜杠添加到子文件夹。假如这样书写链接:href="http://www.runoob.com/html",就会向服务器产生两次 HTTP 请求。这是因为服务器会添加正斜杠到这个地址,然后创建一个新的请求,就像这样:href="http://www.runoob.com/html/"。
图片链接
如何使用图片链接。
在当前页面链接到指定位置
如何使用书签
跳出框架
本例演示如何跳出框架,假如你的页面被固定在框架之内。
创建电子邮件链接
本例演示如何如何链接到一个邮件。(本例在安装邮件客户端程序后才能工作。)
建电子邮件链接 2
本例演示更加复杂的邮件链接。
HTML 链接标签
标签 | 描述 |
---|---|
<a> | 定义一个超级链接 |
如您还有不明白的可以在下面与我留言或是与我探讨QQ群308855039,我们一起飞!
*请认真填写需求信息,我们会在24小时内与您取得联系。