什么是PWA #

webapp用户体验差(不能离线访问),用户粘性低(无法保存入口),pwa就是为了解决这一系列问题(Progressive Web Apps),让webapp具有快速,可靠,安全等特点

PWA一系列用到的技术 #

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" // 状态栏的颜色
}
图标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特点:

image

serviceWorker中的方法 #

实现浏览器离线缓存的功能

// 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();
        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) => {
    // 安装的过程中需要缓存
    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);
            }
        }))
    })
}
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.
       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

增加缓存策略

workbox.routing.registerRoute(
    function(obj){ 
        // 包涵api的就缓存下来
        return obj.url.href.includes('/user')
    },
    workbox.strategies.staleWhileRevalidate()
);

app-skeleton #

配置webpack插件 vue-skeleton-webpack-plugin

单页骨架屏幕

import Vue from 'vue';
import Skeleton from './Skeleton.vue';
export default new Vue({
    components: {
        Skeleton:Skeleton
    },
    template: `
        <Skeleton></Skeleton>    
    `
});


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>
    `
});
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 {
    apply(compiler) {
        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;
                }
            )
        });
    }
}

vue的预渲染插件 #

npm install prerender-spa-plugin 
const PrerenderSPAPlugin = require('prerender-spa-plugin')

plugins: [
    new PrerenderSPAPlugin({
        staticDir: path.join(__dirname, 'dist'),
        routes: [ '/', '/about',],
    })
]

Notification & Push Api #

...