新浦京娱乐场官网-301net-新浦京娱乐www.301net
做最好的网站

使用 Service Worker 做一个 PWA 离线网页应用

应用 Service Worker 做二个 PWA 离线网页应用

2017/10/09 · JavaScript · PWA, Service Worker

最初的稿件出处: 人人网FED博客   

在上一篇《自家是怎么让网址用上HTML5 Manifest》介绍了怎么用Manifest做一个离线网页应用,结果被分布网上好朋友调侃说这几个事物已经被deprecated,移出web规范了,现在被ServiceWorker替代了,不管什么样,Manifest的部分合计依旧得以借用的。作者又将网址晋级到了瑟维斯Worker,假设是用Chrome等浏览器就用ServiceWorker做离线缓存,纵然是Safari浏览器就依旧用Manifest,读者可以张开那几个网址感受一下,断网也是能日常展开。

1. 什么是Service Worker

Service Worker是Google倡导的完毕PWA(Progressive Web App)的几位命关天剧中人物,PWA是为了缓慢解决守旧Web APP的败笔:

(1)没有桌面入口

(2)不能离线使用

(3)没有Push推送

那Service Worker的具体表现是何许的呢?如下图所示:

图片 1

ServiceWorker是在后台运转的一条服务Worker线程,上海体育地方作者开了多个标签页,所以展现了四个Client,不过不管开多少个页面都独有一个Worker在担当管理。那一个Worker的干活是把一些财富缓存起来,然后拦截页面包车型大巴央求,先看下缓存Curry有未有,倘若某些话就从缓存里取,响应200,反之未有的话就走不奇怪的伏乞。具体来讲,ServiceWorker结合Web App Manifest能完毕以下工作(这也是PWA的检查评定标准):

图片 2

包罗能够离线使用、断网时再次回到200、能唤起客户把网址增加贰个Logo到桌面上等。

2. Service Worker的辅助情况

瑟维斯 Worker最近独有Chrome/Firfox/Opera扶植:

图片 3

Safari和Edge也在备选辅助Service Worker,由于ServiceWorker是Google基本的一项正式,对于生态比较封闭的Safari来讲也是迫于时势开始希图帮忙了,在Safari TP版本,能够看出:

图片 4

在推行功效(Experimental Features)里已经有ServiceWorker的菜单项了,只是纵然展开也是不可能用,会唤起您还并未有完结:

图片 5

但随便什么样,起码注明Safari已经企图帮助ServiceWorker了。别的还足以观望在当年二〇一七年5月宣布的Safari 11.0.1版本已经协理WebRTC了,所以Safari依旧一个前行的儿女。

Edge也准备援救,所以Service Worker的前景十一分美好。

3. 使用Service Worker

ServiceWorker的选取套路是首先登场记三个Worker,然后后台就能够运转一条线程,能够在这里条线程运营的时候去加载一些能源缓存起来,然后监听fetch事件,在此个事件里拦截页面包车型大巴央求,先看下缓存里有没有,借使有直接再次回到,不然通常加载。恐怕是一同头不缓存,各个能源哀告后再拷贝一份缓存起来,然后下一回呼吁的时候缓存里就有了。

(1)注册贰个Service Worker

Service Worker对象是在window.navigator里面,如下代码:

JavaScript

window.addEventListener("load", function() { console.log("Will the service worker register?"); navigator.serviceWorker.register('/sw-3.js') .then(function(reg){ console.log("Yes, it did."); }).catch(function(err) { console.log("No it didn't. This happened: ", err) }); });

1
2
3
4
5
6
7
8
9
window.addEventListener("load", function() {
    console.log("Will the service worker register?");
    navigator.serviceWorker.register('/sw-3.js')
    .then(function(reg){
        console.log("Yes, it did.");
    }).catch(function(err) {
        console.log("No it didn't. This happened: ", err)
    });
});

在页面load完之后注册,注册的时候传一个js文件给它,那一个js文件就是ServiceWorker的周转条件,假若无法得逞注册的话就可以抛万分,如Safari TP就算有其一指标,不过会抛万分不可能运用,就能够在catch里面处理。这里有个难题是为何须求在load事件运转呢?因为你要十一分运行二个线程,运行之后您只怕还大概会让它去加载能源,那么些都以急需占用CPU和带宽的,大家应有有限支撑页面能健康加载完,然后再开发银行我们的后台线程,不可能与健康的页面加载爆发竞争,这些在低档移动设备意义比不小。

