在前端的世界里,除了通過input[file]
上傳文件外,我們是無法處理文件內(nèi)容的,處理文件的邏輯都需要依賴于后端完成,現(xiàn)在html5提供了showOpenFilePicker()
, showDirectoryPicker()
, showSaveFilePicker()
等API可以輕松地讓我們在瀏覽器世界里來管理本地文件。所以現(xiàn)在我們來學習一下這個強大的功能。
API介紹
注意:我們訪問一個文件或者目錄的讀寫操作所依賴的文件訪問權限在刷新或關閉頁面并且頁面所屬的源沒有其他標簽頁保持打開的情況下不會繼續(xù)保有。
showDirectoryPicker(options)
用于顯示一個允許用戶選擇一個目錄的目錄選擇器。
options
id
可選,通過指定 ID,瀏覽器可以記住不同 ID 所對應的目錄。如果在另一個選擇器中使用了相同的 ID,則選擇器將在同一目錄中打開。mode
可選,默認為 "read"
,用于只讀訪問,或 "readwrite"
,用于讀寫訪問。startIn
可選,一個FileSystemHandle
對象或者代表某個眾所周知的目錄的字符串(如:"desktop"
、"documents"
、"downloads"
、"music"
、"pictures"
、"videos"
),用于指定選擇器的起始目錄。

如果選中的文件夾包含系統(tǒng)文件,將無法打開,并提示選擇其他文件夾。

成功選擇后,將返回FileSystemDirectoryHandle
對象,如果取消(關閉系統(tǒng)彈框或者點擊取消)時,將返回一個失敗的Promise
。

