本地存储Cookie、Storage、indexDB、ServiceWork离线访问网站

5,150 阅读27分钟

个人博客

在日常开发中,cookie、Session/Local,对后两种运用的较少。

cookie

cookie是客户端的解决方案,最早是网景公司的前雇员Lou Montulli在1993年3月发明的。众所周知,HTTP是一个无状态的协议,客户端发起一次请求,服务器处理来自客户端的请求,然后给客户端回送一条响应。在客户端与服务器数据交换完毕后,服务器端和客户端的连接就会被关闭,Web服务器几乎没有什么信息可以判断是哪个用户发送的请求,也无法记录来访用户的请求序列,每次交换数据都需要建立新的连接,后来有了用户,网站想要去了解用户的需求,但是根据当时场景显然无法满足业务需求,cookie便孕育而出,它可以弥补HTTP协议无状态的部分不足。


我们先来看一下在日常操作中是怎么接收到这个cookie的。


开始之前,将本地cookie的值全部清除。


输入'掘金',百度监听了focusinput事件,所以第一次触发了2次请求,奇怪的是第一次没有Set-cookie返回:



第一次触发的是focus(),这时候Request Header请求头是没有任何Cookie,查询字段wd没有参数,历史记录Hisdata读取的是LocalStorage里客户端的历史查询与时间戳。看第二次请求

服务器传回了7条Set-Cooie,客户端拿到数据后,进行储存


这时候可以看到Cookie中有服务器返回的所有cookie以及相对应的值和其他配置;

每次客户端第一次请求,服务器接收到请求后为该客户端设置cookie,服务器端向客户端发送Cookie是通过HTTP响应报文实现的,在Set-Cookie中设置需要向客户端发送的cookie,浏览器接收到响应缓存cookie到内存或者硬盘(视浏览器机制),之后每次发送请求都会携带上cookie


cookie格式如下:  

接收到的cookie格式

Set-cookie: name=value [; expires=date] [; path=path] [; domain=domain] [;secure=secure]

发送的cookie格式

Cookie: name1=value1 [; name2=value2]

  • name:一个唯一确定的cookie名称。通常来讲cookie的名称是不区分大小写的。 
  • value:存储在cookie中的字符串值。最好为cookie的name和value进行url编码 
  • domain:cookie对于哪个域名下是有效的。所有向该域发送的请求中都会包含这个cookie信息。这个值可以包含子域(如:m.baidu.com),也可以不包含它(如:.baidu.com,则对于baidu.com的所有子域都有效). 
  • path: 表示这个cookie影响到的路径,浏览器跟会根据这项配置,向指定域中匹配的路径发送cookie。
  • expires:过期时间,表示cookie自删除的时间戳。如果不设置这个时间戳,cookie就会变成会话Session类型的cookie,浏览器会在页面关闭时即将删除所有cookie,这个值是GMT时间格式,如果客户端和服务器端时间不一致,使用expires就会存在偏差。 
  • max-age: 与expires作用相同,用来告诉浏览器此cookie多久过期(单位是秒),而不是一个固定的时间点。正常情况下,max-age的优先级高于expires。 
  • HttpOnly: 告知浏览器不允许通过脚本document.cookie去更改这个值,同样这个值在document.cookie中也不可见。但在http请求仍然会携带这个cookie。注意这个值虽然在脚本中不可获取,但仍然在浏览器安装目录中以文件形式存在。这项设置通常在服务器端设置。 
  • secure: 安全标志,指定后只有在使用SSL(https)链接时候才会发送到服务器,如果是http链接则不会传递该值。但是也有其他方法能在本地查看到cookie

我们可以手动设置一下cookie的返回,

var http = require('http');
var fs = require('fs');

http.createServer(function(request, responed) {
    responed.setHeader('status', '200 OK');
    responed.setHeader('Set-Cookie', 'userStatus=true;domain=.juejin.com;path=/;max-age=1000');
    responed.write('掘金');
    responed.end();
}).listen(8888);

然后启动服务器,访问的时候就能看到服务器返回

Set-Cookie:userStatus=true;domain=.juejin.com;path=/;max-age=1000    

Cookie的优点

  • cookie键值对形式,结构简单
  • 可以配置过期时间,不需要任何服务器资源存在于客户端上,
  • 可以弥补HTTP协议无状态的部分不足
  • 无兼容性问题。

