Web离线应用
前言
编写web离线应用,需要把应用文件和数据存在本地,也就是浏览器中。这主要用到了H5的三个重要的知识点,Application Cache(应用程序缓存), localStorage/sessionStorage(web存储), web SQL(web数据库)。
Application cache
如果要启用应用缓存,就需要在html标签加manifest属性1
2
3
4
<html manifest="demo.appcache">
...
</html>
demo.appcache就是写需要缓存页面信息的manifest文件,文件的后缀名可以任写,但建议统一为.appcache。  
manifest文件
告知浏览器需要(或不需要)缓存的文件,包括三部分:
- CACHE MANIFEST 此标题下出现的文件将在首次下载后进行缓存
 - NETWORK 此标题下列出的文件需要在访问服务器,且不被缓存
 - FALLBACK 此标题下列出的文件是页面无法被访问时退回的页面
 
第一行写CACHE MANIFEST是必须的,写法为1
2
3
4CACHE MANIFEST
index.html 
jqury.min.js
#后面的是注释,写上日期和版本号,当index.html等页面有更新时,改日期或者版本号,浏览器会将页面进行更新和缓存,方便。其中能应用默访问的页面,也就是index.html是默认被缓存的,不写也可以。一般写修改不频繁的文件
1  | NETWORK:  | 
一般写星号,也就是其他的页面都要通过访问服务器
1  | FALLBACK:  | 
如果无法建立因特网连接,则用 “offline.html” 替代 /html5/ 目录中的所有文件。
更新缓存的情况
- 用户清空浏览器缓存
 - manifest文件被修改
 - 由程序文件来更新应用缓存
 
设置MIME-type
需要给manifest文件设置MIME-type为text/cache-manifest
- 在Apache服务器
可以在根目录下添加.htaccess文件或者在manifest文件开头添加(前提是文件是php后缀名)1
AddType text/cache-manifest manifest
==貌似使用nodejs做后台不用设置MIME-type也可以将程序文件写进缓存==1
2
3
header("Content-Type: text/cache-manifest"); 
localStorage/sessionStorage
在浏览器上(本地)储存用户的浏览数据
两者的区别就在于,localStorage储存的数据没有时间的限制,sessionStorage储存的数据,当关闭浏览器时,数据就没了。
两者的API都是相同的,主要有一下:
- 保存数据:localStorage.setItem(key,value);
 - 读取数据:localStorage.getItem(key);
 - 删除单个数据:localStorage.removeItem(key);
 - 删除所有数据:localStorage.clear();
 - 得到某个索引的key:localStorage.key(index);
保存数据还可以直接定义localStorage的属性,比如localStorage.resource = value
一般储存变更频繁的文件 
web SQL
位于浏览器中的关系型数据库
核心的方法就三个:
openDatabase:这个方法使用现有数据库或创建新数据库创建数据库对象。
transaction:这个方法允许我们根据情况控制事务提交或回滚,意思是可以划分事务。
executeSql:这个方法用于执行真实的SQL查询。
打开数据库
1  | var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024 * 1024);  | 
openDatebase方法接受五个参数,分别是:
- 数据名称
 - 数据库版本(写死就行)
 - 数据库描述
 - 大小
 - 回调(可选)
 
插入和读取数据
1  | var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024 * 1024);  | 
采用MVC思想写js代码
applicationcontroller.js //程序的入口程序主控制器,向外暴露一个start方法,作为程序的入口。其他的私有函数都是对次控制器暴露出的方法的调用
articlescontroller.js //操作articles模型,调用article暴露出的方法实现增删改查以及调用template的方法渲染页面
article.js //article模型,调用更底层的database对象(对数据库操作的封装)
database.js //更底层的M层,封装对数据库的操作
templates.js //V层,插入html节点
程序的入口就只有一个1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//applicationController.js
//webapp的入口函数,类似C语言中的main,或者jq中的$(document).ready
    function start(resources, storeResources) {
        APP.database.open(function() {
            //监听hash的变化
            $(window).bind("hashchange", route);
            //往DOM里添加CSS
            $("head").append('<style>' + resources.css + '</style>');
            //创造app应用名称节点
            $('body').html(APP.templates.application());
            //移除下载提示
            $('#loading').remove();
            route();
        });
        if(storeResources) {
            localStorage.resources = JSON.stringify(resources);
        }
    }
在index.html调用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60$(document).ready(function() {
            console.log('ready %o', new Date()); //%o代替javascript对象
            var APP_START_FAILED = "I'm sorry, the app can't start right now."
            function startWithResources(resources, storeResources) {
                //执行加载的js函数
                try {
                    //eval(resources.js)
                    insertScript(resources.js);
                    setTimeout(function() {
                        APP.applicationController.start(resources, storeResources);  //程序入口
                    }, 500); 
                } catch(e) {
                    alert(APP_START_FAILED);
                    console.log('%o', e);
                } 
            }
            function startWithOnlineResources(resources) {
                startWithResources(resources, true);
            }
            function startWithOfflineResources() {
                var resources;
                //假如之前已经访问了并且js文件已经缓存进localStorage,执行以下
                if(localStorage && localStorage.resources) {
                    resources = JSON.parse(localStorage.resources);
                    startWithResources(resources, false);
                    //否则输出提醒信息
                }else {
                    alert(APP_START_FAILED);
                }
            }
            
            function insertScript(script) {
                var node = document.createElement('script');
                node.innerHTML = script;
                document.head.appendChild(node);
            }
            //假如设备离线,则执行离线操作
            if(navigator && navigator.onLine === false) {
                startWithOfflineResources();
                //否则,下载资源并执行,假如成功就把资源添加进local storage。
            }else {
                $.ajax({
                    url: 'api/resources',
                    success: startWithOnlineResources,
                    error: startWithOfflineResources,
                    dataType: 'json'
                });
            }
            
        })
程序的设计就类似于树形的结构:
1  | graph TD  | 
主控制器的第一步工作是打开数据库
对象的封装
全局就一个APP对象,为window的全局变量1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55window.APP = {};
(function(APP){
    APP.applicationController = (function(){
        ...
        
        return {
            start: start
        }
    }());
    
    APP.articleController = (funcion(){
       ...
       
       return {
            synchronizeWithServer: synchronizeWithServer,
            showArticle: showArticle,
            showArticleList: showArticleList
        }
    }());
    
    
    
    APP.templates = (function(){}(
        ...
        
        return {
            application: application,
            home: home,
            articleList: articleList,
            article: article,
            articleLoading: articleLoading
        }
        
    )()};
    
    APP.database = (function(){}(
        ...
        
        return {
            open: open,
            runQuery: runQuery
        }
    )()};
    
    APP.article = (function(){}(
        ...
        
        return {
            deleteArticles: deleteArticles,
            insertArticles: insertArticles,
            selectBasicArticles: selectBasicArticles,
            selectFullArticle: selectFullArticle
        }
    )()};
}(APP))
这里运用的JS的闭包思想,每一个(function(){…}()()};都是单独的作用域,代码不会被污染,全局就只有一个对象APP.