将electron 改造成PWA

avatar
前端工程师 @豌豆公主

PWA

前言

  • 由于项目驱动,要改造成pwa,开始感觉还是有点难度的。文档是英文的,实例也比较少。随着慢慢的学习,发现pwa实质,利用现有技术,使传统的网页有更好的体验的一种渐进式的应用程序,

需求背景

公司内部的日志系统,采用electron搭建,每次更新代码由于没有实现热更新,导致都是去下载安装包。非常占内存。 为方便起见,也为学习新技术,就将electron改造成了pwa,每次更新就会自动提示用户,减少了包大小,也方便了用户更新

概念

PWA(progressing web app),渐进式网页应用程序,一个 PWA 应用首先是一个网页, 可以通过 Web 技术编写出一个网页应用. 随后借助于 App Manifest 和 Service Worker 来实现 PWA 的安装和离线等功能。

  • PWA是渐进式的web应用,有没有PWA都行,只是用了PWA会带给用户一些好的体验
  • PWA不是特指某一项新技术,而是运用一些新技术对项目进行改造
  • 只要拥有一个web页面,那么就可以改造成PWA,并且现在vue,react,webpack都集成了PWA功能,开发起来,也是相当方便

特点

  • 渐进式:适用于选用任何浏览器的所有用户,因为它是以渐进式增强作为核心宗旨来开发的。

  • 跨平台:适合任何机型:桌面设备、移动设备、平板电脑或任何未来设备。

  • 连接无关性:能够借助于服务工作线程在离线或低质量网络状况下工作。

  • 离线推送:使用推送消息通知,能够让我们的应用像 Native App 一样,提升用户体验。

  • 安全性:通过 HTTPS 提供,以防止窥探和确保内容不被篡改。

对于我们移动端来讲,用简单的一句话来概况一个PWA应用就是,我们开发的H5页面增加可以添加至屏幕的功能,点击主屏幕图标可以实现启动动画以及隐藏地址栏实现离线缓存功能,即使用户手机没有网络,依然可以使用一些离线功能。这些特点和功能不正是我们目前针对移动web的优化方向吗,有了这些特性将使得 Web 应用渐进式接近原生 App,真正实现秒开优化。


下面开始PWA的核心技术

  • web app manifest.json
  • service worker
  • promise / async / await (由于都是异步,所以采用promsie来方便处理)
  • fetch api (为service worker提供操作缓存,中间代理作用)
  • cache storage (常见的缓存策略)
  • notification (消息推送)

web app manifest

说明: 就是一个json文件,里面都是一些固定配置

作用: manifest.json (应用程序清单): 在网站就可以添加到桌面,不需要用户通过商店进行下载

使用:

  • 在项目根目录下创建一个manifest.json文件
  • 在index.html 里使用link引入manifest.json文件
  • 在manifest里进行一些配置

配置详细参考文档:developer.mozilla.org/zh-CN/docs/…

我自己配置

{
    "short_name": "pwa",
    "name": "pwa-demo",
    "description": "A simply readable Hacker News app.",
    "orientation": "portrait-primary",
    "icons": [  
        {
         "src": "./icons/128.png", 
         "sizes": "128x128",   
         "type": "image/png"        
        },
        {
        "src": "./icons/144.png", 
        "sizes": "144x144",   
        "type": "image/png"        
        },
        {
        "src": "./icons/512.png", 
        "sizes": "512x512",   
        "type": "image/png"        
        }
    ],
    "id": "/",
    "start_url": "/", 
    "display": "fullscreen",  
    "background_color": "#002140", 
    "theme_color": "#002140"
}

在index.html中引入

image.png

现在manifest就配置完毕,看效果图

image.png

推荐一个生成manifest的 icons网站:  https://lp-pwa.gitee.io/pwa-genicon/

补充

一个标准的PWA,必须含有三部分
1. https服务器或者http:localhost或者127.xxx
2. manifest.json 
3. service.worker

Http-server

Http-server是一个轻量级的基于nodejs的http服务器,它最大好处就是:

可以使任意一个目录成为服务器的目录,完全抛开后台的沉重工程,直接运行想要的js代码。

  1. 安装
npm i -g http-server
  1. 运行 在要成为服务器的目录下运行如下命令
http-server

若要禁用缓存,请使用如下命令运行

http-server -c-1

image.png