Cookie的缺点

  • 大小数量受到限制,每个domain最多只能有20条cookie,每个cookie长度不能超过4096 字节,否则会被截掉。尽管在当今新的浏览器和客户端设备开始支持8192字节。
  • 用户配置可能为禁用 有些用户禁用了浏览器或客户端设备接收 Cookie 的能力,因此限制了这一功能。
  • 增加流量消耗,每次请求都需要带上cookie信息。
  • 安全风险,黑客可以进行Cookie拦截、XSS跨站脚本攻击和Cookie欺骗,历史上因为Cookie被攻击的网站用户不在少数,虽然可以对Cookie进行加密解密,但会影响到性能。

Cookie总结

在业务开发场景中,Cookie更多的是作为一种标识,用来记录用户的行为,而并非用户的身份信息,根据用户登录后生成特定Cookie,再次发起其他请求的时候,服务器能够识别用户是否能够继续此次操作,不能则相应操作。如果将用户的身份信息及其他重要信息放在cookie中是十分危险的,而且对于大型流量网站来说,每一个字节的消耗一年下来都是按几十TB算的,

以谷歌为例:
google的流量,占到整个互联网的40%2016年全球网路流量达到1.3ZB1ZB = 10^9TB),那么google2016年的流量就是1.3ZB*40%,如果google1MB请求减少一个字节,每年可以节省近500TB。

Session/Local

SessionStorage简称会话存储,和LocalStorage本地储存是前端现在最为广泛也是操作最简单的本地存储,都遵循同源策略(域名、协议、端口),存放空间很大,一般是5M,极大的解决了之前只能用Cookie来存储数据的容量小、存取不便、容易被清除的问题。这个功能为客户端提供了极大的灵活性。

并非是只支持IE8以上,可以通过MDN官方的方法,变相的存储在Cookie内,不过这样的话有失Storage对象存在的意义了。

Session只作用于当前窗口,不能跨窗口读取其他窗口的SessionStorage数据库信息,浏览器每次新建、关闭都是直接导致当前窗口的数据库新建和销毁。兼容性如下:


Local作用于当前浏览器,即使你开2个Chrome浏览器(不是2个窗口)也还是共用一个路径地址。永远不会自动删除,所以如果我们要用LocalStorage保存敏感重要信息的时候也要注意不要放在Local里,而是放在Session里,关闭后进行清除,否则攻击者可以通过XSS攻击进行信息窃取。兼容性如下:


两者在PC端仅仅Chrome和Firefox有些许兼容偏差,移动端兼容性相同。


PS:当浏览器进入隐私浏览模式,会创建一个新的、临时的数据库来存储local storage的数据;当关闭隐私浏览模式时,该数据库将被清空并丢弃。

只是API名字不同。

localStorage.setItem('key', 'value');  // 设置
localStorage.getItem('key');  // 获取
localStorage.removeItem('key'); // 删除
localStorage.clear(); //清除所有

不过在储存和读取数据的时候,需要将数据进行JSON.stringify和JSON.parse,否则会强制改变数据类型

var obj = {
    name:'掘金',
    url:'juejin.com'
    }
var arr = [1,2,3]

//错误方法
localStorage.setItem('object',obj);  //object:"[object Object]" 无法读取
localStorage.setItem('array',arr);  //array:"1,2,3" 变成string格式

//正确方法:Object
localStorage.setItem('object',JSON.stringify(obj));//存储 object:"{"name":"掘金","url":"juejin.com"}"
JSON.parse(localStorage.getItem('object'));//读取 {name: "掘金", url: "juejin.com"}

//正确方法:Array
localStorage.setItem('array',JSON.stringify(arr));  //存储 array:"[1,2,3]"
JSON.parse(localStorage.getItem('array'));//读取  [1,2,3]

Session/Local优点

  • 存储数据量大,5MB。
  • 不会随http请求一起发送,有效的减少了请求大小
  • local跨窗口处理数据,能够减少相当一部分本地处理与维护状态。

Session/Local缺点

  • 本质是在读写文件,写入数据量大的话会影响性能(firefox是将localstorage写入内存中的)
  • XSS攻击窃取信息(为了安全性还是放session吧)
  • 兼容性,虽然说IE6已经死了,但是我就看过好多掘金段友还在写兼容IE6的文章....真是sun了dog,如果你们项目还在写IE6兼容,我敬你是条汉子!
  • 不能被爬虫读取

Session与Local总结

本来是用来做cookie的解决方案,适合于做小规模的简单结构数据储存与状态维护,不适宜储存敏感信息。可以运用在所有业务场景下。

IndexedDB

IndexedDB是HTML5规范里新出现的浏览器里内置的数据库。跟NoSQL很像,提供了类似数据库风格的数据存储和使用方式。但IndexedDB里的数据是永久保存,适合于储存大量结构化数据,有些数据本应该存在服务器,但是通过indexedDB,可以减轻服务器的大量负担,实现本地读取修改使用,以对象的形式存储,每个对象都有一个key值索引。

IndexedDB里的操作都是事务性的。一种对象存储在一个object store里,object store就相当于关系数据库里的表。IndexedDB可以有很多object store,object store里可以有很多对象。

首先来看兼容性


Window自带浏览器也只是部分支持,window8.1及以上才支持IE11。

首先,我们创建一个数据库

//首先定义一个自己的版本
var my = {
    name:'juejin',
    version:'1',
    db:null
}
//打开仓库,没有则创建
var request = window.indexedDB.open(my.name);
//window.indexedDB.open()的第二个参数即为版本号。在不指定的情况下,默认版本号为1.
//版本号不能是一个小数,否则会被转化成最近的整数。同时可能导致不会触发onupgradeneeded版本更新回调
console.log(request);

返回的是一个名字为IDBOpenDBRequest的对象。


里面有各个状态的回调参数,初始化的时候都是null,需要手动去挂载自定义的回调参数,从而实现window.indexedDB.open函数的状态回调控制,再去控制台Appliation的indexedDB查看


我们已经成功添加该名称为juejin的db对象了,security origin表示安全起源(我是在个人博客控制台进行创建的)

request.onerror = function(event){ //打开失败回调
    console.log(`${my.name} open indexedDB is Fail`);
}
request.onsuccess = function(event){ //打开成功回调
    console.warn(`${my.name} open indexedDB is success`);
    //将返回的值赋给自己控制的db版本对象,下面两种方法都能接收到。
    my.db = event.target.result|| request.result;
}
request.onupgradeneeded = function (event) {//版本变化回调参数,第一次设置版本号也会触发
    console.log('indexDB version change');
}
console.log(my.db);

返回的是一个db对象,里面包含后续对该db对象状态控制的回调方法。这些方法仍然需要自己定义。


怎么关闭db对象和删除db对象呢?关闭和删除是两个概念。

//关闭db对象,之后无法对其进行插入、删除操作。
my.db.close();

//而删除方法则挂载在window.indexedDB下,删除该db仓库
window.indexedDB.deleteDatabase(my.db);

这里需要注意的一点是此onclose()方法非上面代码调用的close()方法,my.db.close()调用的是__proto__原型内的方法。

知道如何建立和操作indexedDB之后,我们对object store进行添加表的操作。上文我们说到,indexedDB中没有表的概念,而是object store,一个数据库中可以包含多个object store,object store是一个灵活的数据结构,可以存放多种类型数据。也就是说一个object store相当于一张表,里面存储的每条数据和一个键相关联。

我们可以使用每条记录中的某个指定字段作为键值(keyPath),也可以使用自动生成的递增数字作为键值(keyGenerator),也可以不指定。选择键的类型不同,objectStore可以存储的数据结构也有差异。

创建object store对象只能从onupgradeneeded版本变化回调中进行。

//创建object store对象
request.onupgradeneeded = function() {    
    var db = request.result;    
    var objectStore = db.createObjectStore("LOL", {keyPath: "isbn"});    
    var titleIndex = objectStore.createIndex("by_hero", "hero", {unique: true});    
    var authorIndex = objectStore.createIndex("by_author", "author");
    objectStore.put({title: "亚索", author: "Roit", isbn: 123456});    
    objectStore.put({title: "提莫", author: "Roit", isbn: 234567});    
    objectStore.put({title: "诺手", author: "Hang", isbn: 345678});
};

createObjectStore方法有2个参数,第一个表示该object store表的名称,第二个是对象,keyPath为存储对象的某个属性(作为key值),options还有个参数:autoIncrement代表是否自增。接下来建立索引

var titleIndex = objectStore.createIndex("by_title", "title", {unique: true});    
var authorIndex = objectStore.createIndex("by_author", "author");

  第一个参数是索引的名称,第二个参数指定了根据存储数据的哪一个属性来构建索引,第三个options对象,其中属性unique的值为true表示不允许索引值相等。第二个索引没有options对象,接下来我们可以通过put方法添加数据了。

objectStore.put({hero: "亚索", author: "Roit", isbn: 123456});    
objectStore.put({hero: "提莫", author: "Roit", isbn: 234567});    
objectStore.put({hero: "诺手", author: "Hang", isbn: 345678});

