Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PWA #1

Open
JSZabc opened this issue Apr 28, 2018 · 0 comments
Open

PWA #1

JSZabc opened this issue Apr 28, 2018 · 0 comments

Comments

@JSZabc
Copy link
Owner

JSZabc commented Apr 28, 2018

什么是PWA

PWA 本质上是 Web App,借助一些新技术也具备了 Native App 的一些特性,兼具 Web App 和 Native App 的优点

一、特点

安全: 基于https
可靠: 通过Service Worker能够在网络条件很差的状况下,读取缓存瞬间加载并响应。
体验: 通过App Shell 设计模式,预缓存界面展现所需的最小资源,保证首屏加载。
粘性: 用户可安装在桌面上,方便多次打开。用户能获得类似于原生 app 的沉浸式体验,脱离了导航栏。并且可以通过给用户发送离线通知,让用户回流。

二、兼容性

每两周更新一次:https://lavas.baidu.com/ready
image

三、改造成本

1、全站https化
2、Service Worker来提升基础性能,离线提供静态文件
3、App Manifest (主屏图标)
4、其他,离线推送等

Service Worker

是用 JavaScript 编写的 JS 文件,能够代理请求,并且能够操作浏览器缓存,通过将缓存的内容直接返回,让请求能够瞬间完成。开发者可以预存储关键文件,可以淘汰过期的文件,定期的后台同步以及推送通知等等,给用户提供可靠的体验。

一、特点

