PhoneGap:檔案存取(一)

PhoneGap 所使用的檔案存取,基本上是 HTML5 所提供的程式架構,所以相同的程式碼也可以在支援 HTML5 檔案存取(FileSystem)的瀏覽器上執行(例如Chrome)。檔案存取的程式架構有一點複雜,如果要看比較詳細的文件可以參考:W3C FileSystemPhoneGap FileSystem 以及 FileSystem 中文說明

另外,如果只是要撰寫瀏覽器端的FileSystem,可以參考 這個 國外網站的部落格,裡面還有 DEMO 可以測式執行的結果。我自己在撰寫的時候也參考他許多,只是 PhoneGap 與瀏覽器在支援上還是有些許的不同,所以直接照抄放到 PhoneGap 上是沒有辦法執行的! 反倒是 PhoneGap 能執行的,放到瀏覽器上大致沒有什麼問題。

載入 API

在撰寫程式之前,我們要先到 config.xml 開啟二項功能,分別是 device 和 file。device 的功用是提供一個 deviceready() 事件,當我們載入 APP 並且手機上的裝置都準備好之後,就會呼叫這個事件。因為檔案存取牽扯到 I/O 的運作,所以我們所有的動作都要等 deviceready 之後才能開始執行。至於後面的 file,當然就是檔案存取的 API 了。

<gap:plugin name="org.apache.cordova.device" />
    <gap:plugin name="org.apache.cordova.file" />

除了 config 的設定之外,也別忘了在 HTML 網頁裡載入 cordova.js (參考PhoneGap建置)。後面的程式碼將直接針對 JavaScript 做解說。

FileSystem 初始化

請參照下面的程式碼。首先第 1 行宣告了 filesystem 的全域物件,用來儲存我們建立好的檔案存取物件。第 2 行宣告的是一個目錄物件,用來儲存我們指定的目錄位置。第 4 行是當裝置都準備好之後,呼叫第 7 行的 requestFS()。如果使用瀏覽器開啟網頁時,會因為看不懂第 7 行的指令導致程式無法執行,可以利用 Navigator 物件判斷是那一種平台,如果是一般瀏覽器,直接執行 requestFS() 即可。如果你的程式像我一樣就是要讓 PhoneGap 編譯在行動裝置上執行,那麼這樣寫就可以了。

var filesystem = null;
var dirEntry = null;    //DirectoryEntry

document.addEventListener("deviceready", requestFS, false); //from org.apache.cordova.device

//測式檔案存取功能,必須在 deviceready 事件呼叫
function requestFS() {
    window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;

    // 測式瀏覽器是否支援 FileSystem
    if (window.requestFileSystem) {
        initFileSystem();
    } else {
        alert("Sorry! Your browser doesn\'t support the FileSystem API :(");
    }
}
//初始化FileSystem,請求系統服務
function initFileSystem() {
    window.requestFileSystem(LocalFileSystem.PERSISTENT, 1024 * 1024 * 5 /* 5MB */, function(fs) {
        filesystem = fs;    //初始化FileSystem物件
        dirEntry = fs.root; //初始化存取目錄

        onLoadFunction();   //自行撰寫此function
    }, errorHandler);
}

由於不同瀏覽器的 requestFileSystem 關鍵字不同,所以第 8 行先統一採用 requestFileSystem。第 11 行測式瀏覽器,若有支援就執行第 18 行的 initFileSystem()。第 19 行設定檔案存取的模式,LocalFileSystem.PERSISTENT 表示永久儲存(除非APP被移除),而 LocalFileSystem.TEMPORARY 表示暫存,會隨系統清理時消失。19 行後面的數字是這個 APP 所需要的儲存空間,如果只有純文字的話不需要太大。第 20、21 行初始化物件。第 23 行是我們自行建立的函式,在FileSystem初始化完成後執行,可以代替 onLoad 的功能。第 24 行初始化錯誤時執行 errorHandler() 處理錯誤訊息,由於錯誤訊息很多地方都會用到所以寫成函式,下面是 errorHandler 參考程式碼。

function errorHandler(error) {
    var message = '';

    switch (error.code) {
        case FileError.SECURITY_ERR:
            message = 'Security Error';
            break;
        case FileError.NOT_FOUND_ERR:
            message = 'Not Found Error';
            break;
        case FileError.QUOTA_EXCEEDED_ERR:
            message = 'Quota Exceeded Error';
            break;
        case FileError.INVALID_MODIFICATION_ERR:
            message = 'Invalid Modification Error';
            break;
        case FileError.INVALID_STATE_ERR:
            message = 'Invalid State Error';
            break;
        default:
            message = 'Unknown Error';
            break;
    }

    console.log(message);
}

指定存檔目錄

有時候我們希望檔案可以分在不同的資料夾儲存。然而在 FileSystem 存檔並不是指定目錄就好,而是要取得目錄物件後,才能在該目錄下存取檔案。為了避免讀取檔案列表的時候太複雜,建議最多只設定一層的子目錄。撰寫好下面的程式碼之後,只要呼叫「setDirectory(目錄名)」就可以設定當前的目錄位置。