showOpenFilePicker(options)
用于顯示一個允許用戶選擇一個或多個文件的文件選擇器,并返回這些文件的句柄。即使單選也是返回數(shù)組。
options
excludeAcceptAllOption
可選,默認為 false
。默認情況下,選擇器應包含一個不應用任何文件類型過濾器的選項(通過下面的類型選項啟動)。將此選項設置為 true
意味著該選項不可用。id
可選,通過指定 ID,瀏覽器可以記住不同 ID 所對應的目錄。如果在另一個選擇器中使用了相同的 ID,則選擇器將在同一目錄中打開。(他會記住上次對應id選擇的目錄位置,在下次使用相同id打開的彈框?qū)⒍ㄎ坏綄哪夸?/strong>)multiple
可選,默認為 false
。當設置為 true
時,可以選擇多個文件。startIn
可選, 一個 FileSystemHandle
對象或一個眾所周知的目錄("desktop"
、"documents"
、"downloads"
、"music"
、"pictures"
或 "videos"
)以指定打開選擇器的起始目錄。types
可選,允許選擇的文件類型的數(shù)組。每個項目都是一個具有以下選項的對象:description
可選,允許的文件類型類別的可選描述。默認為空字符串。accept
一個 Object
,其鍵設置為 MIME 類型,值設置為文件擴展名的數(shù)組。
const pickerOpts = {
types: [
{
description: "Images",
accept: {
"image/*": [".png", ".gif", ".jpeg", ".jpg"],
},
},
],
excludeAcceptAllOption: true,
multiple: false,
};
showSaveFilePicker(options)
用于顯示允許用戶保存一個文件的文件選擇器。用戶可以選擇一個已有文件覆蓋保存,也可以輸入名字新建一個文件。
options
excludeAcceptAllOption
可選,默認為 false
。默認情況下,選擇器應包含一個不應用任何文件類型過濾器的選項(通過下面的類型選項啟動)。將此選項設置為 true
意味著該選項不可用。id
可選,通過指定 ID,瀏覽器可以記住不同 ID 所對應的目錄。如果在另一個選擇器中使用了相同的 ID,則選擇器將在同一目錄中打開。startIn
可選,一個FileSystemHandle
對象或一個眾所周知的目錄("desktop"
、"documents"
、"downloads"
、"music"
、"pictures"
或 "videos"
)以指定打開選擇器的起始目錄。suggestedName
可選,一個字符串。建議的文件名。types
可選,允許選擇的文件類型的數(shù)組。每個項目都是一個具有以下選項的對象:description
可選,允許的文件類型類別的可選描述。默認為空字符串。accept
,一個 Object
,其鍵設置為 MIME 類型,值設置為文件擴展名的數(shù)組。
async function getNewFileHandle() {
const opts = {
suggestedName: "自定義命名.txt",
types: [
{
description: "Text file",
accept: { "text/plain": [".txt"] },
},
],
};
return await window.showSaveFilePicker(opts);
FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemHandle
不管是選擇文件還是文件夾, 確認選擇后,返回的FileSystemDirectoryHandle
,FileSystemFileHandle
對象都繼承自FileSystemHandle
接口,所以我們先來了解下。
FileSystemHandle
代表一個文件或一個目錄的對象。
kind
條目的類型。如果關聯(lián)的條目是一個文件,則此值為 'file'
,否則為 'directory'
。name
關聯(lián)的條目(文件或者文件夾)的名稱。
isSameEntry(fileSystemHandle)
用于比對兩個句柄以查看兩者關聯(lián)的條目(文件或目錄)是否相符。
queryPermission(descriptor)
用于查詢當前句柄目前的權限狀態(tài)。返回值為'granted'
、'denied'
或 'prompt'
。如果返回prompt
,則網(wǎng)站必須先調(diào)用 requestPermission()
,然后才能對句柄執(zhí)行任何操作。
descriptor.mode
值為read'
或 'readwrite'
, 指定需要查詢的權限模式
remove(options)
允許你用對應的句柄直接移除一個文件或一個目錄。
options.recursive
默認為 false
。當設為 true
并且條目是一個目錄時,目錄的內(nèi)容將會被遞歸移除。如果目錄中有內(nèi)容那么recursive
必須設置為true
否則不能刪除。

requestPermission(descriptor)
用于為文件句柄請求讀取或讀寫權限。返回值為'granted'
、'denied'
或 'prompt'
。如果返回prompt
,則網(wǎng)站必須先調(diào)用 requestPermission()
,然后才能對句柄執(zhí)行任何操作。
descriptor.mode
值為read'
或 'readwrite'
, 指定需要查詢的權限模式
對于requestPermission
來說,我如果開始showOpenFilePicker, showDirectoryPicker
mode未指定readwrite
那么我們將可以調(diào)用queryPermission
來讓用戶授權。
const dir = await window.showDirectoryPicker({
id: "dir",
mode: "read",
})
dir.requestPermission({
mode: "readwrite"
}).then(async res => {
console.log(res)
console.log(dir, await dir.queryPermission())
console.log("======")
})

FileSystemDirectoryHandle
提供一個指向目錄條目的句柄。該對象主要提供一些操作目錄的方法,比如刪除,創(chuàng)建(文件或目錄),遍歷等等。
entries()
一個異步迭代器,返回當前選中文件夾下所有直接子內(nèi)容的鍵值對。鍵值對是一個 [key, value]
形式的數(shù)組。有了這個就可以遞歸獲取當前文件夾下所有文件及文件夾的句柄對象了。
for await (const entry of dir.entries()) {
console.log(entry)
}

values()
一個異步迭代器,返回當前選中文件夾下所有直接子內(nèi)容的值(句柄對象)。
for await (const value of dir.values()) {
console.log(value)
}

keys()
一個異步迭代器,返回當前選中文件夾下所有直接子內(nèi)容的文件名。
for await (const key of dir.keys()) {
console.log(key)
}

removeEntry(name, options)
用于嘗試將目錄句柄內(nèi)指定名稱的文件或目錄移除。
name
文件名options.recursive
默認為 false
。當設為 true
時,條目將會被遞歸移除。
resolve(possibleDescendant)
一個包含從父目錄前往指定子條目中間的目錄的名稱的數(shù)組。數(shù)組的最后一項是子條目的名稱。
- 要返回其相對路徑的
FileSystemHandle
對象。
下面這個方法就是一個文件夾中查找一個文件,返回當前文件相對于文件夾的路徑。
async function returnPathDirectories(directoryHandle) {
const [handle] = await self.showOpenFilePicker();
if (!handle) {
return;
}
const relativePaths = await directoryHandle.resolve(handle);
return relativePaths
}
getDirectoryHandle(name, options)
返回一個位于調(diào)用此方法的目錄句柄內(nèi)帶有指定名稱的子目錄的FileSystemDirectoryHandle
。
name
子目錄名稱options.create
默認為 false
。當設為 true
時,如果沒有找到對應的目錄,將會創(chuàng)建一個指定名稱的目錄并將其返回。 此時獲取句柄對象的mode
必須設置為readwrite
。 如果初始化未設置,我們需要通過requestPermission
請求對應的權限。

const dir = await window.showDirectoryPicker({
id: "dir",
mode: "read",
})
const res2 = dir.getDirectoryHandle("zh", {
create: true
})
console.log("res2", res2)

getFileHandle()
返回一個位于調(diào)用此方法的目錄句柄內(nèi)帶有指定名稱的文件的 FileSystemFileHandle
。
name
子文件名稱options.create
默認為 false
。當設為 true
時,如果沒有找到對應的文件,將會創(chuàng)建一個指定名稱的文件并將其返回。 此時獲取句柄對象的mode
必須設置為readwrite
。 如果初始化未設置,我們需要通過requestPermission
請求對應的權限。
FileSystemFileHandle
提供一個指向文件條目的句柄。該對象主要提供一些操作文件的方法,比如寫入,獲取等等。
move(newName?, options)
允許你移動或重命名用戶本地文件系統(tǒng)中的文件。
newName
新文件名options
to
: 目標目錄的 FileSystemDirectoryHandle
(用于跨目錄移動)overwrite
: 默認 false
,是否覆蓋同名文件
createWritable(options)
用于創(chuàng)建一個FileSystemWritableFileStream
對象,可用于寫入文件。
任何通過寫入流造成的更改在寫入流被關閉前都不會反映到文件句柄所代表的文件上。這通常是將數(shù)據(jù)寫入到一個臨時文件來實現(xiàn)的,然后只有在寫入文件流被關閉后才會用臨時文件替換掉文件句柄所代表的文件。
async function writeFile(fileHandle, contents) {
const writable = await fileHandle.createWritable();
await writable.write(contents);
await writable.close();
}
keepExistingData
默認為 false
。當設為 true
時,如果文件存在,則先將現(xiàn)有文件的內(nèi)容復制到臨時文件,否則臨時文件初始時內(nèi)容為空。mode
可選, 指定可寫文件流的鎖定模式的字符串。默認值為 "siloed"
。"exclusive"
只能打開一個 FileSystemWritableFileStream
寫入器。在第一個寫入器關閉之前嘗試打開后續(xù)寫入器會導致拋出 NoModificationAllowedError
異常。"siloed"
可以同時打開多個 FileSystemWritableFileStream
寫入器,每個寫入器都有自己的交換文件,例如在多個標簽頁中使用同一個文件時。最后打開的寫入器會寫入其數(shù)據(jù),因為每個寫入器關閉時都會刷新數(shù)據(jù)。
getFile()
返回一個File
對象,其表示磁盤上句柄所代表的文件。如果磁盤上的文件在調(diào)用了此方法后發(fā)生了更改或是被移除,那么返回的File
對象可能會不再可讀。
const [file] = await window.showOpenFilePicker({
id: "file"
})
console.log(file)
console.log(await file.getFile())

FileSystemWritableFileStream
獲取文件句柄,主要就是為了寫入文件的。所以我們再來看看FileSystemWritableFileStream
對象。
locked
表示可寫流是否已鎖定。如果當前可寫流調(diào)用了getWriter()
那么他將被鎖定,不能進行任何操作。mode
可寫流的鎖定模式的字符串。("siloed"
, "exclusive"
)
abort(reason)
用于中止流,表示生產(chǎn)者不能再向流寫入數(shù)據(jù)(會立刻返回一個錯誤狀態(tài)),并丟棄所有已入隊的數(shù)據(jù)。一般用于不會流錯誤時終止。
reason
一個字符串,用于提供人類可讀的中止原因。
writer.abort("終止寫入")

close()
關閉可寫流。
getWriter()
返回一個新的 WritableStreamDefaultWriter
實例并且將流鎖定到該實例。當流被鎖定時,直到這個流被釋放之前,不能操作其他 writer。
seek(position)
用于更新文件當前指針的偏移到指定的位置(以字節(jié)為單位)。主要改變寫入內(nèi)容插入的位置。
position
一個數(shù)字,表示從文件開頭起的字節(jié)位置。 這里我們就可以結合getFile()
獲取文件大小,然后設置內(nèi)容插入位置。來實現(xiàn)追加文件內(nèi)容的效果。但是需要指定createWritable({ keepExistingData: true })
才會保留以前的文件內(nèi)容。
const writableStream = await newHandle.createWritable({
keepExistingData: true
});
const file = await newHandle.getFile()
console.log("file", file.size)
await writableStream.seek(file.size)
await writableStream.write("追加的內(nèi)容")

truncate(size)
用于將與流相關聯(lián)的文件調(diào)整為指定字節(jié)大?。▌h除文件內(nèi)容到對應的字節(jié))。如果指定的大小大于文件當前的大小,文件會被用 0x00
(即空格) 字節(jié)補充。調(diào)用 truncate()
方法同時也會更新文件的指針。和seek()
一樣
size
一個數(shù)字,表示要將流調(diào)整到的字節(jié)數(shù)。
write(data/options)
用于在調(diào)用此方法的文件上的當前指針偏移處寫入內(nèi)容。傳入一個options
對象可以是truncate, seek, write
的結合體。
data
用于寫入的文件數(shù)據(jù),可以是 ArrayBuffer
、TypedArray
、DataView
、Blob
或 字符串。options
type
一個字符串,值為 "write"
、"seek"
或 "truncate"
之一。data
用于寫入的文件數(shù)據(jù),可以是 ArrayBuffer
、TypedArray
、DataView
、Blob
或 字符串。這個屬性在 type
被設為 "write"
時是必需的。position
當 type
為 "seek"
時,表示文件當前指針應該移動到的位置。當 type
被設為 "write"
時也可以使用,這種情況下將會在指定的位置開始寫入。size
一個數(shù)字,表示流應當包含的字節(jié)數(shù)。這個屬性在 type
被設為 "truncate"
時是必需的。
既然可以調(diào)整流的控制權,那我們就來了解下WritableStreamDefaultWriter
獨有的API
closed
當前流是否被關閉或者釋放鎖定(調(diào)用releaseLock()
)desiredSize
返回填充滿流的內(nèi)部隊列需要的大小。如果無法成功寫入流(由于流發(fā)生錯誤或者中止入隊),則該值為 null
,如果流關閉,則該值為 0。ready
當流填充內(nèi)部隊列的所需大小從非正數(shù)變?yōu)檎龜?shù)時兌現(xiàn),表明它不再應用背壓。
desiredSize, ready
可用背壓控制。
async function writeWithBackpressure(dataChunks) {
const writer = writableStream.getWriter();
for (const chunk of dataChunks) {
if (writer.desiredSize <= 0) {
console.log('背壓:等待流準備就緒');
await writer.ready;
}
await writer.write(chunk);
console.log('已寫入:', chunk);
}
await writer.close();
}
const chunks = ['數(shù)據(jù)A', '數(shù)據(jù)B', '數(shù)據(jù)C'];
writeWithBackpressure(chunks);
releaseLock()
用于釋放 writer 對相應流的鎖定。釋放鎖后,writer 將不再處于鎖定狀態(tài)。如果釋放鎖時關聯(lián)的流出錯,writer 隨后也會以同樣的方式發(fā)生錯誤;此外,writer 將會關閉。
const [newHandle] = await showOpenFilePicker()
const writableStream = await newHandle.createWritable({
keepExistingData: true
});
const writer = writableStream.getWriter()
writer.write("0000000")
writer.write("111111111111")
writer.releaseLock()
writableStream.write("222222222222")
writableStream.close()