整体代码写上

var my = {//定义控制版本       
    name:'juejin',       
    version:'1',       
    db:null     
};
var request = window.indexedDB.open(my.name);  //创建打开仓库     
request.onupgradeneeded = function() {//更新版本回调    
    var db = request.result;    
    var objectStore = db.createObjectStore("LOL", {keyPath: "isbn"});    
    var heroIndex = objectStore.createIndex("by_hero", "hero", {unique: true});    
    var authorIndex = objectStore.createIndex("by_author", "author");
    objectStore.put({hero: "亚索", author: "Roit", isbn: 123456});        
    objectStore.put({hero: "提莫", author: "Roit", isbn: 234567});        
    objectStore.put({hero: "诺手", author: "Hang", isbn: 345678});
};
request.onsuccess = function() {//成功回调    
    my.db = event.target.result|| request.result;    
    console.warn(`${my.name} indexedDB is success open Version ${my.version}`);
};
request.onerror = function() {//失败回调    
    console.warn(`${my.name} indexedDB is fail open  Version ${my.version}`);
};

 注意只有在运行环境下才会进行一个存储,本地打开静态文件是不会储存indexedDB的,虽然能弹出juejin indexedDB is success open。这样我们就成功创建了一个object store,我们到控制台去看下



by_hero表示在创建索引的时候,通过createObjectStore('by_hero','hero',{unique:true})的时候,通过key值为hero的对象,进行索引筛选的数据。再去by_author看下,


同理,通过key值为author的,进行索引的数据。这样就能够储存大量结构化数据。并且拥有索引能力,这一点比Storage强。当然,api也麻烦。接来下进行事务操作。

IndexedDB中,使用事务来进行数据库的操作。事务有三个模式,默认只读

  • readOnly只读。
  • readwrite读写。
  • versionchange数据库版本变化
//首先要创建一个事务,
var transaction = my.db.transaction('LOL', 'readwrite');
//获取objectStore数据
var targetObjectStore = transaction.objectStore('LOL');
//对预先设置的keyPath:isbn进行获取
var obj = targetObjectStore.get(345678);
//如果获取成功,执行回调
obj.onsuccess = function(e){    
    console.log('数据成功获取'+e.target.result)
}
//获取失败obj.onerror = function(e){    
    console.error('获取失败:'+e.target.result)
}


获取成功,拿到isbn为345678的数据。

第一个参数为需要关联的object store名称,第二个参数为事务模式,选择可读可写,与indexedDB一样,调用成功后也会触发onsuccess、onerror回调方法。可以读取了我们尝试去添加

targetObjectStore.add({hero: "盖伦", author: "Yuan", isbn: 163632});        
targetObjectStore.add({hero: "德邦", author: "Dema", isbn: 131245});        
targetObjectStore.add({hero: "皇子", author: "King", isbn: 435112});

有一点要注意,添加重复数据会更新。添加完毕后,去控制台看下


不对啊,肯定有的,刷新无数遍后,终于找到了解决办法。这可能是Chrome的一个BUG吧。


删除数据

//获取数据是get,删除数据是delete
targetObjectStore.delete(345678);


同样 ,需要输入筛选数据才会触发刷新,不过在日常中已经足够我们使用。

更新数据

var obj = targetObjectStore.get(123456);
//如果获取成功,执行回调
obj.onsuccess = function(e){    
    console.log('数据成功获取'+e.target.result);
    var data = e.target.result;
    data.hero = '亚索踩蘑菇挂了';
    //再put回去
    var updata = targetObjectStore.put(data);
    updata.onsuccess = function(event){
        console.log('更新数据成功'+event.target.result);
    }
}


又要筛选一遍.

当你需要便利整个存储空间中的数据时,你就需要使用到游标。游标使用方法如下:

var request = window.indexedDB.open('juejin');

request.onsuccess = function (event) {
    var db = event.target.result;
    var transaction = db.transaction('LOL', 'readwrite');
    //获取object store数据
    var objectStore = transaction.objectStore('LOL');
    //获取该数据的浮标
    var eachData = objectStore.openCursor();
        //openCursor有2个参数(遍历范围,遍历顺序)
    eachData.onsuccess = function (event) {
        var cursor = event.target.result;
        if (cursor){    
            console.log(cursor);
            cursor.continue();
        }
    };

    eachData.onerror = function (event) {
        consoe.error('each all data fail reason:'+event.target.result);
    };
}

