Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
者: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 >訪問菜鳥教程</a>
上面這行代碼顯示為:: 訪問菜鳥教程
點擊這個超鏈接會把用戶帶到菜鳥教程的首頁。
提示: "鏈接文本" 不必一定是文本。圖片或其他 HTML 元素都可以成為鏈接。
HTML 鏈接 - target 屬性
使用 target 屬性,你可以定義被鏈接的文檔在何處顯示。
下面的這行會在新窗口打開文檔:
實例
<a>訪問菜鳥教程!</a>
HTML 鏈接- id 屬性
id屬性可用于創建在一個HTML文檔書簽標記。
提示: 書簽是不以任何特殊的方式顯示,在HTML文檔中是不顯示的,所以對于讀者來說是隱藏的。
實例
在HTML文檔中插入ID:
<a id="tips">有用的提示部分</a>
在HTML文檔中創建一個鏈接到"有用的提示部分(id="tips")":
<a href="#tips">訪問有用的提示部分</a>
或者,從另一個頁面創建一個鏈接到"有用的提示部分(id="tips")":
<a >
訪問有用的提示部分</a>
基本的注意事項 - 有用的提示
注釋: 請始終將正斜杠添加到子文件夾。假如這樣書寫鏈接:,就會向服務器產生兩次 HTTP 請求。這是因為服務器會添加正斜杠到這個地址,然后創建一個新的請求,就像這樣:。
圖片鏈接
如何使用圖片鏈接。
在當前頁面鏈接到指定位置
如何使用書簽
跳出框架
本例演示如何跳出框架,假如你的頁面被固定在框架之內。
創建電子郵件鏈接
本例演示如何如何鏈接到一個郵件。(本例在安裝郵件客戶端程序后才能工作。)
建電子郵件鏈接 2
本例演示更加復雜的郵件鏈接。
HTML 鏈接標簽
標簽 | 描述 |
---|---|
<a> | 定義一個超級鏈接 |
如您還有不明白的可以在下面與我留言或是與我探討QQ群308855039,我們一起飛!
*請認真填寫需求信息,我們會在24小時內與您取得聯系。