Lavas & PWA

什么是 PWA

  • Progressive Web App
  • Native App : 臃肿,吃配置,下载成本...
  • Web App : 不稳定,响应迟钝,流失率高...

什么是 PWA

  • 可靠 - Service Worker
  • 体验 - App shell
  • 粘性 - Web App Manifest

Service Worker

  • 独立的 worker 线程,独立的 worker context

  • 一旦被 install 就永远存在,除非被 uninstall

  • 需要的时候可以直接唤醒,不需要的时候自动睡眠

  • 可编程拦截代理请求和返回,缓存文件

  • 消息推送,不操作DOM

  • Chrome  V40 +

  • Firefox,Opera

  • Android Browser 4.x +

  • Android Chrome

  • iOS 。。

 Service Worker 浏览器兼容性

怎么使用 Service Worker

  • 注册 Work Context
  • 安装 & 资源缓存
  • 激活
  • 定义请求
  • 更新缓存策略

注册 Service Worker

if ('serviceWorker' in navigator) {
    window.addEventListener('load', function () {
        navigator.serviceWorker.register('/sw.js', {scope: '/'})
            .then(function (registration) {

                // 注册成功
                console.log('ServiceWorker registration successful with scope: ', registration.scope);
            })
            .catch(function (err) {

                // 注册失败:(
                console.log('ServiceWorker registration failed: ', err);
            });
    });
}

安装 Service Worker

this.addEventListener('install', function (event) {
    event.waitUntil(
        caches.open('my-test-cache-v1').then(function (cache) {
            return cache.addAll([
                '/',
                '/index.html',
                '/main.css',
                '/main.js',
                '/image.jpg'
            ]);
        })
    );
});

自定义 Service Worker 请求

this.addEventListener('fetch', function (event) {
    event.respondWith(
        caches.match(event.request).then(function (response) {
            // 如果 Service Worker 有自己的返回,就直接返回,减少一次 http 请求
            if (response) {
                return response;
            }
            // 如果 service worker 没有返回,那就得直接请求真实远程服务
            var request = event.request.clone(); // 把原始请求拷过来
            return fetch(request).then(function (httpRes) {
                if (!httpRes || httpRes.status !== 200) {
                    return httpRes;
                }
                // 请求成功的话,将请求缓存起来。
                var responseClone = httpRes.clone();
                caches.open('my-test-cache-v1').then(function (cache) {
                    cache.put(event.request, responseClone);
                });
                return httpRes;
            });
        })
    );
});
// 安装阶段跳过等待,直接进入 active
self.addEventListener('install', function (event) {
    event.waitUntil(self.skipWaiting());
});

self.addEventListener('activate', function (evnet) {
    event.waitUntil(
        Promise.all([
            // 更新客户端
            self.clients.claim(),
            // 清理旧版本
            caches.keys().then(function (cacheList) {
                return Promise.all(
                    cacheList.map(function (cacheName) {
                        if (cacheName !== 'my-test-cache-v1') {
                            return caches.delete(cacheName);
                        }
                    })
                );
            })
        ])
    );
});

更新 Service Worker 缓存策略

App Shell 模型

  • 所有 UI 和基础架构都使用Service Worker本地缓存
  • 随后的加载将仅检索新数据或发生更改的数据

App Shell 模型

var cacheName = 'shell-content';
var filesToCache = [
    '/css/styles.css',
    '/js/scripts.js',
    '/images/logo.svg',
    '/offline.html',
    '/'
];

self.addEventListener('install', function (e) {
    console.log('[ServiceWorker] Install');
    e.waitUntil(
        caches.open(cacheName).then(function (cache) {
            console.log('[ServiceWorker] Caching app shell');
            return cache.addAll(filesToCache);
        })
    );
});

manifest.json

  • 添加到主屏
  • 原生体验

manifest.json

{
    "name": "这是一个完整名称",
    "icons": [
        {
            "url": "path-to-images/logo-144x144.png",
            "type": "image/png",
            "sizes": "144x144"
        }
    ],
    "background_color": "#2196f3",
    "display": "standalone"
}
<link rel="manifest" href="path-to-manifest/manifest.json">

manifest.json

display: {string}

消息通知

  • 使用 showNotification() 弹出通知
    function execute() {
      registerServiceWorker()
        .then(registration => {
          registration
             .showNotification('Hello World!', options);
        });
    }
    
