PWA渐进式Web应用

文末附上全部源码。

PWA是什么?

Progressive Web App,即 渐进式网络应用。

可以循序渐进的实现它,每次实现一部分业务。降低了业务的部署成本、适应了Browser厂商的更新节奏。

PWA组成技术:

  • Service Worker
  • Promise
  • fetch
  • cache API
  • Notification API

1. Service Worker:(PWA最重要的API)

服务工作线程。

Web Worker是一种独立于浏览器主线程的环境,用来执行比较复杂的计算操作,不会阻塞页面的渲染,与主线程通过postMessage通信。

而这里的Service Worker和Web Worker类似并拥有更多特性:

  • 独立页面,常驻内存运行。
  • 代理请求。
  • 只在HTTPS下的环境下运行。(依赖HTTPS)

2.Promise:

“承诺”控制流。

new Promise((resolve,reject)=>{})
    .then(...)
    .catch(...)

Promise的特点:

  • 优化回调地狱
  • 配合async/await语法同步化
  • service Worker的api风格

3.fetch:

现在很多浏览器都支持了。

 fetch特点:

  • 比传统的XMLHttpRequest更简洁
  • Promise风格

4.cache API:

支持资源的缓存系统。

其特点:

  • 缓存资源.例如css script image等
  • 依赖Service Worker代理网络
  • 支持离线程序运行

5.Notification API:

消息推送。

Web界面和APP都是可以发送通知的。

特点:

  • 依赖用户授权
  • 依赖常驻内存的线程,适合在ServiceWorker中推送

具体使用:

第一部分: ServiceWorker:

1.注册ServiceWorker:

先启动一个服务器:

yarn add serve global 

然后serve启动,打开浏览器:

 

说明ServiceWorker注册成功了!

还有一个地方可以观察和调试ServiceWorker:

可以看到右边显示了serviceWorker的当前状态.

2.开始编写ServiceWorker:

需要注意的事情是:

  • 不能在上下文中访问DOM
  • 不能访问诸如Window、LocalStorage一类的对象
  • 只有一些特定的对象可以访问:如self代表serviceWorker的全局作用域对象.只能在self上添加监听事件,才能与serviceWorker打交道。
  • 其生命周期:install、activate、fetch

ServiceWorker编程就是与ServiceWorker的生命周期打交道。

install事件是在一个新的ServiceWorker脚本被安装后触发,只要内容有一点不同,浏览器就会认为是新的版本。新的版本会被下载、安装,但不会立即生效。因为当前生效的是上一个版本。

现在修改下源文件:

可以看到浏览器因为serviceWorker的一点改变而认为是新的版本,对新版本进行下载安装但并未激活。

最新版本的代码处于waiting状态:

点击skipwaiting强制启用activate新版本:

1.fetch事件的关键操作:

fetch事件是专门用于捕获资源请求的:

先新创建一个外链资源

然后html中引入这个css外链文件:

这就是一个典型的外链资源。

此时刷新浏览器,可以看到:

 

index.css的外链资源的请求被捕获到了。

fetch事件可配合cache API可实现本地代理功能。

2.install事件的2个关键操作:

回看install事件:

再回到浏览器,清除所有的servieWorker:

此时刷新浏览器可以看到install事件触发,然后等待5秒后activate事件被触发:

这就是waitUntil()的功能,其会配合特定的行为,比如上面application选项卡中点击的skipWaiting():

强制停止了旧的serviceWorker,激活新的serviceWorker.现在只要serviceWorker的脚本有更新,刷新浏览器后都会直接安装激活新版本ServiceWorker.

3.activate事件的2个关键操作:

2.1 和install事件中的一样:

2.2 self.clients.claim()

这里的clients指的是serviceWorker控制的所有页面。

这个方法能够页面在首次加载后同样受到serviceWorker的控制,在默认情况下,首次是不受控制的。

 4. 除了3种生命周期事件之外的事件:

推送事件和同步事件.


第二部分: Promise:

它主要是处理异步逻辑的。

这里,假设一个业务是异步读取xml文件,传统的方式callback:

当嵌套多层时就会产生典型的回调地狱。

用promise:

上面的2种写法不等价。

对于第1种写法,如果xml=>{}函数抛出异常,后面的err=>{}是不能捕获到的。对于第2种写法catch就能捕获到所有异常。

