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

我的博客css

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

cookie

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


咱们先来看一下在平常操做中是怎么接收到这个cookie的。vue


开始以前,将本地cookie的值所有清除。nginx


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



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

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


这时候能够看到Cookie中有服务器返回的全部cookie以及相对应的值和其余配置;chrome

每次客户端第一次请求,服务器接收到请求后为该客户端设置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不兼容


优势:

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

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

相关文章
相关标签/搜索