PWA
什么是PWA
webapp用户体验差(不能离线访问),用户粘性低(无法保存入口),pwa就是为了解决这一系列问题(Progressive Web Apps),让webapp具有快速,可靠,安全等特点
PWA一系列用到的技术
- Web App Manifest
- Service Worker
- Push Api & Notification Api
- App Shell & App Skeleton
- …
Web App Manifest
将网站添加到桌面、更类似native的体验
Web App Manifest设置
<link rel="manifest" href="/manifest.json">
{
"name":"珠峰课堂", // 应用名称
"short_name":"课堂", // 桌面应用的名称 ✓
"display":"standalone", // fullScreen (standalone) minimal-ui browser ✓
"start_url":"", // 打开时的网址 ✓
"icons":[], // 设置桌面图片 icon图标 修改图标需要重新添加到桌面icons:[{src,sizes,type}]
"background_color":"#aaa", // 启动画面颜色
"theme_color":"#aaa" // 状态栏的颜色
}
ios meta/link 私有属性设置
图标icon
<link rel="apple-touch-icon" href="apple-touch-icon-iphone.png"/>
添加到主屏后的标题 和 short_name一致
<meta name="apple-mobile-web-app-title" content="标题">
隐藏safari地址栏 standalone模式下默认隐藏
<meta name="apple-mobile-web-app-capable" content="yes" />
设置状态栏颜色
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
横幅安装:用户在浏览器中访问至少两次,两次访问间隔至少时间为五分钟(safari不支持横幅)
Service Worker
为了提升用户体验
Service Worker特点:
-
不能访问/操作dom
-
会自动休眠,不会随浏览器关闭所失效(必须手动卸载)
-
离线缓存内容开发者可控
-
必须在https或者localhost下使用
-
所有的api都基于promise
-
安装( installing ):这个状态发生在 Service Worker 注册之后,表示开始安装,触发 install 事件回调指定一些静态资源进行离线缓存。
-
安装后( installed ):Service Worker 已经完成了安装,并且等待其他的 Service Worker 线程被关闭。
-
激活( activating ):在这个状态下没有被其他的 Service Worker 控制的客户端,允许当前的 worker 完成安装,并且清除了其他的 worker 以及关联缓存的旧缓存资源,等待新的 Service Worker 线程被激活。
-
激活后( activated ):在这个状态会处理 activate 事件回调 (提供了更新缓存策略的机会)。并可以处理功能性的事件 fetch (请求)、sync (后台同步)、push (推送)。
-
废弃状态 ( redundant ):这个状态表示一个 Service Worker 的生命周期结束。
serviceWorker中的方法
- self.skipWaiting():表示强制当前处在 waiting 状态的 Service Worker 进入 activate 状态
- event.waitUntil():传入一个 Promise 为参数,等到该 Promise 为 resolve 状态为止。
- self.clients.claim():在 activate 事件回调中执行该方法表示取得页面的控制权, 这样之后打开页面都会使用版本更新的缓存。旧的 Service Worker 脚本不再控制着页面,之后会被停止。
实现浏览器离线缓存的功能
-
注册缓存
window.addEventListener('load',function(){ // 页面加载完成后 注册serviceWorker if('serviceWorker' in navigator){ navigator.serviceWorker.register('./sw.js').then(registeration=>{ console.log(registeration.scope); }); navigator.serviceWorker.addEventListener('controllerchange',()=>{ console.log('change') }) } if(!navigator.onLine){ window.addEventListener('online',()=>{ console.log('更新'); }) } });
-
离线缓存 应用cache缓存请求
// self 当前线程中的this
// 拦截用户发送的所有请求
let CACHE_NAME = `cache_version_` + 81;
let CACHAE_LIST = [
'/',
'/index.css',
'/index.html',
'main.js',
'/getImage'
];
// 独立的线程 可以使用fetch 但是不能使用ajax
// 获取数据后进行缓存
function fetchAndSave(req){
return fetch(req).then(res=>{ // res 是流
// 做缓存操作
let r = res.clone();// res必须clone,因为使用一次就销毁
caches.open(CACHE_NAME).then(cache=>cache.put(req,r));
return res;
})
}
self.addEventListener('fetch', e => {
// 用相应来替换 如果获取不到才用缓存
let url = new URL(e.request.url);
if(url.origin !== self.origin){
return;
}
if(e.request.url.includes('/getImage')){ // 调用了接口
// 如果遇到了接口 更新缓存
e.respondWith(
fetchAndSave(e.request).catch(err=>{
// 如果没网 在缓存中 匹配结果 返回请求
return caches.match(e.request);
})
)
return;
}
e.respondWith(
fetch(e.request).catch(err=>{
// 如果没网 在缓存中 匹配结果 返回请求
return caches.match(e.request);
})
)
}); // 用缓存替换
// serviceWorker安装的阶段
function preCache() {
// 开启缓存空间
return caches.open(CACHE_NAME).then(cache => {
return cache.addAll(CACHAE_LIST);
})
}
self.addEventListener('install', (e) => {
// 安装的过程中需要缓存 手动skipWaiting
e.waitUntil(
preCache().then(skipWaiting)
)
});
function clearCache() {
return caches.keys().then(keys => {
return Promise.all(keys.map(key => {
if (key !== CACHE_NAME) {
return caches.delete(key);
}
}))
})
}
// 激活当前serviceWorker
self.addEventListener('activate', (e) => {
e.waitUntil(
Promise.all([
clearCache(),
self.clients.claim() // 立即使serviceWorker生效
])
)
})
在vue中使用pwa
https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa
vue create pwa-project
npm run build // 才具备pwa效果
vue-cli3.0配置pwa
在public目录下可以更改manifest配置文件
module.exports = {
pwa: {
name: 'My App',
themeColor: '#f2f2f2',
msTileColor: '#aaaaa',
appleMobileWebAppCapable: 'yes',
appleMobileWebAppStatusBarStyle: 'black',
workboxPluginMode: 'InjectManifest',
workboxOptions: {
// swSrc is required in InjectManifest mode.
// 建立dev目录,放置sw.js 内容如下
swSrc: 'dev/sw.js',
}
}
}
需要更改registerServiceWorker文件 更改为sw.js
// 设置缓存前缀
workbox.core.setCacheNameDetails({prefix: "pwa-project"});
// 设置预缓存列表
self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.suppressWarnings();
// 增加缓存列表策略
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
基于workbox 缓存工具包 https://developers.google.com/web/tools/workbox
- 内置manifest.json
- 内置serviceWorker
- 内置了缓存策略
- cachefirst 缓存优先
- cacheonly 仅缓存
- networkfirst 网络优先
- networkonly 仅网络
- StaleWhileRevalidate 从缓存取,用网络数据更新缓存
增加缓存策略
workbox.routing.registerRoute(
function(obj){
// 包涵api的就缓存下来
// 返回true则缓存
return obj.url.href.includes('/user')
},
workbox.strategies.staleWhileRevalidate()
);
app-skeleton
配置webpack插件 vue-skeleton-webpack-plugin
const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin');
单页骨架屏幕
// skeleton.js
import Vue from 'vue';
import Skeleton from './Skeleton.vue';
export default new Vue({
components: {
Skeleton:Skeleton
},
template: `
<Skeleton></Skeleton>
`
});
在vue.config.js中配置一条configureWebpack规则
plugins: [
new SkeletonWebpackPlugin({
webpackConfig: {
entry: {
app: resolve('./src/entry-skeleton.js')
}
}
})
]
多页骨架屏幕
// skeleton.js
import Vue from 'vue';
import Skeleton1 from './Skeleton1';
import Skeleton2 from './Skeleton2';
export default new Vue({
components: {
Skeleton1,
Skeleton2
},
template: `
<div>
<skeleton1 id="skeleton1" style="display:none"/>
<skeleton2 id="skeleton2" style="display:none"/>
</div>
`
});
在vue.config.js中配置一条configureWebpack规则
new SkeletonWebpackPlugin({
webpackConfig: {
entry: {
app: path.join(__dirname, './src/skeleton.js'),
},
},
router: {
mode: 'history',
routes: [
{
path: '/',
skeletonId: 'skeleton1'
},
{
path: '/about',
skeletonId: 'skeleton2'
},
]
},
minimize: true,
quiet: true,
})
实现骨架屏插件
class MyPlugin {
// compiler代表的是当前整个webpack打包对象
apply(compiler) {
// compilation代表当前打包本次打包的
compiler.plugin('compilation', (compilation) => {
compilation.plugin(
'html-webpack-plugin-before-html-processing',
(data) => {
data.html = data.html.replace(`<div id="app"></div>`, `
<div id="app">
<div id="home" style="display:none">首页 骨架屏</div>
<div id="about" style="display:none">about页面骨架屏</div>
</div>
<script>
if(window.hash == '#/about' || location.pathname=='/about'){
document.getElementById('about').style.display="block"
}else{
document.getElementById('home').style.display="block"
}
</script>
`);
return data;
}
)
});
}
}
// 在configureWebpack plugin:[new MyPlugin()]
vue的预渲染插件
npm install prerender-spa-plugin
const PrerenderSPAPlugin = require('prerender-spa-plugin')
plugins: [
new PrerenderSPAPlugin({
staticDir: path.join(__dirname, 'dist'),
routes: [ '/', '/about',],
})
]