上面的promise支持类对象的行为。

在promise的构造方法上还有几个有用的静态方法:

这是一个快捷方式,可以把入参转换为一个完成的promise.

可以把入参转换为一个拒绝的promise.

2者的等价写法为:

其他的静态方法:

一般传入由promise实例对象组成的数组,并返回一个新的Promise。只有当所有的promise都完成后,返回的promise才完成。只要其中一个promise被拒绝,那么返回的promise会被立刻触发拒绝。Promise.all()在执行一系列并行的任务时有用。如果入参数组种的某个成员不是promise,那么可以用promise.resolve()包装一下。

还有一个是Promise.race()。其是只要有其中一项完成或拒绝,那么返回的promise就立刻完成或拒绝。

其实Promise还是有弊端,所以出现了async/await:


第三部分: 网络请求Fetch:

fetch()函数是一个全局函数,用来发起http请求。

先看看如何用传统的XHR发起request请求:

而用fetch():

fetch的更多选项:

还可以将第2个参数封装为独立的Request对象:

注意:serviceWorker中无法访问XMLHttpRequest,fetch是我们唯一的选择.

fetch是比较低级API,不能提供上传进度,也不能控制超时时间,更不能主动中断请求,所以页面中使用axios模块。


第四部分: 资源的缓存系统Cache API:(PWA的顶梁柱)

让web应用在离线环境下运行成为可能。

先说serviceWorker的3个核心生命周期:install、activate、fetch

  • install事件发生在新的serviceWorker下载之后。(发生1次且只发生1次)
  • activate事件发生在新的 serviceWorker被启用之时。(发生1次且只发生1次)
  • fetch事件发生在捕获到资源请求之时。(发生无数次)

这里以catchAPI来做做资源代理,实现页面的离线可用功能:

为了实现需求,在上面的3个生命周期方法中都要相应修改:

  • install中应拉取并缓存必要的资源。
  • activate中应清除旧版本遗留下来的不用的缓存。
  • fetch中应在捕获到资源请求后去查询并返回缓存中的资源。

一般在页面上线之前,都会得到一个可能用到的静态资源的集合。有必要将集合中包含的资源全部写入到缓存中。

这个集合的内容一般可以在编译构建期间得到,只需要操作cache把他们写入缓存即可。

代码解释:

先打开特定的缓存空间,然后写入必要的资源数据。这能确保serviceWorker激活之后立刻就能响应特定的资源请求。

serviceWorker上下文中可以设置多个缓存空间,所有缓存空间的集合是caches,caches是一个全局对象,可以直接使用。

其open()打开一个缓存空间,open()需要传入一个缓存名字。open()方法得到的是一个promise,在其then()中可以得到缓存空间的句柄cache。用句柄cache就可以直接写入缓存了。addAll()需要传入一个数组,数组的每一项是资源的路径,例如把当前的html也买你和css写入到缓存中。【这里的资源列表应该在i项目构建期间自动得到,不能人工维护】

然后是使用缓存:

在fetch()中可以捕获到html和css等所有的资源请求,然后去cache中查询,如果查到了就返回缓存,否则就发起网络请求进行获取。

第二次刷新浏览器后,查看调试:

说明缓存写入到了浏览器中。

这个时候关闭http服务器:

再刷新页面可以看到即使页面离线,只要有足够的缓存资源,界面Web依然能够运行。

但是还没完,缓存是可以变化的,每次缓存变化最好修改cache的名字重新抓取资源写入新的缓存,还应清理以前的无用缓存。

activate事件就是清理旧缓存的最佳时间:

还是在waitUntil里面写,因为我们希望在激活之前完成cache的清理。

现在手动更改一下cache的版本号:

再启动http服务器:

可以看到新的serviceWorker已经开始安装了

当前占用的缓存为6.5kb,因为当前占用的缓存有2份。

然后点击skipWaiting,立即激活新版本serviceWorker:

发现缓存变少了。即清理了旧缓存。

cache API不仅仅可以在serviceWorker中使用。

也可以在页面上下文中调用。

e.g: onload后写缓存


第五部分: Notification API

通知。在app中是为了拉回客户到app中。也有很多其他功能。

web 应用期望即使页面关闭了,只要浏览器进程还在就有弹出通知的途径。

ServiceWorker就是最适合弹出通知的地方,ServiceWorker还可以接收到push事件。

Notifiaction在页面上下文、ServiceWorker上下文都能用,但有差别。

1.页面上下文中使用Notification API:

可以直接获取到Notification全局对象,它也是一个构造函数。

作为对象,它有一个permission属性,代表当前页面已经获取到的授权,并通知需要打扰用户的,受到严格的授权限制。

permission属性有3个值:

  • default:代表用户没有同意也没有拒绝。这时应该向用户弹出授权请求。除非已经明确授权过,否则通知弹不出来。
  • denied:禁止/否定
  • granted:允许。

弹出授权:

点击禁止按钮:

再弹出一次授权请求:

Notification.requestPermission().then(permission=>console.log(permission))

点击允许按钮:

这样就授权通过了,可以弹通知了,这个时候Notification就变成了一个构造函数,创建一个Notification实例就相当于弹出了一个通知:

2.ServiceWorker中使用Notification API:

在ServiceWorker中Notification.permission默认是denied,这是因为ServiceWorker不允许弹出授权请求。因为反正default也不能弹出通知,那么就当作denied处理了。

要i想获取授权就必须在页面的上下文中请求。

先切换到页面上下文中:

点击允许,再回到ServiceWorker上下文环境:

已经被允许了。

 还需要注意的是:创建通知也和在页面中创建通知不一样:

  • 不允许把Notification当作构造函数来创建通知
  • 只有一种方法:

这个registration就是我们在页面上下文中注册ServiceWorker成功后得到的对象:

只不过在页面上下文中没有showNotification()方法。


第六部分: 在项目中开启PWA

Google的Workbox:一个library,利用PWA技术,赋予应用能离线运行的能力。

React中,Workbox在webpack中有对应的插件。

create-react-app已经配置好了workbox.在package.json中可以找到workbox-webpack-plugin:xxxx

React模板项目下的src下有serviceWorker.js文件。并查看index.js下模板的注释,使用方法在注释中已经写明了。


全部源码:

1.demo项目结构:

2.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>PWA</title>
    <link rel="stylesheet" href="index.css">
</head>
<body>
    <h1>PWA 渐进式web应用</h1>
    <script>
        //通常useragent就挂载在navigator下
        //serviceWorker是单例对象,不可以用构造函数创建新的实例。
        //register方法的第1个参数是serviceWorker外链脚本地址,第2个参数是一个选项对象。
        //scope对象代表的是可以控制的页面的相对路径,默认是脚本本身所在的路径。【这里传入根路径代表控制所有的页面】
        navigator.serviceWorker.register('./serviceWorker.js',{scope:'/'}).then(ServiceWorkerRegistration => {
            console.log(ServiceWorkerRegistration);
        },err => {
            console.error(err);
        })
    </script>
</body>
</html>

3.serviceWorker.js:

// const CACHE_NAME ='cache-v1';
const CACHE_NAME ='cache-v2';
self.addEventListener('install',event => {
    console.log('install',event);
    event.waitUntil(caches.open(CACHE_NAME).then(cache=>{
        cache.addAll([
            '/',
            './index.css'
        ]);
    }));
});
self.addEventListener('activate',event => {
    console.log('activate',event);
    event.waitUntil(caches.keys().then(cacheNames=>{
        //清除我们自己不需要的这里
        return Promise.all(cacheNames.map(cachename =>{
            if(cachename!== CACHE_NAME){
                return caches.delete(cachename);//注意需要return ,因为delete也是需要返回一个promise
            }
        }));
    }));
});
self.addEventListener('fetch',event => {
    console.log('fetch',event);
    event.respondWith(caches.open(CACHE_NAME).then(cache => {
        //判断里面有没有当前请求的资源
        return cache.match(event.request).then(response =>{
            if(response){//如果存在就直接返回该资源缓存
                return response;
            }
            //不存在,只有发起网络请求然后并加入缓存,避免下次还网络请求该资源
            return fetch(event.request).then(response=>{
                //这里response是流式的,只能读取一次。为了缓存可读取,需要克隆一份出来
                cache.put(event.request,response.clone());
                return response;
            });
        });
    }));
});

Google关于pwa的介绍:

https://codelabs.developers.google.com/codelabs/your-first-pwapp/#0

发布了268 篇原创文章 · 获赞 36 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/qq_39969226/article/details/103730747