abort(), write(), close()
該方法使用方式同上。
使用WritableStreamDefaultWriter
對象操作寫入文件的原因
鎖機制保證寫入安全
如果直接操作流,那么多個頁面都可以同時訪問一個文件進行操作,導致數(shù)據(jù)寫入混亂。getWriter()
會為流加鎖,確保同一時間只有一個寫入器活躍。
背壓(Backpressure)管理
如果數(shù)據(jù)生產(chǎn)速度遠大于消費速度(如寫入大文件),可能導致內(nèi)存溢出。通過 writer.desiredSize
和 writer.ready
動態(tài)控制寫入節(jié)奏。
如何在開發(fā)中使用?
了解了上面的相關API后,我們就來看一下在開發(fā)中可以有哪些具體的使用。
分塊寫入錄屏流
前端可以知道的錄制瀏覽器標簽頁,沒有黑魔法
遍歷當前選中的目錄(及子目錄)
async function readDir () {
const dirHandle = await window.showDirectoryPicker();
return await recursiveReadDir(dirHandle)
async function recursiveReadDir(dirHandle) {
const entries = [];
for await (const entry of dirHandle.values()) {
if (entry.kind === 'file') {
entries.push({
name: entry.name,
kind: entry.kind,
});
} else if (entry.kind === 'directory') {
entries.push({
name: entry.name,
kind: entry.kind,
children: await recursiveReadDir(entry)
});
}
}
return entries
}
}