这样通过openCursor得到的数据就类似于forEach输出,当表中无数据,仍会书法一次onsuccess回调

上面提到openCursor的两个参数,第一个是遍历范围,由indexedDB的 :IDBKeyRange的API进行实现,主要有以下几个值

//区间向上匹配,第一个参数指定边界值,第二个参数是否包含边界值,默认false包含。
lowerBound('边界值',Boolean);
var index = IDBKeyRange.lowerBound(1);//匹配key>=1
var index = IDBKeyRange.lowerBound(1,true);//匹配key>1

//单一匹配,指定参数值
only('值');
var index = IDBKeyRange.only(1);//匹配key===1;

//区间向下搜索,第一个参数指定边界值,第二个参数是否包含边界值,默认false包含。
upperBound('边界值',Boolean);
var index = IDBKeyRange.upperBound(2);//匹配key<=2
var index = IDBKeyRange.upperBound(2,true);//匹配key<2

//区间搜索,第一个参数指定开始边界值,第二个参数结束边界值,
//        第三个指定开始边界值是否包含边界值,默认false包含。第四个指定结束边界值是否包含边界值,默认false
bound('边界值',Boolean);
var index = IDBKeyRange.bound(1,10,true,false);//匹配key>1&&key<=10;

openCursor第二个参数,遍历顺序,指定游标遍历时的顺序和处理相同id(keyPath属性指定字段)重复时的处理方法。改范围通过特定的字符串来获取。其中:

  • IDBCursor.next,从前往后获取所有数据(包括重复数据)
  • IDBCursor.prev,从后往前获取所有数据(包括重复数据)
  • IDBCursor.nextunique,从前往后获取数据(重复数据只取第一条)
  • IDBCursor.prevunique,从后往前获取数据(重复数据只取第一条)

我们来试一下

var request = window.indexedDB.open('juejin');
request.onsuccess = function (event) {
    var db = event.target.result;
    var transaction = db.transaction('LOL', 'readwrite');
    //获取object store数据
    var objectStore = transaction.objectStore('LOL');
    //bound('边界值',Boolean);匹配key>22000&&key<=400000;
    var index = IDBKeyRange.bound(220000,400000,true,false);
    //获取该数据的浮标,从前往后顺序索引,包括重复数据
    var eachData = objectStore.openCursor(index,IDBCursor.NEXT);
    eachData.onsuccess = function (event) {
        var cursor = event.target.result;
        console.log(cursor);
        if (cursor) cursor.continue();
    };
    eachData.onerror = function (event) {
        consoe.error('each all data fail reason:'+event.target.result);
    };
}

搜索key值为220000到40000万之间的数据,搜索出一条。


好了,indexedDB基本和事务操作讲的差不多了,现在说说它另一方面:

目前为止,所知道在IndexedDB中,键值对中的key值可以接受以下几种类型的值:

  • Number
  • String
  • Array
  • Object
  • Binary二进制

但是储存数据千万要注意的一点是,如果储存了isbn相同的数据,是无效操作,甚至可能引起报错。

keyPath能够接受的数据格式,示例中createObjectStore时设置的{KeyPath:'isbn'},为主键

  • Blob
  • File
  • Array
  • String

至于value几乎能接受所有数据格式。

indexedDB优点

  • 替代web SQL,与service work搭配简直无敌,实现离线访问不在话下,
  • 数据储存量无限大(只要你硬盘够),Chrome规定了最多只占硬盘可用空间的1/3,可以储存结构化数据带来的好处是可以节省服务器的开支。

indexedDB缺点

  • 兼容性问题,只有ie11以上,根据业务场景慎重考虑需求。
  • 同源策略,部分浏览器如Safari手机版隐私模式在访问IndexedDB时,可能会出现由于没有权限而导致的异常(LocalStorage也会),需要进行异常处理。
  • API类似SQL比较复杂,操作大量数据的时候,可能存在性能上的消耗。
  • 用户在清除浏览器缓存时,可能会清除IndexedDB中相关的数据。

ServiceWork

什么是ServiceWork?serviceWork是W3C 2014年提出的草案,是一种独立于当前页面在后台运行的脚本。这里的后台指的是浏览器后台,能够让web app拥有和native app一样的离线程序访问能力,让用户能够进行离线体验,消息推送体验。service worker是一段脚本,与web worker一样,也是在后台运行。作为一个独立的线程,运行环境与普通脚本不同,所以不能直接参与web交互行为。native app可以做到离线使用、消息推送、后台自动更新,service worker的出现是正是为了使得web app也可以具有类似的能力。