现在我们已经满足一个标准的PWA两部分的要求,但是会在manifest中报

image.png ,这是我们缺service worker, 下面我们就开始service worker 的探索

service worker

  • 说明:

service worker 在 Web Worker 的基础上加上了持久离线缓存和网络代理能力,结合Cache API面向提 供了JavaScript来操作浏览器缓存的能力,这使得Service Worker和PWA密不可分。

  • 作用:

Service workers 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。这个 API 旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。

image.png

  • 使用:
  1. 在网页加载完毕后,注册serviceWorker。由于浏览器对serviceWorker的兼容性,所以要做兼容处理
// 1. 需要网页加载完成的
window.addEventListener('load', async()=>{
    console.log('333')
    // 2. 能力检测
    if ('serviceWorker' in navigator) {
        const registration = await navigator.serviceWorker.register('./sw.js')
    }
})

image.png 2.接下来就是对sw.js做逻辑处理,其实里面主要就是使用service worker的生命周期事件结合Cache API,提供JavaScript来操作浏览器缓存的能力

service worker的生命周期事件

  • install 事件会在 sw 注册成功时触发,主要用于缓存资源

  • activate 事件会在sw激活时触发,主要用于删除旧资源

  • fetch事件会在发送请求时触发,主要用于操作缓存或者读取网络资源

self.addEventListener('install', async (event) => {
  console.log('install', event);
});
self.addEventListener('activate', async (event) => {
  console.log('activate', event);
});
self.addEventListener('fetch', async (event) => {
  console.log('fetch', event);
});

到此,你如果只想将网站就可以添加到桌面,到这就OK了

image.png

image.png

如果还想操作浏览器缓存,那么接着往下来:


image.png

当我们修改sw.js中代码,比如

    + console.log('22222')

image.png

发现初次没有监听到fetch事件,第二次没有监听到activate。并且每次都会出现install。这是service worker中比较细的点

  1. 只要sw.js 发生变化,install事件就会重新触发
  2. activate事件会在install事件后触发,但是如果已经存在sw,那么就处于等待状态,直到当前sw 终止,可以通过self.skipWaiting() 方法跳过等待
  3. sw 激活后,会在下一次刷新页面的时候生效,可以通过self.clients.clain() 立即获取控制权

我们观察控制台

image.png

那么我们实现上述代码

self.addEventListener('install', async (event) => {
  await self.skipWaiting(); // 防止还没跳完,就进入下一阶段了,用await来阻塞async的函数执行
  console.log('install', event);
});
self.addEventListener('activate', async (event) => {
  console.log('activate', event);
  await self.clients.claim(); // 表示service worker 激活后,会立即获取控制权
});
self.addEventListener('fetch', async (event) => {
  console.log('fetch', event);
});

Cache API

说明:

caches 这个 API 是针对 Request Response 的。caches 一般结合 Service Worker 使用,因为请求级别的缓存与具有页面拦截功能的 Service Worker 最配。

使用: caches api 类似于数据库的操作

  1. 打开缓存 caches.open(cacheName)
  2. 获取包括所有缓存的key, caches.keys()
  3. 根据key删除对应的缓存 caches.delete(key)

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

image.png

image.png

下面利用service Worker 和caches 来进缓存操作

  1. 在install 里缓存
 const CACH_NAME = 'cache_v1';
 self.addEventListener('install', (event) => {
 
 // 开启一个cache 得到一个cache对象
  const cache = await caches.open(CACH_NAME);
  // cache对象就可以存储资源
  //  等待catch把所有的资源存储起来
  await cache.addAll([
    '/',
    '/index.css',
    '/data.json',
    '/manifest.json',
    '/icons/128.png',
    '/icons/144.png',
    '/icons/512.png',
  ]);
  await self.skipWaiting();
  })
  1. 在active里删除旧资源
 // 清除旧的资源,获取所有资源的key
 self.addEventListener('activate', (event) => {
  const keys = await caches.keys();
  keys.forEach((key) => {
    if (key !== CACH_NAME) {
      caches.delete(key);
    }
  });
   await self.clients.claim(); 
 })
  1. 在fetch里操作缓存或者读取网络资源
// fetch 事件会在请求发送时触发
/**
 * 判断资源是否能请求成功,请求成功,就响应成功结果。如果断网,读取caches缓存
 */