还也许有有个别亟需注意的是ServiceWorker和Cookie同样是有Path路线的概念的,借使你设定贰个cookie假使叫time的path=/page/A,在/page/B这几个页面是不可见获取到那几个cookie的,假若设置cookie的path为根目录/,则具有页面都能获得到。类似地,假设注册的时候使用的js路线为/page/sw.js,那么那几个ServiceWorker只好管理/page路线下的页面和能源,而不可以看到管理/api路线下的,所以平时把ServiceWorker注册到五星级目录,如上面代码的”/sw-3.js”,那样这一个ServiceWorker就会接管页面包车型客车具备财富了。

(2)Service Worker安装和激活

登记完未来,ServiceWorker就博览会开设置,那个时候会触发install事件,在install事件之中能够缓存一些能源,如下sw-3.js:

JavaScript

const CACHE_NAME = "fed-cache"; this.addEventListener("install", function(event) { this.skipWaiting(); console.log("install service worker"); // 创设和开垦一个缓存库 caches.open(CACHE_NAME); // 首页 let cacheResources = ["]; event.waitUntil( // 诉求财富并加多到缓存里面去 caches.open(CACHE_NAME).then(cache => { cache.addAll(cacheResources); }) ); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const CACHE_NAME = "fed-cache";
this.addEventListener("install", function(event) {
    this.skipWaiting();
    console.log("install service worker");
    // 创建和打开一个缓存库
    caches.open(CACHE_NAME);
    // 首页
    let cacheResources = ["https://fed.renren.com/?launcher=true"];
    event.waitUntil(
        // 请求资源并添加到缓存里面去
        caches.open(CACHE_NAME).then(cache => {
            cache.addAll(cacheResources);
        })
    );
});

透过上边的操作,创造和增添了一个缓存库叫fed-cache,如下Chrome调节台所示:

图片 6

ServiceWorker的API基本上都以回来Promise对象防止堵塞,所以要用Promise的写法。上面在设置ServiceWorker的时候就把首页的央求给缓存起来了。在ServiceWorker的运行意况之中它有二个caches的大局对象,那么些是缓存的入口,还应该有八个常用的clients的全局对象,二个client对应一个标签页。

在ServiceWorker里面能够利用fetch等API,它和DOM是割裂的,未有windows/document对象,不可能直接操作DOM,无法直接和页面交互,在ServiceWorker里面不可能得到消息当前页面张开了、当前页面包车型地铁url是何等,因为贰个ServiceWorker管理当前开发的多少个标签页,能够因而clients知道全数页面包车型客车url。还会有能够通过postMessage的点子和主页面相互传递音讯和多少,进而做些调整。

install完事后,就能触发Service Worker的active事件:

JavaScript

this.addEventListener("active", function(event) { console.log("service worker is active"); });

1
2
3
this.addEventListener("active", function(event) {
    console.log("service worker is active");
});

ServiceWorker激活之后就能够监听fetch事件了,大家希望每获得三个财富就把它缓存起来,就不要像上一篇涉嫌的Manifest须求先生成叁个列表。

您大概会问,当本人刷新页面包车型客车时候不是又再次注册安装和激活了一个ServiceWorker?即使又调了叁遍注册,但并不会重新挂号,它发现”sw-3.js”这几个已经注册了,就不会再登记了,进而不会触发install和active事件,因为脚下ServiceWorker已是active状态了。当必要更新ServiceWorker时,如变成”sw-4.js”,只怕改动sw-3.js的文书内容,就能够重新挂号,新的ServiceWorker会先install然后步入waiting状态,等到重启浏览器时,老的ServiceWorker就能被轮换掉,新的ServiceWorker步入active状态,假若不想等到再一次启航浏览器能够像下面同样在install里面调skipWaiting:

JavaScript

this.skipWaiting();

1
this.skipWaiting();

(3)fetch资源后cache起来

正如代码,监听fetch事件做些管理:

JavaScript

this.addEventListener("fetch", function(event) { event.respondWith( caches.match(event.request).then(response => { // cache hit if (response) { return response; } return util.fetchPut(event.request.clone()); }) ); });

1
2
3
4
5
6
7
8
9
10
11
12
this.addEventListener("fetch", function(event) {
    event.respondWith(
        caches.match(event.request).then(response => {
            // cache hit
            if (response) {
                return response;
            }
            return util.fetchPut(event.request.clone());
        })
    );
});

先调caches.match看一下缓存里面是否有了,若是有直接重回缓存里的response,不然的话不奇怪须求能源并把它放到cache里面。放在缓存里能源的key值是Request对象,在match的时候,必要伏乞的url和header都同一才是千篇一律的财富,能够设定第1个参数ignoreVary:

JavaScript

caches.match(event.request, {ignoreVary: true})

1
caches.match(event.request, {ignoreVary: true})

表示要是伏乞url同样就觉着是同一个能源。

下边代码的util.fetchPut是如此完成的:

JavaScript

let util = { fetchPut: function (request, callback) { return fetch(request).then(response => { // 跨域的能源直接return if (!response || response.status !== 200 || response.type !== "basic") { return response; } util.putCache(request, response.clone()); typeof callback === "function" && callback(); return response; }); }, putCache: function (request, resource) { // 后台不要缓存,preview链接也决不缓存 if (request.method === "GET" && request.url.indexOf("wp-admin") < 0 && request.url.indexOf("preview_id") < 0) { caches.open(CACHE_NAME).then(cache => { cache.put(request, resource); }); } } };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let util = {
    fetchPut: function (request, callback) {
        return fetch(request).then(response => {
            // 跨域的资源直接return
            if (!response || response.status !== 200 || response.type !== "basic") {
                return response;
            }
            util.putCache(request, response.clone());
            typeof callback === "function" && callback();
            return response;
        });
    },
    putCache: function (request, resource) {
        // 后台不要缓存,preview链接也不要缓存
        if (request.method === "GET" && request.url.indexOf("wp-admin") < 0
              && request.url.indexOf("preview_id") < 0) {
            caches.open(CACHE_NAME).then(cache => {
                cache.put(request, resource);
            });
        }
    }
};

内需小心的是跨域的财富不可能缓存,response.status会重回0,即使跨域的财富支撑COEnclaveS,那么可以把request的mod改成cors。假使诉求失利了,如404也许是过期等等的,那么也直接重返response让主页面管理,不然的话说明加载成功,把这么些response克隆一个平放cache里面,然后再重临response给主页面线程。注意能舒缓存里的财富平常只好是GET,通过POST获取的是无法缓存的,所以要做个判定(当然你也能够手动把request对象的method改成get),还只怕有把部分私家不希望缓存的能源也做个决断。

如此那般假如客户打开过一回页面,瑟维斯Worker就安装好了,他刷新页面大概打开首个页面的时候就可以知道把要求的财富一一做缓存,满含图形、CSS、JS等,只要缓存里有了随意顾客在线或然离线都能够正常访谈。那样大家本来会有叁个主题素材,这几个缓存空间到底有多大?上一篇大家关系Manifest也算是地点存款和储蓄,PC端的Chrome是5Mb,其实那几个说法在新本子的Chrome已经不纯粹了,在Chrome 61本子能够见见地点存款和储蓄的空春天利用状态:

图片 7

在那之中Cache Storage是指ServiceWorker和Manifest占用的长空尺寸和,上航海用教室能够看见总的空间尺寸是20GB,差不离是unlimited,所以基本上不用顾虑缓存会非常不足用。

(4)cache html

地方第(3)步把图片、js、css缓存起来了,可是如若把页面html也缓存了,举例把首页缓存了,就能够有八个难堪的主题材料——ServiceWorker是在页面注册的,可是未来获得页面的时候是从缓存取的,每一次都是同一的,所以就产生不能创新ServiceWorker,如形成sw-5.js,可是PWA又供给大家能缓存页面html。那咋做吧?谷歌(Google)的开采者文书档案它只是提到会存在此个主题素材,但并未表达怎么消除那些标题。那些的标题标化解即将求大家要有二个体制能明了html更新了,进而把缓存里的html给替换掉。

Manifest更新缓存的机制是去看Manifest的文件内容有没有爆发变化,倘诺发生变化了,则会去创新缓存,ServiceWorker也是依照sw.js的文本内容有未有发生变化,大家能够借鉴那么些思量,假使央求的是html并从缓存里抽出来后,再发个哀告获取贰个文书看html更新时间是或不是发生变化,要是产生变化了则印证发生变动了,进而把缓存给删了。所以能够在服务端通过决定这么些文件进而去立异顾客端的缓存。如下代码:

JavaScript

this.add伊夫ntListener("fetch", function(event) { event.respondWith( caches.match(event.request).then(response => { // cache hit if (response) { //假使取的是html,则看发个乞请看html是或不是更新了 if (response.headers.get("Content-Type").indexOf("text/html") >= 0) { console.log("update html"); let url = new UPRADOL(event.request.url); util.updateHtmlPage(url, event.request.clone(), event.clientId); } return response; } return util.fetchPut(event.request.clone()); }) ); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
this.addEventListener("fetch", function(event) {
 
    event.respondWith(
        caches.match(event.request).then(response => {
            // cache hit
            if (response) {
                //如果取的是html,则看发个请求看html是否更新了
                if (response.headers.get("Content-Type").indexOf("text/html") >= 0) {
                    console.log("update html");
                    let url = new URL(event.request.url);
                    util.updateHtmlPage(url, event.request.clone(), event.clientId);
                }
                return response;
            }
 
            return util.fetchPut(event.request.clone());
        })
    );
});

因此响应头header的content-type是或不是为text/html,即使是的话就去发个伏乞获取一个文书,依据那么些文件的源委决定是或不是须求删除缓存,那些立异的函数util.updateHtmlPage是如此实现的:

JavaScript

let pageUpdateTime = { }; let util = { updateHtmlPage: function (url, htmlRequest) { let pageName = util.getPageName(url); let jsonRequest = new Request("/html/service-worker/cache-json/" pageName ".sw.json"); fetch(jsonRequest).then(response => { response.json().then(content => { if (pageUpdateTime[pageName] !== content.update提姆e) { console.log("update page html"); // 如果有更新则再次猎取html util.fetchPut(htmlRequest); pageUpdateTime[pageName] = content.updateTime; } }); }); }, delCache: function (url) { caches.open(CACHE_NAME).then(cache => { console.log("delete cache "

  • url); cache.delete(url, {ignoreVary: true}); }); } };
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
let pageUpdateTime = {
 
};
let util = {
    updateHtmlPage: function (url, htmlRequest) {
        let pageName = util.getPageName(url);
        let jsonRequest = new Request("/html/service-worker/cache-json/" pageName ".sw.json");
        fetch(jsonRequest).then(response => {
            response.json().then(content => {
                if (pageUpdateTime[pageName] !== content.updateTime) {
                    console.log("update page html");
                    // 如果有更新则重新获取html
                    util.fetchPut(htmlRequest);
                    pageUpdateTime[pageName] = content.updateTime;
                }
            });
        });
    },
    delCache: function (url) {
        caches.open(CACHE_NAME).then(cache => {
            console.log("delete cache " url);
            cache.delete(url, {ignoreVary: true});
        });
    }
};

代码先去取得一个json文件,三个页面会对应贰个json文件,那个json的原委是这么的:

JavaScript

{"updateTime":"10/2/2017, 3:23:57 PM","resources": {img: [], css: []}}

1
{"updateTime":"10/2/2017, 3:23:57 PM","resources": {img: [], css: []}}

其间根本有三个updateTime的字段,要是本地内部存款和储蓄器未有这几个页面包车型客车updateTime的多少依旧是和新型updateTime不雷同,则重复去赢得 html,然后放到缓存里。接着须求文告页面线程数据发生变化了,你刷新下页面吗。那样就不要等顾客刷新页面手艺卓有作用了。所以当刷新完页面后用postMessage文告页面:

JavaScript

let util = { postMessage: async function (msg) { const allClients = await clients.matchAll(); allClients.forEach(client => client.postMessage(msg)); } }; util.fetchPut(htmlRequest, false, function() { util.postMessage({type: 1, desc: "html found updated", url: url.href}); });

1
2
3
4
5
6
7
8
9
let util = {
    postMessage: async function (msg) {
        const allClients = await clients.matchAll();
        allClients.forEach(client => client.postMessage(msg));
    }
};
util.fetchPut(htmlRequest, false, function() {
    util.postMessage({type: 1, desc: "html found updated", url: url.href});
});

并规定type: 1就意味着那是一个创新html的信息,然后在页面监听message事件:

JavaScript

if("serviceWorker" in navigator) { navigator.serviceWorker.addEventListener("message", function(event) { let msg = event.data; if (msg.type === 1 && window.location.href === msg.url) { console.log("recv from service worker", event.data); window.location.reload(); } }); }

1
2
3
4
5
6
7
8
9
if("serviceWorker" in navigator) {
    navigator.serviceWorker.addEventListener("message", function(event) {
        let msg = event.data;
        if (msg.type === 1 && window.location.href === msg.url) {
            console.log("recv from service worker", event.data);
            window.location.reload();
        }  
    });
}

然后当我们要求更新html的时候就更新json文件,那样顾客就能够收看最新的页面了。或许是当客商重新启航浏览器的时候会招致ServiceWorker的运转内部存款和储蓄器都被清空了,即存储页面更新时间的变量被清空了,这年也会另行供给页面。

亟待注意的是,要把那些json文件的http cache时间设置成0,那样浏览器就不会缓存了,如下nginx的安排:

JavaScript

location ~* .sw.json$ { expires 0; }

1
2
3
location ~* .sw.json$ {
    expires 0;
}

因为那么些文件是供给实时获取的,无法被缓存,firefox私下认可会缓存,Chrome不会,加上http缓存时间为0,firefox也不会缓存了。

再有一种更新是客户更新的,比方客户发表了商酌,要求在页面文告service worker把html缓存删了再度赢得,那是多个扭曲的音信文告:

JavaScript

if ("serviceWorker" in navigator) { document.querySelector(".comment-form").addEventListener("submit", function() { navigator.serviceWorker.controller.postMessage({ type: 1, desc: "remove html cache", url: window.location.href} ); } }); }

1
2
3
4
5
6
7
8
9
10
if ("serviceWorker" in navigator) {
    document.querySelector(".comment-form").addEventListener("submit", function() {
            navigator.serviceWorker.controller.postMessage({
                type: 1,
                desc: "remove html cache",
                url: window.location.href}
            );
        }
    });
}

Service Worker也监听message事件:

JavaScript

const messageProcess = { // 删除html index 1: function (url) { util.delCache(url); } }; let util = { delCache: function (url) { caches.open(CACHE_NAME).then(cache => { console.log("delete cache "

  • url); cache.delete(url, {ignoreVary: true}); }); } }; this.addEventListener("message", function(event) { let msg = event.data; console.log(msg); if (typeof messageProcess[msg.type] === "function") { messageProcess[msg.type](msg.url); } });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const messageProcess = {
    // 删除html index
    1: function (url) {
        util.delCache(url);
    }
};
 
let util = {
    delCache: function (url) {
        caches.open(CACHE_NAME).then(cache => {
            console.log("delete cache " url);
            cache.delete(url, {ignoreVary: true});
        });
    }
};
 
this.addEventListener("message", function(event) {
    let msg = event.data;
    console.log(msg);
    if (typeof messageProcess[msg.type] === "function") {
        messageProcess[msg.type](msg.url);
    }
});

依据不一样的音信类型调不相同的回调函数,如若是1的话正是去除cache。顾客公布完评论后会触发刷新页面,刷新的时候缓存已经被删了就能再也去乞请了。

这么就减轻了实时更新的难题。

4. Http/Manifest/Service Worker三种cache的关系

要缓存可以采用二种花招,使用Http Cache设置缓存时间,也得以用Manifest的Application Cache,还足以用ServiceWorker缓存,假如三者都用上了会如何呢?

会以Service Worker为事先,因为ServiceWorker把伏乞拦截了,它最初做管理,如若它缓存Curry部分话平昔重回,未有的话常常央浼,就一定于尚未ServiceWorker了,这年就到了Manifest层,Manifest缓存里假若有的话就取这些缓存,如果未有的话就一定于尚未Manifest了,于是就能够从Http缓存里取了,若是Http缓存里也绝非就能发央浼去得到,服务端依据Http的etag只怕Modified Time或者会回去304 Not Modified,不然不荒谬重回200和数据内容。那正是整几个获得的进程。

由此假设既用了Manifest又用ServiceWorker的话应该会招致同一个能源存了五回。不过能够让帮忙ServiceWorker的浏览器选取Service Worker,而不补助的选用Manifest.

5. 利用Web App Manifest加多桌面入口

细心这里说的是其余一个Manifest,那一个Manifest是多个json文件,用来放网址icon名称等音信以便在桌面增添叁个Logo,乃至创制一种张开那么些网页就像是展开App同样的效率。上面一贯说的Manifest是被打消的Application Cache的Manifest。

其一Maifest.json文件可以这么写:

JavaScript

{ "short_name": "人人FED", "name": "人人网FED,专心于前面四个本事", "icons": [ { "src": "/html/app-manifest/logo_48.png", "type": "image/png", "sizes": "48x48" }, { "src": "/html/app-manifest/logo_96.png", "type": "image/png", "sizes": "96x96" }, { "src": "/html/app-manifest/logo_192.png", "type": "image/png", "sizes": "192x192" }, { "src": "/html/app-manifest/logo_512.png", "type": "image/png", "sizes": "512x512" } ], "start_url": "/?launcher=true", "display": "standalone", "background_color": "#287fc5", "theme_color": "#fff" }

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
{
  "short_name": "人人FED",
  "name": "人人网FED,专注于前端技术",
  "icons": [
    {
      "src": "/html/app-manifest/logo_48.png",
      "type": "image/png",
      "sizes": "48x48"
    },
    {
      "src": "/html/app-manifest/logo_96.png",
      "type": "image/png",
      "sizes": "96x96"
    },
    {
      "src": "/html/app-manifest/logo_192.png",
      "type": "image/png",
      "sizes": "192x192"
    },
    {
      "src": "/html/app-manifest/logo_512.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ],
  "start_url": "/?launcher=true",
  "display": "standalone",
  "background_color": "#287fc5",
  "theme_color": "#fff"
}

icon需求积谷防饥种种尺度,最大要求512px * 512px的,那样Chrome会自动去挑选合适的图形。若是把display改成standalone,从扭转的Logo张开就能像展开贰个App同样,没有浏览器地址栏那么些东西了。start_url钦定展开现在的进口链接。

下一场加多叁个link标签指向那么些manifest文件:

JavaScript

<link rel="manifest" href="/html/app-manifest/manifest.json">

1
<link rel="manifest" href="/html/app-manifest/manifest.json">

如此那般组合Service Worker缓存:
图片 8把start_url指向的页面用ServiceWorker缓存起来,那样当客商用Chrome浏览器展开这么些网页的时候,Chrome就能够在底层弹贰个升迁,询问客户是不是把那几个网页增加到桌面,若是点“增加”就能够生成二个桌面Logo,从那些Logo点进去就好像张开多个App同样。感受如下:

图片 9

相比难堪的是Manifest这段时间独有Chrome帮忙,并且不得不在安卓系统上使用,IOS的浏览器不能增加叁个桌面Logo,因为IOS未有开放这种API,可是自个儿的Safari却又是足以的。

综上,本文介绍了怎么用Service Worker结合Manifest做三个PWA离线Web APP,主假诺用ServiceWorker调节缓存,由于是写JS,相比较灵活,还可以与页面实行通讯,其他通过央浼页面包车型大巴换代时间来推断是还是不是供给更新html缓存。ServiceWorker的包容性不是特意好,但是前景相比较光明,浏览器都在预备协理。现阶段得以构成offline cache的Manifest做离线应用。

连锁阅读:

  1. 缘何要把网址晋级到HTTPS
  2. 怎么着把网站晋级到http/2
  3. 笔者是什么让网址用上HTML5 Manifest

1 赞 1 收藏 评论

图片 10

本文由新浦京娱乐场官网-301net-新浦京娱乐www.301net发布于www.301net,转载请注明出处:使用 Service Worker 做一个 PWA 离线网页应用

您可能还会对下面的文章感兴趣: