PWA详解

1,621 阅读10分钟

PWA 渐进式的web应用

介绍

我将带你快速熟悉一个pwa应用的基本使用

本文中涉及到的内容:

  • fetch
  • CacheStorage
  • service worker
  • SyncMannager
  • postManager 与 messageChannal
  • manifest.json
  • 消息推送

Fetch

Fetch 提供了许多与XMLHttpRequest相同的功能,为什么要在这里提及这个,因为在我们在service worker环境中是不能去使用XMLHttpRequest对象的,故而这是一个非常重要的api。

Fetch 的核心在于对 HTTP 接口的抽象,包括 Request,Response,Headers,Body这些接口

这里只是简单介绍

基本用法

Promise<Response> fetch(input[, init]);

参数介绍:

  • input定义要获取的资源。可能值:
  1. string:资源的 URL。一些浏览器会接受 blob: 和 data: 作为 schemes.
  2. Request 对象。
  • init 可选
  1. method: 请求方法,如 GET、POST。
  2. headers: 请求的头,可以为 Headers 的对象,或者形如{'Content-Type': 'image/jpeg'}。
  3. body: 请求体。可能为 Blob、BufferSource、FormData、URLSearchParams 或者 USVString 对象。GET 或 HEAD 无请求体。
  4. mode: 请求的模式,如 cors、 no-cors 或者 same-origin。
  5. credentials: 请求的 credentials,如 omit、same-origin 或者 include。为了在当前域名内自动发送 cookie , 必须提供这个选项, 从 Chrome 50 开始, 这个属性也可以接受 FederatedCredential 实例或是一个 PasswordCredential 实例。
  6. ...

CacheStorage

这是一个可以缓存浏览器缓存的接口,离线应用的核心就靠他了

CacheStorage常用方法介绍

name 描述
Cache.match(request, options) 该方法会检查是否存在该request的缓存,返回 Promise对象,resolve的结果是跟 Cache 对象匹配的第一个已经缓存的请求。
Cache.matchAll(request, options) 同上,不同的是resolve的结果是跟Cache对象匹配的所有请求组成的数组。
Cache.add(request) 抓取这个URL, 检索并把返回的response对象添加到给定的Cache对象.这在功能上等同于调用 fetch(), 然后使用 Cache.put() 将response添加到cache中.
Cache.addAll(requests) 抓取一个URL数组,检索并把返回的response对象添加到给定的Cache对象。
Cache.put(request, response) 同时抓取一个请求及其响应,并将其添加到给定的cache。
Cache.delete(request, options) 搜索key值为request的Cache 条目。如果找到,则删除该Cache 条目,并且返回一个resolve为true的Promise对象;如果未找到,则返回一个resolve为false的Promise对象。
Cache.keys(request, options) 返回一个Promise对象,resolve的结果是Cache对象key值组成的数组。

options参数的解释:

  • ignoreSearch(Boolean): 忽略url中的query部分(?后面的参数)默认值:false
  • ignoreMethod(Boolean): 如果设置为 true在匹配时就不会验证 Request 对象的http 方法 (通常只允许是 GET 或 HEAD 。) 默认值:false
  • ignoreVary(Boolean): 该值如果为 true 则匹配时不进行 VARY 部分的匹配。默认值:false
  • cacheName(DOMString): 代表一个具体的要被搜索的缓存。注意该选项被 Cache.match()方法忽略。

service worker

这是我们要花时间最多的地方!

生命周期

  1. installing(正在安装)

当你调用 navigator.serviceWorker.register 注册一个新的 service worker ,service worker 代码就会被下载、解析、进入installing阶段。若安装成功则进入installed,失败则进入redundant

  1. installed/waiting(已安装/等待中)

installed 状态下的service worker会判断自己是否已经被注册过,如果是第一次注册将进入activating状态,如果发现自己被注册且处于activated状态,那么将进入waiting状态

  1. activating(激活中)