self.addEventListener('fetch', (event) => {
  // 请求对象
  const req = event.request;
  // 给浏览器响应
  event.respondWith(networkFirst(req));
});
// 网络优先
async function networkFirst(req) {
  try {
    const fresh = await fetch(req);
    return fresh;
  } catch (error) {
    // 从缓存中读取
    const cache = await caches.open(CACH_NAME);
    const cached = await cache.match(req);
    return cached;
  }
}
// 缓存优先
async function cachesFirse() {}

当我们有了缓存,就可以,在断网的时候,也能渲染页面

image.png 断网情况

image.png

至此,我们在网站就可以添加到桌面,并且可以操作缓存,自定义缓存策略。我们实现了简单的一个pwa,但是我们要实现的功能,还有跟多,比如

  1. 现在的缓存文件都是写死的,对打包的hash文件如何做实时缓存
  2. 这张图锁衍生出的多种缓存策略 image.png
  • CacheFirst:优先取缓存中的数据,若没有则请求网络,请网络也失败就会报错
  • CacheOnly:只从缓存中获取,若没有则报错
  • NetworkFirst:优先从网络获取,若没有则从缓存中获取,缓存获取失败则报错
  • NetworkOnly:只从网络获取,若没有则报错
  • StaleWhileRevalidate:同时从网络与缓存获取,如果缓存可用,取缓存数据,否则从网络中请求,同时缓存会随着网络请求而更新
  • 总之,四个人双向的关系,可以玩出很多种组合
  1. 处理生命周期的坑,怎么跳过等待,怎么立即获取控制权
  2. 缓存哪些资源, 不缓存哪些资源 等等等等,前面也说过,现在vue ,reace ,webpack 都集成了PWA功能,那么下面我们使用webpack 的一个插件offline-plugin 来轻松实现PWA

offline-plugin

offline-plugin 传送门github.com/NekR/offlin…

这次是实际的项目spa开发

安装

npm install offline-plugin -S
vue add pwa -D
npm i element-ui -S  

使用

  1. 在vue.config中引入,并实例化offline-plugin
var OfflinePlugin = require('offline-plugin');
module.exports = {
  configureWebpack: {
    plugins: [
      new OfflinePlugin({
        // 要求触发ServiceWorker事件回调
        ServiceWorker: {
          events: true,
        },
      }),
    ],
  },
};
  1. 新建sw.js文件,操作 offline-plugin
import { MessageBox } from 'element-ui';
import * as OfflinePluginRuntime from 'offline-plugin/runtime';
OfflinePluginRuntime.install({
  onUpdateReady: () => {
    //    更新完成之后,调用applyUpdate即skipwaiting()方法
    OfflinePluginRuntime.applyUpdate();
  },
  onUpdated: () => {
    MessageBox.confirm('发现版本更新,是否更新', {
      confirmButtonText: '更新',
      cancelButtonText: '取消',
      type: 'warning',
    }).then(() => {
      window.location.reload();
    });
  },
});

  1. 在main.js中引入sw.js文件,并引入element, 做弹框更新提示
import "./sw.js";
import "element-ui/lib/theme-chalk/index.css";
import ElementUI from "element-ui";
Vue.use(ElementUI);

image.png

至此offline-plugin的基本使用就完结了,具体配置可参考官网

传送门[github.com/NekR/offlin… ],或者直接把源代码考下来研究

这里说明一下:

  • 选择了offline-plugin插件之后呢,之前我们手写的注册Service Worker和Service Worker缓存相关逻辑都可以去掉了,因为offline-plugin会帮我们做这些事情。
  • offline-plugin插件会自动扫描webpack构建出来的dist目录里的文件,对这些文件配置缓存列表,正如上面插件里面的配置。
  • event:true指定了要触发Service Worker事件的回调,这个main.js里的配置是相对应的,只有这里设置成true,那边的回调才会触发。
  • 我们在main.js里的配置是为了,当Service Worker有更新时,立刻进行更新,而不让Service Worker进入wait状态,这和上面我们讲到的Service Worker更新流程相对应。

当然更多的offline-plugin相关配置,也可以去官网看文档。

参考文章链接

1. 使用offline-plugin配置service-worker的问题

2.使用offline-plugin搭配webpack轻松实现PWA

3. 网站渐进式增强体验(PWA)改造:Service Worker 应用详解

  1. vuecli3项目添加pwa支持

  2. 视频推荐 www.bilibili.com/video/BV1wt…