中國招標投標網(wǎng)查詢平臺站長之家 seo查詢
一、簡介
? 通常情況下,前端在使用post請求提交數(shù)據(jù)的時候,請求都是采用application/json 或 application/x-www-form-urlencoded編碼類型,分別是借助JSON字符串來傳遞參數(shù)或者key=value格式字符串(多參數(shù)通過&進行連接)來傳遞參數(shù),確實足以覆蓋大多數(shù)業(yè)務(wù)場景。但是在文件上傳等特殊業(yè)務(wù)場景下,這兩種編碼類型就有些捉襟見肘了,例如選擇JSON字符串傳遞參數(shù),在使用JSON.stringify()格式化參數(shù)數(shù)據(jù)時,會將File和Blob對象轉(zhuǎn)化成{},文件數(shù)據(jù)會丟失。所以此時我們就需要使用第三種編碼類型multipart/form-data,使用FormData對象來傳遞參數(shù)。
? FormData 提供了一種以 key/value鍵值對集合表示表單數(shù)據(jù)的數(shù)據(jù)構(gòu)造方式,通過該方式我們可以將file、blob等不易傳輸?shù)臄?shù)據(jù)通過 ajax 請求輕松的發(fā)送到服務(wù)器端。
? 當使用FormData 對象作為參數(shù)時,無需手動設(shè)置請求的編碼類型,瀏覽器會自動將請求的編碼類型Content-type設(shè)置為multipart/form-data。
瀏覽器兼容性:
二、相關(guān)方法
1、FormData()
? FormData([form]) 方法是FormData 對象的構(gòu)造函數(shù),用來創(chuàng)建一個新的FormData 對象。
// 創(chuàng)建空的 FormData 對象
const formData = new FormData()
? 該方法擁有一個可選參數(shù)form,值為頁面HTML中的一個表單元素,當設(shè)置該參數(shù)時,創(chuàng)建的FormData對象將自動的將form表單中的值包含進去,包括file文件內(nèi)容也會被編碼之后包含進去。但是要注意給表單中所有的輸入元素(、)設(shè)置name屬性,否則無法被FormData對象包含,輸入元素的name屬性將會成為FormData對象中數(shù)據(jù)鍵值對的key,輸入元素的值將會成為對應(yīng)的value。
<!-- form表單元素 -->
<form action="#" id="form1"><div><label for="name">姓名:</label><input type="text" id="name" name="name"></div><div><label for="age">年齡:</label><input type="text" id="age" name="age"></div><div><label for="sex">性別:</label><!-- 未設(shè)置name屬性不會被 formData 包含 --><input type="text" id="sex"></div></form><br /><button onclick="logFormData()">輸出formData對象</button>
<script>
// 輸出 FormData 對象的數(shù)據(jù)
function logFormData () {// 獲取表單元素const form = document.getElementById('form1')// 創(chuàng)建帶有預(yù)置數(shù)據(jù)的 FormData 對象const formData = new FormData(form)// 輸出formData對象中的所有鍵值對for (var pair of formData.entries()) {console.log(pair[0] + '----' + pair[1]);}
}
</script>
2、FormData.append()
? FormData.append(name,value,[filename]) 方法用于向FormData 對象中添加一個新的值,該方法擁有兩個必選參數(shù)name和value,以及一個可選參數(shù)filename。name對應(yīng)FormData 對象中鍵值對數(shù)據(jù)的key,value對應(yīng)鍵值對數(shù)據(jù)的值。如果name這個key在FormData中已經(jīng)存在,則會將新值value添加到原有值集合的后面,先添加的值在前面,后添加的值在后面,多個值同時以集合的形式存在;如果name這個key在FormData中不存在,則會新增這個key,并賦予對應(yīng)的值value。
// 創(chuàng)建空的 FormData 對象
const formData = new FormData()
// 添加一個鍵值對 此時并不存在對應(yīng)的key 會新增這個key
formData.append('name', '張三')
// 給同一個key 再次添加值
formData.append('name', '李四')
// 輸出這個key對應(yīng)的所有value值
console.log("formData.getAll('name')----", formData.getAll('name'));
執(zhí)行結(jié)果1
可選參數(shù)filename是當?shù)诙€參數(shù)value為Blob或file文件數(shù)據(jù)時,設(shè)置傳給服務(wù)器端的文件名稱。如果不設(shè)置該參數(shù),則Blob類型默認文件名為blob,file類型的默認文件名為文件本身的名稱。
// 創(chuàng)建空的 FormData 對象
const formData = new FormData()
// 添加一個file鍵值對數(shù)據(jù) 取默認文件名稱
formData.append('file', file)
// 添加一個file鍵值對數(shù)據(jù) 并設(shè)置文件名稱
formData.append('file', file, 'test.png')
// 輸出這個key對應(yīng)的所有value值
console.log("formData.getAll('file')----", formData.getAll('file'));
執(zhí)行結(jié)果2
3、FormData.set()
? FormData.set(name,value,[filename]) 方法與FormData.append()方法類似,都是用于向FormData 對象中添加一個新的值,如果name這個key在FormData中不存在,則會新增這個key,并賦予對應(yīng)的值value;但是如果name這個key在FormData中已經(jīng)存在,那么該方法會直接覆蓋掉原來的value,無論原有值集合有幾個數(shù)據(jù),全都被覆蓋。
? 其余用法與FormData.append()方法相同。
// 創(chuàng)建空的 FormData 對象
const formData = new FormData()
// 使用append()添加一個鍵值對 此時并不存在對應(yīng)的key 會新增這個key
formData.append('name', '張三')
// 使用append()給同一個key 再次添加值
formData.append('name', '李四')
// 輸出這個key對應(yīng)的所有value值
console.log("append()兩次數(shù)據(jù)后----", formData.getAll('name'));
// 使用set()給同一個key 設(shè)置值 會覆蓋之前的值
formData.set('name', '王五')
// 輸出這個key對應(yīng)的所有value值
console.log("set()一次數(shù)據(jù)后----", formData.getAll('name'));
4、FormData.delete()
? FormData.delete(name) 方法用于從FormData對象中刪除name這個key及其對應(yīng)的所有value。
// 創(chuàng)建空的 FormData 對象
const formData = new FormData()
// 使用append()添加一個鍵值對 此時并不存在對應(yīng)的key 會新增這個key
formData.append('name', '張三')
// 使用append()給同一個key 再次添加值
formData.append('name', '李四')
// 輸出這個key對應(yīng)的所有value值
console.log("append()兩次數(shù)據(jù)后----", formData.getAll('name'));
// 使用delete()刪除一個key及其所有的value
formData.delete('name')
// 再次輸出這個key
console.log("delete()刪除一次后----", formData.getAll('name'));
5、FormData.entries()
? FormData.entries() 方法用于獲取一個由FormData對象中所有鍵值對組成的iterator(迭代器)對象,然后通過該對象可以遍歷訪問所有的鍵值對數(shù)據(jù)。
? 該方法獲取的iterator(迭代器)對象,需要通過for…of…的形式來進行遍歷,每個遍歷元素都是數(shù)組類型,數(shù)組中有兩個元素,第一個為key,另一個為value。如果FormData對象中的某個key有多個
// 創(chuàng)建空的 FormData 對象
const formData = new FormData()
// 使用append()添加一個鍵值對
formData.append('name', '張三')
// 使用append()給同一個key 再次添加值
formData.append('name', '李四')
// 使用append()添加另外一個鍵值對
formData.append('sex', '男')
// 獲取迭代器對象
const entries = formData.entries()
// 輸出迭代器對象
console.log('entries-----', entries);
// 遍歷迭代器對象
for (var pair of entries) {// 輸出遍歷元素console.log('pair---', pair);// 輸出元素的key和valueconsole.log(pair[0] + '----' + pair[1]);
}
除了該方法外,我們還可以通過for…of…形式直接遍歷FormData對象,其作用與結(jié)果與該方法完全相同:
// 創(chuàng)建空的 FormData 對象
const formData = new FormData()
// 使用append()添加一個鍵值對
formData.append('name', '張三')
// 使用append()給同一個key 再次添加值
formData.append('name', '李四')
// 使用append()添加另外一個鍵值對
formData.append('sex', '男')
// 遍歷formData對象
for (var pair of formData) {console.log('當前遍歷元素---', pair);console.log(pair[0] + '----' + pair[1]);
}
6、FormData.keys()
? FormData.keys() 方法用于獲取一個由FormData對象中所有鍵值對中的key組成的iterator(迭代器)對象,然后通過該對象可以遍歷訪問所有的key,類型為String。與entries()方法相同的是:如果FormData對象中的某個key有多個value,則該key會被遍歷多次,每次對應(yīng)一個value。
// 創(chuàng)建空的 FormData 對象
const formData = new FormData()
// 使用append()添加一個鍵值對
formData.append('name', '張三')
// 使用append()給同一個key 再次添加值
formData.append('name', '李四')
// 使用append()添加另外一個鍵值對
formData.append('sex', '男')
// 獲取key組成的迭代器對象
const keys = formData.keys()
// 輸出迭代器對象
console.log('keys-----', keys);
// 遍歷迭代器對象
for (var key of keys) {console.log('key---', key);
}
7、FormData.values()
? FormData.values() 方法用于獲取一個由FormData對象中所有鍵值對中的value組成的iterator(迭代器)對象,然后通過該對象可以遍歷訪問所有的value,類型為String、File、Blob。如果FormData對象中的某個key有多個value,則每個value都會遍歷一次
// 創(chuàng)建空的 FormData 對象
const formData = new FormData()
// 使用append()添加一個鍵值對
formData.append('name', '張三')
// 使用append()給同一個key 再次添加值
formData.append('name', 333444)
// 使用append()添加另外一個鍵值對
formData.append('sex', '男')
// 獲取value組成的迭代器對象
const values = formData.values()
// 輸出迭代器對象
console.log('values-----', values);
// 遍歷迭代器對象
for (var value of values) {console.log('value---', value);
}
8、FormData.has()
? FormData.has() 該方法用于判斷FormData 對象中是否含有某個key,返回值為一個布爾值。
// 創(chuàng)建空的 FormData 對象
const formData = new FormData()
// 使用append()添加一個鍵值對
formData.append('name', '張三')
// 使用append()添加另外一個鍵值對
formData.append('sex', '男')
// 使用has()判斷是否存在某個key
console.log('has()判斷是否存在name---', formData.has('name'));
// 使用delete()刪除一個key及其所有的value
formData.delete('sex')
// 使用has()判斷一個已經(jīng)被刪除的key
console.log('has()判斷被delete()刪除的sex---', formData.has('sex'));
// 使用has()判斷一個不存在的key
console.log('has()判斷不存在的age---', formData.has('age'));
9、FormData.get()
? FormData.get(name) 方法用于獲取FormData 對象中name這個key所對應(yīng)的value集合里的第一個value,value集合中值的順序,按照添加的順序進行排序。
// 創(chuàng)建空的 FormData 對象
const formData = new FormData()
// 使用append()添加一個鍵值對
formData.append('name', '張三')
// 使用append()給同一個key 再次添加值
formData.append('name', 333)
// 使用append()添加另外一個鍵值對
formData.append('sex', '男')
// 使用get()獲取name對應(yīng)的第一個value
console.log('get()獲取name對應(yīng)的第一個value---', formData.get('name'));
// 使用get()獲取sex對應(yīng)的第一個value
console.log('get()獲取sex對應(yīng)的第一個value---', formData.get('sex'));
一、進階知識
在前一篇博客中,我講解了FormData對象的基礎(chǔ)概念、相關(guān)方法和基本用法,本篇博客我將講解一些FormDate對象相關(guān)的進階知識,主要包含F(xiàn)ormData與其他對象結(jié)合使用的各類場景,以及一些使用技巧。
1、FormData對象、JSON字符串、key=value字符串 三種參數(shù)形式對比
① 使用FormData對象傳遞參數(shù)
? 該參數(shù)形式對應(yīng)請求頭Content-type類型中的 multipart/form-data,以鍵值對的形式存儲參數(shù)數(shù)據(jù),參數(shù)中允許包含F(xiàn)ile、Blob類型的數(shù)據(jù)。
// 發(fā)送FormData對象參數(shù)
function ajaxFormData () {// 創(chuàng)建空的 FormData 對象const formData = new FormData()// 創(chuàng)建Blob對象var aFileParts = ['<a id="a"><b id="b">hey!</b></a>']; // 一個包含 DOMString 的數(shù)組var blob = new Blob(aFileParts, { type: 'text/html' });// 添加一個Blob鍵值對數(shù)據(jù) 并設(shè)置文件名稱formData.append('content', blob)// 添加一個字符串鍵值對數(shù)據(jù)formData.append('name', '張三')// 添加一個字符串鍵值對數(shù)據(jù)formData.append('age', '18')// 創(chuàng)建 XMLHttpRequest 實例對象const xhr = new XMLHttpRequest();// 設(shè)置發(fā)送POST請求的URL地址const url = 'http://example.com/api/user';// 配置請求對象xhr.open('POST', url);// 無需設(shè)置請求頭信息 瀏覽器會自動設(shè)置 Content-type 為 multipart/form-data// 設(shè)置請求完成后的回調(diào)xhr.onreadystatechange = function () {if (xhr.readyState === 4 && xhr.status === 200) {console.log(xhr.responseText);}};// 發(fā)送請求并將參數(shù)加入進去xhr.send(formData);
}
瀏覽器查看請求頭和請求參數(shù):
② 使用JSON字符串傳遞參數(shù)
? 該參數(shù)形式對應(yīng)請求頭Content-type類型中的 application/json,參數(shù)中的File、Blob類型的數(shù)據(jù)會被轉(zhuǎn)換成{},數(shù)據(jù)會丟失。當然我們也可以通過將File、Blob對象轉(zhuǎn)成base64格式的方式來傳遞數(shù)據(jù),但是不夠優(yōu)雅,而且在轉(zhuǎn)換格式的時候,如果文件過大,會占用大量內(nèi)存,影響瀏覽器性能,因此并不推薦采用這種形式來傳遞File、Blob類型數(shù)據(jù)。
function ajaxJSON () {// 創(chuàng)建Blob對象var aFileParts = ['<a id="a"><b id="b">hey!</b></a>']; // 一個包含 DOMString 的數(shù)組var blob = new Blob(aFileParts, { type: 'text/html' });// 創(chuàng)建一個 FileReader 對象var reader = new FileReader();// 當以 DataURL 格式讀取成功后,執(zhí)行回調(diào)函數(shù)reader.onload = (event) => {// 將blob對象轉(zhuǎn)換為bas64字符串var blobBase64 = event.target.result// 創(chuàng)建要發(fā)送的參數(shù)對象var params = {name: '張三',age: 18,content: blob,contentBase64: blobBase64}// 將參數(shù)對象轉(zhuǎn)換為JSON字符串var JSONParams = JSON.stringify(params)// 創(chuàng)建 XMLHttpRequest 實例對象const xhr = new XMLHttpRequest();// 設(shè)置發(fā)送POST請求的URL地址const url = 'http://example.com/api/user';// 配置請求對象xhr.open('POST', url);// 設(shè)置請求頭信息xhr.setRequestHeader('Content-type', 'application/json')// 設(shè)置請求完成后的回調(diào)xhr.onreadystatechange = function () {if (xhr.readyState === 4 && xhr.status === 200) {console.log(xhr.responseText);}};// 發(fā)送請求并將參數(shù)加入進去xhr.send(JSONParams);};// 以 DataURL 的形式讀取 Blob 數(shù)據(jù)reader.readAsDataURL(blob);
}
瀏覽器查看請求頭和請求參數(shù):
③ 使用key=value字符串傳遞參數(shù)
? 該參數(shù)形式對應(yīng)請求頭Content-type類型中的 application/x-www-form-urlencoded,傳遞過程中只能傳遞字符串類型的參數(shù),參數(shù)組成key=value格式字符串,多個參數(shù)之間通過&進行連接。參數(shù)中的File、Blob類型的數(shù)據(jù)會被轉(zhuǎn)換成[object File]、[object Blob]字符串,數(shù)據(jù)會丟失。同理,我們也可以通過將File、Blob對象轉(zhuǎn)成base64格式的方式來傳遞數(shù)據(jù),但缺點也相同,在轉(zhuǎn)換格式的時候,如果文件過大,會占用大量內(nèi)存,影響瀏覽器性能,因此并不推薦采用這種形式來傳遞File、Blob類型數(shù)據(jù)。
function ajaxString () {// 創(chuàng)建Blob對象var aFileParts = ['<a id="a"><b id="b">hey!</b></a>']; // 一個包含 DOMString 的數(shù)組var blob = new Blob(aFileParts, { type: 'text/html' });// 創(chuàng)建一個 FileReader 對象var reader = new FileReader();// 當以 DataURL 格式讀取成功后,執(zhí)行回調(diào)函數(shù)reader.onload = (event) => {// 將blob對象轉(zhuǎn)換為bas64字符串var blobBase64 = event.target.result// 創(chuàng)建要發(fā)送的參數(shù)字符串var params = 'name=張三&age=18&content=' + blob + '&contentBase64=' + blobBase64// 創(chuàng)建 XMLHttpRequest 實例對象const xhr = new XMLHttpRequest();// 設(shè)置發(fā)送POST請求的URL地址const url = 'http://example.com/api/user';// 配置請求對象xhr.open('POST', url);// 設(shè)置請求頭信息xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')// 設(shè)置請求完成后的回調(diào)xhr.onreadystatechange = function () {if (xhr.readyState === 4 && xhr.status === 200) {console.log(xhr.responseText);}};// 發(fā)送請求并將參數(shù)加入進去xhr.send(params);}// 以 DataURL 的形式讀取 Blob 數(shù)據(jù)reader.readAsDataURL(blob);
}
瀏覽器查看請求頭和請求參數(shù):
2、對FormData對象中的數(shù)據(jù)進行過濾
? 我們使用FormData對象向服務(wù)端傳輸數(shù)據(jù)時,通常是為了進行文件上傳,以及一些相關(guān)數(shù)據(jù)的上傳。為了數(shù)據(jù)安全,我們需要對要加入到FormData中的數(shù)據(jù)進行校驗過濾,比如對文件名、文件類型、文件內(nèi)容等等進行過濾,只有過濾后的數(shù)據(jù)才能加入到FormData中,并發(fā)送到服務(wù)端。
① 文件名過濾
? 文件名過濾可以防止用戶上傳的文件名中包含違規(guī)內(nèi)容和字符等,具體實現(xiàn)可以結(jié)合正則表達式和字符串操作兩者來實現(xiàn)。
? 例如:上傳文件的文件名不能包含&、#和%三個特殊字符,且文件名不能包含sb和2b兩個違規(guī)詞。
// 聲明FormData
const formData = new FormData();// 省略...// 獲取文件對象
var file = e.target.files[0]
// 獲取文件名
var fileName = file.name
// 校驗文件名中是否包含sb或2b 兩個違規(guī)詞
const pattern = /^(?!.*([sS]b|2[Bb])).*$/i;
// 進行文件名過濾 不能含有違規(guī)詞 且不能含有特殊字符
if (pattern.test(fileName) && fileName.indexOf('&') === -1 && fileName.indexOf('#') === -1 && fileName.indexOf('%') === -1) {formData.append('file',file)
} else {alert('文件名不符合規(guī)范,請修改后再上傳~');
}
② 文件類型過濾
? 文件類型過濾可以防止用戶上傳不支持的文件類型,雖然前端可以通過標簽的accept屬性來限制用戶選擇的文件類型,但是這并不嚴謹,用戶可以通過操作文件選擇框的選項來解除限制,所以在文件上傳之前對文件類型進行過濾是有必要的。
? 例如:上傳文件的類型限制為圖片類型,且只能為.jpg、.png、.gif三種類型的文件。
// 創(chuàng)建空的 FormData 對象
const formData = new FormData()// 省略...// 獲取文件對象
var file = e.target.files[0]
// 獲取文件名
var fileName = file.name
// 獲取文件類型
var fileType = file.type
// 校驗文件名是否以規(guī)定格式 jpg、png、gif 結(jié)尾
const pattern = /\.jpg$|\.png$|\.gif$/i;
// 進行文件類型過濾
if (pattern.test(fileName) && fileType.indexOf('image') === 0) {formData.append('file', file)
} else {alert('文件類型不符,請修改后再上傳~');
}
③ 文件內(nèi)容過濾
? 文件內(nèi)容過濾可以防止用戶上傳包含惡意代碼和違規(guī)內(nèi)容的文件,可以使用JS來過濾部分文件的內(nèi)容,也可以借助一些完善第三方的庫來檢查文件內(nèi)容,如:js-xss、Filter.js等等。
? 例如:對用戶上傳的.txt文件,進行簡單的敏感詞匯校驗過濾。
// 創(chuàng)建空的 FormData 對象
const formData = new FormData()
// 調(diào)起文件選擇框
document.getElementById('file').click()
// 監(jiān)聽文件選擇框的change事件
document.getElementById('file').onchange = function (e) {// 獲取文件對象var file = e.target.files[0]// 聲明一個 FileReader 對象const reader = new FileReader();// 當以文本形式讀取成功后,執(zhí)行回調(diào)函數(shù)reader.onload = (event) => {const content = event.target.result;console.log('原文件內(nèi)容---', content);// 過濾敏感詞匯const filteredContent = content.replace(/sb|智障|2B/gi, '**');// 顯示過濾后的內(nèi)容console.log('過濾后的文件內(nèi)容---', filteredContent);// 將過濾后的內(nèi)容寫入FormDataformData.append('fileText', filteredContent)};// 以文本形式讀取文件內(nèi)容reader.readAsText(file);}
④ 白名單過濾
? 白名單過濾是指根據(jù)過濾條件批量設(shè)置允許名單,只有符合白名單的數(shù)據(jù)才能通過過濾。
? 例如:設(shè)置文件類型白名單,只允許jpg、png、gif類型的圖片文件加入到FormData中。
// 創(chuàng)建空的 FormData 對象
var formData = new FormData()
// 聲明一個白名單數(shù)組
const whitelist = ['image/jpg', 'image/png', 'image/gif'];
// 調(diào)起文件選擇框
document.getElementById('file').click()
// 監(jiān)聽文件選擇框的change事件
document.getElementById('file').onchange = function (e) {// 獲取文件對象var file = e.target.files[0]// 獲取文件類型var fileType = file.type// 判斷文件類型是否在白名單中if (whitelist.indexOf(fileType) > -1) {formData.append('file', file)} else {alert('文件類型不符,請修改后再上傳~');}
}
⑤ 黑名單過濾
? 黑名單過濾是指根據(jù)過濾條件批量設(shè)置禁止名單,凡是符合黑名單的數(shù)據(jù)都禁止通過。
? 例如:設(shè)置文件類型黑名單,禁止.exe和.bat類型的文件加入到FormData中。
//
創(chuàng)建空的 FormData 對象
var formData = new FormData()
// 聲明一個黑名單數(shù)組
const blackList = ['application/x-msdownload', 'application/x-msdos-program',];
// 調(diào)起文件選擇框
document.getElementById('file').click()
// 監(jiān)聽文件選擇框的change事件
document.getElementById('file').onchange = function (e) {// 獲取文件對象var file = e.target.files[0]// 獲取文件類型var fileType = file.type// 判斷文件類型是否在黑名單中if (blackList.indexOf(fileType) === -1) {formData.append('file', file)} else {alert('文件類型不允許上傳~');}
}
3、FormData對象結(jié)合同步token預(yù)防CSRF攻擊
? CSRF(Cross-site Request Forgery,跨站請求偽造)攻擊是一種常見的網(wǎng)絡(luò)攻擊,攻擊者通過偽造用戶的身份,利用用戶在某些站點上的登錄狀態(tài),來構(gòu)造并發(fā)送篡改數(shù)據(jù)的請求。防范CSRF攻擊方式有很多,在涉及表單提交的頁面中,我們常用的是FormData對象結(jié)合同步token(又稱CSRF token)的防范策略,來防范攻擊者惡意偽造表單數(shù)據(jù)提交,具體操作步驟如下:
? ① 當用戶請求訪問表單頁面時,服務(wù)端生成一個隨機且唯一的token,服務(wù)端存儲一份,并將該token存儲在cookie之中,發(fā)送給前端。
? ② 前端從cookie中獲取token,然后將token添加到要提交的FormData對象中。
? ③ 前端觸發(fā)表單提交接口,發(fā)送FormData對象,服務(wù)端收到請求后,對比FormData對象中的token與服務(wù)端存儲的token是否一致,如果一致,則認為是合法請求,否則,認為是CSRF攻擊,拒絕請求。
? 該防范策略的核心在于攻擊者雖然在調(diào)用提交接口時能攜帶相關(guān)的cookie信息(接口攜帶的cookie取決于接口的域名),但是無法通過js獲取相關(guān)cookie的值(js只能獲取當前頁面域名下的cookie),因而也就拿不到有效的token,無法構(gòu)造有效的表單數(shù)據(jù),請求就會被服務(wù)端所拒絕。
? 該防范策略的優(yōu)點在于安全性高、操作簡單、支持性好,缺點在于需要增加額外的計算量和存儲開銷。
? 除此之外,我們還可以給存儲token的那個cookie設(shè)置SameSite=Strict或lax,進一步防范CSRF攻擊。
4、FormData對象結(jié)合input實現(xiàn)選擇文件夾,批量上傳文件
? 之前我們批量上傳文件時,都是讓用戶一個個的去選擇文件,操作繁多;或者就是讓用戶將文件放到文件夾下,統(tǒng)一打包成壓縮包,作為一個文件上傳,但是文件的壓縮格式有很多,服務(wù)端基本不可能全部支持,因此也有一定的局限性。所以我想到了另一種方案就是:讓用戶直接去選擇文件夾,然后前端獲取文件夾中的所有文件,逐一加入到FormData對象中,最后統(tǒng)一上傳到服務(wù)端。我們還可以結(jié)合黑白名單過濾的方式,對文件夾中的文件進行過濾,只保留允許上傳的文件,發(fā)送到服務(wù)端。
? 想要通過實現(xiàn)文件夾上傳需要借助該元素的webkitdirectory屬性,設(shè)置該屬性后,將限制用戶只能選擇文件夾,而無法選擇文件。但是該屬性并非標準屬性,所以請慎用!!!
瀏覽器兼容性:
示例代碼
<input type="file" id="folder" name="folder" webkitdirectory />
<div id="showBox">文件夾內(nèi)文件展示區(qū)域
</div>
// 創(chuàng)建空的 FormData 對象
var formData = new FormData()
// 聲明一個白名單數(shù)組 表示可以上傳的文件后綴名
const whiteList = ['ppt', 'pptx', 'txt', 'xlsx'];
// 調(diào)起文件選擇框
document.getElementById('folder').click()
// 監(jiān)聽文件選擇框的change事件
document.getElementById('folder').onchange = function (e) {// 獲取文件列表類數(shù)組對象let files = e.target.files// 輸出文件列表類數(shù)組對象console.log(files);// 將類數(shù)組對象轉(zhuǎn)換為數(shù)組 且對文件后綴名進行過濾files = Array.from(files).filter(item => {// 過濾掉文件夾對象if (item.type !== "" && item.name !== '.DS_Store') {// 獲取文件后綴名const suffix = item.name.split('.').pop()// 過濾掉不足在白名單中的文件if (whiteList.indexOf(suffix) > -1) {return true}}})// 輸出過濾后的文件對象列表console.log('選擇文件夾中的所有文件過濾后的結(jié)果---', files);// 用于顯示的html字符串let html = ''// 遍歷文件對象列表files.forEach(item => {// 將文件對象的信息拼接到html字符串中html = html + `<p>文件名:${item.name} <br />文件路徑:${item.webkitRelativePath}</p>`// 將文件對象添加到FormData中formData.append('file', item)})// 將html字符串渲染到頁面中document.getElementById('showBox').innerHTML = html// 后續(xù)上傳文件的邏輯...
選擇文件夾上傳后,首先瀏覽器會彈窗獲取用戶授權(quán)(Safari瀏覽器在本地環(huán)境時無需授權(quán),線上環(huán)境未驗證):
用戶授權(quán)之后,我們可以監(jiān)聽標簽的onchange事件,然后通過event.target.files獲取所選文件夾本身及其的所有子文件和子文件夾組成的文件類數(shù)組,在進行相關(guān)處理時,建議使用Array.from()轉(zhuǎn)換真正的數(shù)組類型。
原始目錄層級:
獲取的文件類數(shù)組以及過濾后的文件結(jié)果:
頁面渲染結(jié)果:
從上面的示例中可以看出獲取文件列表中,包含一種name為.DS_Store并且type為""的特殊文件,這類特殊文件文件表示的就是文件夾,我們可以通過該文件的webkitdirectory屬性來獲取文件夾的真實名稱。
? 而且此時獲取的各文件之間無法體現(xiàn)原始目錄層級關(guān)系,但是我們可以通過每個file文件的webkitRelativePath屬性來得知每個文件的層級關(guān)系,各級路徑之間通過 / 連接,我們可以通過/ 分割webkitRelativePath屬性值,從而還原文件夾的原始層級關(guān)系。
? 注意: 文件名和文件夾名最好不要包含/、\等特殊字符,因為獲取的File中的name和webkitRelativePath屬性,會將他們轉(zhuǎn)義,很有可能會影響層級的拆分和判斷。例如:/在File中的name和webkitRelativePath中都會被轉(zhuǎn)義為:,\在File中的name中會被轉(zhuǎn)義為\,在webkitRelativePath中會被轉(zhuǎn)義為/?(奇奇怪怪的規(guī)則(╯°□°)╯︵┻━┻)。
5、FormData對象結(jié)合dataTransfer實現(xiàn)拖拽文件夾,批量上傳文件
可以實現(xiàn),但其中涉及知識點太多,暫時還沒完全搞懂,想了解的建議查閱最后一篇相關(guān)資料。