此状态下的sw将进入activated(必将进入activated状态)

  1. activated(已激活)

一旦 service worker 激活,它就准备好接管页面并监听功能性事件了(例如 fetch 事件)

  1. redundant(废弃)

当sw注册失败或者被新的sw替换将进入此状态(只有这两种情况会进入redundant)

结合代码理解service worker生命周期

页面中的代码:

if("serviceWorker" in navigator){
    window.onload = function(){
        navigator.serviceWorker.register("./sw.js").then((registration)=>{
            var sw = null, state;
            if(registration.installing) {
                sw = registration.installing;
                state = 'installing';
            } else if(registration.waiting) {
                sw = registration.waiting;
                state = 'installed'
            } else if(registration.active) {
                sw = registration.active;
                state = 'activated'
            }
            state && console.log(`sw state is ${state}`);
            if(sw) {
                sw.onstatechange = function() {
                    console.log(`sw state is ${sw.state}`);
                }
            }
        }).catch(()=>{
            console.log('sw fail');
        })
    }

}

sw.js:

self.addEventListener('install',function () {
    console.log('install callback');
})

self.addEventListener('activate',function () {
    console.log('activate callback');
})

self.addEventListener('fetch',function () {
    console.log('fetch callback');
})

首次刷新页面控制台输出:

install callback
sw state is installing
sw state is installed
activate callback
sw state is activating
sw state is activated

再次刷新页面控制台输出:

sw state is activated
fetch callback

install,与active事件仅仅执行一次,fetch在首次刷新也面试时是不请求的

更新service worker

当更新sw时,刷新页面,此时:

  1. 新的sw的install回调触发,进入installing
  2. 新的sw发现上一个sw还处于actived状态则进入waiting状态
  3. 此时调用slkipwating方法或者关闭浏览器再次打开浏览器,会使旧的sw进入redundant,新的sw进入actived

代码此处可以自己尝试

waitUntil延长生命周期

waitUntil函数传入promise作为参数,当promise执行完成才会接着往下走。

在install事件中调用该方法

1.加入成功的回调:

self.addEventListener('install',function (event) {
    event.waitUntil(new Promise(function(resolve) {
        setTimeout(() => {
            console.log('install 2s')
            resolve()
        }, 2000)
    }))
})

控制台输出:

sw state is installing
install 2s
sw state is installed
activate callback
sw state is activating
sw state is activated
  1. 加入失败的回调:
self.addEventListener('install',function (event) {
    event.waitUntil(new Promise(function(resolve,reject) {
        setTimeout(() => {
            console.log('install 2s')
            reject()
        }, 2000)
    }))
})

控制台输出:

sw state is installing
install 2s
sw state is redundant
Uncaught (in promise) undefined

sw直接进入redundant阶段

在activate事件中调用该方法

与install大致都一样,不同的是当你调用reject时,sw不会进入redundant阶段,而是最终还是进入actived阶段

生命周期常见应用

  1. 一般我们会在install中缓存请求,这是为了能够在下一次请求中使用到这些缓存。
  2. 在active中我们应该清除旧的缓存。
  3. 在fetch中我们便可以使用这些缓存,并且更新他们

service worker的作用域范围?

service worker只能捕获当前目录及其子目录下的请求!

