PWA 技术应用
PWA 简介
1. PWA 是什么?
- PWA 是 Progressive Web App 的缩写,渐进式网络应用程序,是提升WEB APP体验的一种新方法,能给用户原生应用的体验。
- PWA 可以用来开发跨平台的应用程序,可以在各种设备上运行,包括桌面、移动设备等。
- PWA 运用现代的 WEB API 以及传统的渐进式增强策略来创建跨平台 Web 应用程序。
- PWA 能做到原生应用的体验不是靠特指某一项技术,而是经过应用一些新技术进行改进。
- 现在vue、react的脚手架中都已经集成了PWA功能。
2. PWA 的优势
- 渐进式:适用于所有浏览器,因为它是以渐进式增强作为宗旨开发的
- 流畅:能够借助Service Worker在离线或者网络较差的情况下正常访问
- 可安装:用户可以添加常用的webapp到桌面,免去去应用商店下载的麻烦
- 原生体验:可以和app一样,拥有首屏加载动画,可以隐藏地址栏等沉浸式体验
- 粘性:通过推送离线通知等,可以让用户回流
3. web app manifest: 应用程序清单
1.介绍:
- web app manifest是PWA技术集合中的一部分
- web app manifest可以让网站安装到设备的主屏幕,而不需要用户通过应用商店进行下载。
- web app manifest在一个JSON文本文件中提供有关应用程序的信息(如名称、作者、图标、描述)
- 传统的web App 入口:1.网址 2.书签,收藏夹 3.直接搜索
- web app manifest:
- 可以添加到桌面,有唯一的图标和名称
- 有启动时界面,避免生硬的过渡
- 隐藏浏览器相关的UI,如地址栏等
2.使用步骤:
- 在项目的根目录下创建一个manifest.json文件
- 在index.html中引入manifest.json文件
- 在manifest.json文件中提供常见的配置
- 需要在https协议或者http:s://localhost下访问项目
- html
<link rel="manifest" href="/manifest.json"> 3.常见配置1
- name:用于指定应用的名称,用户安装横幅提示名称,和启动画面中的文字
- short_name:用于指定应用的短名称,用于主屏幕图标下的文字
- start_url:用于指定用户从设备启动应用程序时加载的URL,可以是绝对路径和相对路径
- icons:用于指定可在各种环境中用作应用程序图标的图像对象数组,144x144
- background_color:用于指定启动画面的背景颜色
- theme_color:用于指定应用程序的主题颜色
- display:用于指定应用程序的显示模式
- fullscreen:全屏显示,所有可用的显示区域都被使用,并且不显示状态栏
- standalone:独立模式,让这个应用看起来像一个独立的应用程序,包括具有不同的窗口,在应用程序启动器中拥有自己的图标等
- minimal-ui:该应用程序将看起来像一个独立的应用程序,但会有浏览器地址栏
4. web worker
- 1.web worker 介绍
- 1.web worker 是运行在后台的 JavaScript,独立于其他脚本,不会影响页面的性能。
- 2.您可以创建多个 web worker,以充分利用多核 CPU。一旦被创建,web worker 就可以独立运行,而不会影响主线程,也不会影响页面的性能。
- 3.web worker 可以用来执行一些耗时的操作,比如计算、数据请求等,而不会阻塞主线程的执行,但不能操作DOM 和 BOM。
- 4.web worker 是临时的,每次做的事情的结果还不能被持久存下来,如果下次有同样的复杂操作,还得费时间重新来一遍
- 2.web worker使用
- 1.创建一个 worker 线程 var worker = new Worker('worker.js');
- 2.在web worker 中执行一些耗时的操作
- 3.web worker 中执行完毕后,通过 self.postMessage() 方法将结果发送给主线程
- 4.主线程中通过 worker.onmessage= function(msg){} 方法接收 web worker 发送过来的结果
- 5.主线程也可以用同样的方法来给 web worker 发送消息
- 6.主线程中通过 worker.terminate() 方法终止 web worker
- 3.示例代码:
- html
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <script> let worker = new Worker('work.js') console.log('start') // 接受 worker 发来的消息 worker.addEventListener("message", e=> { console.log(e.data) }) // 向 worker 发送消息 worker.postMessage('hello') console.log('end') </script> </body> </html> - js
// work.js 文件 // 计算 1- 1亿之间所有数的和 let sum = 0 for (let i = 1; i <= 100000000; i++) { sum += i } // 获取结果 向主线程发送消息 self.postMessage({sum:sum}) // 监听主线程消息 self.addEventListener('message', (event) => { console.log(event.data) }) 5. service worker:服务工作者
- 1.介绍:
- 1.一个标准的PWA程序,必须包含3个部分
- 1.manifest.json:应用程序清单
- 2.service worker:服务工作者
- 3.https服务器 或者 http://localhost
- 2.W3C组织早在2014年5月就提出过 Service Worker 这样的一个HTML5 API,主要用来做持久的离线缓存。
- 3.前端有很多性能优化的手段:CDN、CSS Sprite、文件的合并压缩、异步加载、资源缓存等等,这些手段都是用来做性能优化的,但是如果断网了会发生什么
- 4.service worker 允许web应用在网络环境较差或者是离线的环境下依旧可以使用
- 5.service worker 可以极大的提升web App 的用户体验
- 6.service worker 是一个独立的worker线程,独立于浏览器主线程,不会阻塞主线程的执行,是一种特殊的 web worker
- 2.service worker使用
- 1.在window.onload中注册 service worker,防止与其他资源竞争
- 2.navigator对象中内置了 serviceWorker属性,可以用来注册service worker
- 3.service worker 在老版本的浏览器中不支持,需要进行浏览器兼容 if('serviceWorker' in navigator) {}
- 4.注册service worker navigator.serviceWorker.register('./sw.js'),返回一个promise对象
- 5.一旦被install,就永远存在,除非被手动unregister
- 6.用到的时候可以直接唤醒,不用的时候自动休眠
- 7.可编程拦截代理请求和返回,缓存文件,缓存的文件可以被网页进程取到(包括网络离线状态)
- 8.离线内容开发者可控
- 9.必须在HTTPS环境下才能工作
- 10.异步实现,内部大都是通过Promise实现,不能使用同步API
- 3.示例代码:
- js
window.onload = function () { if ('serviceWorker' in navigator) { navigator.serviceWorker.register('./sw.js').then((registration) => { console.log(registration) }).catch((error) => { console.log(error) }) } }
- 4.service worker 的生命周期
- 1.install事件会在 service worker 注册成功的时候触发,主要用于缓存资源
- 2.activate 事件会在 service worker 激活的时候触发,主要用于删除旧的资源
- 3.fetch 事件会在发送请求的时候触发,主要用于操作缓存或者读取网络资源
- 4.如果sw.js发生了变化,install事件会重新触发,但是activate事件不会重新触发,需要手动删除旧的sw.js
- 5.activate事件会在install事件后触发,但是如果现在已经存在 service worker,那么就处于等待状态,直到当前 service worker 终止
- 6.可以通过self.skipWaiting()方法跳过等待,返回一个promise对象
- 7.可以通过event.waitUntil(promise)方法,会在promise结束后才会结束当前生命周期函数,防止浏览器在异步操作之前就停止了生命周期
- 8.service worker 激活后,会在下一次刷新页面的时候生效,可以通过self.clients.claim()方法,立即获取控制权
- js
// sw.js 文件 主要用来缓存 和 处理请求 // service worker install event self.addEventListener('install',(event)=>{ // 111 console.log('installing service worker',event); // 会让 service worker 跳过等待 ,直接进入active 状态 // 等待 skipWaiting() 执行完 才会执行 activate event.waitUntil(self.skipWaiting()) }) // service worker activate event self.addEventListener('activate',(event)=>{ console.log('activating service worker',event); // 表示 service worker 激活后,立即获取控制权 不然只能刷新之后才会执行 event.waitUntil(self.clients.claim()) }) // service worker fetch event // self.addEventListener('fetch',(event)=>{ console.log('fetching service worker',event); })
6.promise
- 基本使用
- 1.promise 是异步编程的一种解决方案,比传统的解决方案 --- 回调函数和事件 -- 更合理和强大
- 2.promise 可以以链式的方式来进行异步编程,解决了回调地狱的问题
- 3.promise 常用的静态方法
- 1.Promise.resolve(value) 返回一个以给定值解析后的 Promise 对象,如果返回值是一个 Promise 对象,则直接返回该对象。
- 2.Promise.reject(reason) 静态函数Promise.reject返回一个以给定 reason 拒绝的 Promise 对象。
- 3.Promise.all(iterable) 返回一个 Promise 实例,等待所有 Promise 对象都成功了,才会成功
- 4.Promise.race(iterable) 竞速,只要有一个 Promise 对象成功或者失败,就会成功或者失败
7. async await
- 基本使用
- 1.ES2017 引入的 async/await,是 Generator 函数的语法糖,用于简化异步操作
- 2.async 用于修饰一个函数,async function fn (){} , await 函数会返回一个 Promise 对象
- 3.await 只能出现在 async 函数中,await 后面跟一个 promise 对象 ,用于获取Promise 成功的结果,如果不是 Promise对象,直接返回
- 4.await 会阻塞 async 函数的执行,直到 Promise 对象成功或者失败,才会继续执行 async 函数后面的代码
- 5.await 后面的 Promise 对象如果失败,会抛出异常,需要用 try/catch 来捕获异常
8. fetch(处理兼容问题 使用 polyfill)
- 基本使用
- 1.fetch API 提供了一个Javascript接口,用于访问和操作HTTP请求和响应。 它返回一个 Promise 对象,该对象表示一个异步操作的结果
- 2.在 service worker 中,如果想要发送请求,无法使用XMLHttpRequest,只能使用 fetch API
- 3.fetch API 是基于Promise实现的
- 4.fetch(url,options) 用于发送HTTP请求,返回一个包含响应结果的Promise对象
- 5.response 是一个二进制数据流,需要调用json()方法,将二进制数据流转换为JSON对象
- 6.options 常见参数:
- 1.method:请求方法,例如 GET、POST、PUT、DELETE 等
- 2.headers:请求头,是一个 Headers 对象,可以设置请求头信息
- 3.body:请求体,可以是一个字符串、FormData 对象或者 Blob 对象
9. cache storage
- 1.基本使用
- 1.cacheStorage 接口表示 Cache 对象的存储。配合service worker 来实现资源的缓存
- 2.caches api 类似于数据库的操作
- 1.caches.open(cacheName).then(function(cache){}):用于打开缓存,返回一个匹配cacheName的 Cache 对象的Promise,类似与连接数据库
- 2.caches.keys().then(function(names){}):返回一个promise 对象,包括所有的缓存的key(数据库名)
- 3.caches.delete(cacheName):用于删除指定的缓存(数据库)
- 3.cache对象常用方法(单条数据的操作)
- 1.cache接口为缓存的Request/Response对象对提供存储和检索功能。它类似于数据库事务,允许一组操作被原子地执行。
- 2.cache.put(req,res):把请求当成key,并且把对应的响应存储起来
- 3.cache.add(url):根据url发起请求,并且把响应结果存储起来
- 4.cache.addAll(urls):抓取一个url数组,并且把结果都存储起来
- 5.cache.match(req):获取req对应的response
- 2.离线缓存
- 1.Caches 中已经缓存了我们需要的静态资源
- 2.当请求失败的时候,就可以去缓存中读取对应的数据了
- 3.通过event.respondWith(response) 可以控制响应的内容
- 代码示例:
- js
// 缓存名称 const CACHE_NAME = 'my-cache-v3'; // 用于 缓存内容 self.addEventListener('install', async (event)=> { // 开启一个cache, 得到一个cache对象 const cache = await caches.open(CACHE_NAME) // cache对象就可以存储资源 await cache.addAll([ "/", //这里踩坑 不能缓存 /index.html ,否则刷新页面会404,直接 / 就行了 "/images/aiqiyi.png", "/manifest.json", "/index.css" ]); await self.skipWaiting(); }) // 用于清除旧的缓存 self.addEventListener('activate', async (event) =>{ // 会清除掉旧的资源 // 获取 const keys = await caches.keys(); for(const key in keys){ // 如果key不等于CACHE_NAME,就删除掉 if(keys[key] !== CACHE_NAME){ await caches.delete(keys[key]); } } await self.clients.claim(); }) // 用于拦截请求 // 判断资源是否能够请求成功,如果请求成功,就响应成功的结果,如果断网了,请求失败了,读取caches中的资源 self.addEventListener('fetch', (event)=> { // 请求对象 const request = event.request; // 给浏览器响应 event.respondWith(networkFirst(request)) console.log(event.request.url); }) // 定义网络优先方法 async function networkFirst(req) { // 先从网络读取资源 try{ const fresh = await fetch(req) return fresh; }catch (error){ // 如果读取失败,从caches中读取资源 const cache = await caches.open(CACHE_NAME) const cached = await cache.match(req) return cached } }
10.notification API
- 1.Notification API基本使用
- 1.Notifications API 的通知接口用于向用户配置和显示桌面通知。
- 2.Notification.permission 可以获取当前用户的授权情况:
- 1.granted:用户允许接收通知
- 2.denied:用户拒绝接收通知
- 3.default:默认的,用户未授权;
- 3.通过Notification.requestPermission()方法可以请求用户授权
- 4.通过new Notification('title',{body:',icon:')可以显示通知
- 5.在授权通过的情况下,可以在service worker中通过self.registration.showNotification('title',{body:msg',icon:xxx')来显示通知
- 2.案例代码:
- html
<script> // 注册 service worker window.addEventListener('load', function () { // 判断浏览器是否支持 if ('serviceWorker' in navigator) { // 使用 try catch 捕获异常 try{ // 注册 service worker navigator.serviceWorker.register('./sw.js').then(function (registration) { console.log('service 注册成功', registration) }) }catch (e) { console.log('service 注册失败', e) } } }) // 判断用户是否允许通知 if(Notification.permission === 'default'){ // 询问用户是否开启通知 Notification.requestPermission() } // 如果用户进来发现没有网络 发送给用户通知 // if(!navigator.onLine){ // console.log(123) // new Notification('提示!',{body: '网络连接失败!'}) // } // 监听网络连接给提示 window.addEventListener('online',()=>{ new Notification('提示!',{body: '网络已连接!'}) }) // 监听网络断开给提示 window.addEventListener('offline',()=>{ new Notification('提示!',{body: '网络连接失败!'}) }) </script>
11.缓存策略
- 1.对于不同的数据,需要不同的缓存策略
- 2.本地的静态资源,缓存优先
- 3.对于需要动态更新的数据,网络优先
- 4.避免缓存跨域资源
- 1.由于更新机制的问题,如果Service worker 缓存了错误结果,将会对web应用造成灾难性后果。我们必须小心翼翼的检查网络返回是否准确。
- 2.一种常见的做法是只缓存满足如下条件的结果:
- 1.响应状态码为200;避免缓存304、404、50x等常见结果。
- 2.响应类型为basic或者cors;即只缓存同源、或者正确地跨域请求结果;避免缓存错误的响应和不正确的跨域请求响应(opaque)
- js
const CACHE_NAME = 'v1'; self.addEventListener('fetch', async function (event) { // 只缓存同源的内容 cosnt req = event.request; // 获取url对象 const url = new URL(req.url); // 判断是否同源 // 如果不同源 直接返回 if(url.origin !== location.origin){ return; } // 判断是静态资源还是动态资源 if(req.url.includes('/api')){ // 如果动态资源 网络优先 await event.respondWith(networkFirst(event)); } else{ // 如果静态资源 缓存优先 await event.respondWith(cacheFirst(event)); } }) // cache 优先,一般适用于静态资源 async function cacheFirst(event) { const req = event.request; const cache = await caches.open(CACHE_NAME); const cachedResponse = await cache.match(req); return cachedResponse || await fetch(req); } // 网络优先,一般适用于动态数据 async function networkFirst(event) { const req = event.request; const cache = await caches.open(CACHE_NAME); try{ const response = await fetch(req); // 把响应的备份存储到缓存中 cache.put(req, response.clone()); return response; }catch (e){ return await cache.match(req); } } // 如果网络断开,则使用缓存 async function offlineFirst(event) { const req = event.request; const cache = await caches.open(CACHE_NAME); const cachedResponse = await cache.match(req); }