Skip to content

PWA笔记 #21

Open
Open
@isNeilLin

Description

@isNeilLin

Progressive Web Apps

  • 渐进增强 - 能够让每位用户使用,无论用户使用什么浏览器
  • 不依赖网络连接 - 通过ServiceWorker可以在离线或网速极差的环境下工作
  • 类原生应用 - 有像原生应用般的交互和导航给用户原生应用般的体验,因为它是建立在 app shell 模型上的。
  • 持续更新 - 受益于ServiceWorker的更新进程,应用能够始终保持更新
  • 安全 - 通过HTTPS来提供服务
  • 可发现 - 得益于 manifest 元数据和 ServiceWorker 的登记,让搜索引擎可以找到web应用
  • 再次访问 - 通过消息推送等特性让用户再次访问变得容易
  • 可安装 - 允许用户保留对他们有用的应用在主屏幕上,不需要通过应用商店
  • 可连接性 - 通过URL可以轻松分享应用,不用复杂的安装即可运行

浏览器支持

ServiceWorker 是什么

service worker 是独立于当前页面的一段运行在浏览器后台进程里的脚本。

service worker不需要用户打开 web 页面,也不需要其他交互,异步地运行在一个完全独立的上下文环境,不会对主线程造成阻塞。

service worker提供一种渐进增强的特性,使用特性检测来渐渐增强,不会在老旧的不支持 service workers 的浏览器中产生影响。可以通过service workers解决让应用程序能够离线工作,让存储数据在离线时使用的问题。

ServiceWorker 特点

  • 网站必须使用 HTTPS。除了使用本地开发环境调试时(如域名使用 localhost)
  • ServiceWorker是一个可编程的网络代理,允许开发者控制页面上处理的网络请求
  • 单独的作用域范围,单独的运行环境和执行线程。运行在它们自己的完全独立异步的�全局上下文中
  • 不能操作页面DOM,但可以通过 postMessage 方法与Web页面通信,让页面操作DOM
  • 浏览器可能随时回收ServiceWorker,在不被使用的时候他会自己终止,而当他再次被用到的时候,会被重新激活
  • ServiceWorker的生命周期是由事件驱动的而不是通过 Client

ServiceWorker 生命周期

install -> installed -> activating -> activate -> activated -> redundant

lifecycle

install

// 用于标注创建的缓存,也可以根据它来建立版本规范
const CACHE_NAME = "cache_v1.0.0";
// 列举要默认缓存的静态资源,一般用于离线使用
const urlsToCache = [
    '/offline.html',
    '/offline.png'
];

// self 为当前 scope 内的上下文
self.addEventListener('install', event => {
    // event.waitUtil 用于在安装成功之前执行一些预装逻辑
    // 但是建议只做一些轻量级和非常重要资源的缓存,减少安装失败的概率
    // 安装成功后 ServiceWorker 状态会从 installing 变为 installed
    event.waitUntil(
        // 使用 cache API 打开指定的 cache 文件
        caches.open(CACHE_NAME).then(cache => {
            console.log(cache);
            // 添加要缓存的资源列表
            return cache.addAll(urlsToCache);
        })
    );
});

只有urlsToCache中的文件全部安装成功,ServiceWorker才会认为安装成功。否则会认为安装失败,失败则进入redundant�(�废弃)状态。所以应当尽量少地缓存资源(一般认为离线时需要,联网时不会访问到的内容),以提升成功率。

ServiceWorker 更新

有更新的ServiceWorker完成安装后会进入waiting状态。直到所有已打开的页面都关闭,旧的 Service Worker 自动停止,新的 Service Worker 才会在接下来打开的页面里生效。
如果希望在有系版本时所有的页面都得到更新,可以在install事件中执行skipWaiting方法跳过waiting状态,然后回会直接进入activate阶段。接着在activate事件发生时,通过执行self.clients.claim()方法,更新所有客户端上的ServiceWorker

// 安装阶段跳过等待,直接进入 active
self.addEventListener('install', function(event) {
    event.waitUntil(self.skipWaiting());
});

self.addEventListener('activate', evnet => event.waitUntil(
    Promise.all([
        // 更新客户端
        clients.claim(),
        // 清理旧版本
        caches.keys().then(cacheList => Promise.all(
            cacheList.map(cacheName => {
                if (cacheName !== CACHE_NAME) {
                    caches.delete(cacheName);
                }
            })
        ))
    ])
));

添加到主屏幕(显示应用安装横幅)

显示应用安装横幅的条件:

  • 站点部署manifest.json,该文件需配置如下属性
    • short_name 用于主屏幕显示
    • name 用于安装横幅显示
    • icons 其中必须包含一个 mime 类型为 image/png 的图标声明
    • start_url 应用启动地址
    • display 必须为standalone�或fullscreen
  • 站点注册ServiceWorker
  • 站点支持HTTPS访问
  • 站点在同一浏览器中被访问至少两次,两次间隔至少5分钟

manifest.json的配置

{
    "short_name": "短名称",
    "name": "这是一个完整名称",
    "icon": [
        {
            "src": "../../images/logo-144x144.png",
            "type": "image/png",
            "sizes": "144x144"
        }
    ],
    "start_url": "./index.html"
}

应用横幅安装事件

beforeinstallprompt返回一个名为userChoice的Promise对象,并在用户对横幅�进行操作时进行解析,Promise会返回属性outcome,该属性的值为dismissedaccepted,如果用户将网页添加到主屏幕则返回accepted

window.addEventListener('beforeinstallprompt', function(e){
    e.userChoice.then(function(choiceResult){
        if(choiceResult.outcome==='dismissed'){
            console.log('用户取消�了安装')
        }else{
            console.log('用户安装了应用')
        }
    })
})

取消或延迟安装横幅的触发事件

网站虽然不能主动触发安装横幅的显示事件,但是当该事件被浏览器触发之后,可以对其进行取消或者延迟。

通过阻止 beforeinstallprompt 事件的默认行为,即可取消横幅弹出:

window.addEventListener('beforeinstallprompt', function (e) {
    e.preventDefault();
    return false;
});

beforeinstallprompt 事件返回一个名为prompt的方法,通过执行该方法可以触发安装横幅的显示。

var deferPrompt = null;
window.addEventListener('beforeinstallprompt',function(e){
    deferPrompt = e;
    e.preventDefault();
    return false;
})

button.addEventListener('click', function(){
    if(deferPrompt!=null){
        deferPrompt.prompt();
        deferPrompt.userChoice.then(function(choiceResult){
            console.log(choiceResult.outcome);
        })
        deferPrompt = null;
    }
})

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions