在移動環境或者離線環境中,WebDataBase 雖然能夠存儲並有效地管理和維護客戶端的數據集合,但是仍不能滿足對包含大段數據文件的存儲和多種不同格式文件的保存,於是我們就需要離線的文件管理系統來維護我們工作了,基於HTML5的FileSystem API 就充當這這個角色。
通過這個FileSystem API,我們的Web應用程序可以閱讀,瀏覽,編輯和操縱本地文件系統。
FileSystem API的主要功能有:
Reading and manipulating files: File/Blob, FileList, FileReader |
Creating and writing: BlobBuilder, FileWriter |
Directories and filesystem access:DirectoryReader,FileEntry/DirectoryEntry,LocalFileSystem |
支持情況和存儲空間的限制:
目前主流瀏覽器中,chrome應該是支持文件操作系統最好的瀏覽器,只要你配置好相關的操作數據,瀏覽器允許你創建沒有限制的存儲空間。
現在我們來封裝和提取基於FileSystem API的公用方法。
首先,我們需要拿到FileSystem API的可操作的數據上下文:
FileSystem API通過調用 window.requestFileSystem() 來請求文件系統進行操作,

1 /*-----執行腳本注入,文件系統的基本操作-----*/ 2 window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem; //文件系統請求標識
然后編寫一個DSDataFactory的函數(這個函數旨在對文件夾的操作),
在這個函數中內置相應的屬性和方法,包含了文件請求系統的大小,默認為1兆,文件類型,默認為臨時空間;內置的錯誤信息,這個錯誤信息可以自定義,在后面會講到。

