前幾天我才剛寫完 BPS 開發分享之 7:檔案下載,我突然就發現原來之前眾人一直在敲碗的 File System Access API 最近已經可以在 Chromium 系列的瀏覽器以及其 PWA 當中使用了(暫時為桌機限定),雖然非常侷限,但是因為它的使用體驗真的比起檔案下載要好,所以還是值得實作。當然,因為不支援的環境太多了,暫時前一篇文章中的作法還是一樣必須保留。同時必須認知到這項功能仍然只算是草案階段,所以不排除未來還是有變更規格的可能。
內容目錄
使用前提
跟近來很多的 API 一樣,File System Access API 的使用前提是網頁一定必須是在 HTTPS 環境下運作的。除此之外,前面也提到暫時只有桌機版本的 Chromium 系列瀏覽器支援它(最新情況可參見 Can I Use 的報告),不過我們倒也不需要做很複雜的瀏覽器判別,只要去檢查 window
底下有沒有定義 API 提供的方法就好了,例如:
const FileApiEnabled = typeof window.showSaveFilePicker != 'undefined';
除此之外,此 API 提供的所有方法都必須在使用者手勢之下呼叫,也就是說它必須在使用者操作(點擊、觸碰等等)的事件生命週期之中被呼叫。很大的機會是我們會在非同步函數裡面呼叫它們,此時我們必須確保它們是非同步函數裡面第一個被等候的 Promise,才不會導致當我們準備呼叫它們的時候事件週期早已經結束了。
開發環境設置
對於有使用 TypeScript 的開發者來說,因為這套 API 太新了,一般的定義檔裡面是不會有那些對應的定義的。為了解決此問題,可以安裝 @types/wicg-file-system-access 這個 NPM 套件,裡面有提供絕大部份的定義。不過這個定義檔也沒有完全跟上此 API 的最新發展;截至本文寫成為止,至少 SaveFilePickerOptions
介面裡面就遺漏了最新的 suggestedName
屬性,不過除此之外我並沒有發現其它遺漏之處,還算 OK。1
另存新檔
我們先從大家敲碗最久的「另存新檔」對話方塊開始介紹,直接用一個範例程式(我用 TypeScript 寫)解說比較快:
async function saveAs(): Promise<void> {
// 底下的程式碼都要用一個 try-catch 包起來,
// 因為使用者如果按了對話方塊的取消是會丟出例外的
try {
// 顯示對話方塊,該方法會傳回一個 FileSystemFileHandle 物件
let handle = await showSaveFilePicker({
suggestedName: "建議的檔名",
types: [
// 這裡面接受一個陣列的檔案類別清單,
// 可以高度自訂對話方塊要支援的檔案類別
{
description: "要顯示在對話方塊中的檔案類別說明",
accept: {
// 這裡面是一個 MIME 代碼和對應副檔名陣列的字典。
// 要注意的是如果指定的 MIME 的代碼是瀏覽器認得的,
// 那麼所有瀏覽器認知中的對應副檔名都會被自動加上去;
// 如果不想要這個行為,那麼請指定一個自訂的 MIME 代碼
'text/plain': ['.txt'],
},
}
],
// 底下這個設為 true 的話就不會顯示「所有檔案 (*.*)」的類別選項,預設為 false
excludeAcceptAllOption: false
} as SaveFilePickerOptions); // 修正定義檔漏了 suggestedName 屬性的問題
// 產生寫入用的 FileSystemWritableFileStream
let writable = await handle.createWritable();
// 看你要用什麼方式產生檔案內容的 Blob,非同步方法也 OK
let blob: Blob = ...;
// 把 Blob 寫進檔案之中;這個方法也可以用字串或 ArrayBuffer 當參數
await writable.write(blob);
// 關閉檔案;這一步很重要,沒這樣做的話你會看到檔案系統中有一個暫存檔在那邊
await writable.close();
} catch(e) {
// 使用者取消對話方塊的話會跑到這邊來,看你要不要處理
}
}
開啟檔案之後儲存修改
顯示存檔對話方塊當然不錯,不過更加提昇使用者體驗的功能應該是:開啟了一個檔案之後可以直接把修改儲存到該檔案當中,而不是「重新下載並且覆蓋掉既有檔案」這樣的體驗。當然,因為這個動作有一定的濫用危險性存在,所以目前在使用這項功能的時候,瀏覽器都會跳出一個類似這樣的原生對話方塊:
而使用者只要同意一次之後,在應用程式離開之前繼續儲存同一個檔案都不會需要繼續詢問。雖然這個對話方塊談不上很美觀,但是基於安全性考量其存在也是不可少的,而且整體而言的使用體驗也還是比起下載覆蓋要理想。
要做到這個機制,首先我們必須先取得 FileSystemFileHandle
物件:
async function open(): Promise<FileSystemFileHandle[]> {
// 跟存檔一樣,必須 catch 使用者的取消行為
try {
// 此方法會傳回 FileSystemFileHandle 的陣列,對應於每一個開啟的檔案
let handles = await showOpenFilePicker({
// 開啟檔案對話方塊可以決定是否允許多檔案開啟
multiple: true,
types: [
// 這邊的設定跟存檔對話方塊是一樣的
]
});
return handles;
} catch(e) {
return [];
}
}
拿到這些 FileSystemFileHandle
物件之後,如果我們要開啟檔案,可以使用該物件的 .getFile()
方法,該方法會傳回對應的 File
物件,然後我們就可以用跟 <input type="file">
一樣的方式去讀取檔案內容。這些 FileSystemFileHandle
物件必須設法儲存在記憶體中,然後當我們準備好要存檔的時候,就可以類似前面的作法這樣寫:
async function save(handle: FileSystemFileHandle): Promise<void> {
try {
// 這一行會顯示授權對話方塊,如果還沒授權的話
let writable = await handle.createWritable();
try {
// 利用一行程式碼來檢查看看檔案是否存在;
// 因為假如檔案在開啟到儲存的這段時間之中曾被刪除,
// 預設的行為是裝作沒事繼續重建檔案並儲存,
// 但我並不喜歡這個行為,我希望在這種情況中能中斷,
// 而底下這一行在檔案找不到的時候會丟錯
await handle.getFile();
// 沒問題的話就繼續儲存
let blob: Blob = ...;
await writable.write(blob);
await writable.close();
} catch(e) {
// 找不到檔案的時候會進到這邊來,
// 此時要記得呼叫放棄方法,以清除暫存檔
await writable.abort();
// 繼續丟出例外讓上一層來接
throw e;
}
} catch(e) {
// 處理存檔的失敗,包括使用者拒絕授權、檔案無法寫入等等
// 這邊可以改走「另存新檔」的流程,該流程無須使用者額外授權
}
}
持續儲存 FileSystemFileHandle
如果我們能夠讓 FileSystemFileHandle
物件持續儲存直到網頁下一次開啟,那麼就能夠在檔案選單裡面寫出例如「最近開啟的檔案」這樣的功能。而厲害的是這點還真的有辦法做到,辦法就是把它儲存在 IndexedDB 裡頭。我之前曾經寫過一篇文章稍微提到過 IndexedDB,也提到了幾個常見的框架,那些都可以用,而這邊為了示範起見我改用另外一個超精簡的框架 idb-keyval 來寫範例;它是利用最單純的鍵值對應來存取資料。
例如若使用它的 IIFE 建置版本,它會產生一個全域的 idbKeyval
物件。此時我們就可以用如下的語法儲存我們的 FileSystemFileHandle
:
idbKeyval.set('myHandleKey', handle);
然後下次網頁開啟的時候,就可以用下列語法來取回:
let handle = idbKeyval.get('myHandleKey') as FileSystemFileHandle;
但是要注意一點,在網頁重新打開之後,取回的 handle 暫時是沒有任何權限的(連讀取都沒有)。此時我們必須先利用 requestPermission()
方法取得使用者同意,才能呼叫 getFile()
方法:
await handle.requestPermission({ mode: 'readwrite' })
或者如果只需要讀而不需要寫,上面的 mode
可以改成 'read'
。這個方法會傳回一個狀態字串,其中 'granted'
就表示使用者同意權限了。
小秘訣:你也可以在 IndexedDB 的一個鍵值之上一口氣儲存一整個陣列的 FileSystemFileHandle
,而不用每一個都各自佔用一個 key。
結語
File System Access API 提供了很多令人興奮的強大功能、可以大幅改善牽涉到檔案編輯的 web 應用程式的使用者體驗。這裡面的 API 還有很多可以持續改進的地方,例如更多的選項、更好的錯誤處理機制等等。當然也希望其它的瀏覽器以及手機環境可以跟進支援這個 API。這些都是可以期待的未來發展。
-
什麼?等待這個定義更新?省省吧,@types/wicg-file-system-access 的作者是 Ingvar Stepanyan,我對他的射後不理領教過太多了,等他更新還不如等官方定義出來比較快。 ↩
留言