比如:
当service worker在/pwa/sw.js下时那么只能捕获/pwa/*的请求,所以一般我们都应该将sw.js放置于/根目录下。

代码示例

如果你复制以下代码将在页面显示css代码

var CACHE_NAME = "gih-cache";
var CACHED_URLS = [
    "/pwa/test.css"
];

self.addEventListener('install',function (event) {
    event.waitUntil(
        caches.open(CACHE_NAME).then(function(cache) {
            return cache.addAll(CACHED_URLS);
        })
    );
})

self.addEventListener('activate',function (event) {
    // event.waitUntil(
    //     caches.keys().then(function(cacheNames) {
    //         return Promise.all(
    //             cacheNames.map(function(cacheName) {
    //                 if (CACHE_NAME !== cacheName && cacheName.startsWith("gih-cache")) {
    //                     return caches.delete(cacheName);
    //                 }
    //             })
    //         );
    //     })
    // );
})

self.addEventListener('fetch',function (event) {
    event.respondWith(
        caches.open(CACHE_NAME).then(function(cache) {
            return cache.match(event.request).then(function(response) {
                let fetchPromise = fetch(event.request).then(function(networkResponse) {
                    cache.put(event.request, networkResponse.clone());
                    return networkResponse;
                })
                return response || fetchPromise;
            })
        })
    );
})

离线开发的策略

  1. 仅网络
  2. 先网络后缓存:当我们希望用户看见内容一直是最新的,那么可以使用这种模式,在fetch中更新缓存数据
  3. 缓存后网络:当不需要用户看见最新的内容,我们可以先将缓存呈现给用户,在fetch中更新缓存,下一次用户刷新可以看见最新的缓存。(以上代码采取的就是这种)

syncManager 后台同步

在用户使用web app时,网页可能会被关闭,用户连接可能会断开,甚至服务器有时候也会故障。但是,只要用户设备上安装了浏览器,后台同步中的操作就不会消失,直到它成功完成为止。

注册后台同步事件

在页面中:

navigator.serviceWorker.ready.then(function(registration) {     
    registration.sync.register('send-messages');
});

监听sync事件

在sw.js中

self.addEventListener("sync", function(event) { 
    if (event.tag === "send-messages") {
        event.waitUntil(function() { 
            var sent = sendMessages(); 
            if (sent) {
                return Promise.resolve(); 
            }else{
                return Promise.reject(); 
                
            }
        }); 
    }
});

sync事件何时结束?

当后台多次尝试不成功时,那么sync事件也会提供结束时的标识符event.lastChance

self.addEventListener("sync", event => { 
    if (event.tag == "add-reservation") {
        event.waitUntil( 
            addReservation()
            .then(function() {
                return Promise.resolve();
            }).catch(function(error) {
                if (event.lastChance) { 
                    return removeReservation();
                } else {
                    return Promise.reject();
                }
            })
        ); 
    }
});

postMessage

当我们将逻辑代码放入service worker中时,我们就一定会有页面与service worker通信的需求,此时postMessage便是这么一个担任通信的角色。

1. 窗口向service worker通信

页面代码:

navigator.serviceWorker.controller.postMessage( {
    arrival: "05/11/2022", 
    nights: 3, 
    guests: 2
})

service worker代码:

self.addEventListener("message", function (event) { 
    console.log(event.data);
});

2. service worker向所有打开的窗口通信

页面代码:

navigator.serviceWorker.addEventListener("message", function (event) {
    console.log(event.data);
});

service worker代码:

self.clients.matchAll().then(function(clients) {
    clients.forEach(function(client) {
        if (client.url.includes("/my-account")) { 
            client.postMessage("Hi client: "+client.id);
        } 
    });
});

3. service worker向特定窗口通信

service worker代码:

//当你可以获得某个客户端id时便可以向特定的客户端发送消息
self.clients.get("d2069ced-8f96-4d28").then(function(client) {
    client.postMessage("Hi window, you are currently " +
                        client.visibilityState);
});

获得特定的客户端id


//通过clients对象获取客户端id
self.clients.matchAll().then(function(clients) {
    clients.forEach(function(client) {
        self.clients.get(client.id).then(function(client) {  
            client.postMessage("Messaging using clients.matchAll()");
        });
    });
});

//通过event.sourse.id 获得·客户端id
self.addEventListener("message", function(event) {
    self.clients.get(event.source.id).then(function(client) {
        client.postMessage("Messaging using clients.get(event.source.id)");
    });
});

//简化写法
self.clients.matchAll().then(function(clients) {
    clients.forEach(function(client) {
        client.postMessage("Messaging using clients.matchAll()"); 
    });
});

MessageChannel

在介绍第四种通信方式(窗口间通信)时,我想先插入介绍一下MessageChannel这个对象,他是实现我们service worker与窗口间相互通信的一种有效的技术手段。

演示代码

// 窗口代码
var msgChan = new MessageChannel(); 
msgChan.port1.onmessage = function(event) {
    console.log("Message received in page:", event.data); 
};
var msg = {action: "triple", value: 2}; 
//这里可以是postMessage的第二个参数
navigator.serviceWorker.controller.postMessage(msg, [msgChan.port2]);


// service worker代码 
self.addEventListener("message", function (event) {
    var data = event.data;
    var openPort = event.ports[0]; 
    if (data.action === "triple") {
        openPort.postMessage(data.value*3); 
    }
});

4. 窗口间的通信

窗口间通信方式有多种,你如localStorage这些都可以实现,这里还是应该思考如何使用service worker来进行通信,这里先不再赘述。

manifest.json

当我们的web应用已经使用以上技术做到了一系列的离线优化后,我们可以考虑将我们的应用安装在本地。

页面引入:

<link rel="manifest" href="/manifest.json">

manifest.json:

{
    "short_name": "Gotham Imperial",
    "name": "Gotham Imperial Hotel",
    "description": "Book your next stay, manage reservations, and explore Gotham", "start_url": "/my-account?utm_source=pwa",
    "scope": "/",
    "display": "fullscreen",
    "icons": [
        {
        "src": "/img/app-icon-192.png", "type": "image/png",
        "sizes": "192x192"
        }, {
        } 
    ],
    "theme_color": "#242424",
    "background_color": "#242424" 
}

属性介绍

  • name与/或short_name:
    name 是应用的全名。当空间足够长时,就会使用这个字段作为显示名称,short_name 可以作为短 名的备选方案

  • start_url:
    当用户点击图标时,打开的 URL。可以是根域名,也可以是内部页面。

  • icon:
    包含了一个或多个对象的数组,对象属性:src(图标的绝对路径或者相对路径)、type(文件类型)和 sizes(图片的像素尺寸)。要触发Web应用安装横条,清单中至少要包含一个图标, 尺寸至少是 144像素×144像素。 由于每个设备都会根据设备分辨率,从这个数组中选择最佳的图标尺寸,因此建议至少 包含 192×192 的图标和 512×512 的图标,以覆盖大多数的设备和用途。

  • display:

  1. browser——在浏览器中打开应用。
  2. standalone——打开应用时不显示浏览器栏(不显示浏览器界面,例如地址栏)。
  3. fullscreen——打开应用时不显示浏览器栏和设备栏(例如在安卓设备上,这意味着 同时隐藏浏览器界面和屏幕顶部的状态栏)。
  • description:
    应用的描述。

  • orientation:
    允许你强制指定某个屏幕方向。

  • theme_color
    主题颜色可以让浏览器和设备调整 UI 以匹配你的网站(见图 9-5)。这个颜色的选择会 影响浏览器地址栏颜色、任务切换器中的应用颜色,甚至是设备状态栏的颜色。主题颜色也可以通过页面的meta标签进行设置(例如:)。如果页面带有 theme-color 的 meta 标签,则该设置会覆盖清单 中的 theme_color 设置。请注意,虽然 meta 标签可以让你设置或者覆盖单个页面的主 题颜色,但是清单文件中的 theme_color 设置是会影响整个应用的。

  • background_color
    设置应用启动画面的颜色以及应用加载时的背景色。一旦加载后,页面中定义的任何背 景色(通过样式表或者内联 HTML 标签设置)都会覆盖这一设置;但是,通过将其设 置为与页面背景色相同的颜色,就可以实现从页面启动的瞬间到完全渲染之间的平滑过 渡。如果不设置这一颜色,页面就会从白色背景启动,随后被页面的背景色替换。

更新中