**前端實現大文件上傳**
**引言:**
隨著互聯網技術的發展,用戶在線處理大量數據的需求日益增強,其中涉及大文件上傳的功能已成為許多Web應用不可或缺的一部分。然而,傳統表單提交往往受限于瀏覽器的限制和服務器處理能力,無法很好地滿足大文件高效穩定上傳的需求。本文將深入探討前端實現大文件上傳的關鍵技術和策略,輔以實際HTML+JS代碼示例,助您構建高性能、用戶友好的文件上傳體驗。
## **一、理解瀏覽器上傳限制**
**1.1 瀏覽器最大請求大小限制**
大多數現代瀏覽器默認允許的最大HTTP POST請求大小約為2GB到4GB不等,但具體值會受到服務器配置的影響。因此,在實現大文件上傳之前,需要確保服務器端的接收限制足夠高。
**1.2 超時問題**
大文件上傳過程中,網絡狀況不佳或文件過大可能導致請求超時。對此,可通過設置合理的超時重試機制,以及使用分片上傳來解決。
## **二、分片上傳與斷點續傳**
**2.1 分片上傳概念**
分片上傳是將大文件分割成多個小塊,獨立上傳每一塊,最后在服務器端重組的方式。這樣可以有效避免一次性上傳大文件可能引發的問題。
```html
<!-- HTML 文件選擇器 -->
<input type="file" id="fileInput" accept=".zip,.rar">
<script>
document.getElementById('fileInput').addEventListener('change', function(e) {
const file=e.target.files[0];
// 假設每個分片大小為1MB
const chunkSize=1 * 1024 * 1024;
// 計算分片數量
const chunks=Math.ceil(file.size / chunkSize);
for (let i=0; i < chunks; i++) {
const start=i * chunkSize;
const end=Math.min(start + chunkSize, file.size);
// 創建File Slice
const chunk=file.slice(start, end);
// 發起異步上傳請求
uploadChunk(chunk, i, chunks);
}
});
function uploadChunk(chunk, index, total) {
// 這里僅展示發起上傳請求的邏輯,實際需要包含chunk索引和總數量等信息
const xhr=new XMLHttpRequest();
xhr.open('POST', '/api/upload/chunk', true);
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
xhr.onload=()=> {
if (xhr.status===200) {
// 上傳成功處理邏輯
} else {
// 處理錯誤或重試
}
};
xhr.onerror=()=> {
// 錯誤處理
};
xhr.send(chunk);
}
</script>
```
**2.2 斷點續傳**
斷點續傳是在分片上傳的基礎上,記錄已上傳成功的分片信息,如果上傳過程因網絡問題中斷,可以從上次失敗的地方繼續上傳。這通常需要在客戶端存儲上傳進度信息,并在下次上傳時發送給服務器校驗。
```javascript
// 假設有本地持久化存儲已上傳分片信息的方法
function saveUploadProgress(progressData) {
localStorage.setItem('uploadProgress', JSON.stringify(progressData));
}
// 加載已上傳的分片信息
function loadUploadProgress() {
const progressData=localStorage.getItem('uploadProgress');
return progressData ? JSON.parse(progressData) : null;
}
// 在初始化上傳階段檢查并恢復未完成的上傳任務
const previousProgress=loadUploadProgress();
if (previousProgress) {
for (const {index, chunk} of previousProgress.unfinishedChunks) {
// 繼續上傳未完成的分片
uploadChunk(chunk, index, previousProgress.totalChunks);
}
}
```
## **三、前端上傳組件與庫推薦**
**3.1 React Dropzone Uploader**
React Dropzone Uploader是一個基于React的組件庫,內置了分片上傳和斷點續傳功能,可輕松集成至您的React項目中。
**3.2 Resumable.js**
Resumable.js 是一個輕量級、跨瀏覽器的大文件上傳庫,它支持分片上傳、斷點續傳及自定義事件通知等功能。
## **四、實時進度顯示與用戶體驗優化**
**4.1 實現上傳進度條**
在每個分片上傳完成后更新進度條,讓用戶體驗更加直觀。
```javascript
xhr.upload.onprogress=function(event) {
if (event.lengthComputable) {
const percentComplete=event.loaded / event.total;
updateProgressBar(percentComplete);
}
};
function updateProgressBar(percentage) {
// 更新頁面上的進度條UI
}
```
**4.2 錯誤處理與提示**
對于上傳過程中可能出現的各類錯誤,如網絡中斷、服務器異常等,都需要提供清晰且友好的錯誤提示,并賦予用戶重新上傳或恢復上傳的能力。
總結:
前端實現大文件上傳不僅涉及到技術層面的挑戰,還要求關注用戶體驗的設計。通過合理利用分片上傳、斷點續傳等技術,結合優秀的前端組件或庫,我們可以打造出穩定可靠、易用性高的大文件上傳功能,從而提升產品的綜合競爭力。同時,針對不同的業務場景,還需考慮文件安全性、并發控制、隊列管理等問題,確保整個上傳流程的健壯性。
日常工作中,文件上傳是一個很常見的功能。在項目開發過程中,我們通常都會使用一些成熟的上傳組件來實現對應的功能。一般來說,成熟的上傳組件不僅會提供漂亮 UI 或好的交互體驗,而且還會提供多種不同的上傳方式,以滿足不同的場景需求。
一般在我們工作中,主要會涉及到 8 種文件上傳的場景,每一種場景背后都使用不同的技術,其中也有很多細節需要我們額外注意。今天阿寶哥就來帶大家總結一下這 8 種場景,讓大家能更好地理解成熟上傳組件所提供的功能。閱讀本文后,你將會了解以下的內容:
一、單文件上傳
對于單文件上傳的場景來說,最常見的是圖片上傳的場景,所以我們就以圖片上傳為例,先來介紹單文件上傳的基本流程。
1.1 前端代碼
html
在以下代碼中,我們通過 input 元素的 accept 屬性限制了上傳文件的類型。這里使用 image/* 限制只能選擇圖片文件,當然你也可以設置特定的類型,比如 image/png 或 image/png,image/jpeg。
<input id="uploadFile" type="file" accept="image/*" />
<button id="submit" onclick="uploadFile()">上傳文件</button>
需要注意的是,雖然我們把 input 元素的 accept 屬性設置為 image/png。但如果用戶把 jpg/jpeg 格式的圖片后綴名改為 .png,就可以成功繞過這個限制。要解決這個問題,我們可以通過讀取文件中的二進制數據來識別正確的文件類型。
要查看圖片對應的二進制數據,我們可以借助一些現成的編輯器,比如 Windows 平臺下的 WinHex 或 macOS 平臺下的 Synalyze It! Pro 十六進制編輯器。這里我們使用 Synalyze It! Pro 這個編輯器,來查看阿寶哥頭像對應的二進制數據。
那么在前端能否不借助工具,讀取文件的二進制數據呢?答案是可以的,這里阿寶哥就不展開介紹了。感興趣的話,你可以閱讀 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",
timeout: 60000,
});
async function uploadFile() {
if (!uploadFileEle.files.length) return;
const file=uploadFileEle.files[0]; // 獲取單個文件
// 省略文件的校驗過程,比如文件類型、大小校驗
upload({
url: "/single",
file,
});
}
function upload({ url, file, fieldName="file" }) {
let formData=new FormData();
formData.set(fieldName, file);
request.post(url, formData, {
// 監聽上傳進度
onUploadProgress: function (progressEvent) {
const percentCompleted=Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
console.log(percentCompleted);
},
});
}
在以上代碼中,我們先把讀取的 File 對象封裝成 FormData 對象,然后利用 Axios 實例的 post 方法實現文件上傳的功能。 在上傳前,通過設置請求配置對象的 onUploadProgress 屬性,就可以獲取文件的上傳進度。
1.2 服務端代碼
Koa 是一個簡單易用的 Web 框架,它的特點是優雅、簡潔、輕量、自由度高。所以我們選擇它來搭建文件服務,并使用以下中間件來實現相應的功能:
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({
destination: async function (req, file, cb) {
// 設置文件的存儲目錄
cb(null, UPLOAD_DIR);
},
filename: function (req, file, cb) {
// 設置文件名
cb(null, `${file.originalname}`);
},
});
const multerUpload=multer({ storage });
router.get("/", async (ctx)=> {
ctx.body="歡迎使用文件服務(by 阿寶哥)";
});
router.post(
"/upload/single",
async (ctx, next)=> {
try {
await next();
ctx.body={
code: 1,
msg: "文件上傳成功",
url: `${RESOURCE_URL}/${ctx.file.originalname}`,
};
} catch (error) {
ctx.body={
code: 0,
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 內核很簡潔,擴展功能都是通過中間件來實現。比如示例中使用到的路由、CORS、靜態資源處理等功能都是通過中間件實現。因此要想掌握 Koa 這個框架,核心是掌握它的中間件機制。如果你想深入了解的話,可以閱讀 如何更好地理解中間件和洋蔥模型 這篇文章。其實除了單文件上傳外,在文件上傳的場景中,我們也可以同時上傳多個文件。
單文件上傳示例:single-file-upload
https://github.com/semlinker/file-upload-demos/tree/master/single-file-upload
二、多文件上傳
要上傳多個文件,首先我們需要允許用戶同時選擇多個文件。要實現這個功能,我們可以利用 input 元素的 multiple 屬性。跟前面介紹的 accept 屬性一樣,該屬性也存在兼容性問題,具體如下圖所示:
(圖片來源 —— https://caniuse.com/mdn-api_htmlinputelement_multiple)
2.1 前端代碼
html
相比單文件上傳的代碼,多文件上傳場景下的 input 元素多了一個 multiple 屬性:
<input id="uploadFile" type="file" accept="image/*" multiple />
<button id="submit" onclick="uploadFile()">上傳文件</button>
js
在單文件上傳的代碼中,我們通過 uploadFileEle.files[0] 獲取單個文件,而對于多文件上傳來說,我們需要獲取已選擇的文件列表,即通過 uploadFileEle.files 來獲取,它返回的是一個 FileList 對象。
async function uploadFile() {
if (!uploadFileEle.files.length) return;
const files=Array.from(uploadFileEle.files);
upload({
url: "/multiple",
files,
});
}
因為要支持上傳多個文件,所以我們需要同步更新一下 upload 函數。對應的處理邏輯就是遍歷文件列表,然后使用 FormData 對象的 append 方法來添加多個文件,具體代碼如下所示:
function upload({ url, files, fieldName="file" }) {
let formData=new FormData();
files.forEach((file)=> {
formData.append(fieldName, file);
});
request.post(url, formData, {
// 監聽上傳進度
onUploadProgress: function (progressEvent) {
const percentCompleted=Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
console.log(percentCompleted);
},
});
}
2.2 服務端代碼
在以下代碼中,我們定義了一個新的路由 —— /upload/multiple 來處理多文件上傳的功能。當所有文件都成功上傳后,就會返回一個已上傳文件的 url 地址列表:
router.post(
"/upload/multiple",
async (ctx, next)=> {
try {
await next();
urls=ctx.files.file.map(file=> `${RESOURCE_URL}/${file.originalname}`);
ctx.body={
code: 1,
msg: "文件上傳成功",
urls
};
} catch (error) {
ctx.body={
code: 0,
msg: "文件上傳失敗",
};
}
},
multerUpload.fields([
{
name: "file", // 與FormData表單項的fieldName想對應
},
])
);
介紹完單文件和多文件上傳的功能,接下來我們來介紹目錄上傳的功能。
多文件上傳示例:multiple-file-upload
https://github.com/semlinker/file-upload-demos/tree/master/multiple-file-upload
三、目錄上傳
可能你還不知道,input 元素上還有一個的 webkitdirectory 屬性。當設置了 webkitdirectory 屬性之后,我們就可以選擇目錄了。
<input id="uploadFile" type="file" accept="image/*" webkitdirectory />
當我們選擇了指定目錄之后,比如阿寶哥桌面上的 images 目錄,就會顯示以下確認框:
點擊上傳按鈕之后,我們就可以獲取文件列表。列表中的文件對象上含有一個 webkitRelativePath 屬性,用于表示當前文件的相對路徑。
雖然通過 webkitdirectory 屬性可以很容易地實現選擇目錄的功能,但在實際項目中我們還需要考慮它的兼容性。比如在 IE 11 以下的版本就不支持該屬性,其它瀏覽器的兼容性如下圖所示:
(圖片來源 —— https://caniuse.com/?search=webkitdirectory)
了解完 webkitdirectory 屬性的兼容性,我們先來介紹前端的實現代碼。
3.1 前端代碼
為了讓服務端能按照實際的目錄結構來存放對應的文件,在添加表單項時我們需要把當前文件的路徑提交到服務端。此外,為了確保@koa/multer 能正確處理文件的路徑,我們需要對路徑進行特殊處理。即把 / 斜杠替換為 @ 符號。對應的處理方式如下所示:
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); // 省略上傳進度處理
}
3.2 服務端代碼
目錄上傳與多文件上傳,服務端代碼的主要區別就是 @koa/multer 中間件的配置對象不一樣。在 destination 屬性對應的函數中,我們需要把文件名中 @ 還原成 /,然后根據文件的實際路徑來生成目錄。
const fse=require("fs-extra");
const storage=multer.diskStorage({
destination: async function (req, file, cb) {
// images@image-1.jpeg=> 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));
// 確保文件目錄存在,若不存在的話,會自動創建
await fse.ensureDir(fileDir);
cb(null, fileDir);
},
filename: function (req, file, cb) {
let parts=file.originalname.split("@");
cb(null, `${parts[parts.length - 1]}`);
},
});
現在我們已經實現了目錄上傳的功能,那么能否把目錄下的文件壓縮成一個壓縮包后再上傳呢?答案是可以的,接下來我們來介紹如何實現壓縮目錄上傳的功能。
目錄上傳示例:directory-upload
https://github.com/semlinker/file-upload-demos/tree/master/directory-upload
四、壓縮目錄上傳
在 JavaScript 如何在線解壓 ZIP 文件? 這篇文章中,介紹了在瀏覽器端如何使用 JSZip 這個庫實現在線解壓 ZIP 文件的功能。 JSZip 這個庫除了可以解析 ZIP 文件之外,它還可以用來 創建和編輯 ZIP 文件。利用 JSZip 這個庫提供的 API,我們就可以把目錄下的所有文件壓縮成 ZIP 文件,然后再把生成的 ZIP 文件上傳到服務器。
4.1 前端代碼
JSZip 實例上的 file(name, data [,options]) 方法,可以把文件添加到 ZIP 文件中。基于該方法我們可以封裝了一個 generateZipFile 函數,用于把目錄下的文件列表壓縮成一個 ZIP 文件。以下是 generateZipFile 函數的具體實現:
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);
});
});
}
在創建完 generateZipFile 函數之后,我們需要更新一下前面已經介紹過的 uploadFile 函數:
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 函數中,我們會對返回的 FileList 對象進行處理,即調用 generateZipFile 函數來生成 ZIP 文件。此外,為了在服務端接收壓縮文件時,能獲取到文件名,我們為 upload 函數增加了一個 fileName 參數,該參數用于調用 formData.append 方法時,設置上傳文件的文件名:
function upload({ url, file, fileName, fieldName="file" }) {
if (!url || !file) return;
let formData=new FormData();
formData.append(
fieldName, file, fileName
);
request.post(url, formData); // 省略上傳進度跟蹤
}
以上就是壓縮目錄上傳,前端部分的 JS 代碼,服務端的代碼可以參考前面單文件上傳的相關代碼。
壓縮目錄上傳示例:directory-compress-upload
https://github.com/semlinker/file-upload-demos/tree/master/directory-compress-upload
五、拖拽上傳
要實現拖拽上傳的功能,我們需要先了解與拖拽相關的事件。比如 drag、dragend、dragenter、dragover 或 drop 事件等。這里我們只介紹接下來要用到的拖拽事件:
基于上面的這些事件,我們就可以提高用戶拖拽的體驗。比如當用戶拖拽的元素進入目標區域時,對目標區域進行高亮顯示。當用戶拖拽的元素離開目標區域時,移除高亮顯示。很明顯當 drop 事件觸發后,拖拽的元素已經放入目標區域了,這時我們就需要獲取對應的數據。
那么如何獲取拖拽對應的數據呢?這時我們需要使用 DataTransfer 對象,該對象用于保存拖動并放下過程中的數據。它可以保存一項或多項數據,這些數據項可以是一種或者多種數據類型。若拖動操作涉及拖動文件,則我們可以通過 DataTransfer 對象的 files 屬性來獲取文件列表。
介紹完拖拽上傳相關的知識后,我們來看一下具體如何實現拖拽上傳的功能。
5.1 前端代碼
html
<div id="dropArea">
<p>拖拽上傳文件</p>
<div id="imagePreview"></div>
</div>
css
#dropArea {
width: 300px;
height: 300px;
border: 1px dashed gray;
margin-bottom: 20px;
}
#dropArea p {
text-align: center;
color: #999;
}
#dropArea.highlighted {
background-color: #ddd;
}
#imagePreview {
max-height: 250px;
overflow-y: scroll;
}
#imagePreview img {
width: 100%;
display: block;
margin: auto;
}
js
為了讓大家能夠更好地閱讀拖拽上傳的相關代碼,我們把代碼拆成 4 部分來講解:
1、阻止默認拖拽行為
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、切換目標區域的高亮狀態
["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、處理圖片預覽
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];
// 省略圖片預覽代碼
files.forEach((file)=> {
upload({
url: "/single",
file,
});
});
}
const request=axios.create({
baseURL: "http://localhost:3000/upload",
timeout: 60000,
});
function upload({ url, file, fieldName="file" }) {
let formData=new FormData();
formData.set(fieldName, file);
request.post(url, formData, {
// 監聽上傳進度
onUploadProgress: function (progressEvent) {
const percentCompleted=Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
console.log(percentCompleted);
},
});
}
拖拽上傳算是一個比較常見的場景,很多成熟的上傳組件都支持該功能。其實除了拖拽上傳外,還可以利用剪貼板實現復制上傳的功能。
拖拽上傳示例:drag-drop-upload
https://github.com/semlinker/file-upload-demos/tree/master/drag-drop-upload
六、剪貼板上傳
在介紹如何實現剪貼板上傳的功能前,我們需要了解一下 Clipboard API。Clipboard 接口實現了 Clipboard API,如果用戶授予了相應的權限,就能提供系統剪貼板的讀寫訪問。在 Web 應用程序中,Clipboard API 可用于實現剪切、復制和粘貼功能。該 API 用于取代通過 document.execCommand API 來實現剪貼板的操作。
在實際項目中,我們不需要手動創建 Clipboard 對象,而是通過 navigator.clipboard 來獲取 Clipboard 對象:
在獲取 Clipboard 對象之后,我們就可以利用該對象提供的 API 來訪問剪貼板,比如:
navigator.clipboard.readText().then(
clipText=> document.querySelector(".editor").innerText=clipText
);
以上代碼將 HTML 中含有 .editor 類的第一個元素的內容替換為剪貼板的內容。如果剪貼板為空,或者不包含任何文本,則元素的內容將被清空。這是因為在剪貼板為空或者不包含文本時,readText 方法會返回一個空字符串。
利用 Clipboard API 我們可以很方便地操作剪貼板,但實際項目使用過程中也得考慮它的兼容性:
(圖片來源 —— https://caniuse.com/async-clipboard)
要實現剪貼板上傳的功能,可以分為以下 3 個步驟:
了解完上述步驟,接下來我們來分析一下具體實現的代碼。
6.1 前端代碼
html
<div id="uploadArea">
<p>請先復制圖片后再執行粘貼操作</p>
</div>
css
#uploadArea {
width: 400px;
height: 400px;
border: 1px dashed gray;
display: table-cell;
vertical-align: middle;
}
#uploadArea p {
text-align: center;
color: #999;
}
#uploadArea img {
max-width: 100%;
max-height: 100%;
display: block;
margin: auto;
}
js
在以下代碼中,我們使用 addEventListener 方法為 uploadArea 容器添加 paste 事件。在對應的事件處理函數中,我們會優先判斷當前瀏覽器是否支持異步 Clipboard API。如果支持的話,就會通過 navigator.clipboard.read 方法來讀取剪貼板中的內容。在讀取內容之后,我們會通過正則判斷剪貼板項中是否包含圖片資源,如果有的話會調用 previewImage 方法執行圖片預覽操作并把返回的 blob 對象保存起來,用于后續的上傳操作。
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("剪貼板檢測到圖片文件,是否執行上傳操作?")
&& upload({
url: "/multiple",
files,
});
}
});
若當前瀏覽器不支持異步 Clipboard API,則我們會嘗試通過 e.clipboardData.items 來訪問剪貼板中的內容。需要注意的是,在遍歷剪貼板內容項的時候,我們是通過 getAsFile 方法來獲取剪貼板的內容。當然該方法也存在兼容性問題,具體如下圖所示:
(圖片來源 —— https://caniuse.com/mdn-api_datatransferitem_getasfile)
前面已經提到,當從剪貼板解析到圖片資源時,會讓用戶進行預覽,該功能是基于 FileReader API 來實現的,對應的代碼如下所示:
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);
}
當用戶預覽完成后,如果確認上傳我們就會執行文件的上傳操作。因為文件是從剪貼板中讀取的,所以在上傳前我們會根據文件的類型,自動為它生成一個文件名,具體是采用時間戳加文件后綴的形式:
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);
}
前面我們已經介紹了文件上傳的多種不同場景,接下來我們來介紹一個 “特殊” 的場景 —— 大文件上傳。
剪貼板上傳示例:clipboard-upload
https://github.com/semlinker/file-upload-demos/tree/master/clipboard-upload
七、大文件分塊上傳
相信你可能已經了解大文件上傳的解決方案,在上傳大文件時,為了提高上傳的效率,我們一般會使用 Blob.slice 方法對大文件按照指定的大小進行切割,然后通過多線程進行分塊上傳,等所有分塊都成功上傳后,再通知服務端進行分塊合并。具體處理方案如下圖所示:
因為在 JavaScript 中如何實現大文件并發上傳? 這篇文章中,阿寶哥已經詳細介紹了大文件并發上傳的方案,所以這里就不展開介紹了。我們只回顧一下大文件并發上傳的完整流程:
前面我們都是介紹客戶端文件上傳的場景,其實也有服務端文件上傳的場景。比如在服務端動態生成海報后,上傳到另外一臺服務器或云廠商的 OSS(Object Storage Service)。下面我們就以 Node.js 為例來介紹在服務端如何上傳文件。
大文件分塊上傳示例:big-file-upload
https://github.com/semlinker/file-upload-demos/tree/master/big-file-upload
八、服務端上傳
服務器上傳就是把文件從一臺服務器上傳到另外一臺服務器。借助 Github 上 form-data 這個庫提供的功能,我們可以很容易地實現服務器上傳的功能。下面我們來簡單介紹一下單文件和多文件上傳的功能:
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("多圖上傳成功");
});
觀察以上代碼可知,創建完 FormData 對象之后,我們只需要通過 fs.createReadStream API 創建可讀流,然后調用 FormData 對象的 append 方法添加表單項,最后再調用 submit 方法執行提交操作即可。
其實除了 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);
});
服務端文件上傳的內容就介紹到這里,關于 form-data 這個庫的其他用法,感興趣的話,可以閱讀對應的使用文檔。其實除了以上介紹的八種場景外,在日常工作中,你也可能會使用一些同步工具,比如 Syncthing 文件同步工具實現文件傳輸。好的,本文的所有內容都已經介紹完了,最后我們來做一個總結。
服務端上傳示例:server-upload
https://github.com/semlinker/file-upload-demos/tree/master/server-upload
九、總結
本文阿寶哥詳細介紹了文件上傳的八種場景,希望閱讀完本文后,你對八種場景背后使用的技術有一定的了解。由于篇幅有限,阿寶哥就沒有展開介紹與 multipart/form-data 類型相關的內容,感興趣的小伙伴可以自行了解一下。
此外,在實際項目中,你可以考慮直接使用成熟的第三方組件,比如 Github 上的 Star 數 11K+ 的 filepond。該組件采用插件化的架構,以插件的方式,提供了非常多的功能,比如 File encode、File rename、File poster、Image preview 和 Image crop 等。總之,它是一個很不錯的組件,以后有機會的話,大家可以嘗試一下。
目前很多UI框架也帶有上傳組件,其實原理都是差不多的。大家可以看一下,熟悉一下哈。有什么補充的可以隨時補充。
傳大的附件分為兩種情況,
第2種使用分片上傳
優勢:可以突破服務器上傳大小的限制,可以web存儲上傳到哪一塊了,在瀏覽器關閉或者刷新的情況下可以斷點續傳;
劣勢:上傳速度慢,在我本地電腦測試,200M的文件,改變配置按照正常方式上傳大約需要12到15秒,但是使用第2種分片上傳,大約需要40多秒,也就是所需時間是正常上傳的3倍,我測試了for循環同時上傳幾個碎片,電腦直接很卡,點擊別的瀏覽器或者文件夾之類的全部是沒有響應,所以放棄了使用循環同時上傳多個;
如果上傳大的文件實現進度條是很有必要的,否則用戶看不到進度會等得不耐煩了。
小的文件只需要2秒左右,是否有進度條沒有關系。
第一種,在可以改變服務器配置的前提下
圖1 帶進度條文件上傳
1、配置php.ini
如果上傳的文件比較大,以上4點都需要修改,特別是第2點,盡可能配置,否則上傳最后會比較慢。
2、iis上傳大小限制,可以在web.config直接修改,加入以下代碼
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="1073741824" />
</requestFiltering>
</security>
<requestLimits maxAllowedContentLength="1073741824" /> 這里是限制的大小,默認大小忘記了,我這里設置的是可以上傳1G進行的測試。
3、nginx上傳大小限制
我開始想把這些加到location / {}里邊,提示錯誤client_header_timeout不允許放到里邊,所以全部加到外邊了,也就是server {}里邊;
注意下第6點,如果不配置,nginx上傳文件會存到緩存,然后再一點一點傳到upload_tmp_dir的目錄下,導致上傳所需時間是iis的2倍。
4、html代碼
<tr class=''>
<td width="90" align="right" rowspan='2'>超大附件</td>
<td>
<div class='jdfull '><div class='jdperc'></div></div>
<div class='jdtis '>0%</div>
<input type='file' id="down_bfule_tpfile" class='inpMain layui-inpu' name='' size='30'><button type='button' class='layui-btn submitpfile' >上傳</button> </td>
</tr>
<tr class=''>
<td>
<input type='text' class='inpMain' placeholder='不限大小' style='width:calc(100% - 20px);' value='' name='down_bfule' id="down_bfule" ></td>
</tr>
5、css代碼 用來設置進度條
/*進度條*/
.jdfull{width:calc(100%);height:20px;background:#FFF;margin-bottom:10px;border-radius:10px;border:1px solid #DDD;}
.jdperc{height:20px;background:#5FB878;width:0px;border-radius:10px;}
.jdtis{margin-bottom:10px;}
6、js代碼(jquery使用監聽progress實現進度條功能)
$(".submitpfile").click(function() {
var fileext=['rar','zip','pdf'];
var formData=new FormData();
formData.append("file", $("#down_bfule_tpfile").get(0).files[0]);
if (!$("#down_bfule_tpfile").get(0).files[0]) {
alert("請選擇要上傳的文件!");
return false
};
//判斷擴展名 是否支持上傳
var patharr=$("#down_bfule_tpfile").get(0).files[0].name.split(".");
//上傳文件擴展名轉小寫,ZIP zip 2個是同一個文件
var ext=patharr[patharr.length - 1].toLowerCase();
if (fileext.indexOf(ext) < 0) {
alert('只支持' + fileext.join(','));
return false;
}
$.ajax({
url: '處理圖片上傳的url',
data: formData,
type: "post",
dataType: "json",
contentType: false,
processData: false,
//監聽progress 實現上傳進度條
xhr: function() {
var xhr=new XMLHttpRequest();
xhr.upload.addEventListener('progress',
function(e) {
console.log(e);
//百分比取整
var progressRate=parseInt((e.loaded / e.total) * 100);
$('.jdfull .jdperc').css('width', progressRate + "%");
$('.jdtis').html(progressRate + "%")
});
return xhr
},
/*進度條結束*/
success: function(result) {
if (result.error=="0000") {
console.log(result.msg);
//上傳完成以后文件地址賦值給文本框,用于表單提交
$("#down_bfule").val(result.msg);
} else {
//錯誤信息提示
alert(result.msg);
return false
}
//上傳框初始化
var objFile=document.getElementById("down_bfule_tpfile");
objFile.outerHTML=objFile.outerHTML.replace(/(value=\").+\"/i, '$1"')
}
})
})
7、后端php處理程序
foreach($_FILES as $k=>$v){
$v['name']//文件名稱
$v['size']//文件大小
$v["tmp_name"]//臨時文件名包含了物理路徑E:\temp\文件名
move_uploaded_file($v["tmp_name"],'文件存放路徑(物理路徑+文件名)')
}
第二種,不能改變服務器的配置使用分片上傳
圖2 進度條顯示多少片
html,css不變,只是改變js,下面是用到的js代碼
代碼1:點擊事件
$(".submitpfile").click(function() {
var fileext=['rar','zip','pdf'];//支持格式
var ranksize=1024*1024;//分片每次上傳的大小 1M
if (!$("#down_bfule_tpfile").get(0).files[0]) {
alert("請選擇要上傳的文件!");
return false
};
var patharr=$("#down_bfule_tpfile").get(0).files[0].name.split(".");
var ext=patharr[patharr.length - 1].toLowerCase();
if (fileext.indexOf(ext) < 0) {
alert('只支持' + fileext.join(','));
return false
}
var size=$("#down_bfule_tpfile").get(0).files[0].size;
var start=true;//是否從第一個開始上傳
var localname=$("#down_bfule_tpfile").get(0).files[0].name;
/*
使用localStorage 文件名作文變量存儲
存儲大小,還有上傳了多少片
文件名存放 判斷大小 如果大小也和上傳的文件相同判斷為同一個文件
如果用戶把文件名改成了上傳過的,不加判斷會造成上傳文件錯誤
window.localStorage.getItem 獲取存儲的數據
window.localStorage.setItem 設置存儲的數據 key value
window.localStorage.removeItem 移除存儲的數據
JSON.parse 字符串轉成json格式
*/
if(JSON.parse(window.localStorage.getItem(localname))){
var localsize=JSON.parse(window.localStorage.getItem(localname)).localsize;
var ranknum=JSON.parse(window.localStorage.getItem(localname)).ranknum;
if(size==localsize){
//已經上傳過 但是沒有上傳完,中斷了
start=false;
}
}
if(start==true){
//第一片開始上傳
uploadtp(1);
}else{
//存儲的下一片開始上傳
uploadtp(ranknum+1);
}
})
代碼2:uploadtp方法
function uploadtp(m){
var size=$("#down_bfule_tpfile").get(0).files[0].size;
var formData=new FormData();
formData.append("filename", $("#down_bfule_tpfile").get(0).files[0].name);
formData.append("size", size);
//Math.ceil 向上取整 獲得上傳總得片數 例如:1.1 1.9 都會取整為2,和php的分頁一個性質
rankcount=Math.ceil(size/ranksize);
/*
uptemp 用于存放 要上傳大小 上傳完多少片
uptemp['ranknum']=m; 上傳完多少片
uptemp['localsize']=size; 上傳文件總得大小,用于判斷要上傳的是否同一個文件
JSON.stringify 數組轉成json字符串
*/
var uptemp={};
uptemp['ranknum']=m;
uptemp['localsize']=size;
window.localStorage.setItem($("#down_bfule_tpfile").get(0).files[0].name,JSON.stringify(uptemp));
/*存儲完成*/
/*
$("#down_bfule_tpfile").get(0).files[0].slice 對選擇的文件進行切割
(m-1)*ranksize 起始位置
m*ranksize 結束位置
*/
formData.append("photo", $("#down_bfule_tpfile").get(0).files[0].slice((m-1)*ranksize,m*ranksize));
$.ajax({
url: "處理上傳的url",
data: formta,
type: "post",
dataType: "json",
contentType: false,
processData: false,
/*
進度條開始
這是一種豎線方式 監聽progress,監聽每一片的上傳進度
*/
xhr: function() {
var xhr=new XMLHttpRequest();
xhr.upload.addEventListener('progress',
function(e) {
console.log(e);
var progressRate=parseInt((e.loaded / e.total) * 100);
$('.jdfull .jdperc').css('width', progressRate + "%");
$('.jdtis').html("總共:<span style='color:red'>"+rankcount+"</span>片,當前:第<span style='color:red'>"+m+"</span>片"+progressRate + "%")
});
return xhr
},
/*進度條結束*/
success: function(result) {
if (result.error=="0000") {
$('.jdtis').html("全部上傳完成!");
//上傳完成,清除以前的分片存儲
window.localStorage.removeItem($("#down_bfule_tpfile").get(0).files[0].name);
var objFile=document.getElementById("down_bfule_tpfile");
objFile.outerHTML=objFile.outerHTML.replace(/(value=\").+\"/i, '$1"');
$("#down_bfule").val(result.msg)
} else if(result.error=="0001") {
//0001文件沒有上傳完 需要繼續上傳
uploadtp(m+1);
}else{
//php處理返回的錯誤提示 比如長時間沒有刷新頁面,導致已經退出登陸了
alert(result.msg);
return false
}
}
/*
第2種 去掉上邊進度條開始 到結束的代碼,使用下面的success 代替上邊的success
根據上傳了多少片除以總數 顯示當前的百分比 var progressRate=parseInt((m / rankcount) * 100);
*/
/*
success: function(result) {
if (result.error=="0000") {
var progressRate=100;
$('.jdfull .jdperc').css('width', progressRate + "%");
$('.jdtis').html("全部上傳完成!");
window.localStorage.removeItem($("#down_bfule_tpfile").get(0).files[0].name);
var objFile=document.getElementById("down_bfule_tpfile");
objFile.outerHTML=objFile.outerHTML.replace(/(value=\").+\"/i, '$1"');
$("#down_bfule").val(result.msg)
} else if(result.error=="0001") {
var progressRate=parseInt((m / rankcount) * 100);
$('.jdfull .jdperc').css('width', progressRate + "%");
$('.jdtis').html(progressRate + "%");
//0001文件沒有上傳完 需要繼續上傳
uploadtp(m+1);
}else{
//php處理返回的錯誤提示 比如長時間沒有刷新頁面,導致已經退出登陸了
alert(result.msg);
return false
}
}
*/
})
}
php后端處理程序
$filename=input('post.filename');//原始文件名
$size=input('post.size');//原始文件大小
//創建存放的文件夾
foreach($_FILES as $k=>$v){
//$v["tmp_name"]臨時文件 含路徑
$images_dir='/public/upload/file/'.date("Ymd")."/";
if (!file_exists(ROOT_PATH."".$images_dir)) {
mkdir(ROOT_PATH."".$images_dir,0777);
}
$name=explode(".", $filename); //將上傳前的文件以“.”分開取得文件類型 支持png jpg gif
$imgtype=strtolower($name[count($name)-1]);//類型轉成小寫
$newname=date("YmdHis".rand(10000,99999)).".".$imgtype;//新的文件名
//file_put_contents 追加 注意有FILE_APPEND,不加的話會覆蓋原來的
//file_get_contents讀取內容
file_put_contents(ROOT_PATH.$images_dir.$filename,file_get_contents($v["tmp_name"]),FILE_APPEND);
//判斷大小 是否已經上傳完成
if(filesize(ROOT_PATH.$images_dir.$filename)>=$size){
rename(ROOT_PATH.$images_dir.$filename,ROOT_PATH.$images_dir.$newname);
exit( json_encode(array('error'=>'0000','msg'=>$images_dir.$newname),JSON_UNESCAPED_UNICODE));
}else{
exit( json_encode(array('error'=>'0001'),JSON_UNESCAPED_UNICODE));
}
}
說明:
nginx上傳文件很慢
處理辦法:關閉 fastcgi_request_buffering,如果不關閉,上傳所需時間至少是iis的2倍;
具體操作:編輯nginx.conf,
server {
fastcgi_request_buffering off;
}
*請認真填寫需求信息,我們會在24小時內與您取得聯系。