果你需要使用 JS 來(lái)處理網(wǎng)址,那么可以使用:
獲取網(wǎng)址中的參數(shù):
let url="https://jiaoxue.xyz/index.php?i=zhangbo";
let search=(new URL(url)).searchParams;
let i=search.get("i"); //獲取網(wǎng)址中的參數(shù) zhangbo
簡(jiǎn)單方便,不用自己編寫(xiě)復(fù)雜的函數(shù)。
更多知識(shí):https://developer.mozilla.org/zh-CN/docs/Web/API/URL
日常工作中,文件上傳是一個(gè)很常見(jiàn)的功能。在項(xiàng)目開(kāi)發(fā)過(guò)程中,我們通常都會(huì)使用一些成熟的上傳組件來(lái)實(shí)現(xiàn)對(duì)應(yīng)的功能。一般來(lái)說(shuō),成熟的上傳組件不僅會(huì)提供漂亮 UI 或好的交互體驗(yàn),而且還會(huì)提供多種不同的上傳方式,以滿足不同的場(chǎng)景需求。
一般在我們工作中,主要會(huì)涉及到 8 種文件上傳的場(chǎng)景,每一種場(chǎng)景背后都使用不同的技術(shù),其中也有很多細(xì)節(jié)需要我們額外注意。今天阿寶哥就來(lái)帶大家總結(jié)一下這 8 種場(chǎng)景,讓大家能更好地理解成熟上傳組件所提供的功能。閱讀本文后,你將會(huì)了解以下的內(nèi)容:
一、單文件上傳
對(duì)于單文件上傳的場(chǎng)景來(lái)說(shuō),最常見(jiàn)的是圖片上傳的場(chǎng)景,所以我們就以圖片上傳為例,先來(lái)介紹單文件上傳的基本流程。
1.1 前端代碼
html
在以下代碼中,我們通過(guò) input 元素的 accept 屬性限制了上傳文件的類型。這里使用 image/* 限制只能選擇圖片文件,當(dāng)然你也可以設(shè)置特定的類型,比如 image/png 或 image/png,image/jpeg。
<input id="uploadFile" type="file" accept="image/*" />
<button id="submit" onclick="uploadFile()">上傳文件</button>
需要注意的是,雖然我們把 input 元素的 accept 屬性設(shè)置為 image/png。但如果用戶把 jpg/jpeg 格式的圖片后綴名改為 .png,就可以成功繞過(guò)這個(gè)限制。要解決這個(gè)問(wèn)題,我們可以通過(guò)讀取文件中的二進(jìn)制數(shù)據(jù)來(lái)識(shí)別正確的文件類型。
要查看圖片對(duì)應(yīng)的二進(jìn)制數(shù)據(jù),我們可以借助一些現(xiàn)成的編輯器,比如 Windows 平臺(tái)下的 WinHex 或 macOS 平臺(tái)下的 Synalyze It! Pro 十六進(jìn)制編輯器。這里我們使用 Synalyze It! Pro 這個(gè)編輯器,來(lái)查看阿寶哥頭像對(duì)應(yīng)的二進(jìn)制數(shù)據(jù)。
那么在前端能否不借助工具,讀取文件的二進(jìn)制數(shù)據(jù)呢?答案是可以的,這里阿寶哥就不展開(kāi)介紹了。感興趣的話,你可以閱讀 JavaScript 如何檢測(cè)文件的類型? 這篇文章。另外,需要注意的是 input 元素 accept 屬性有存在兼容性問(wèn)題。比如 IE 9 以下不支持,具體如下圖所示:
(圖片來(lái)源 —— 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]; // 獲取單個(gè)文件
// 省略文件的校驗(yàn)過(guò)程,比如文件類型、大小校驗(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)聽(tīng)上傳進(jìn)度
onUploadProgress: function (progressEvent) {
const percentCompleted=Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
console.log(percentCompleted);
},
});
}
在以上代碼中,我們先把讀取的 File 對(duì)象封裝成 FormData 對(duì)象,然后利用 Axios 實(shí)例的 post 方法實(shí)現(xiàn)文件上傳的功能。 在上傳前,通過(guò)設(shè)置請(qǐng)求配置對(duì)象的 onUploadProgress 屬性,就可以獲取文件的上傳進(jìn)度。
1.2 服務(wù)端代碼
Koa 是一個(gè)簡(jiǎn)單易用的 Web 框架,它的特點(diǎn)是優(yōu)雅、簡(jiǎn)潔、輕量、自由度高。所以我們選擇它來(lái)搭建文件服務(wù),并使用以下中間件來(lái)實(shí)現(xiàn)相應(yīng)的功能:
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}`;
// 存儲(chǔ)上傳文件的目錄
const UPLOAD_DIR=path.join(__dirname, "/public/upload");
const storage=multer.diskStorage({
destination: async function (req, file, cb) {
// 設(shè)置文件的存儲(chǔ)目錄
cb(null, UPLOAD_DIR);
},
filename: function (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={
code: 1,
msg: "文件上傳成功",
url: `${RESOURCE_URL}/${ctx.file.originalname}`,
};
} catch (error) {
ctx.body={
code: 0,
msg: "文件上傳失敗"
};
}
},
multerUpload.single("file")
);
// 注冊(cè)中間件
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}`);
});
以上代碼相對(duì)比較簡(jiǎn)單,我們就不展開(kāi)介紹了。Koa 內(nèi)核很簡(jiǎn)潔,擴(kuò)展功能都是通過(guò)中間件來(lái)實(shí)現(xiàn)。比如示例中使用到的路由、CORS、靜態(tài)資源處理等功能都是通過(guò)中間件實(shí)現(xiàn)。因此要想掌握 Koa 這個(gè)框架,核心是掌握它的中間件機(jī)制。如果你想深入了解的話,可以閱讀 如何更好地理解中間件和洋蔥模型 這篇文章。其實(shí)除了單文件上傳外,在文件上傳的場(chǎng)景中,我們也可以同時(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 屬性一樣,該屬性也存在兼容性問(wèn)題,具體如下圖所示:
(圖片來(lái)源 —— https://caniuse.com/mdn-api_htmlinputelement_multiple)
2.1 前端代碼
html
相比單文件上傳的代碼,多文件上傳場(chǎng)景下的 input 元素多了一個(gè) multiple 屬性:
<input id="uploadFile" type="file" accept="image/*" multiple />
<button id="submit" onclick="uploadFile()">上傳文件</button>
js
在單文件上傳的代碼中,我們通過(guò) uploadFileEle.files[0] 獲取單個(gè)文件,而對(duì)于多文件上傳來(lái)說(shuō),我們需要獲取已選擇的文件列表,即通過(guò) uploadFileEle.files 來(lái)獲取,它返回的是一個(gè) FileList 對(duì)象。
async function uploadFile() {
if (!uploadFileEle.files.length) return;
const files=Array.from(uploadFileEle.files);
upload({
url: "/multiple",
files,
});
}
因?yàn)橐С稚蟼鞫鄠€(gè)文件,所以我們需要同步更新一下 upload 函數(shù)。對(duì)應(yīng)的處理邏輯就是遍歷文件列表,然后使用 FormData 對(duì)象的 append 方法來(lái)添加多個(gè)文件,具體代碼如下所示:
function upload({ url, files, fieldName="file" }) {
let formData=new FormData();
files.forEach((file)=> {
formData.append(fieldName, file);
});
request.post(url, formData, {
// 監(jiān)聽(tīng)上傳進(jìn)度
onUploadProgress: function (progressEvent) {
const percentCompleted=Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
console.log(percentCompleted);
},
});
}
2.2 服務(wù)端代碼
在以下代碼中,我們定義了一個(gè)新的路由 —— /upload/multiple 來(lái)處理多文件上傳的功能。當(dāng)所有文件都成功上傳后,就會(huì)返回一個(gè)已上傳文件的 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表單項(xiàng)的fieldName想對(duì)應(yīng)
},
])
);
介紹完單文件和多文件上傳的功能,接下來(lái)我們來(lái)介紹目錄上傳的功能。
多文件上傳示例: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 目錄,就會(huì)顯示以下確認(rèn)框:
點(diǎn)擊上傳按鈕之后,我們就可以獲取文件列表。列表中的文件對(duì)象上含有一個(gè) webkitRelativePath 屬性,用于表示當(dāng)前文件的相對(duì)路徑。
雖然通過(guò) webkitdirectory 屬性可以很容易地實(shí)現(xiàn)選擇目錄的功能,但在實(shí)際項(xiàng)目中我們還需要考慮它的兼容性。比如在 IE 11 以下的版本就不支持該屬性,其它瀏覽器的兼容性如下圖所示:
(圖片來(lái)源 —— https://caniuse.com/?search=webkitdirectory)
了解完 webkitdirectory 屬性的兼容性,我們先來(lái)介紹前端的實(shí)現(xiàn)代碼。
3.1 前端代碼
為了讓服務(wù)端能按照實(shí)際的目錄結(jié)構(gòu)來(lái)存放對(duì)應(yīng)的文件,在添加表單項(xiàng)時(shí)我們需要把當(dāng)前文件的路徑提交到服務(wù)端。此外,為了確保@koa/multer 能正確處理文件的路徑,我們需要對(duì)路徑進(jìn)行特殊處理。即把 / 斜杠替換為 @ 符號(hào)。對(duì)應(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 中間件的配置對(duì)象不一樣。在 destination 屬性對(duì)應(yīng)的函數(shù)中,我們需要把文件名中 @ 還原成 /,然后根據(jù)文件的實(shí)際路徑來(lái)生成目錄。
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));
// 確保文件目錄存在,若不存在的話,會(huì)自動(dòng)創(chuàng)建
await fse.ensureDir(fileDir);
cb(null, fileDir);
},
filename: function (req, file, cb) {
let parts=file.originalname.split("@");
cb(null, `${parts[parts.length - 1]}`);
},
});
現(xiàn)在我們已經(jīng)實(shí)現(xiàn)了目錄上傳的功能,那么能否把目錄下的文件壓縮成一個(gè)壓縮包后再上傳呢?答案是可以的,接下來(lái)我們來(lái)介紹如何實(shí)現(xiàn)壓縮目錄上傳的功能。
目錄上傳示例:directory-upload
https://github.com/semlinker/file-upload-demos/tree/master/directory-upload
四、壓縮目錄上傳
在 JavaScript 如何在線解壓 ZIP 文件? 這篇文章中,介紹了在瀏覽器端如何使用 JSZip 這個(gè)庫(kù)實(shí)現(xiàn)在線解壓 ZIP 文件的功能。 JSZip 這個(gè)庫(kù)除了可以解析 ZIP 文件之外,它還可以用來(lái) 創(chuàng)建和編輯 ZIP 文件。利用 JSZip 這個(gè)庫(kù)提供的 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)介紹過(guò)的 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ù)中,我們會(huì)對(duì)返回的 FileList 對(duì)象進(jìn)行處理,即調(diào)用 generateZipFile 函數(shù)來(lái)生成 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)的事件。比如 drag、dragend、dragenter、dragover 或 drop 事件等。這里我們只介紹接下來(lái)要用到的拖拽事件:
基于上面的這些事件,我們就可以提高用戶拖拽的體驗(yàn)。比如當(dāng)用戶拖拽的元素進(jìn)入目標(biāo)區(qū)域時(shí),對(duì)目標(biāo)區(qū)域進(jìn)行高亮顯示。當(dāng)用戶拖拽的元素離開(kāi)目標(biāo)區(qū)域時(shí),移除高亮顯示。很明顯當(dāng) drop 事件觸發(fā)后,拖拽的元素已經(jīng)放入目標(biāo)區(qū)域了,這時(shí)我們就需要獲取對(duì)應(yīng)的數(shù)據(jù)。
那么如何獲取拖拽對(duì)應(yīng)的數(shù)據(jù)呢?這時(shí)我們需要使用 DataTransfer 對(duì)象,該對(duì)象用于保存拖動(dòng)并放下過(guò)程中的數(shù)據(jù)。它可以保存一項(xiàng)或多項(xiàng)數(shù)據(jù),這些數(shù)據(jù)項(xiàng)可以是一種或者多種數(shù)據(jù)類型。若拖動(dòng)操作涉及拖動(dòng)文件,則我們可以通過(guò) DataTransfer 對(duì)象的 files 屬性來(lái)獲取文件列表。
介紹完拖拽上傳相關(guān)的知識(shí)后,我們來(lái)看一下具體如何實(shí)現(xiàn)拖拽上傳的功能。
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
為了讓大家能夠更好地閱讀拖拽上傳的相關(guān)代碼,我們把代碼拆成 4 部分來(lái)講解:
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",
timeout: 60000,
});
function upload({ url, file, fieldName="file" }) {
let formData=new FormData();
formData.set(fieldName, file);
request.post(url, formData, {
// 監(jiān)聽(tīng)上傳進(jìn)度
onUploadProgress: function (progressEvent) {
const percentCompleted=Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
console.log(percentCompleted);
},
});
}
拖拽上傳算是一個(gè)比較常見(jiàn)的場(chǎng)景,很多成熟的上傳組件都支持該功能。其實(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)剪貼板的讀寫(xiě)訪問(wèn)。在 Web 應(yīng)用程序中,Clipboard API 可用于實(shí)現(xiàn)剪切、復(fù)制和粘貼功能。該 API 用于取代通過(guò) document.execCommand API 來(lái)實(shí)現(xiàn)剪貼板的操作。
在實(shí)際項(xiàng)目中,我們不需要手動(dòng)創(chuàng)建 Clipboard 對(duì)象,而是通過(guò) navigator.clipboard 來(lái)獲取 Clipboard 對(duì)象:
在獲取 Clipboard 對(duì)象之后,我們就可以利用該對(duì)象提供的 API 來(lái)訪問(wèn)剪貼板,比如:
navigator.clipboard.readText().then(
clipText=> document.querySelector(".editor").innerText=clipText
);
以上代碼將 HTML 中含有 .editor 類的第一個(gè)元素的內(nèi)容替換為剪貼板的內(nèi)容。如果剪貼板為空,或者不包含任何文本,則元素的內(nèi)容將被清空。這是因?yàn)樵诩糍N板為空或者不包含文本時(shí),readText 方法會(huì)返回一個(gè)空字符串。
利用 Clipboard API 我們可以很方便地操作剪貼板,但實(shí)際項(xiàng)目使用過(guò)程中也得考慮它的兼容性:
(圖片來(lái)源 —— https://caniuse.com/async-clipboard)
要實(shí)現(xiàn)剪貼板上傳的功能,可以分為以下 3 個(gè)步驟:
了解完上述步驟,接下來(lái)我們來(lái)分析一下具體實(shí)現(xiàn)的代碼。
6.1 前端代碼
html
<div id="uploadArea">
<p>請(qǐng)先復(fù)制圖片后再執(zhí)行粘貼操作</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 事件。在對(duì)應(yīng)的事件處理函數(shù)中,我們會(huì)優(yōu)先判斷當(dāng)前瀏覽器是否支持異步 Clipboard API。如果支持的話,就會(huì)通過(guò) navigator.clipboard.read 方法來(lái)讀取剪貼板中的內(nèi)容。在讀取內(nèi)容之后,我們會(huì)通過(guò)正則判斷剪貼板項(xiàng)中是否包含圖片資源,如果有的話會(huì)調(diào)用 previewImage 方法執(zhí)行圖片預(yù)覽操作并把返回的 blob 對(duì)象保存起來(lái),用于后續(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("剪貼板檢測(cè)到圖片文件,是否執(zhí)行上傳操作?")
&& upload({
url: "/multiple",
files,
});
}
});
若當(dāng)前瀏覽器不支持異步 Clipboard API,則我們會(huì)嘗試通過(guò) e.clipboardData.items 來(lái)訪問(wèn)剪貼板中的內(nèi)容。需要注意的是,在遍歷剪貼板內(nèi)容項(xiàng)的時(shí)候,我們是通過(guò) getAsFile 方法來(lái)獲取剪貼板的內(nèi)容。當(dāng)然該方法也存在兼容性問(wèn)題,具體如下圖所示:
(圖片來(lái)源 —— https://caniuse.com/mdn-api_datatransferitem_getasfile)
前面已經(jīng)提到,當(dāng)從剪貼板解析到圖片資源時(shí),會(huì)讓用戶進(jìn)行預(yù)覽,該功能是基于 FileReader API 來(lái)實(shí)現(xiàn)的,對(duì)應(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)上傳我們就會(huì)執(zhí)行文件的上傳操作。因?yàn)槲募菑募糍N板中讀取的,所以在上傳前我們會(huì)根據(jù)文件的類型,自動(dòng)為它生成一個(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)介紹了文件上傳的多種不同場(chǎng)景,接下來(lái)我們來(lái)介紹一個(gè) “特殊” 的場(chǎng)景 —— 大文件上傳。
剪貼板上傳示例:clipboard-upload
https://github.com/semlinker/file-upload-demos/tree/master/clipboard-upload
七、大文件分塊上傳
相信你可能已經(jīng)了解大文件上傳的解決方案,在上傳大文件時(shí),為了提高上傳的效率,我們一般會(huì)使用 Blob.slice 方法對(duì)大文件按照指定的大小進(jìn)行切割,然后通過(guò)多線程進(jìn)行分塊上傳,等所有分塊都成功上傳后,再通知服務(wù)端進(jìn)行分塊合并。具體處理方案如下圖所示:
因?yàn)樵?JavaScript 中如何實(shí)現(xiàn)大文件并發(fā)上傳? 這篇文章中,阿寶哥已經(jīng)詳細(xì)介紹了大文件并發(fā)上傳的方案,所以這里就不展開(kāi)介紹了。我們只回顧一下大文件并發(fā)上傳的完整流程:
前面我們都是介紹客戶端文件上傳的場(chǎng)景,其實(shí)也有服務(wù)端文件上傳的場(chǎng)景。比如在服務(wù)端動(dòng)態(tài)生成海報(bào)后,上傳到另外一臺(tái)服務(wù)器或云廠商的 OSS(Object Storage Service)。下面我們就以 Node.js 為例來(lái)介紹在服務(wù)端如何上傳文件。
大文件分塊上傳示例:big-file-upload
https://github.com/semlinker/file-upload-demos/tree/master/big-file-upload
八、服務(wù)端上傳
服務(wù)器上傳就是把文件從一臺(tái)服務(wù)器上傳到另外一臺(tái)服務(wù)器。借助 Github 上 form-data 這個(gè)庫(kù)提供的功能,我們可以很容易地實(shí)現(xiàn)服務(wù)器上傳的功能。下面我們來(lái)簡(jiǎn)單介紹一下單文件和多文件上傳的功能:
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 對(duì)象之后,我們只需要通過(guò) fs.createReadStream API 創(chuàng)建可讀流,然后調(diào)用 FormData 對(duì)象的 append 方法添加表單項(xiàng),最后再調(diào)用 submit 方法執(zhí)行提交操作即可。
其實(shí)除了 ReadableStream 之外,FormData 對(duì)象的 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è)庫(kù)的其他用法,感興趣的話,可以閱讀對(duì)應(yīng)的使用文檔。其實(shí)除了以上介紹的八種場(chǎng)景外,在日常工作中,你也可能會(huì)使用一些同步工具,比如 Syncthing 文件同步工具實(shí)現(xiàn)文件傳輸。好的,本文的所有內(nèi)容都已經(jīng)介紹完了,最后我們來(lái)做一個(gè)總結(jié)。
服務(wù)端上傳示例:server-upload
https://github.com/semlinker/file-upload-demos/tree/master/server-upload
九、總結(jié)
本文阿寶哥詳細(xì)介紹了文件上傳的八種場(chǎng)景,希望閱讀完本文后,你對(duì)八種場(chǎng)景背后使用的技術(shù)有一定的了解。由于篇幅有限,阿寶哥就沒(méi)有展開(kāi)介紹與 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ī)會(huì)的話,大家可以嘗試一下。
目前很多UI框架也帶有上傳組件,其實(shí)原理都是差不多的。大家可以看一下,熟悉一下哈。有什么補(bǔ)充的可以隨時(shí)補(bǔ)充。
一篇文章講到一個(gè)簡(jiǎn)單的例子,那么如果我們要講某些變量的值傳遞給瀏覽器顯示該如何做呢?我們?nèi)フ业絭iews.py使用render函數(shù),如下:
render函數(shù)中第一個(gè)參數(shù)是request,為函數(shù)默認(rèn)參數(shù)第二個(gè)為templates下的html文件第三個(gè)參數(shù)為需要向?yàn)g覽器中傳遞的參數(shù)下來(lái)我們?cè)倏纯礊g覽器中如何用html接收過(guò)來(lái)的參數(shù),如下:
在html中通過(guò){{參數(shù)名獲取}}
*請(qǐng)認(rèn)真填寫(xiě)需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。