//指定存檔目錄
function setDirectory(dir) {
    filesystem.root.getDirectory(dir, {create: true}, function(entry) {
        dirEntry = entry;   //指定目錄物件
        afterSetDirectory();    //自行撰寫此function
    }, errorHandler);
}

第 54 行一律從根目錄(root)取得指定的目錄物件,如果目錄不存在則自動建立(create:true)。第 56 行是設定完目錄之後執行 afterSetDirectory() 函式,存取失敗時則呼叫 errorHandler()。

儲存檔案

這裡的檔案是存成純文字文件,因此存檔要傳入的內容也很簡單,只有檔名和內文(檔案會存在 dirEntry 物件指定的目錄下)。下面第 60 行取得指定檔名的檔案,成功時會產生一個 fileEntry 物件。第 62、65 行分別是寫入成功與失敗要執行的函式,我們在 63 行呼叫 saveFileSuccess(),在成功寫入檔案之後執行。

function saveFile(filename, content) {
    dirEntry.getFile(filename, {create: true, exclusive: false}, function(fileEntry) {
        fileEntry.createWriter(function(fileWriter) {
            fileWriter.onwriteend = function(e) {   //設定寫入成功事件
                saveFileSuccess(filename);  //自行撰寫此function
            };
            fileWriter.onerror = function(e) {  //設定寫入失敗事件
                console.log('Write error: ' + e.toString());
                alert('無法儲存');
            };
            fileWriter.write(content);  //開始寫入內容
        }, errorHandler);
    }, errorHandler);
}

讀取檔案

讀取檔案不需要提供內容,只要傳入檔名即可(必須在 dirEntry 物件指定目錄下的檔案)。因此第 73 行指定檔名之後,74 行直接取得檔案物件。第 76 行另外宣告了一個 FileReader 物件來讀取檔案。讀取成功時,將檔案內容傳入 79 行的 loadFileSuccess() 中。

function loadFile(filename) {
    dirEntry.getFile(filename, {}, function(fileEntry) {
        fileEntry.file(function(file) {
            var reader = new FileReader();

            reader.onload = function(e) {
                loadFileSuccess(this.result);   //自行撰寫此function
            };
            reader.readAsText(file);

        }, errorHandler);
    }, errorHandler);
}

刪除檔案

刪除檔案一樣只需提供檔名,即可刪除(必須在 dirEntry 物件指定目錄下的檔案)。刪除完成之後,呼叫 89 行的 deleteFileSuccess() 執行想要的動作。

function deleteFile(filename) {
    dirEntry.getFile(filename, {create: false}, function(fileEntry) {
        fileEntry.remove(function(e) {
            deleteFileSuccess();    //自行撰寫此function
        }, errorHandler);
    }, errorHandler);
}

檔案列表

檔案列表是一個遞迴程式,不斷的讀取檔案物件直到結束為止。因為要重復呼叫的關係,第 97 行將這個函式命名為 fetchEntries,並且在 109 行進行第一次的呼叫。當讀取未結束時,103 行會將本次讀取到的檔案物件(results)儲存在 entries 物件的後面,然後繼續讀下一筆。讀取結束時呼叫 displayEntries(),並傳回檔案物件的陣列。

function listFiles() {
    var dirReader = dirEntry.createReader();
    var entries = [];

    var fetchEntries = function() {
        dirReader.readEntries(function(results) {
            if (!results.length) {  //讀取結束
                displayEntries(entries);    //自行撰寫此function
                //回傳Entry陣列
            } else {
                entries = entries.concat(results);
                fetchEntries(); //讀取下一筆
            }
        }, errorHandler);
    };

    fetchEntries();
}

使用者自訂函數

對初學者來說,FileSystem 最困難的地方,大概就是它幾乎都不是循序執行,而是不斷的呼叫 function,也就是說它執行的順序是看你的函式怎麼呼叫。從前面的程式碼可以看得出來,我預留了很多必須自行撰寫的函式。這些函式如果沒有建立,程式可是會出錯的。所以下面列出所有需要自行撰寫的 function,可以放在一開始的地方。

/****使用者必須自行建立的function****/
function onLoadFunction() { /* 系統服務初始化完成後呼叫 */ }
function afterSetDirectory() { /* 指定目錄後呼叫,參照setDirectory() */ }
function saveFileSuccess(filename) { /* 儲存檔案成功時呼叫,參照saveFile() */ }
function loadFileSuccess(content) { /* 讀取檔案成功時呼叫,參照loadFile() */ }
function deleteFileSuccess() { /* 刪除檔案成功時呼叫,參照deleteFile() */ }
function displayEntries(entries) { /* 讀取檔案清單完成時呼叫,參照listFiles() */ }

以上的程式碼因為是通用的,所以建議存在單一個 .js 檔案裡,需要用到檔案存取的時候,直接載入此 JS 檔即可。如果有需要用到自行建立的函式,可以將它在 .html 檔裡複寫。