ServiceWork产生的意义

打开了现在浏览器单线程的革面,随着前端性能越来越强,要求越来越高,我们都知道在浏览器中,JavaScript是单线程执行的,如果涉及到大量运算的话,很有可能阻碍css tree的渲染,从而阻塞后续代码的执行运算速度,ServiceWork的出现正好解决了这个问题,将一些需要大规模数据运算和获取  资源文件在后台进行处理,然后将结果返回到主线程,由主线程来执行渲染,这样可以避免主线程被巨量的逻辑和运算所阻塞。这样的大大的提升了JavaScript线程在处理大规模运算时候的一个能力, 这也是ServiceWork本身的巨大优势,比如我们要进行WebGBL场景下3D模型和数据的运算,一个普通的数据可能高达几MB,如果放在主线程进行运算的话,会严重阻碍页面的渲染,这个时候就非常适合ServiceWork进行后台计算,再将结果返回到主线程进行渲染。

ServiceWork的功能

service worker可以:

  1. 消息推送、传递
  2. 在不影响页面通信以及阻塞线程的情况下,后台同步运算。
  3. 网络拦截、代理,转发请求,伪造响应
  4. 离线缓存
  5. 地理围栏定位

说了这么多,到底跟我们实际工作中有什么用处呢,这里就要介绍google 的PWD(Progressive Web Apps),它是一种Web App新模型,渐进式的web App,它依赖于Service Work,是现在没有网络的环境中也能够提供基本的页面访问,不会出现‘未连接到互联网’,可以优化网页渲染及网络数据访问,并且可以添加到手机桌面,和普通应用一样有全屏状态和消息推送的功能。



这是Service Work的生命周期,首先没有Service Work的情况下会进行一个安装中的状态,返回一个promise实例,reject的会走到Error这一步,resolve安装成功,当安装完成后,进入Activated激活状态,在这一阶段,你还可以升级一个service worker的版本,具体内容我们会在后面讲到。在激活之后,service worker将接管所有在自己管辖域范围内的页面,但是如果一个页面是刚刚注册了service worker,那么它这一次不会被接管,到下一次加载页面的时候,service worker才会生效。当service worker接管了页面之后,它可能有两种状态:要么被终止以节省内存,要么会处理fetch(拦截和发出网络请求)和message(信息传递)事件,这两个事件分别是页面初始化的时候产生了一个网络请求出现或者页面上发送了一个消息。

目前有哪些页面支持service Work呢?

在Chrome浏览器地址栏输入chrome://inspect/#service-workers,可以看到目前为止你访问过所有支持service work的网站

你也可以打开控制台,到Application,点击serviceWork这一栏,

那怎么才能体验到service Work呢,我们以Vue官网为例,首先打开https://cn.vuejs.org/,等待加载完成,现在关掉你的WiFi和所有能连上互联网的工具。再刷新地址栏页面



是不是感觉很新奇,怎么做到呢,继续往下看。需要运行本地环境,各种方法自行百度,我使用的是自己购买的腾讯云服务器,nginx多开几个端口,SFTP自动上传。也可以搭建本地localhost,切记不可以用IP地址,ServiceWork不支持域名为IP的网站,做好这些我们开始。

首先创建一个文件夹,再创建index.htmlindex.cssapp.jsservicework.js这些文件我们后续都要用到。

index.html

<!DOCTYPE html>
<html lang="en">
<head>  
    <meta charset="UTF-8">  
    <meta name="viewport" content="width=device-width, initial-scale=1.0">  
    <meta http-equiv="X-UA-Compatible" content="ie=edge">  
    <title>ServiceWork</title>
</head>
<body>
    <h1>ServiceWork</h1>
</body>
<script src="./app.js"></script>
<link rel="stylesheet" href="./index.css">
</html>

引入了一个main.css文件和一个app.js

main.css

h1{  
    color:red;
    text-align:center;
}

app.js

alert(1);

测试成功弹出alert(1)后,我们开始写代码。

首先要确定是否支持ServiceWork

app.js

if (navigator.serviceWorker) {   
//先注入注册文件,第二个参数为作用域,为当前目录    
navigator.serviceWorker.register('./servicework.js', {
        scope: './'    
    }).then(function (reg) {
        console.warn('install ServiceWork success',reg)    
    }).catch(function (err) {        
        console.error(err)    
    })
} else {    
    //不支持serviceWork操作
}