{
    // 视觉相关
    "body": "<String>",
    "icon": "<URL String>",
    "image": "<URL String>",
    "badge": "<URL String>",
    "vibrate": "<Array of Integers>",
    "sound": "<URL String>",
    "dir": "<String of 'auto' | 'ltr' | 'rtl'>",

    // 行为相关
    "tag": "<String>",
    "data": "<Anything>",
    "requireInteraction": "<boolean>",
    "renotify": "<Boolean>",
    "silent": "<Boolean>",

    // 视觉行为均会影响
    "actions": "<Array of Strings>",

    // 定时发送时间戳
    "timestamp": "<Long>"
}

消息通知

消息通知

  • event.notificationclick - 捕获用户点击
  • event.notification.data - 获取消息数据
  • clients.openWindow()
  • clients.focus()
  • clients.matchAll()
  • clients.postMessage()

Lavas 的意义

  • 一个基于 Vue 的 PWA 完整解决方案
    • Service worker
    • manifest.json
    • App Shell & 离线 UX
  • 项目模板: basic 、AppShell、ssr 、mpa
  • 开发者只需要关注业务

Lavas 脚手架

$ npm install -g lavas
$ lavas init & cd pwa-demo
$ npm install
$ npm run dev
pwa-project/ (项目根目录)
    | - build/ (Webpack 和 dev-server 相关调试和构建配置文件)
    | - config/ (通用模块配置)
    | - src/ (源代码)
        | - assets/ (依赖的静态资源)
        | - components/ (业务开发的 Vue Component)
        | - pages/ (具体业务开发的 Page 页面 Component)
        | - store/ (Vuex store)
        | - app.js (Vue 入口执行文件)
        | - App.vue (项目的根 Component)
        | - entry-client.js (前端渲染入口文件)
        | - entry-skeleton.js(skeleton 渲染入口)
        | - router.js (Vue-router 路由配置文件)
        | - sw-register.js (注册 service worker 文件入口)
    | - static/ (不需要经过构建的一些静态资源)
    | - index.html
    | - package.json

Lavas 开发

$ npm run build
  • 自动生成可见的文件
    /dist/service-worker.js
    
  • 支持离线缓存静态资源能力
  • 动态网络请求
  • 静态资源文件的缓存和更新
  • service-worker.js 文件更新时,页面提示更新重载

Lavas 开发

  • 自定义配置
/* sw-precache.js中的配置 */
build: {
    cacheId: 'my-vue-app',
    // 生成的文件名称
    filename: 'service-worker.js',
    // 需缓存的文件配置, 可以逐项添加, 需动态缓存的放到runtimeCaching中处理
    staticFileGlobs: [
        // 'dist/index.html',
        // 'dist/static/**/**.*'
    ],
    // 忽略的文件
    staticFileGlobsIgnorePatterns: [
        /\.map$/ // map文件不需要缓存
    ],
    // 当请求路径不在缓存里的返回,对于单页应用来说,入口点是一样的
    navigateFallback: '/index.html',
    // 白名单包含所有的.html (for HTML imports) 和路径中含 `/data/`
    navigateFallbackWhitelist: [/^(?!.*\.html$|\/data\/).*/],

    // 最大缓存大小
    maximumFileSizeToCacheInBytes: 4194304,
}

Lavas 开发

  • 自定义配置
/* sw-precache.js中的配置 */
build: {
    // 生成service-worker.js的文件配置模板,不配置时采用默认的配置
    // 本demo做了sw的更新策略,所以在原有模板基础做了相应的修改
    templateFilePath: 'config/sw.tmpl.js',

    // 需要根据路由动态处理的文件
    runtimeCaching: [
    ]
}
// webpack.prod.conf.js 中通过组件引入配置,生成文件
new SWPrecacheWebpackPlugin(config.swPrecache.build);
  • 通知更新
config/sw.tmpl.js

Lavas 开发

  • 自动生成App-Shell 模板组件结构
src/components
    ├── AppBottomNavigator.vue
    ├── AppHeader.vue
    ├── AppMask.vue
    ├── AppSidebar.vue
    ├── ProgressBar.vue
    └── Sidebar.vue
  • 管理组件状态
/src/store/modules/app-shell.js
  • Vuex
mapStates() mapActions() - state, actions, mutations

More Lavas Features

  • Vuetify 组件库 - Material Design & Vue

  • Lavas Server Side Rendering - SSR 模板

THANKS.