1、 https协议 (本地 http://localhosthttp://127.0.0.1能跑)
2、 另外一个独立的线程,不能操作DOM Window
3、 完全异步,不能使用同步的API(比如localStorage等)
4、 事件驱动,可以编程拦截代理请求,缓存文件,缓存的文件可以被网页进程读取到
5、 离线内容开发者可控
6、 能向客户端推送消息
7、 一旦被 install,就永远存在,除非被 uninstall

二、生命周期

image

  • 用户首次访问service worker控制的网站或页面时,service worker会立刻被下载。
    之后至少每24小时它会被下载一次。它可能被更频繁地下载,不过每24小时一定会被下载一次,以避免不良脚本长时间生效。

  • 如果这是首次启用service worker,页面会首先尝试安装,安装成功后它会被激活。
    如果现有service worker已启用,新版本会在后台安装,但不会被激活,直到所有已加载的页面不再使用旧的service worker才会激活新的service worker

  • 激活之后,服务工作线程将会对其作用域内的所有页面实施控制,不过,首次注册该服务工作线程的页面需要再次加载才会受其控制。
    服务工作线程实施控制后,它将处于以下两种状态之一:服务工作线程终止以节省内存,或处理获取和消息事件

  • redundant 废弃状态,表示一个 Service Worker 的生命周期结束。只有 install 中的文件全部安装成功,Service Worker 才会认为安装完成。否则会认为安装失败,安装失败则进入 redundant状态

install 事件常用回调:
event.waitUntil() 传入一个promise,等待其resolve。确保 Service Worker 在所有依赖的核心cache被缓存之前都不会被安装
self.skipWaiting() debug常用,使得 waiting 状态的 sw 进入activate 状态

activate 事件常用回调:
event.waitUntil() 同上,确保任何功能事件不会被分派到 ServiceWorkerGlobalScope 对象,直到它升级数据库模式并删除过期的缓存条目

三、使用方法

1、JS中注册serviceWorker,成功后将作用于整个域内用户可访问的URL ,也可配置 scope 指定网域目录。

if ('serviceWorker' in navigator) {
        window.addEventListener('load', function() {
            navigator.serviceWorker.register('/sw.js').then(function(registration) {
                //  navigator.serviceWorker.register('/sw.js', {scope: '/'})
                // Registration was successful
                console.log('注册成功', registration.scope);
            }).catch(function(err) {
                // registration failed :(
                console.log('ServiceWorker registration failed: ', err);
            });
        });
    }

2、Service Worker文件监听install事件

var CACHE_VERSION = 'app-v1'; // 缓存文件的版本
var CACHE_FILES = [ // 需要缓存的页面文件
    '/',
    'images/background.jpeg',
    'js/app.js',
    'css/styles.css'
];


self.addEventListener('install', function (event) { // 监听worker的install事件
    event.waitUntil( // 延迟install事件直到缓存初始化完成
        caches.open(CACHE_VERSION)
            .then(function (cache) {
                console.log('Opened cache');
                return cache.addAll(CACHE_FILES);
            })
    );
});
self.addEventListener('activate', function (event) { // 监听worker的activate事件
    event.waitUntil( // 延迟activate事件直到
        caches.keys().then(function(keys){
            return Promise.all(keys.map(function(key, i){ // 清除旧版本缓存
                if(key !== CACHE_VERSION){
                    return caches.delete(keys[i]);
                }
            }))
        })
    )
});

3、拦截请求

self.addEventListener('fetch', function (event) {
    const url = new URL(event.request.url);

    if (url.origin == location.origin && url.pathname == 'static/timg.jpg') {
        event.respondWith(caches.match('static/timg.jpg'));
    }
})
self.addEventListener('fetch', function (event) {
    event.respondWith(
        caches.match(event.request)  // 检查传入的请求 URL 是否匹配当前缓存中存在的任何内容
            .then(response => {
                if (response) { // 如果有 response 并且它不是 undefined 或 null 的话就将它返回
                    return response;
                }
                // 否则继续通过网络获取预期的资源
                var requestToCache = event.request.clone();  // 我们克隆了请求。请求是一个流,只能消耗一次
                return fetch(requestToCache) // 尝试按预期一样发起原始的 HTTP 请求
                    .then(response => {
                        if (!response || response.status !== 200) {
                            return response;  // 如果由于任何原因请求失败或者服务器响应了错误代码,则立即返回错误信息
                        }
                        // 再一次,我们需要克隆响应,因为我们需要将其添加到缓存中,而且它还将用于最终返回响应
                        var responseToCache = response.clone();
                        caches.open(cacheName) // 打开名称为 “helloWorld” 的缓存
                            .then(cache => {
                                cache.put(requestToCache, responseToCache)  // 将响应添加到缓存中
                            })
                        return response
                    })
            })
    )
});

例子

调试:

  • chrome://inspect/#service-workers 看正在运行的 serivce worker

  • chrome://serviceworker-internals/ 看console.log打印结果

四、支持的事件

image

install:Service Worker 安装成功后被触发的事件,在事件处理函数中可以添加需要缓存的文件

activate:当 Service Worker 安装完成后并进入激活状态,会触发 activate 事件。通过监听 activate 事件你可以做一些预处理,如对旧版本的更新、对无用缓存的清理等。

message:Service Worker 运行于独立 context 中,无法直接访问当前页面主线程的 DOM 等信息,但是通过 postMessage API,可以实现他们之间的消息传递,这样主线程就可以接受 Service Worker 的指令操作 DOM。

fetch (请求):当浏览器在当前指定的 scope 下发起请求时,会触发 fetch 事件,并得到传有 response 参数的回调函数,回调中就可以做各种代理缓存的事情了。

push (推送):push 事件是为推送准备的。不过首先需要了解一下 Notification API 和 PUSH API。通过 PUSH API,当订阅了推送服务后,可以使用推送方式唤醒 Service Worker 以响应来自系统消息传递服务的消息,即使用户已经关闭了页面。

sync (后台同步):sync 事件由 background sync (后台同步)发出。background sync 配合 Service Worker 推出的 API,用于为 Service Worker 提供一个可以实现注册和监听同步处理的方法。但它还不在 W3C Web API 标准中。在 Chrome 中这也只是一个实验性功能,需要访问 chrome://flags/#enable-experimental-web-platform-features ,开启该功能,然后重启生效。

五、更新问题

浏览器每天会至少更新一次 Service Worker
注册新的 Service Worker,带上版本号,如 /sw.js?t=20180428
手动更新 registration.update()
逐字节对比新的 sw 文件和旧的 sw 文件,有区别才更新

index.html

navigator.serviceWorker.addEventListener('message', e => {
    if (e.data === 'sw.update') {
        // 提醒用户刷新
    }
})

sw.js

self.clients.matchAll().then(function (clients) {
    if (clients && clients.length) {
        clients.forEach(client => {
            client.postMessage('sw.update')
        })
    }
})

六、常用接口

  • Cache
    表示用于Request/Response对象对的存储,作为ServiceWorker生命周期的一部分被缓存

Cache.match(request, options)
返回一个 Promise对象,resolve的结果是跟 Cache 对象匹配的第一个已经缓存的请求
Cache.matchAll(request, options)
返回数组
Cache.add(request)
抓取一个URL数组,检索并把返回的response对象添加到给定的Cache对象
Cache.addAll(requests)
数组
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值组成的数组

  • CacheStorage
    表示Cache对象的存储。提供一个所有命名缓存的主目录,ServiceWorker可以访问并维护名字字符串到Cache对象的映射。可以通过 caches 全局属性访问 CacheStorage

CacheStorage.match()
检查request是否是cache对象中的键,返回匹配的promise
CacheStorage.has()
返回一个promise,如果存在与名字匹配的cache对象,则promise解析为true
CacheStorage.open()
返回一个promise,用于解析与名字匹配的cache对象,如果不存在,则创建新的缓存
CacheStorage.delete()
查找与名字匹配的cache对象,如果找到就删除并返回true,否则false
CacheStorage.keys()
返回一个promise,返回所有cache对象的字符串数组。 用于遍历所有cache对象的列表。

Clients.get()
根据 id 匹配clients对象
Clients.matchAll()
返回所有匹配的clients
Clients.openWindow()
打开给定网址的新浏览器窗口
Clients.claim()

七、SEO

百度、谷歌的搜索引擎目前已经支持了SPA,对于别的爬虫,需要使用 SSR 来解决 SEO 的问题。
第一次请求,拿到的是直出的HTML,页面会安装Service Worker,Service Worker会预缓存一些文件,比如说js css app shell结构。
image

第二次请求,在Service Worker中判断
image

八、其他应用场景

后台数据同步
响应来自其它源的资源请求
集中接收计算成本高的数据更新,比如地理位置和陀螺仪信息,这样多个页面就可以利用同一组数据
在客户端进行CoffeeScript,LESS,CJS/AMD等模块编译和依赖管理(用于开发目的)
后台服务钩子
自定义模板用于特定URL模式
性能增强,比如预取用户可能需要的资源,比如相册中的后面数张图片

App Shell

image

将核心应用基础架构和 UI 从数据中分离出来,使初始加载尽可能简单,在打开网络应用后仅显示页面的布局。
包含的内容:

  • 用户界面“主干”的 HTML 和 CSS,包含导航和内容占位符

  • 用于处理导航和 UI 逻辑的外部 JavaScript 文件 (app.js),以及用于显示从服务器中检索的帖子并使用 IndexedDB 等存储机制将其存储在本地的代码

  • 网络应用清单和用于启用离线功能的服务工作线程加载程序

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>App Shell</title>
  <link rel="manifest" href="/manifest.json">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>App Shell</title>
  <link rel="stylesheet" type="text/css" href="styles/inline.css">
</head>

<body>
  <header class="header">
    <h1 class="header__title">App Shell</h1>
  </header>

  <nav class="nav">
  ...
  </nav>

  <main class="main">
  ...
  </main>

  <div class="dialog-container">
  ...
  </div>

  <div class="loader">
    <!-- Show a spinner or placeholders for content -->
  </div>

  <script src="app.js" async></script>
  <script>
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/sw.js').then(function(registration) {
      // Registration was successful
      console.log('ServiceWorker registration successful with scope: ', registration.scope);
    }).catch(function(err) {
      // registration failed :(
      console.log('ServiceWorker registration failed: ', err);
    });
  }
  </script>