导入注册配置文件,返回一个promise,触发相应回调,然后再去修改servicework.js文件,

//self是serviceWorker的实例。
console.log(self)//
给实例监听安装事件,成功触发回调self.addEventListener('install', function (e) {
    //ExtendableEvent.waitUntil()扩展事件的生命周期。    
    e.waitUntil(        
    //我们通过打开名称为'app-v1'的缓存,将读取到的文件储存到cache里
        caches.open('app-v1').then(function (cache) {
           console.log('caches staticFile success');
           //添加cache
           return cache.addAll([
               './app.js',
               './servicework.html',
               './servicework.js',
               './index.css'
           ]);
         })
    );
 });

ExtendableEvent.waitUntil()接受一个promise对象,它可以扩展时间的生命周期,延长事件的寿命从而阻止浏览器在事件中的异步操作完成之前终止服务工作线程。它可以扩展时间的生命周期,延长事件的寿命从而阻止浏览器在事件中的异步操作完成之前终止服务工作线程。


install的时候,它会延迟将安装的works视为installing,直到传递的Promise被成功地resolve。确保服务工作线程在所有依赖的核心cache被缓存之前都不会被安装。

一旦 Service Worker 成功安装,它将转换到Activation阶段。如果以前的 Service Worker 还在服务着任何打开的页面,则新的 Service Worker 进入 waiting 状态。新的 Service Worker 仅在旧的 Service Worker 没有任何页面被加载时激活。这确保了在任何时间内只有一个版本的 Service Worker 正在运行。当进行

activited
的时候,它延迟将active work视为已激活的,直到传递的Promise被成功地resolve。确保功能时间不会被分派到ServiceWorkerGlobalScope 对象。


更详细的可以到MDN查看该API更多说明。

成功丢到缓存里后,就可以使用fetch进行网络拦截了。


  //同样的方法,监听fetch事件, 
self.addEventListener('fetch', function (event) {
    //respondWith方法产生一个request,response。
    event.respondWith(
      //利用match方法对event.request所请求的文件进行查询
      caches.match(event.request).then(
        function (res) {
          console.log(res, event.request);
          //如果cache中有该文件就返回。
          if (res) {
            return res
          } else {
            //没有找到缓存的文件,再去通过fetch()请求资源
            fetch(res.url).then(function (res) {
              if (res) {
                if (!res || res.status !== 200 || res.type !== 'basic') {
                  return res;
                }
                //再将请求到的数据丢到cache缓存中..
                var fetchRequest = event.request.clone();
                var fileClone = res.clone();
                caches.open('app-v1')
                  .then(function (cache) {
                    cache.put(event.request, fileClone);
                  });
              } else {
                //没有请求到该文件,报错处理
                console.error('file not found:' + event.reuqest + '==>' + res.url)
              }
            })
          }
        }
      )
    );
  });

对于前端大家肯定很熟悉requestresponse代表着什么,event.respondWith()会根据当前控制的页面产生一个requestrequest再去生成自定义的responsenetwork error 或者 Fetch的方式resolve


fetch()对网络进行拦截,代理请求,先读取本地文件,没有资源再去请求,很大程度的节约了网络请求消耗。

现在我们去试试有没有成功!


啊哈,漂亮!这样就实现了离线访问,但是在实际项目中,尽量不要缓存servicework.js文件,可能无法及时生效,进行后续修改。我们去控制台看下

已经安装好了,并且在运行中。

整体大概的流程如下

service work.js 的更新

Service Work.js 的更新不仅仅只是简单的更新,为了用户可靠性体验,里面还是有很多门道的。

  • 首先更新ServiceWork.js 文件,这是最主要的。只有更新 ServiceWork.js 文件之后,之后的流程才能触发。ServiceWork.js 的更新也很简单,直接改动 ServiceWork.js 文件即可。浏览器会自动检查差异性(就算只有 1B 的差异也行),然后进行获取。
  • 新的 ServiceWork.js 文件开始下载,并且 install 事件被触发
  • 此时,旧的 ServiceWork 还在工作,新的 ServiceWork 进入 waiting 状态。注意,此时并不存在替换
  • 现在,两个 ServiceWork 同时存在,不过还是以前的 ServiceWork 在掌管当前网页。只有当 old service work 不工作,即,被 terminated 后,新的 ServiceWork 才会发生作用。具体行为就是,该网页被关闭一段时间,或者手动的清除 service worker。然后,新的 Service Work 就度过可 waiting 的状态。
  • 一旦新的 Service Work 接管,则会触发 activate 事件。