1 /*-----文件夾系統的工廠業務-----*/ 2 function DSDataFactory(size, type) { 3 var ds = this; 4 var fs; 5 6 this.size = size || 1024 * 1024; 7 this.type = type || window.TEMPORARY; 8 9 this.fs = fs; 10 11 function errorHandler(e) { 12 var msg = ''; 13 14 switch (e.code) { 15 case FileError.QUOTA_EXCEEDED_ERR: 16 msg = 'QUOTA_EXCEEDED_ERR'; 17 break; 18 case FileError.NOT_FOUND_ERR: 19 msg = 'NOT_FOUND_ERR'; 20 break; 21 case FileError.SECURITY_ERR: 22 msg = 'SECURITY_EcRR'; 23 break; 24 case FileError.INVALID_MODIFICATION_ERR: 25 msg = 'INVALID_MODIFICATION_ERR'; 26 break; 27 case FileError.INVALID_STATE_ERR: 28 msg = 'INVALID_STATE_ERR'; 29 break; 30 default: 31 msg = 'Unknown Error'; 32 break; 33 }; 34 35 log.debug('Error: ' + msg); 36 } 37 38 window.requestFileSystem(this.type, this.size, function (f) { 39 fs = f; 40 ds.fs = fs; 41 }, errorHandler);
這個DSDataFacory函數里面,還包含着另外兩個函數,一個函數用於創建文件,一個函數用於移除文件夾(同時移除該文件夾內的所有文件)。
創建一個文件夾(包含兩個參數,一個是文件夾名稱 directoryName,一個是回調函數callback):

1 /*--創建一個文件夾--*/ //L:創建在緩存中 2 this.createDirectory = function (directoryName, callback) { 3 fs.root.getDirectory(directory, { create: true }, function (dirEntry) { 4 //log.debug(directoryName + "目錄創建成功!"); 5 if (callback) callback(directoryName); 6 }, errorHandler); 7 } 8 /*--創建一個文件夾--*/
這樣就在瀏覽器緩存中創建了一個文件夾,並返回文件夾的名稱。
移除文件夾以及文件夾下面的所有文件(遞歸移除),這邊有兩個參數:一個是direcotoryName,指的是待文件夾的名稱,這個其實應該指的是該文件夾的路徑,一個是回調函數,返回移除的文件夾的名稱,下面的dirEntry.removeRecursively指的是遞歸刪除:

1 /*--遞歸地移除文件夾內容--*/ 2 this.removeDirectoryAll = function (directoryName, callback) { 3 fs.root.getDirectory(directoryName, {}, function (dirEntry) { 4 dirEntry.removeRecursively(function () { 5 //log.debug(directoryName + "目錄及子目錄成功刪除!"); 6 if (callback) callback(directoryName); 7 }, errorHandler); 8 9 }, errorHandler); 10 } 11 /*--遞歸地移除文件夾內容--*/
第三步驟我們編寫一個FSDataFactory的函數(這個函數旨在對文件的操作),這邊我們把對文件夾的操作和對文件的操作分離成兩個不同的類是為了更加清晰的操作,同樣的,我們通過訪問FileSystem API來請求文件系統作為入口操作:

1 /*-----文件系統的工廠業務(begin)-----*/ 2 function FSDataFactory(size, type) { 3 var ds = new DSDataFactory(size, type); 4 5 var fs; 6 7 this.size = size || 1024 * 1024; 8 this.type = type || window.TEMPORARY; 9 this.ds = ds; 10 this.errorHandler = errorHandler; 11 12 function errorHandler(e) { 13 var msg = ''; 14 15 switch (e.code) { 16 case FileError.QUOTA_EXCEEDED_ERR: 17 msg = 'QUOTA_EXCEEDED_ERR'; 18 break; 19 case FileError.NOT_FOUND_ERR: 20 msg = 'NOT_FOUND_ERR'; 21 break; 22 case FileError.SECURITY_ERR: 23 msg = 'SECURITY_ERR'; 24 break; 25 case FileError.INVALID_MODIFICATION_ERR: 26 msg = 'INVALID_MODIFICATION_ERR'; 27 break; 28 case FileError.INVALID_STATE_ERR: 29 msg = 'INVALID_STATE_ERR'; 30 break; 31 default: 32 msg = 'Unknown Error'; 33 break; 34 }; 35 36 log.debug('Error: ' + msg); 37 } 38 39 window.requestFileSystem(this.type, this.size, function (f) { 40 fs = f; 41 // Log.debug("requestFileSystem ok"); 42 }, errorHandler);
在這個FSDataFactory函數內我們包含了一些我們最經常用的,對文件系統的CURD的操作,下面我們會一個一個講到。
逐級創建文件和文件夾(包含了兩個參數fileName(你要創建的文件名稱,更確切的說應該是文件路徑)和callback回調函數):

1 /*--逐級創建創建文件和文件夾--*/ 2 this.createFileWithPath = function (fileName, callback) { 3 var paths = fileName.split('/'); 4 5 createDir(fs.root, paths); 6 7 function createDir(rootDirEntry, folders) { 8 // Throw out './' or '/' and move on to prevent something like '/foo/.//bar'. 9 if (folders[0] == '.' || folders[0] == '') { 10 folders = folders.slice(1); 11 } 12 //Log.debug("createDir " + folders[0]); 13 if (folders.length == 1 && folders[0].split('.').length > 1) { 14 rootDirEntry.getFile(folders[0], { create: true, exclusive: false }, function (fileEntry) { 15 //Log.debug("create file " + fileEntry.fullPath); 16 if (callback) callback(fileEntry); 17 }, errorHandler); 18 } 19 else { 20 rootDirEntry.getDirectory(folders[0], { create: true, exclusive: false }, function (dirEntry) { 21 // Recursively add the new subfolder (if we still have another to create). 22 if (folders.length) { 23 createDir(dirEntry, folders.slice(1)); 24 } 25 }, errorHandler); 26 } 27 }; 28 } 29 /*--逐級創建創建文件和文件夾--*/
這里面其實是遞歸調用了createDir函數,來逐級地創建文件夾,檢查到最后一級的時候,檢查是文件還是文件夾,如果包含 . 則認定為文件,否則為文件夾。
如fileName=”/BenFirst/BenSecond/BenThird/”,則會按順序相繼地創建這三個文件夾,
如果fileName=” /BenFirst/BenSecond/BenThird/Ben.txt”,則會相繼創建文件夾,並在BenThird文件夾下面創建Ben.txt文件
逐級創建文件和文件夾並寫入內容(包含了三個參數fileName(你要創建的文件名稱,更確切的說應該是文件路徑)、content(你要寫入的內容)和callback回調函數):

1 /*--逐級創建文件並寫入--*/ //L:writeNewFile--先創建文件夾然后寫入文件,有則覆蓋,沒有則創建文件夾再創建文件 2 function writeNewFile(fileName, content, callback) { 3 fsDataFactory.createFileWithPath(fileName, function (fileEntry) { 4 // Log.debug("write file " + fileEntry.fullPath); 5 6 fileEntry.createWriter(function (fileWriter) { 7 fileWriter.onwriteend = function (e) { 8 //log.debug(fileName + '寫入成功!'); 9 if (callback) callback(fileEntry.fullPath); 10 }; 11 12 fileWriter.onerror = function (e) { 13 //log.error(fileName + '寫入錯誤: ' + e.toString()); 14 if (callback) callback(fileEntry.fullPath, e); 15 }; 16 17 // 創建一個 Blob 並寫入文件. 18 var bb = new window.WebKitBlobBuilder(); // Note: window.WebKitBlobBuilder in Chrome 12. 19 bb.append(content); 20 fileWriter.write(bb.getBlob('text/plain')); 21 }, errorHandler); 22 }); 23 }
做法與上面的一樣,就是多了一個參數content,傳入你需要寫入的文字,他會在系統中創建一個Blob並寫入文件中,該方法適用於創建.txt類型的文件。
根據文件名(其實是根據文件路徑)來讀取文件,包含兩個參數,一個文件名稱fileName和一個回調函數callback:

1 /*--根據文件名(即文件路徑)讀取文件--*/ 2 this.readFileByName = function (fileName, callback) { 3 fs.root.getFile(fileName, {}, function (fileEntry) { 4 5 log.debug("File Address:" + fileEntry.toURL()); 6 7 fileEntry.file(function (file) { 8 var reader = new FileReader(); 9 10 reader.onloadend = function (e) { 11 if (callback) callback(this.result); 12 }; 13 14 reader.readAsText(file); 15 }, errorHandler); 16 }, function (e) { 17 if (callback) callback("0");//為0代表這個文件不存在 18 }); 19 } 20 /*--根據文件名讀取文件--*/
根據文件路徑來讀取文件並輸出文件內容,這邊還自定義了錯誤輸出:如果出現錯誤,則調用了回調函數,輸出字符串“0”。所以當這邊出現NOT_FOUND_ERR
錯誤的時候不會輸出系統定義的錯誤信息二回輸出我們定義的錯誤信息。這樣有利用我們將信息反饋給用戶。
根據文件名稱(也就是完整的文件路徑)刪除文件:通過查找到該文件,並執行刪除,包含兩個參數,一個文件名稱fileName和一個回調函數callback:

1 /*--根據文件名刪除文件--*/ 2 this.deleteFile = function (fileName, callback) { 3 fs.root.getFile(fileName, { create: false }, function (fileEntry) { 4 fileEntry.remove(function () { 5 //log.debug(fileName + '文件刪除成功.'); 6 if (callback) callback(fileName); 7 }, errorHandler); 8 }, errorHandler); 9 } 10 /*--根據文件名刪除文件--*/
刪除完成之后通過回調函數返回被刪除的文件的名稱
根據文件名稱來對文件的內容進行追加(先讀取文件,然后將傳入的內容添加到文件中):

1 /*--將內容追加進文件--*/ 2 this.appendFile = function (fileName, content, callback) { 3 fs.root.getFile(fileName, { create: false }, function (fileEntry) { 4 // 讀取一個已經存在的文件,並使用CreateWriter追加數據 5 fileEntry.createWriter(function (fileWriter) { 6 fileWriter.seek(fileWriter.length); 7 8 var bb = new BlobBuilder(); 9 bb.append(fileContent); 10 fileWriter.write(bb.getBlob('text/plain')); 11 12 if (callback) callback(fileName); 13 }, errorHandler); 14 }, errorHandler); 15 } 16 /*--將內容追加進文件--*/
逐級創建文件和文件夾並寫入內容(包含了三個參數fileName(你要創建的文件名稱,更確切的說應該是文件路徑)、content(你要寫入的內容)和callback回調函數):

1 /*--逐級創建創建文件並寫入--*/ //L:writeNewFile--創建文件, 2 this.writeFile = function (fileName, content, callback) { 3 fs.root.getFile(fileName, {}, function (fileEntry) { 4 fileEntry.remove(function () { 5 writeNewFile(fileName, content, callback); 6 }); 7 }, function () { 8 writeNewFile(fileName, content, callback); 9 }); 10 }
這個調用了之前的writeNewFile函數,唯一的區別就是他在調用writeNewFile之前還調用了fileEntry.Remove函數,就是先對文件進行刪除,然后再創建文件。
至此,在 HTML5 下的文件的處理方法我們基本有了,我們可以靈活地對文件進行操作。如果有不夠的地方,我們可以繼續修改完善。
在代碼的結尾我們進行了實例化,

1 /*-------實例化-------*/ 2 var dsDataFactory = new DSDataFactory(); //實例化文件夾操作 3 var fsDataFactory = new FSDataFactory(); //實例化文件操作 4 /*-------實例化-------*/
我們把這些代碼獨立地存放到FileSystem.js文件里面,這樣可以在繼承這個腳本文件的頁面里直接調用這個腳本庫的方法。
我們源碼中的public.js腳本中有GetRequest()函數,用於解析地址參數的:
我們的log.js腳本頁面里面,包含了對console.debug(msg),控制台信息輸出的二次封裝,所以下面會經常看到里面的一個方法:log.debug(msg),用於調試時輸出我們需要的的信息。
我們的 FormSerialy.js里面的序列化函數,在下面序列化表單的時候也有用到。
這些腳本文件都在我們的源碼里面,有興趣可以系統地看一看
離線工作系統在用戶工作日志保存那一塊就是將填寫的數據保存在離線的文件系統中,通過txt文件來保存的。
現在來看這個應用的實際操作:
在DraftBox.htm 這個頁面,我們存放我們在網絡離線情況下寫好的工作日志,並且把它保存在客戶端的WebDataBase和FileSystem里面。所以我們可以在這個頁面看到我們離線時的數據日志列表。
相關的業務代碼如下:

1 $(document).ready(function () { 2 var TableName = "WorkDiary"; 3 var fields = new Array("WorkDiary_Title", "WorkDiary_UpdateTime", "WorkDiary_User", "WorkDiary_ContentPath", "WorkDiary_Remark", "WorkDiary_State"); 4 sqlProvider.createTable(TableName, fields, function () { 5 log.debug("創建成功!"); 6 sqlProvider.loadTable(TableName, function (result) { 7 for (var i = 0; i < result.rows.length; i++) { 8 var row = result.rows.item(i); 9 var Content = "<table class='ListContent' cellpadding='0' cellspacing='0'><tr><td width='5%' > <input type='checkbox' class='CheckSingle' /> <input type='hidden' name='SEC' value='" + row.WorkDiary_SEC + "' /> <input type='hidden' name='Path' value='" + row.WorkDiary_ContentPath + "' /> </td> <td width='65%' >" + row.WorkDiary_Title + "</td> <td width='20%' >" + row.WorkDiary_UpdateTime + "</td> <td width='10%' >" + row.WorkDiary_User + "</td></tr></table>"; 10 $(".UserInfo").append(Content); 11 } 12 13 $(".ListContent").dblclick(function () { 14 log.debug("雙擊 :"+$(this).find("input[name='SEC']").val()); 15 window.location.href = "WorkDiary.htm?WorkDiary_SEC=" + $(this).find("input[name='SEC']").eq(0).val(); 16 }); 17 18 }); 19 }); 20 }); 21 22 function AddWorkDiary() { 23 log.debug("添加工作日志"); 24 location.href = "WorkDiary.htm"; 25 } 26 27 function UpdWorkDiary() { 28 log.debug("更新工作日志"); 29 var CKlen = $(".CheckSingle:checked").length; 30 if (CKlen != 1) alert("請選擇一項進行修改!"); 31 else { 32 var SEC = $(".CheckSingle:checked").eq(0).next("input[name='SEC']").val(); 33 window.location.href = "WorkDiary.htm?WorkDiary_SEC="+SEC; 34 } 35 } 36 37 function DelWorkDiary() { 38 var CKlen = $(".CheckSingle:checked").length; 39 if (CKlen == 0) alert("請至少選擇一項進行刪除!"); 40 else { 41 $(".CheckSingle:checked").each(function (i) { 42 var SEC = $(".CheckSingle:checked").eq(i).next("input[name='SEC']").val(); 43 var Path = $(".CheckSingle:checked").eq(i).next().next("input[name='Path']").val(); 44 sqlProvider.deleteRow("WorkDiary", SEC, function () { 45 fsDataFactory.deleteFile(Path, function () { 46 log.debug("刪除成功!"); 47 if (i == CKlen-1) { 48 alert("刪除成功!"); 49 } 50 }) 51 }); 52 }); 53 } 54 } 55 </script>
而WorkDiary.htm 這個頁面,是我們設計好的工作日志的填寫表單:包含了標題,工作時間,工作計時,工作內容等字段。
下面是WorkDiary.htm頁面的相關業務代碼:

1 //獲取地址欄傳遞的,如果包含WorkDiary_SEC參數,則說明是修改的,如果沒有,則說明是添加參數。 2 var Request = new Object(); 3 Request = GetRequest(); 4 var WorkDiary_SEC = Request['WorkDiary_SEC']; 5 6 //下面這個是載入函數,判斷是否有WorkDiary_SEC,有則載入修改,沒有則是添加: 7 $(document).ready(function () { 8 if (WorkDiary_SEC) { //進入更新路徑 9 sqlProvider.readRow("WorkDiary", WorkDiary_SEC, function (row) { 10 InitWorkDiary(row); 11 }) 12 }; 13 }) 14 15 //載入日志報告,進行數據綁定,因為我們只是對基本數據進行數據庫保存,其他的數據比 16 //如WorkDiary_Content是保存在txt文件里面的。所以我們載入的時候需要讀取文件的內容 17 function InitWorkDiary(row) { 18 $("#WorkDiary_Title").val(row.WorkDiary_Title); 19 $("#WorkContent_Path").val(row.WorkDiary_ContentPath); 20 21 fsDataFactory.readFileByName(row.WorkDiary_ContentPath, function (content) { 22 log.debug("序列化內容為:" + content); 23 var JsonContent = JSON.parse(content);//解析json串 24 25 $("#WorkDiary_StartTime").val(JsonContent.WorkDiary_StartTime); 26 log.debug(JsonContent.WorkDiary_StartTime); 27 28 $("#WorkDiary_FinishTime").val(JsonContent.WorkDiary_FinishTime); 29 log.debug(JsonContent.WorkDiary_FinishTime); 30 31 $("#WorkDiary_Hours").val(JsonContent.WorkDiary_Hours); 32 log.debug(JsonContent.WorkDiary_Hours); 33 34 $("#WorkDiary_Content").val(JsonContent.WorkDiary_Content); 35 log.debug(JsonContent.WorkDiary_Content); 36 }) 37 } 38 39 //提交操作 40 function onformsumit() { 41 var NDate = new Date(); 42 NDate = NDate.Format("yyyy-MM-dd HH:mm:SS"); 43 44 var PathDate = new Date(); 45 PathDate = PathDate.Format("yyyyMMddHHmmSS"); 46 47 var WorkDiary_ContentPath = "/WorkDiary/" + PathDate +".txt"; 48 49 log.debug("lala"+$("#WorkContent_Path").val()); 50 51 if (WorkDiary_SEC) { //如果有日志主鍵,說明是修改的 52 UpdWorkDiary(WorkDiary_SEC, NDate, $("#WorkContent_Path").val()); 53 } 54 else { //如果沒有日志主鍵,說明是添加的 55 AddWorkDiary(NDate,WorkDiary_ContentPath); 56 } 57 return false; 58 } 59 60 //這邊無論添加和修改都是做兩個主要的數據保存操作,一個是將基本數據保存在 61 //WebDataBase數據中,一個是將完整的數據保存在txt文件里面 62 //WorkDiary表包含如下字段 63 //WorkDiary_SEC(默認主鍵,增量標識) 64 //WorkDiary_Title 65 //WorkDiary_UpdateTime 66 //WorkDiary_User 67 //WorkDiary_ContentPath 68 //WorkDiary_Remark 69 //WorkDiary_State 70 71 //添加工作日志 72 function AddWorkDiary(NDate,WorkDiary_ContentPath) { //兩個參數,日期和路徑 73 var fields = new Array("WorkDiary_Title", "WorkDiary_UpdateTime", "WorkDiary_User", "WorkDiary_ContentPath", "WorkDiary_Remark", "WorkDiary_State"); 74 var values = new Array($("#WorkDiary_Title").val(), NDate, "Ben", WorkDiary_ContentPath, "", ""); 75 sqlProvider.insertRow("WorkDiary", fields, values, function () { 76 log.debug("數據表添加成功"); 77 var serialStr = WorkDiary_Serialy(); 78 79 fsDataFactory.writeFile(WorkDiary_ContentPath, serialStr, function () { 80 alert("提交成功!"); 81 }); 82 83 }); 84 } 85 86 //更新工作日志 87 function UpdWorkDiary(WorkDiary_SEC, NDate, WorkDiary_ContentPath) { //三個參數:主鍵,日期和日志內容 88 var fields = new Array("WorkDiary_SEC", "WorkDiary_Title", "WorkDiary_UpdateTime", "WorkDiary_User"); 89 var values = new Array(WorkDiary_SEC, $("#WorkDiary_Title").val(), NDate, "Ben"); 90 sqlProvider.updateRow("WorkDiary", fields, values, function () { 91 log.debug("數據表更新成功!"); 92 var serialStr = WorkDiary_Serialy(); 93 94 fsDataFactory.writeFile(WorkDiary_ContentPath, serialStr, function () { 95 alert("提交成功!"); 96 }); 97 98 }); 99 } 100 101 //序列化操作,序列化類名為UserInfo下面的所有表單控件,序列化成json串 102 function WorkDiary_Serialy() { 103 var Serialy_Result = "{"; 104 Serialy_Result += formsSerialy.formSerialyByClass("UserInfo"); 105 var len = Serialy_Result.length - 1; 106 Serialy_Result = Serialy_Result.substring(0, len); 107 Serialy_Result += "}"; 108 return Serialy_Result; 109 }
保存到數據庫的結果如圖:
保存到離線文件中的結果如圖:
這樣子,我們不但將數據保存到離線數據庫中,也將表單的數據序列化之后以JSON格式存入txt中。
優點在於:
1、可以在這個txt里面放大量數據,譬如這個WorkDiary_Content這個字段,是填寫工作日志的,可能大數據量,存在文件里面比較適合。
2、可以在某種程度上提高了重要數據的安全性,一般用戶如果沒有操作toURL函數,是不能直接獲取到該txt文件的路徑進而看到內容的,不像WebDataBase,用戶可以直接在瀏覽器開發者工具的Resources面板中直接看到。
3、文件格式的多樣性,除了保存txt文件之外,還可以保存多媒體文件如mp3,圖片文件如png。
本文源碼下載:CRX_FielSystemAPI