<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【W(wǎng)eb技術(shù)】1016- 全面理解 8 種文件上傳場景

          共 27305字,需瀏覽 55分鐘

           ·

          2021-07-14 15:23

          在日常工作中,文件上傳是一個(gè)很常見的功能。在項(xiàng)目開發(fā)過程中,我們通常都會使用一些成熟的上傳組件來實(shí)現(xiàn)對應(yīng)的功能。一般來說,成熟的上傳組件不僅會提供漂亮 UI 或好的交互體驗(yàn),而且還會提供多種不同的上傳方式,以滿足不同的場景需求。

          一般在我們工作中,主要會涉及到 8 種文件上傳的場景,每一種場景背后都使用不同的技術(shù),其中也有很多細(xì)節(jié)需要我們額外注意。今天阿寶哥就來帶大家總結(jié)一下這 8 種場景,讓大家能更好地理解成熟上傳組件所提供的功能。閱讀本文后,你將會了解以下的內(nèi)容:

          • 單文件上傳:利用 input 元素的 accept 屬性限制上傳文件的類型、利用 JS 檢測文件的類型及使用 Koa 實(shí)現(xiàn)單文件上傳的功能;

          • 多文件上傳:利用 input 元素的 multiple 屬性支持選擇多文件及使用 Koa 實(shí)現(xiàn)多文件上傳的功能;

          • 目錄上傳:利用 input 元素上的 webkitdirectory 屬性支持目錄上傳的功能及使用 Koa 實(shí)現(xiàn)目錄上傳并按文件目錄結(jié)構(gòu)存放的功能;

          • 壓縮目錄上傳:在目錄上傳的基礎(chǔ)上,利用 JSZip 實(shí)現(xiàn)壓縮目錄上傳的功能;

          • 拖拽上傳:利用拖拽事件和 DataTransfer 對象實(shí)現(xiàn)拖拽上傳的功能;

          • 剪貼板上傳:利用剪貼板事件和 Clipboard API 實(shí)現(xiàn)剪貼板上傳的功能;

          • 大文件分塊上傳:利用 Blob.slice、SparkMD5 和第三方庫 async-pool 實(shí)現(xiàn)大文件并發(fā)上傳的功能;

          • 服務(wù)端上傳:利用第三方庫 form-data 實(shí)現(xiàn)服務(wù)端文件流式上傳的功能。

          一、單文件上傳

          對于單文件上傳的場景來說,最常見的是圖片上傳的場景,所以我們就以圖片上傳為例,先來介紹單文件上傳的基本流程。

          1.1 前端代碼

          html

          在以下代碼中,我們通過 input 元素的 accept 屬性限制了上傳文件的類型。這里使用 image/* 限制只能選擇圖片文件,當(dāng)然你也可以設(shè)置特定的類型,比如 image/pngimage/png,image/jpeg

          <input id="uploadFile" type="file" accept="image/*" />
          <button id="submit" onclick="uploadFile()">上傳文件</button>

          需要注意的是,雖然我們把 input 元素的 accept 屬性設(shè)置為 image/png。但如果用戶把 jpg/jpeg 格式的圖片后綴名改為 .png,就可以成功繞過這個(gè)限制。要解決這個(gè)問題,我們可以通過讀取文件中的二進(jìn)制數(shù)據(jù)來識別正確的文件類型。

          要查看圖片對應(yīng)的二進(jìn)制數(shù)據(jù),我們可以借助一些現(xiàn)成的編輯器,比如 Windows 平臺下的 WinHex 或 macOS 平臺下的 Synalyze It! Pro 十六進(jìn)制編輯器。這里我們使用 Synalyze It! Pro 這個(gè)編輯器,來查看阿寶哥頭像對應(yīng)的二進(jìn)制數(shù)據(jù)。

          那么在前端能否不借助工具,讀取文件的二進(jìn)制數(shù)據(jù)呢?答案是可以的,這里阿寶哥就不展開介紹了。感興趣的話,你可以閱讀 JavaScript 如何檢測文件的類型? 這篇文章。另外,需要注意的是 input 元素 accept 屬性有存在兼容性問題。比如 IE 9 以下不支持,具體如下圖所示:

          (圖片來源 —— https://caniuse.com/input-file-accept)

          js

          const uploadFileEle = document.querySelector("#uploadFile");

          const request = axios.create({
            baseURL"http://localhost:3000/upload",
            timeout60000
          });

          async function uploadFile({
            if (!uploadFileEle.files.length) return;
            const file = uploadFileEle.files[0]; // 獲取單個(gè)文件
            // 省略文件的校驗(yàn)過程,比如文件類型、大小校驗(yàn)
            upload({
              url"/single",
              file,
            });
          }

          function upload({ url, file, fieldName = "file" }{
            let formData = new FormData();
            formData.set(fieldName, file);
            request.post(url, formData, {
              // 監(jiān)聽上傳進(jìn)度
              onUploadProgressfunction (progressEvent{
                const percentCompleted = Math.round(
                  (progressEvent.loaded * 100) / progressEvent.total
                );
                console.log(percentCompleted);
               },
            });
          }

          在以上代碼中,我們先把讀取的 File 對象封裝成 FormData 對象,然后利用 Axios 實(shí)例的 post 方法實(shí)現(xiàn)文件上傳的功能。在上傳前,通過設(shè)置請求配置對象的 onUploadProgress 屬性,就可以獲取文件的上傳進(jìn)度。

          1.2 服務(wù)端代碼

          Koa 是一個(gè)簡單易用的 Web 框架,它的特點(diǎn)是優(yōu)雅、簡潔、輕量、自由度高。所以我們選擇它來搭建文件服務(wù),并使用以下中間件來實(shí)現(xiàn)相應(yīng)的功能:

          • koa-static:處理靜態(tài)資源的中間件;
          • @koa/cors:處理跨域請求的中間件;
          • @koa/multer:處理 multipart/form-data 的中間件;
          • @koa/router:處理路由的中間件。
          const path = require("path");
          const Koa = require("koa");
          const serve = require("koa-static");
          const cors = require("@koa/cors");
          const multer = require("@koa/multer");
          const Router = require("@koa/router");

          const app = new Koa();
          const router = new Router();
          const PORT = 3000;
          // 上傳后資源的URL地址
          const RESOURCE_URL = `http://localhost:${PORT}`;
          // 存儲上傳文件的目錄
          const UPLOAD_DIR = path.join(__dirname, "/public/upload");

          const storage = multer.diskStorage({
            destinationasync function (req, file, cb{
              // 設(shè)置文件的存儲目錄
              cb(null, UPLOAD_DIR);
            },
            filenamefunction (req, file, cb{
              // 設(shè)置文件名
              cb(null`${file.originalname}`);
            },
          });

          const multerUpload = multer({ storage });

          router.get("/"async (ctx) => {
            ctx.body = "歡迎使用文件服務(wù)(by 阿寶哥)";
          });

          router.post(
            "/upload/single",
            async (ctx, next) => {
              try {
                await next();
                ctx.body = {
                  code1,
                  msg"文件上傳成功",
                  url`${RESOURCE_URL}/${ctx.file.originalname}`,
                };
              } catch (error) {
                ctx.body = {
                  code0,
                  msg"文件上傳失敗"
                };
              }
            },
            multerUpload.single("file")
          );

          // 注冊中間件
          app.use(cors());
          app.use(serve(UPLOAD_DIR));
          app.use(router.routes()).use(router.allowedMethods());

          app.listen(PORT, () => {
            console.log(`app starting at port ${PORT}`);
          });

          以上代碼相對比較簡單,我們就不展開介紹了。Koa 內(nèi)核很簡潔,擴(kuò)展功能都是通過中間件來實(shí)現(xiàn)。比如示例中使用到的路由、CORS、靜態(tài)資源處理等功能都是通過中間件實(shí)現(xiàn)。因此要想掌握 Koa 這個(gè)框架,核心是掌握它的中間件機(jī)制。如果你想深入了解的話,可以閱讀 如何更好地理解中間件和洋蔥模型 這篇文章。其實(shí)除了單文件上傳外,在文件上傳的場景中,我們也可以同時(shí)上傳多個(gè)文件。

          單文件上傳示例:single-file-upload

          https://github.com/semlinker/file-upload-demos/tree/master/single-file-upload

          二、多文件上傳

          要上傳多個(gè)文件,首先我們需要允許用戶同時(shí)選擇多個(gè)文件。要實(shí)現(xiàn)這個(gè)功能,我們可以利用 input 元素的 multiple 屬性。跟前面介紹的 accept 屬性一樣,該屬性也存在兼容性問題,具體如下圖所示:

          (圖片來源 —— https://caniuse.com/mdn-api_htmlinputelement_multiple)

          2.1 前端代碼

          html

          相比單文件上傳的代碼,多文件上傳場景下的 input 元素多了一個(gè) multiple 屬性:

          <input id="uploadFile" type="file" accept="image/*" multiple />
          <button id="submit" onclick="uploadFile()">上傳文件</button>

          js

          在單文件上傳的代碼中,我們通過 uploadFileEle.files[0] 獲取單個(gè)文件,而對于多文件上傳來說,我們需要獲取已選擇的文件列表,即通過 uploadFileEle.files 來獲取,它返回的是一個(gè) FileList 對象。

          async function uploadFile({
            if (!uploadFileEle.files.length) return;
            const files = Array.from(uploadFileEle.files);
            upload({
              url"/multiple",
              files,
            });
          }

          因?yàn)橐С稚蟼鞫鄠€(gè)文件,所以我們需要同步更新一下 upload 函數(shù)。對應(yīng)的處理邏輯就是遍歷文件列表,然后使用 FormData 對象的 append 方法來添加多個(gè)文件,具體代碼如下所示:

          function upload({ url, files, fieldName = "file" }{
            let formData = new FormData();
            files.forEach((file) => {
              formData.append(fieldName, file);
            });
            request.post(url, formData, {
              // 監(jiān)聽上傳進(jìn)度
              onUploadProgressfunction (progressEvent{
                const percentCompleted = Math.round(
                  (progressEvent.loaded * 100) / progressEvent.total
                );
                console.log(percentCompleted);
              },
            });
          }

          2.2 服務(wù)端代碼

          在以下代碼中,我們定義了一個(gè)新的路由 —— /upload/multiple 來處理多文件上傳的功能。當(dāng)所有文件都成功上傳后,就會返回一個(gè)已上傳文件的 url 地址列表:

          router.post(
            "/upload/multiple",
            async (ctx, next) => {
              try {
                await next();
                urls = ctx.files.file.map(file => `${RESOURCE_URL}/${file.originalname}`);
                ctx.body = {
                  code1,
                  msg"文件上傳成功",
                  urls
                };
              } catch (error) {
                ctx.body = {
                  code0,
                  msg"文件上傳失敗",
                };
              }
            },
            multerUpload.fields([
              {
                name"file"// 與FormData表單項(xiàng)的fieldName相對應(yīng)
              },
            ])
          );

          介紹完單文件和多文件上傳的功能,接下來我們來介紹目錄上傳的功能。

          多文件上傳示例:multiple-file-upload

          https://github.com/semlinker/file-upload-demos/tree/master/multiple-file-upload

          三、目錄上傳

          可能你還不知道,input 元素上還有一個(gè)的 webkitdirectory 屬性。當(dāng)設(shè)置了 webkitdirectory 屬性之后,我們就可以選擇目錄了。

          <input id="uploadFile" type="file" accept="image/*" webkitdirectory />

          當(dāng)我們選擇了指定目錄之后,比如阿寶哥桌面上的 images 目錄,就會顯示以下確認(rèn)框:

          點(diǎn)擊上傳按鈕之后,我們就可以獲取文件列表。列表中的文件對象上含有一個(gè) webkitRelativePath 屬性,用于表示當(dāng)前文件的相對路徑。

          雖然通過 webkitdirectory 屬性可以很容易地實(shí)現(xiàn)選擇目錄的功能,但在實(shí)際項(xiàng)目中我們還需要考慮它的兼容性。比如在 IE 11 以下的版本就不支持該屬性,其它瀏覽器的兼容性如下圖所示:

          (圖片來源 —— https://caniuse.com/?search=webkitdirectory)

          了解完 webkitdirectory 屬性的兼容性,我們先來介紹前端的實(shí)現(xiàn)代碼。

          3.1 前端代碼

          為了讓服務(wù)端能按照實(shí)際的目錄結(jié)構(gòu)來存放對應(yīng)的文件,在添加表單項(xiàng)時(shí)我們需要把當(dāng)前文件的路徑提交到服務(wù)端。此外,為了確保@koa/multer 能正確處理文件的路徑,我們需要對路徑進(jìn)行特殊處理。即把 / 斜杠替換為 @ 符號。對應(yīng)的處理方式如下所示:

          function upload({ url, files, fieldName = "file" }{
            let formData = new FormData();
            files.forEach((file, i) => {
              formData.append(
                fieldName, 
                files[i],
                files[i].webkitRelativePath.replace(/\//g"@");
              );
            });
            request.post(url, formData); // 省略上傳進(jìn)度處理
          }

          3.2 服務(wù)端代碼

          目錄上傳與多文件上傳,服務(wù)端代碼的主要區(qū)別就是 @koa/multer 中間件的配置對象不一樣。在 destination 屬性對應(yīng)的函數(shù)中,我們需要把文件名中 @ 還原成 /,然后根據(jù)文件的實(shí)際路徑來生成目錄。

          const fse = require("fs-extra");
          const storage = multer.diskStorage({
            destinationasync function (req, file, cb{
              // [email protected] => images/image-1.jpeg
              let relativePath = file.originalname.replace(/@/g, path.sep);
              let index = relativePath.lastIndexOf(path.sep);
              let fileDir = path.join(UPLOAD_DIR, relativePath.substr(0, index));
              // 確保文件目錄存在,若不存在的話,會自動創(chuàng)建
              await fse.ensureDir(fileDir); 
              cb(null, fileDir);
            },
            filenamefunction (req, file, cb{
              let parts = file.originalname.split("@");
              cb(null`${parts[parts.length - 1]}`); 
            },
          });

          現(xiàn)在我們已經(jīng)實(shí)現(xiàn)了目錄上傳的功能,那么能否把目錄下的文件壓縮成一個(gè)壓縮包后再上傳呢?答案是可以的,接下來我們來介紹如何實(shí)現(xiàn)壓縮目錄上傳的功能。

          目錄上傳示例:directory-upload

          https://github.com/semlinker/file-upload-demos/tree/master/directory-upload

          四、壓縮目錄上傳

          JavaScript 如何在線解壓 ZIP 文件? 這篇文章中,介紹了在瀏覽器端如何使用 JSZip 這個(gè)庫實(shí)現(xiàn)在線解壓 ZIP 文件的功能。JSZip 這個(gè)庫除了可以解析 ZIP 文件之外,它還可以用來 創(chuàng)建和編輯 ZIP 文件。利用 JSZip 這個(gè)庫提供的 API,我們就可以把目錄下的所有文件壓縮成 ZIP 文件,然后再把生成的 ZIP 文件上傳到服務(wù)器。

          4.1 前端代碼

          JSZip 實(shí)例上的 file(name, data [,options]) 方法,可以把文件添加到 ZIP 文件中。基于該方法我們可以封裝了一個(gè) generateZipFile 函數(shù),用于把目錄下的文件列表壓縮成一個(gè) ZIP 文件。以下是 generateZipFile 函數(shù)的具體實(shí)現(xiàn):

          function generateZipFile(
            zipName, files,
            options = { type: "blob", compression: "DEFLATE" }
          {
            return new Promise((resolve, reject) => {
              const zip = new JSZip();
              for (let i = 0; i < files.length; i++) {
                zip.file(files[i].webkitRelativePath, files[i]);
              }
              zip.generateAsync(options).then(function (blob{
                zipName = zipName || Date.now() + ".zip";
                const zipFile = new File([blob], zipName, {
                  type"application/zip",
                });
                resolve(zipFile);
              });
            });
          }

          在創(chuàng)建完 generateZipFile 函數(shù)之后,我們需要更新一下前面已經(jīng)介紹過的 uploadFile 函數(shù):

          async function uploadFile({
            let fileList = uploadFileEle.files;
            if (!fileList.length) return;
            let webkitRelativePath = fileList[0].webkitRelativePath;
            let zipFileName = webkitRelativePath.split("/")[0] + ".zip";
            let zipFile = await generateZipFile(zipFileName, fileList);
            upload({
              url"/single",
              file: zipFile,
              fileName: zipFileName
            });
          }

          在以上的 uploadFile 函數(shù)中,我們會對返回的 FileList 對象進(jìn)行處理,即調(diào)用 generateZipFile 函數(shù)來生成 ZIP 文件。此外,為了在服務(wù)端接收壓縮文件時(shí),能獲取到文件名,我們?yōu)?upload 函數(shù)增加了一個(gè) fileName 參數(shù),該參數(shù)用于調(diào)用 formData.append 方法時(shí),設(shè)置上傳文件的文件名:

          function upload({ url, file, fileName, fieldName = "file" }{
            if (!url || !file) return;
            let formData = new FormData();
            formData.append(
              fieldName, file, fileName
            );
            request.post(url, formData); // 省略上傳進(jìn)度跟蹤
          }

          以上就是壓縮目錄上傳,前端部分的 JS 代碼,服務(wù)端的代碼可以參考前面單文件上傳的相關(guān)代碼。

          壓縮目錄上傳示例:directory-compress-upload

          https://github.com/semlinker/file-upload-demos/tree/master/directory-compress-upload

          五、拖拽上傳

          要實(shí)現(xiàn)拖拽上傳的功能,我們需要先了解與拖拽相關(guān)的事件。比如 dragdragenddragenterdragoverdrop 事件等。這里我們只介紹接下來要用到的拖拽事件:

          • dragenter:當(dāng)拖拽元素或選中的文本到一個(gè)可釋放目標(biāo)時(shí)觸發(fā);
          • dragover:當(dāng)元素或選中的文本被拖到一個(gè)可釋放目標(biāo)上時(shí)觸發(fā)(每100毫秒觸發(fā)一次);
          • dragleave:當(dāng)拖拽元素或選中的文本離開一個(gè)可釋放目標(biāo)時(shí)觸發(fā);
          • drop:當(dāng)元素或選中的文本在可釋放目標(biāo)上被釋放時(shí)觸發(fā)。

          基于上面的這些事件,我們就可以提高用戶拖拽的體驗(yàn)。比如當(dāng)用戶拖拽的元素進(jìn)入目標(biāo)區(qū)域時(shí),對目標(biāo)區(qū)域進(jìn)行高亮顯示。當(dāng)用戶拖拽的元素離開目標(biāo)區(qū)域時(shí),移除高亮顯示。很明顯當(dāng) drop 事件觸發(fā)后,拖拽的元素已經(jīng)放入目標(biāo)區(qū)域了,這時(shí)我們就需要獲取對應(yīng)的數(shù)據(jù)。

          那么如何獲取拖拽對應(yīng)的數(shù)據(jù)呢?這時(shí)我們需要使用 DataTransfer 對象,該對象用于保存拖動并放下過程中的數(shù)據(jù)。它可以保存一項(xiàng)或多項(xiàng)數(shù)據(jù),這些數(shù)據(jù)項(xiàng)可以是一種或者多種數(shù)據(jù)類型。若拖動操作涉及拖動文件,則我們可以通過 DataTransfer 對象的 files 屬性來獲取文件列表。

          介紹完拖拽上傳相關(guān)的知識后,我們來看一下具體如何實(shí)現(xiàn)拖拽上傳的功能。

          5.1 前端代碼

          html

          <div id="dropArea">
             <p>拖拽上傳文件</p>
             <div id="imagePreview"></div>
          </div>

          css

          #dropArea {
            width300px;
            height300px;
            border1px dashed gray;
            margin-bottom20px;
          }
          #dropArea p {
            text-align: center;
            color#999;
          }
          #dropArea.highlighted {
            background-color#ddd;
          }
          #imagePreview {
            max-height250px;
            overflow-y: scroll;
          }
          #imagePreview img {
            width100%;
            display: block;
            margin: auto;
          }

          js

          為了讓大家能夠更好地閱讀拖拽上傳的相關(guān)代碼,我們把代碼拆成 4 部分來講解:

          1、阻止默認(rèn)拖拽行為

          const dropAreaEle = document.querySelector("#dropArea");
          const imgPreviewEle = document.querySelector("#imagePreview");
          const IMAGE_MIME_REGEX = /^image\/(jpe?g|gif|png)$/i;

          ["dragenter""dragover""dragleave""drop"].forEach((eventName) => {
             dropAreaEle.addEventListener(eventName, preventDefaults, false);
             document.body.addEventListener(eventName, preventDefaults, false);
          });

          function preventDefaults(e{
            e.preventDefault();
            e.stopPropagation();
          }

          2、切換目標(biāo)區(qū)域的高亮狀態(tài)

          ["dragenter""dragover"].forEach((eventName) => {
              dropAreaEle.addEventListener(eventName, highlight, false);
          });
          ["dragleave""drop"].forEach((eventName) => {
              dropAreaEle.addEventListener(eventName, unhighlight, false);
          });

          // 添加高亮樣式
          function highlight(e{
            dropAreaEle.classList.add("highlighted");
          }

          // 移除高亮樣式
          function unhighlight(e{
            dropAreaEle.classList.remove("highlighted");
          }

          3、處理圖片預(yù)覽

          dropAreaEle.addEventListener("drop", handleDrop, false);

          function handleDrop(e{
            const dt = e.dataTransfer;
            const files = [...dt.files];
            files.forEach((file) => {
              previewImage(file, imgPreviewEle);
            });
            // 省略文件上傳代碼
          }

          function previewImage(file, container{
            if (IMAGE_MIME_REGEX.test(file.type)) {
              const reader = new FileReader();
              reader.onload = function (e{
                let img = document.createElement("img");
                img.src = e.target.result;
                container.append(img);
              };
              reader.readAsDataURL(file);
            }
          }

          4、文件上傳

          function handleDrop(e{
            const dt = e.dataTransfer;
            const files = [...dt.files];
            // 省略圖片預(yù)覽代碼
            files.forEach((file) => {
              upload({
                url"/single",
                file,
              });
            });
          }

          const request = axios.create({
            baseURL"http://localhost:3000/upload",
            timeout60000,
          });

          function upload({ url, file, fieldName = "file" }{
            let formData = new FormData();
            formData.set(fieldName, file);
            request.post(url, formData, {
              // 監(jiān)聽上傳進(jìn)度
              onUploadProgressfunction (progressEvent{
                const percentCompleted = Math.round(
                  (progressEvent.loaded * 100) / progressEvent.total
                );
                console.log(percentCompleted);
              },
            });
          }

          拖拽上傳算是一個(gè)比較常見的場景,很多成熟的上傳組件都支持該功能。其實(shí)除了拖拽上傳外,還可以利用剪貼板實(shí)現(xiàn)復(fù)制上傳的功能。

          拖拽上傳示例:drag-drop-upload

          https://github.com/semlinker/file-upload-demos/tree/master/drag-drop-upload

          六、剪貼板上傳

          在介紹如何實(shí)現(xiàn)剪貼板上傳的功能前,我們需要了解一下 Clipboard API。Clipboard 接口實(shí)現(xiàn)了 Clipboard API,如果用戶授予了相應(yīng)的權(quán)限,就能提供系統(tǒng)剪貼板的讀寫訪問。在 Web 應(yīng)用程序中,Clipboard  API 可用于實(shí)現(xiàn)剪切、復(fù)制和粘貼功能。該 API 用于取代通過 document.execCommand API 來實(shí)現(xiàn)剪貼板的操作。

          在實(shí)際項(xiàng)目中,我們不需要手動創(chuàng)建 Clipboard 對象,而是通過 navigator.clipboard 來獲取 Clipboard 對象:

          在獲取 Clipboard 對象之后,我們就可以利用該對象提供的 API 來訪問剪貼板,比如:

          navigator.clipboard.readText().then(
            clipText => document.querySelector(".editor").innerText = clipText
          );

          以上代碼將 HTML 中含有 .editor 類的第一個(gè)元素的內(nèi)容替換為剪貼板的內(nèi)容。如果剪貼板為空,或者不包含任何文本,則元素的內(nèi)容將被清空。這是因?yàn)樵诩糍N板為空或者不包含文本時(shí),readText 方法會返回一個(gè)空字符串。

          利用 Clipboard API 我們可以很方便地操作剪貼板,但實(shí)際項(xiàng)目使用過程中也得考慮它的兼容性:

          (圖片來源 —— https://caniuse.com/async-clipboard)

          要實(shí)現(xiàn)剪貼板上傳的功能,可以分為以下 3 個(gè)步驟:

          • 監(jiān)聽容器的粘貼事件;
          • 讀取并解析剪貼板中的內(nèi)容;
          • 動態(tài)構(gòu)建 FormData 對象并上傳。

          了解完上述步驟,接下來我們來分析一下具體實(shí)現(xiàn)的代碼。

          6.1 前端代碼

          html

          <div id="uploadArea">
             <p>請先復(fù)制圖片后再執(zhí)行粘貼操作</p>
          </div>

          css

          #uploadArea {
             width400px;
             height400px;
             border1px dashed gray;
             display: table-cell;
             vertical-align: middle;
          }
          #uploadArea p {
             text-align: center;
             color#999;
          }
          #uploadArea img {
             max-width100%;
             max-height100%;
             display: block;
             margin: auto;
          }

          js

          在以下代碼中,我們使用 addEventListener 方法為 uploadArea 容器添加 paste 事件。在對應(yīng)的事件處理函數(shù)中,我們會優(yōu)先判斷當(dāng)前瀏覽器是否支持異步 Clipboard API。如果支持的話,就會通過 navigator.clipboard.read 方法來讀取剪貼板中的內(nèi)容。在讀取內(nèi)容之后,我們會通過正則判斷剪貼板項(xiàng)中是否包含圖片資源,如果有的話會調(diào)用 previewImage 方法執(zhí)行圖片預(yù)覽操作并把返回的 blob 對象保存起來,用于后續(xù)的上傳操作。

          const IMAGE_MIME_REGEX = /^image\/(jpe?g|gif|png)$/i;
          const uploadAreaEle = document.querySelector("#uploadArea");

          uploadAreaEle.addEventListener("paste"async (e) => {
            e.preventDefault();
            const files = [];
            if (navigator.clipboard) {
              let clipboardItems = await navigator.clipboard.read();
              for (const clipboardItem of clipboardItems) {
                for (const type of clipboardItem.types) {
                  if (IMAGE_MIME_REGEX.test(type)) {
                     const blob = await clipboardItem.getType(type);
                     insertImage(blob, uploadAreaEle);
                     files.push(blob);
                   }
                 }
               }
            } else {
                const items = e.clipboardData.items;
                for (let i = 0; i < items.length; i++) {
                  if (IMAGE_MIME_REGEX.test(items[i].type)) {
                    let file = items[i].getAsFile();
                    insertImage(file, uploadAreaEle);
                    files.push(file);
                  }
                }
            }
            if (files.length > 0) {
              confirm("剪貼板檢測到圖片文件,是否執(zhí)行上傳操作?"
                && upload({
                     url"/multiple",
                     files,
                });
             }
          });

          若當(dāng)前瀏覽器不支持異步 Clipboard API,則我們會嘗試通過 e.clipboardData.items 來訪問剪貼板中的內(nèi)容。需要注意的是,在遍歷剪貼板內(nèi)容項(xiàng)的時(shí)候,我們是通過 getAsFile 方法來獲取剪貼板的內(nèi)容。當(dāng)然該方法也存在兼容性問題,具體如下圖所示:

          (圖片來源 —— https://caniuse.com/mdn-api_datatransferitem_getasfile)

          前面已經(jīng)提到,當(dāng)從剪貼板解析到圖片資源時(shí),會讓用戶進(jìn)行預(yù)覽,該功能是基于 FileReader API 來實(shí)現(xiàn)的,對應(yīng)的代碼如下所示:

          function previewImage(file, container{
            const reader = new FileReader();
            reader.onload = function (e{
              let img = document.createElement("img");
              img.src = e.target.result;
              container.append(img);
            };
            reader.readAsDataURL(file);
          }

          當(dāng)用戶預(yù)覽完成后,如果確認(rèn)上傳我們就會執(zhí)行文件的上傳操作。因?yàn)槲募菑募糍N板中讀取的,所以在上傳前我們會根據(jù)文件的類型,自動為它生成一個(gè)文件名,具體是采用時(shí)間戳加文件后綴的形式:

          function upload({ url, files, fieldName = "file" }{
            let formData = new FormData();
            files.forEach((file) => {
              let fileName = +new Date() + "." + IMAGE_MIME_REGEX.exec(file.type)[1];
              formData.append(fieldName, file, fileName);
            });
            request.post(url, formData);
          }

          前面我們已經(jīng)介紹了文件上傳的多種不同場景,接下來我們來介紹一個(gè) “特殊” 的場景 —— 大文件上傳

          剪貼板上傳示例:clipboard-upload

          https://github.com/semlinker/file-upload-demos/tree/master/clipboard-upload

          七、大文件分塊上傳

          相信你可能已經(jīng)了解大文件上傳的解決方案,在上傳大文件時(shí),為了提高上傳的效率,我們一般會使用 Blob.slice 方法對大文件按照指定的大小進(jìn)行切割,然后通過多線程進(jìn)行分塊上傳,等所有分塊都成功上傳后,再通知服務(wù)端進(jìn)行分塊合并。具體處理方案如下圖所示:

          因?yàn)樵?JavaScript 中如何實(shí)現(xiàn)大文件并發(fā)上傳? 這篇文章中,阿寶哥已經(jīng)詳細(xì)介紹了大文件并發(fā)上傳的方案,所以這里就不展開介紹了。我們只回顧一下大文件并發(fā)上傳的完整流程:

          前面我們都是介紹客戶端文件上傳的場景,其實(shí)也有服務(wù)端文件上傳的場景。比如在服務(wù)端動態(tài)生成海報(bào)后,上傳到另外一臺服務(wù)器或云廠商的 OSS(Object Storage Service)。下面我們就以 Node.js 為例來介紹在服務(wù)端如何上傳文件。

          大文件分塊上傳示例:big-file-upload

          https://github.com/semlinker/file-upload-demos/tree/master/big-file-upload

          八、服務(wù)端上傳

          服務(wù)器上傳就是把文件從一臺服務(wù)器上傳到另外一臺服務(wù)器。借助 Github 上 form-data 這個(gè)庫提供的功能,我們可以很容易地實(shí)現(xiàn)服務(wù)器上傳的功能。下面我們來簡單介紹一下單文件和多文件上傳的功能:

          8.1 單文件上傳

          const fs = require("fs");
          const path = require("path");
          const FormData = require("form-data");

          const form1 = new FormData();
          form1.append("file", fs.createReadStream(path.join(__dirname, "images/image-1.jpeg")));
          form1.submit("http://localhost:3000/upload/single", (error, response) => {
            if(error) {
              console.log("單圖上傳失敗");
              return;
            }
            console.log("單圖上傳成功");
          });

          8.2 多文件上傳

          const form2 = new FormData();
          form2.append("file", fs.createReadStream(path.join(__dirname, "images/image-2.jpeg")));
          form2.append("file", fs.createReadStream(path.join(__dirname, "images/image-3.jpeg")));
          form2.submit("http://localhost:3000/upload/multiple", (error, response) => {
            if(error) {
              console.log("多圖上傳失敗");
              return;
            }
            console.log("多圖上傳成功");
          });

          觀察以上代碼可知,創(chuàng)建完 FormData 對象之后,我們只需要通過 fs.createReadStream API 創(chuàng)建可讀流,然后調(diào)用 FormData 對象的  append 方法添加表單項(xiàng),最后再調(diào)用 submit 方法執(zhí)行提交操作即可。

          其實(shí)除了 ReadableStream 之外,FormData 對象的 append 方法還支持以下類型:

          const FormData = require('form-data');
          const http = require('http');

          const form = new FormData();
          http.request('http://nodejs.org/images/logo.png'function(response{
            form.append('my_field''my value');
            form.append('my_buffer'new Buffer(10));
            form.append('my_logo', response);
          });

          服務(wù)端文件上傳的內(nèi)容就介紹到這里,關(guān)于 form-data 這個(gè)庫的其他用法,感興趣的話,可以閱讀對應(yīng)的使用文檔。其實(shí)除了以上介紹的八種場景外,在日常工作中,你也可能會使用一些同步工具,比如 Syncthing 文件同步工具實(shí)現(xiàn)文件傳輸。好的,本文的所有內(nèi)容都已經(jīng)介紹完了,最后我們來做一個(gè)總結(jié)。

          服務(wù)端上傳示例:server-upload

          https://github.com/semlinker/file-upload-demos/tree/master/server-upload

          九、總結(jié)

          本文阿寶哥詳細(xì)介紹了文件上傳的八種場景,希望閱讀完本文后,你對八種場景背后使用的技術(shù)有一定的了解。由于篇幅有限,阿寶哥就沒有展開介紹與 multipart/form-data 類型相關(guān)的內(nèi)容,感興趣的小伙伴可以自行了解一下。

          此外,在實(shí)際項(xiàng)目中,你可以考慮直接使用成熟的第三方組件,比如 Github 上的 Star 數(shù) 11K+ 的 filepond。該組件采用插件化的架構(gòu),以插件的方式,提供了非常多的功能,比如 File encode、File rename、File poster、Image preview 和 Image crop 等。總之,它是一個(gè)很不錯(cuò)的組件,以后有機(jī)會的話,大家可以嘗試一下。

          十、參考資源

          1. JavaScript 重溫系列(22篇全)
          2. ECMAScript 重溫系列(10篇全)
          3. JavaScript設(shè)計(jì)模式 重溫系列(9篇全)
          4. 正則 / 框架 / 算法等 重溫系列(16篇全)
          5. Webpack4 入門(上)|| Webpack4 入門(下)
          6. MobX 入門(上) ||  MobX 入門(下)
          7. 120+篇原創(chuàng)系列匯總

          回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~

          點(diǎn)擊“閱讀原文”查看 120+ 篇原創(chuàng)文章

          瀏覽 64
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  精品av国产日韩一区二区 | 强伦轩人妻一区二区电影 | 一区二区3区免费 | 99在线精品视频在线观看 | 怡红院在线播放 |