整个流程图为:

                

一个版本的缓存创建好之后,我们也可以设置多个缓存,那怎去删除不在白名单中的缓存呢

self.addEventListener('activate', function(event) {
    //上个版本,我们使用的是'app-v1'的缓存,所以就需要进行清除,进行'app-v2'版本的缓存储存
  var cacheWhitelist = ['app-v1'];

  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

如果 new service work要立即生效呢,那就要用到skipWaiting,install 阶段使用 self.skipWaiting(); ,因为上面说到 new Service Work 加载后会触发 install 然后进入 waiting 状态。那么,我们可以直接在 install 阶段跳过等待,直接让 new Service Work 进行接管。

self.addEventListener('install',function(event) {
  self.skipWaiting();

  event.waitUntil(
    // caching etc
  );
});

上面的 service work 更新都是在一开始加载的时候进行的,那么,如果用户需要长时间停留在你的网页上,有没有什么手段,在间隔时间检查更新呢?

有的,可以使用 registration.update() 来完成。

navigator.serviceWorker.register('./ServiceWork.js').then(function(reg){
  // sometime later…
  reg.update();
});

另外,如果你一旦用到了 ServiceWork.js 并且确定路由之后,请千万不要在更改路径了,因为,浏览器判断 ServiceWork.js 是否更新,是根据 ServiceWork.js 的路径来的。如果你修改的 ServiceWork.js 路径,而以前的 ServiceWork.js 还在作用,那么你新的 ServiceWork 永远无法工作。除非你手动启用 update 进行更新。

你想要一个文件更新,只需要在 ServiceWork 的 fetch阶段使用 caches 进行缓存即可。一开始我们的 install 阶段的代码为:

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('app-v1').then(function(cache) {
      return cache.addAll([
        './app.js',
        './servicework.html',
        './index.css'
      ]);
    })
  );
});


self.addEventListener('install', function (event) {
      var now = Date.now();
      // 事先设置好需要进行更新的文件路径
      var urlsToPrefetch = [
        './index.css',
        './servicework.html'
      ];
      event.waitUntil(
        caches.open(CURRENT_CACHES.prefetch).then(function (cache) {
          var cachePromises = urlsToPrefetch.map(function (urlToPrefetch) {
            // 使用 url 对象进行路由拼接
            var url = new URL(urlToPrefetch, location.href);
            url.search += (url.search ? '&' : '?') + 'cache-bust=' + now;
            // 创建 request 对象进行流量的获取
             var request = new Request(url, {
              mode: 'no-cors'
            });
            // 手动发送请求,用来进行文件的更新
            return fetch(request).then(function (response) {
              if (response.status >= 400) {
                // 解决请求失败时的情况
                     throw new Error('request for ' + urlToPrefetch +
                  ' failed with status ' + response.statusText);
              }
             // 将成功后的 response 流,存放在 caches 套件中,完成指定文件的更新。
              return cache.put(urlToPrefetch, response);
            }).catch(function (error) {
              console.error('Not caching ' + urlToPrefetch + ' due to ' + error);
            });
          });
          return Promise.all(cachePromises).then(function () {
            console.log('Pre-fetching complete.');
          });
        }).catch(function (error) {
             console.error('Pre-fetching failed:', error);
         })
        );
});

传送门Github查看该段代码。


当成功获取到缓存之后, ServiceWork 并不会直接进行替换,他会等到用户下一次刷新页面过后,使用新的缓存文件。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.open('app-v1').then(function(cache) {
      return cache.match(event.request).then(function(response) {
        var fetchPromise = fetch(event.request).then(function(res) {
          cache.put(event.request, res.clone());
          return res;
        })
        return response || fetchPromise;
      })
    })
  );
});

               

更详细的其他方法运用可以参考这篇文章.

本文只讲本地缓存,之后会将地理围栏、消息推送相关信息更新在博客,有兴趣的朋友可以收藏本文章,更具体规范的代码内容可以到这查看

service work(PWA)缺点:

  •     缓存的问题,要定期清理。超出的时候会出现 Uncaught (in promise) DOMException: Quota exceeded. 异常。清理后必须要重启浏览器才生效。
  •     浏览器兼容,头疼的问题。IE和safari不兼容


优点:

    如上文所述,有着消息推送、网络拦截代理、后台运算、离线缓存、地理围栏等很实用的一些技术。

本文参考了很多大神的代码,不喜勿喷,诚心学习请指教。