</body>
</html>

案例

Web App Manifest

将Web应用程序安装到设备的主屏幕,为用户提供更快的访问和更丰富的体验。

1、在文件头部添加一个链接标记

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

2、manifest.json文件
示例

{
  "name": "HackerWeb",
  "short_name": "HackerWeb",
  "start_url": ".",
  "display": "standalone",
  "background_color": "#fff",
  "description": "A simply readable Hacker News app.",
  "icons": [{
    "src": "images/touch/homescreen48.png",
    "sizes": "48x48",
    "type": "image/png"
  }, {
    "src": "images/touch/homescreen96.png",
    "sizes": "96x96",
    "type": "image/png"
  }, {
    "src": "images/touch/homescreen168.png",
    "sizes": "168x168",
    "type": "image/png"
  }],
  "related_applications": [{
    "platform": "web"
  }, {
    "platform": "play",
    "url": "https://play.google.com/store/apps/details?id=cheeaun.hackerweb"
  }]
}

3、iOS Safari 添加到桌面
其对 Web App Manifest 的支持并不好,但支持通过meta/link 声明的一些私有属性

<!-- 指定桌面icon -->
<link rel="apple-touch-icon" href="touch-icon.png">
<!-- 指定应用名称 -->
<meta name="apple-mobile-web-app-title" content="AppTitle">
<!-- 是否隐藏 Safari 地址栏等 -->
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- 修改 iOS 状态栏颜色 -->
<meta name="apple-mobile-web-app-status-bar-style" content="black">

参考

https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API
https://github.com/SangKa/PWA-Book-CN
https://developers.google.cn/web/fundamentals/primers/service-workers/
https://lavas.baidu.com/pwa
https://developers.google.cn/web/fundamentals/architecture/app-shell
https://lzw.me/a/pwa-service-worker.html
https://segmentfault.com/a/1190000006095018#articleHeader8
https://xunleif2e.github.io/vue-lazy-component/demo/dist/index.html#/large-page

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant