整合營(yíng)銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          Vue+Element UI實(shí)現(xiàn)斷點(diǎn)續(xù)傳、分片上傳、

          Vue+Element UI實(shí)現(xiàn)斷點(diǎn)續(xù)傳、分片上傳、秒傳

          者:Pseudo

          轉(zhuǎn)發(fā)鏈接:https://segmentfault.com/a/1190000023434864

          凡是要知其然知其所以然

          文件上傳相信很多朋友都有遇到過(guò),那或許你也遇到過(guò)當(dāng)上傳大文件時(shí),上傳時(shí)間較長(zhǎng),且經(jīng)常失敗的困擾,并且失敗后,又得重新上傳很是煩人。那我們先了解下失敗的原因吧!

          前面小編也整理過(guò)關(guān)于文件上傳的詳細(xì)原理和文件上傳技巧:

          手把手教你前端的各種文件上傳攻略和大文件斷點(diǎn)續(xù)傳

          一文了解文件上傳全過(guò)程(1.8w字深度解析)「前端進(jìn)階必備」

          據(jù)我了解大概有以下原因:

          1. 服務(wù)器配置:例如在PHP中默認(rèn)的文件上傳大小為8M【post_max_size=8m】,若你在一個(gè)請(qǐng)求體中放入8M以上的內(nèi)容時(shí),便會(huì)出現(xiàn)異常
          2. 請(qǐng)求超時(shí):當(dāng)你設(shè)置了接口的超時(shí)時(shí)間為10s,那么上傳大文件時(shí),一個(gè)接口響應(yīng)時(shí)間超過(guò)10s,那么便會(huì)被Faild掉。
          3. 網(wǎng)絡(luò)波動(dòng):這個(gè)就屬于不可控因素,也是較常見(jiàn)的問(wèn)題。

          基于以上原因,聰明的人們就想到了,將文件拆分多個(gè)小文件,依次上傳,不就解決以上1,2問(wèn)題嘛,這便是分片上傳。 網(wǎng)絡(luò)波動(dòng)這個(gè)實(shí)在不可控,也許一陣大風(fēng)刮來(lái),就斷網(wǎng)了呢。那這樣好了,既然斷網(wǎng)無(wú)法控制,那我可以控制只上傳以經(jīng)上傳的文件內(nèi)容,不就好了,這樣大大加快了重新上傳的速度。所以便有了“斷點(diǎn)續(xù)傳”一說(shuō)。此時(shí),人群中有人插了一嘴,有些文件我已經(jīng)上傳一遍了,為啥還要在上傳,能不能不浪費(fèi)我流量和時(shí)間。喔...這個(gè)嘛,簡(jiǎn)單,每次上傳時(shí)判斷下是否存在這個(gè)文件,若存在就不重新上傳便可,于是又有了“秒傳”一說(shuō)。從此這"三兄弟" 便自行CP,統(tǒng)治了整個(gè)文件界?!?/p>

          注意文中的代碼并非實(shí)際代碼,請(qǐng)移步至github查看最新代碼

          github: https://github.com/pseudo-god/vue-simple-upload


          分片上傳

          HTML

          原生INPUT樣式較丑,這里通過(guò)樣式疊加的方式,放一個(gè)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>恢復(fù)</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 數(shù)據(jù)
           
          var chunkSize=10 * 1024 * 1024; // 切片大小
          var fileIndex=0; // 當(dāng)前正在被遍歷的文件下標(biāo)
          
           data: ()=> ({
              container: {
                files: null
              },
              tempFilesArr: [], // 存儲(chǔ)files信息
              cancels: [], // 存儲(chǔ)要取消的請(qǐng)求
              tempThreads: 3,
              // 默認(rèn)狀態(tài)
              status: Status.wait
            }),
              

          一個(gè)稍微好看的UI就出來(lái)了。

          選擇文件

          選擇文件過(guò)程中,需要對(duì)外暴露出幾個(gè)鉤子,熟悉elementUi的同學(xué)應(yīng)該很眼熟,這幾個(gè)鉤子基本與其一致。onExceed:文件超出個(gè)數(shù)限制時(shí)的鉤子、beforeUpload:文件上傳之前

          fileIndex 這個(gè)很重要,因?yàn)槭嵌辔募蟼?,所以定位?dāng)前正在被上傳的文件就很重要,基本都靠它

          handleFileChange(e) {
            const files=e.target.files;
            if (!files) return;
            Object.assign(this.$data, this.$options.data()); // 重置data所有數(shù)據(jù)
          
            fileIndex=0; // 重置文件下標(biāo)
            this.container.files=files;
            // 判斷文件選擇的個(gè)數(shù)
            if (this.limit && this.container.files.length > this.limit) {
              this.onExceed && this.onExceed(files);
              return;
            }
          
            // 因filelist不可編輯,故拷貝filelist 對(duì)象
            var index=0; // 所選文件的下標(biāo),主要用于剔除文件后,原文件list與臨時(shí)文件list不對(duì)應(yīng)的情況
            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);
          }
          

          分片上傳

          • 創(chuàng)建切片,循環(huán)分解文件即可
          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;
          }
          • 循環(huán)創(chuàng)建切片,既然咱們做的是多文件,所以這里就有循環(huán)去處理,依次創(chuàng)建文件切片,及切片的上傳。
          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;
              //創(chuàng)建切片
              const fileChunkList=this.createFileChunk(
                filesArr[tempFilesArr[i].index]
              );
                
              tempFilesArr[i].fileHash='xxxx'; // 先不用看這個(gè),后面會(huì)講,占個(gè)位置
              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, // 每個(gè)塊的上傳進(jìn)度
                status: 'wait' // 上傳狀態(tài),用作進(jìn)度狀態(tài)顯示
              }));
              
              //上傳切片
              await this.uploadChunks(this.tempFilesArr[i]);
            }
          }
          • 上傳切片,這個(gè)里需要考慮的問(wèn)題較多,也算是核心吧,uploadChunks方法只負(fù)責(zé)構(gòu)造傳遞給后端的數(shù)據(jù),核心上傳功能放到sendRequest方法中
           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); // 文件名使用切片的下標(biāo)
                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 {
              // 執(zhí)行合并
              await this.mergeRequest(data);
            }
          }
          • sendReques。上傳這是最重要的地方,也是容易失敗的地方,假設(shè)有10個(gè)分片,那我們?nèi)羰侵苯影l(fā)10個(gè)請(qǐng)求的話,很容易達(dá)到瀏覽器的瓶頸,所以需要對(duì)請(qǐng)求進(jìn)行并發(fā)處理。并發(fā)處理:這里我使用for循環(huán)控制并發(fā)的初始并發(fā)數(shù),然后在 handler 函數(shù)里調(diào)用自己,這樣就控制了并發(fā)。在handler中,通過(guò)數(shù)組API.shift模擬隊(duì)列的效果,來(lái)上傳切片。重點(diǎn): retryArr 數(shù)組存儲(chǔ)每個(gè)切片文件請(qǐng)求的重試次數(shù),做累加。比如[1,0,2],就是第0個(gè)文件切片報(bào)錯(cuò)1次,第2個(gè)報(bào)錯(cuò)2次。為保證能與文件做對(duì)應(yīng),const index=formInfo.index; 我們直接從數(shù)據(jù)中拿之前定義好的index。 若失敗后,將失敗的請(qǐng)求重新加入隊(duì)列即可。關(guān)于并發(fā)及重試我寫了一個(gè)小Demo,若不理解可以自己在研究下,文件地址:https://github.com/pseudo-god/vue-simple-upload/blob/master/src/utils/sendRequest-domo.js, 重試代碼好像被我弄丟了,大家要是有需求,我再補(bǔ)吧!
              // 并發(fā)處理
          sendRequest(forms, chunkData) {
            var finished=0;
            const total=forms.length;
            const that=this;
            const retryArr=[]; // 數(shù)組存儲(chǔ)每個(gè)文件hash請(qǐng)求的重試次數(shù),做累加 比如[1,0,2],就是第0個(gè)文件切片報(bào)錯(cuò)1次,第2個(gè)報(bào)錯(cuò)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);
                    // 更改狀態(tài)
                    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;
                      }
          
                      // 更新狀態(tài)
                      chunkData[index].status='warning';
          
                      // 累加錯(cuò)誤次數(shù)
                      retryArr[index]++;
          
                      // 重試3次
                      if (retryArr[index] >=this.chunkRetry) {
                        return reject('重試失敗', retryArr);
                      }
          
                      this.tempThreads++; // 釋放當(dāng)前占用的通道
          
                      // 將失敗的重新加入隊(duì)列
                      forms.push(formInfo);
                      handler();
                    });
                }
          
                if (finished >=total) {
                  resolve('done');
                }
              };
          
              // 控制并發(fā)
              for (let i=0; i < this.tempThreads; i++) {
                handler();
              }
            });
          }
          • 切片的上傳進(jìn)度,通過(guò)axios的onUploadProgress事件,結(jié)合createProgresshandler方法進(jìn)行維護(hù)
          // 切片上傳進(jìn)度
          createProgresshandler(item) {
            return p=> {
              item.progress=parseInt(String((p.loaded / p.total) * 100));
              this.fileProgress();
            };
          }

          Hash計(jì)算

          其實(shí)就是算一個(gè)文件的MD5值,MD5在整個(gè)項(xiàng)目中用到的地方也就幾點(diǎn)。

          • 秒傳,需要通過(guò)MD5值判斷文件是否已存在。
          • 續(xù)傳:需要用到MD5作為key值,當(dāng)唯一值使用。

          本項(xiàng)目主要使用worker處理,性能及速度都會(huì)有很大提升.
          由于是多文件,所以HASH的計(jì)算進(jìn)度也要體現(xiàn)在每個(gè)文件上,所以這里使用全局變量fileIndex來(lái)定位當(dāng)前正在被上傳的文件

          // 生成文件 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。需要單獨(dú)去下載spark-md5.js文件,并引入

          //hash.js
          
          self.importScripts("/spark-md5.min.js"); // 導(dǎo)入腳本
          // 生成文件 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);
          };

          文件合并

          當(dāng)我們的切片全部上傳完畢后,就需要進(jìn)行文件的合并,這里我們只需要請(qǐng)求接口即可

          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: 至此一個(gè)分片上傳的功能便已完成


          斷點(diǎn)續(xù)傳

          顧名思義,就是從那斷的就從那開始,明確思路就很簡(jiǎn)單了。一般有2種方式,一種為服務(wù)器端返回,告知我從那開始,還有一種是瀏覽器端自行處理。2種方案各有優(yōu)缺點(diǎn)。本項(xiàng)目使用第二種。

          思路:已文件HASH為key值,每個(gè)切片上傳成功后,記錄下來(lái)便可。若需要續(xù)傳時(shí),直接跳過(guò)記錄中已存在的便可。本項(xiàng)目將使用Localstorage進(jìn)行存儲(chǔ),這里我已提前封裝好addChunkStorage、getChunkStorage方法。

          存儲(chǔ)在Stroage的數(shù)據(jù)

          緩存處理

          在切片上傳的axios成功回調(diào)中,存儲(chǔ)已上傳成功的切片

           instance.post('fileChunk', formData, )
            .then(res=> {
              // 存儲(chǔ)已上傳的切片下標(biāo)
          + 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), // 標(biāo)識(shí):是否已完成上傳
          +        progress: getChunkStorage && getChunkStorage.includes(index) ? 100 : 0,
          +        status: getChunkStorage && getChunkStorage.includes(index)? 'success'
          +              : 'wait' // 上傳狀態(tài),用作進(jìn)度狀態(tài)顯示
                }));
          
              }

          構(gòu)造切片數(shù)據(jù)時(shí),過(guò)濾掉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); // 文件名使用切片的下標(biāo)
                return { formData, index, fileName };
              })
          }

          垃圾文件清理

          隨著上傳文件的增多,相應(yīng)的垃圾文件也會(huì)增多,比如有些時(shí)候上傳一半就不再繼續(xù),或上傳失敗,碎片文件就會(huì)增多。解決方案我目前想了2種

          • 前端在localstorage設(shè)置緩存時(shí)間,超過(guò)時(shí)間就發(fā)送請(qǐng)求通知后端清理碎片文件,同時(shí)前端也要清理緩存。
          • 前后端都約定好,每個(gè)緩存從生成開始,只能存儲(chǔ)12小時(shí),12小時(shí)后自動(dòng)清理

          以上2種方案似乎都有點(diǎn)問(wèn)題,極有可能造成前后端因時(shí)間差,引發(fā)切片上傳異常的問(wèn)題,后面想到合適的解決方案再來(lái)更新吧。

          Done: 續(xù)傳到這里也就完成了。


          秒傳

          這算是最簡(jiǎn)單的,只是聽起來(lái)很厲害的樣子。原理:計(jì)算整個(gè)文件的HASH,在執(zhí)行上傳操作前,向服務(wù)端發(fā)送請(qǐng)求,傳遞MD5值,后端進(jìn)行文件檢索。若服務(wù)器中已存在該文件,便不進(jìn)行后續(xù)的任何操作,上傳也便直接結(jié)束。大家一看就明白

          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校驗(yàn),是否為秒傳
          +      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]);
                }
              }
            }
            // 文件上傳之前的校驗(yàn): 校驗(yàn)文件是否已存在
            verifyUpload(fileName, fileHash) {
              return new Promise(resolve=> {
                const obj={
                  md5: fileHash,
                  fileName,
                  ...this.uploadArguments //傳遞其他參數(shù)
                };
                instance
                  .post('fileChunk/presence', obj)
                  .then(res=> {
                    resolve(res.data);
                  })
                  .catch(err=> {
                    console.log('verifyUpload -> err', err);
                  });
              });
            }

          Done: 秒傳到這里也就完成了。


          后端處理

          文章好像有點(diǎn)長(zhǎng)了,具體代碼邏輯就先不貼了,除非有人留言要求,嘻嘻,有時(shí)間再更新

          Node版

          請(qǐng)前往 https://github.com/pseudo-god... 查看

          JAVA版

          下周應(yīng)該會(huì)更新處理

          PHP版

          1年多沒(méi)寫PHP了,抽空我會(huì)慢慢補(bǔ)上來(lái)

          待完善

          • 切片的大?。哼@個(gè)后面會(huì)做出動(dòng)態(tài)計(jì)算的。需要根據(jù)當(dāng)前所上傳文件的大小,自動(dòng)計(jì)算合適的切片大小。避免出現(xiàn)切片過(guò)多的情況。
          • 文件追加:目前上傳文件過(guò)程中,不能繼續(xù)選擇文件加入隊(duì)列。(這個(gè)沒(méi)想好應(yīng)該怎么處理。)

          更新記錄

          組件已經(jīng)運(yùn)行一段時(shí)間了,期間也測(cè)試出幾個(gè)問(wèn)題,本來(lái)以為沒(méi)BUG的,看起來(lái)BUG都挺嚴(yán)重

          BUG-1:當(dāng)同時(shí)上傳多個(gè)內(nèi)容相同但是文件名稱不同的文件時(shí),出現(xiàn)上傳失敗的問(wèn)題。

          預(yù)期結(jié)果:第一個(gè)上傳成功后,后面相同的文文件應(yīng)該直接秒傳

          實(shí)際結(jié)果:第一個(gè)上傳成功后,其余相同的文件都失敗,錯(cuò)誤信息,塊數(shù)不對(duì)。

          原因:當(dāng)?shù)谝粋€(gè)文件塊上傳完畢后,便立即進(jìn)行了下一個(gè)文件的循環(huán),導(dǎo)致無(wú)法及時(shí)獲取文件是否已秒傳的狀態(tài),從而導(dǎo)致失敗。

          解決方案:在當(dāng)前文件分片上傳完畢并且請(qǐng)求合并接口完畢后,再進(jìn)行下一次循環(huán)。

          將子方法都改為同步方式,mergeRequest 和 uploadChunks 方法

          BUG-2: 當(dāng)每次選擇相同的文件并觸發(fā)beforeUpload方法時(shí),若第二次也選擇了相同的文件,beforeUpload方法失效,從而導(dǎo)致整個(gè)流程失效。

          原因:之前每次選擇文件時(shí),沒(méi)有清空上次所選input文件的數(shù)據(jù),相同數(shù)據(jù)的情況下,是不會(huì)觸發(fā)input的change事件。

          解決方案:每次點(diǎn)擊input時(shí),清空數(shù)據(jù)即可。我順帶優(yōu)化了下其他的代碼,具體看提交記錄吧。

          <input
            v-if="!changeDisabled"
            type="file"
            :multiple="multiple"
            class="select-file-input"
            :accept="accept"
          +  οnclick="f.outerHTML=f.outerHTML"
            @change="handleFileChange"/>

          重寫了暫停和恢復(fù)的功能,實(shí)際上,主要是增加了暫停和恢復(fù)的狀態(tài)

          之前的處理邏輯太簡(jiǎn)單粗暴,存在諸多問(wèn)題?,F(xiàn)在將狀態(tài)定位在每一個(gè)文件之上,這樣恢復(fù)上傳時(shí),直接跳過(guò)即可

          封裝組件

          寫了一大堆,其實(shí)以上代碼你直接復(fù)制也無(wú)法使用,這里我將此封裝了一個(gè)組件。大家可以去github下載文件,里面有使用案例 ,若有用記得隨手給個(gè)star,謝謝!

          偷個(gè)懶,具體封裝組件的代碼就不列出來(lái)了,大家直接去下載文件查看,若有不明白的,可留言。

          組件文檔

          Attribute


          Slot



          后端接口文檔:按文檔實(shí)現(xiàn)即可

          代碼地址:https://github.com/pseudo-god/vue-simple-upload

          接口文檔地址 https://docs.apipost.cn/view/0e19f16d4470ed6b#287746

          作者:Pseudo

          轉(zhuǎn)發(fā)鏈接:https://segmentfault.com/a/1190000023434864

          、建立站點(diǎn)后,在文件夾上右鍵新建一個(gè)文件,改名為音樂(lè)制作網(wǎng)頁(yè),然后雙擊進(jìn)入網(wǎng)頁(yè),首先,插入表格,17行,2列,表格寬度和表格粗細(xì)都為0,確定。選中表格,下邊的對(duì)齊方式為,居中對(duì)齊

          2、選中第一個(gè)格,按住Ctrl鍵再選中第二個(gè)格,右鍵,表格,合并單元格,點(diǎn)擊插入,圖像,選擇建的站點(diǎn)下的素材,確定

          3、用剛才的方法合并第二行單元格,填寫導(dǎo)航欄文字,選擇拆分,找到對(duì)應(yīng)代碼位置,填寫空格代碼 在文字前后都添加空格,使導(dǎo)航欄文字間隙均勻,下邊背景顏色改為紫色

          4、編寫下一行文字,背景顏色為綠色,金曲列表文字,下邊,HTML,格式為標(biāo)題5,歌曲下載文字,下邊,HTML,格式為標(biāo)題3,找到代碼中金曲列表文字對(duì)應(yīng)位置,添加空格代碼

          5、在歌曲下載文字后面插入,圖像,選擇下載圖標(biāo)圖片,點(diǎn)擊圖片,選擇連接指向你的歌曲MP3文件

          6、在每個(gè)格中加入歌名,每個(gè)都要插入,布局對(duì)象,Div標(biāo)簽,然后添加歌曲名稱

          7、右半部分,前兩行合并,插入布局對(duì)象,添加文字那女孩對(duì)我說(shuō),HTML格式改為標(biāo)題2,然后將下邊剩余所有行合并,插入布局,添加歌詞

          8、選擇歌詞,將HTML的格式改為標(biāo)題5

          9、點(diǎn)擊代碼,找到歌詞位置,復(fù)制空格,在每一行歌詞前面招貼空格,刷新一下,使歌詞居中一些

          10、按F12預(yù)覽

          小伙伴們,有沒(méi)有看懂呢,看不懂可以去看視頻呦!

          HTML 使用超級(jí)鏈接與網(wǎng)絡(luò)上的另一個(gè)文檔相連。幾乎可以在所有的網(wǎng)頁(yè)中找到鏈接。點(diǎn)擊鏈接可以從一張頁(yè)面跳轉(zhuǎn)到另一張頁(yè)面。

          HTML 鏈接

          如何在HTML文檔中創(chuàng)建鏈接。

          (可以在本頁(yè)底端找到更多實(shí)例)

          HTML 超鏈接(鏈接)

          HTML使用標(biāo)簽 <a>來(lái)設(shè)置超文本鏈接。

          超鏈接可以是一個(gè)字,一個(gè)詞,或者一組詞,也可以是一幅圖像,您可以點(diǎn)擊這些內(nèi)容來(lái)跳轉(zhuǎn)到新的文檔或者當(dāng)前文檔中的某個(gè)部分。

          當(dāng)您把鼠標(biāo)指針移動(dòng)到網(wǎng)頁(yè)中的某個(gè)鏈接上時(shí),箭頭會(huì)變?yōu)橐恢恍∈帧?/p>

          在標(biāo)簽<a> 中使用了href屬性來(lái)描述鏈接的地址。

          默認(rèn)情況下,鏈接將以以下形式出現(xiàn)在瀏覽器中:

          • 一個(gè)未訪問(wèn)過(guò)的鏈接顯示為藍(lán)色字體并帶有下劃線。

          • 訪問(wèn)過(guò)的鏈接顯示為紫色并帶有下劃線。

          • 點(diǎn)擊鏈接時(shí),鏈接顯示為紅色并帶有下劃線。

          注意:如果為這些超鏈接設(shè)置了 CSS 樣式,展示樣式會(huì)根據(jù) CSS 的設(shè)定而顯示。

          HTML 鏈接語(yǔ)法

          鏈接的 HTML 代碼很簡(jiǎn)單。它類似這樣::

          <a href="url">鏈接文本</a>

          href 屬性描述了鏈接的目標(biāo)。.

          實(shí)例

          <a >訪問(wèn)菜鳥教程</a>

          上面這行代碼顯示為:: 訪問(wèn)菜鳥教程

          點(diǎn)擊這個(gè)超鏈接會(huì)把用戶帶到菜鳥教程的首頁(yè)。

          提示: "鏈接文本" 不必一定是文本。圖片或其他 HTML 元素都可以成為鏈接。

          HTML 鏈接 - target 屬性

          使用 target 屬性,你可以定義被鏈接的文檔在何處顯示。

          下面的這行會(huì)在新窗口打開文檔:

          實(shí)例

          <a>訪問(wèn)菜鳥教程!</a>

          HTML 鏈接- id 屬性

          id屬性可用于創(chuàng)建在一個(gè)HTML文檔書簽標(biāo)記。

          提示: 書簽是不以任何特殊的方式顯示,在HTML文檔中是不顯示的,所以對(duì)于讀者來(lái)說(shuō)是隱藏的。

          實(shí)例

          在HTML文檔中插入ID:

          <a id="tips">有用的提示部分</a>

          在HTML文檔中創(chuàng)建一個(gè)鏈接到"有用的提示部分(id="tips")":

          <a href="#tips">訪問(wèn)有用的提示部分</a>

          或者,從另一個(gè)頁(yè)面創(chuàng)建一個(gè)鏈接到"有用的提示部分(id="tips")":

          <a >

          訪問(wèn)有用的提示部分</a>

          基本的注意事項(xiàng) - 有用的提示

          注釋: 請(qǐng)始終將正斜杠添加到子文件夾。假如這樣書寫鏈接:,就會(huì)向服務(wù)器產(chǎn)生兩次 HTTP 請(qǐng)求。這是因?yàn)榉?wù)器會(huì)添加正斜杠到這個(gè)地址,然后創(chuàng)建一個(gè)新的請(qǐng)求,就像這樣:。

          圖片鏈接

          如何使用圖片鏈接。

          在當(dāng)前頁(yè)面鏈接到指定位置

          如何使用書簽

          跳出框架

          本例演示如何跳出框架,假如你的頁(yè)面被固定在框架之內(nèi)。

          創(chuàng)建電子郵件鏈接

          本例演示如何如何鏈接到一個(gè)郵件。(本例在安裝郵件客戶端程序后才能工作。)

          建電子郵件鏈接 2

          本例演示更加復(fù)雜的郵件鏈接。

          HTML 鏈接標(biāo)簽

          標(biāo)簽描述
          <a>定義一個(gè)超級(jí)鏈接

          如您還有不明白的可以在下面與我留言或是與我探討QQ群308855039,我們一起飛!


          主站蜘蛛池模板: 亚洲AV成人精品日韩一区18p | 国产成人精品一区二区三区无码| 日本大香伊一区二区三区| 天天综合色一区二区三区| 亚洲视频一区在线播放| 久久se精品一区二区国产| 久久se精品一区二区国产| 国产成人久久一区二区三区| 日韩少妇无码一区二区三区| 欧洲精品码一区二区三区免费看| 精彩视频一区二区| 中文字幕在线一区二区在线| 日韩AV无码一区二区三区不卡| 国产免费无码一区二区| 无码人妻一区二区三区免费视频 | 精品乱码一区二区三区在线| 一本大道东京热无码一区| 午夜在线视频一区二区三区| 东京热无码av一区二区| 国产精品综合AV一区二区国产馆| 视频一区视频二区制服丝袜| 在线观看精品一区| 农村乱人伦一区二区| 亚洲一区二区三区香蕉| 秋霞电影网一区二区三区| 国产精品亚洲不卡一区二区三区| 精品欧洲av无码一区二区| 久久久老熟女一区二区三区| 亚洲一区二区三区首页| 日韩精品一区二区三区色欲AV | 国内精品一区二区三区东京| 亚洲Aⅴ无码一区二区二三区软件| 亚洲丶国产丶欧美一区二区三区 | 亚洲一区免费视频| 日韩精品一区二区三区不卡| 精品国产a∨无码一区二区三区| 日韩精品无码免费一区二区三区 | 在线精品动漫一区二区无广告| 一区二区免费国产在线观看| 精品人妻少妇一区二区| 无码人妻AV免费一区二区三区 |