遞歸刪除指定目錄
async function recursiveRemvoeDir() {
const dir = await window.showDirectoryPicker()
dir.remove({
recursive: true
})
}
批量處理文件
例如批量修改文件名
async function batchRenameImages() {
const dirHandle = await window.showDirectoryPicker();
for await (const entry of dirHandle.values()) {
if (entry.kind === 'file' && entry.name.endsWith('.jpg')) {
const newName = entry.name.replace('.jpg', '@zh.jpg');
await entry.move(newName);
}
}
}
追加文件內(nèi)容
默認情況下寫入文件都是直接覆蓋寫入。
async function appendFile(data) {
const [newHandle] = await showOpenFilePicker()
const writableStream = await newHandle.createWritable({
keepExistingData: true
});
const {size} = await newHandle.getFile()
await writableStream.seek(size)
await writableStream.write(data)
await writableStream.close()
}
分片寫入大文件
async function saveLargeFile(url) {
const fileHandle = await window.showSaveFilePicker();
const writableStream = await fileHandle.createWritable();
const writer = writableStream.getWriter();
const response = await fetch(url);
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
await writer.write(value);
}
await writer.close();
console.log('文件保存完成');
}
saveLargeFile("https://p26-passport.byteacctimg.com/img/user-avatar/b515ec49c88a2c9e23fb5727652cb8f9~90x90.awebp")
轉(zhuǎn)自https://juejin.cn/post/7494870286645346355
該文章在 2025/9/8 14:37:05 編輯過