HTML5项目笔记6:使用HTML5 FileSystem API设计离线文件存储
翁智华  发布于 2018-10-30

在移动环境或者离线环境中,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() 来请求文件系统进行操作, 

View Code
1 /*-----执行脚本注入,文件系统的基本操作-----*/
2 window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem; //文件系统请求标识

  

然后编写一个DSDataFactory的函数(这个函数旨在对文件夹的操作)

在这个函数中内置相应的属性和方法,包含了文件请求系统的大小,默认为1兆,文件类型,默认为临时空间;内置的错误信息,这个错误信息可以自定义,在后面会讲到。 

View Code
 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):

View Code
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指的是递归删除: 

View Code
 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来请求文件系统作为入口操作: 

View Code
 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回调函数): 

View Code
 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回调函数):

View Code
 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

View Code
 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:

View Code
 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     /*--根据文件名删除文件--*/

删除完成之后通过回调函数返回被删除的文件的名称

 

根据文件名称来对文件的内容进行追加(先读取文件,然后将传入的内容添加到文件中):

View Code
 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回调函数):

View Code
 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 下的文件的处理方法我们基本有了,我们可以灵活地对文件进行操作。如果有不够的地方,我们可以继续修改完善。

 

在代码的结尾我们进行了实例化,

View Code
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 这个页面,我们存放我们在网络离线情况下写好的工作日志,并且把它保存在客户端的WebDataBaseFileSystem里面。所以我们可以在这个页面看到我们离线时的数据日志列表。 

 

相关的业务代码如下:

View Code
 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页面的相关业务代码: 

View Code
  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

推荐阅读