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

clipboard.png

谈起PWA,许多人可能还只停留在“了解”的层面,比较少在实践中真正地尝试过,更多的仅仅是对着网上的教程和例子大概玩过。然而,网络上的例子可能是简单的demo,鲜有与真正的开发相结合,例如和webpack的工程化结合。这篇文章将会从一个webpack plugin出发,谈一谈如何使用这个名为offline-plugin的webpack插件轻松实现PWA。html

因为PWA相关的文章太多,因此本文再也不对“什么是PWA”,“PWA的生命周期”等基础内容再次赘述。android

offline-plugin相关连接:webpack

1、自动生成service-worker.js

PWA的核心可谓是service-worker(之后简称SW),任何一个PWA都有且只有一个service-worker.js文件,用于为SW添加资源列表,进行注册、激活等生命周期操做。可是在webpack构建的项目中,生成一个service-worker.js可能会面临两个较大的问题:git

  • 一、webpack生成的资源多会生成一串hash,sw的资源列表里面须要同步更新这些带hash的资源;
  • 二、每次更新代码,都须要经过更新sw文件版本号来通知客户端对所缓存的资源进行更新。(其实只要这一次的sw代码和上一次的sw代码不同便可触发更新,但使用明确的版本号会更加合适)。

看到这你可能已经想到,万能的webpack社区是否已经提供了相应的plugin来帮咱们自动处理这些事情呢?答案是确定的。除了官方推荐的sw-precache-webpack-plugin以外,还有咱们今天的主角offline-plugingithub

相比与sw-precache-webpack-plugin,我的认为offline-plugin具备以下优势:web

  • 一、更多的可选配置项,知足更加细致的配置要求;
  • 二、更为详细的文档和例子;
  • 三、更新频率相对更高,star数更多;
  • 四、自动处理生命周期,用户无需纠结生命周期的坑;
  • *五、支持AppCache;
  • 六、自动生成manifest文件。
  • ...

2、基本使用

安装

npm install offline-plugin [--save-dev]

初始化

第一步,进入webpack.config:chrome

// webpack.config.js example

var OfflinePlugin = require('offline-plugin');

module.exports = {
  // ...

  plugins: [
    // ... other plugins
    // it's always better if OfflinePlugin is the last plugin added
    new OfflinePlugin()
  ]
  // ...
}

第二步,把runtime添加到你的入口js文件当中:npm

require('offline-plugin/runtime').install();

ES6/Babel/TypeScriptjson

import * as OfflinePluginRuntime from 'offline-plugin/runtime';
OfflinePluginRuntime.install();

通过上面的步骤,offline-plugin已经集成到项目之中,经过webpack构建便可。数组

3、配置

前面说过,offline-plugin支持细致的配置,以知足不一样的需求。下面将介绍几个比较经常使用的配置项,方便你们进一步使用。

  • Caches: 'all' | Object

    告诉插件应该缓存什么东西,并以何种方式进行缓存
    `all`: 意味着全部webpack构建出来的资源,以及在`externals`选项中的资源都会被缓存。
    `Object`: 包含三个数组或正则的配置对象(`main`, `additional`, `optional`),它们都是可选的,且默认为空。
    默认:`all`。
  • externals: Array<string>

    容许开发者指定一些外部资源(好比CDN引用,或者不是经过webpack生成的资源)。配合`Caches`的`additional`项,可以实现缓存外部资源的功能。
    
    默认:`null`
    举例:`['fonts/roboto.woff']`
  • ServiceWorker: Object | null | false

    该对象包含多个配置项,这里仅列举最经常使用的。
    
    `events`:布尔值。容许runtime接受来自sw的消息,默认值为false。
    `navigateFallbackURL`:当一个URL请求从缓存或网络都没法被获取时,将会重定向到该选项所指向的URL。
  • AppCache: Object | null | false

    `offline-plugin`默认支持`AppCache`,可是`AppCache`草案已经被web标准所废弃,不建议使用。
    可是因为仍然有部分浏览器支持,因此插件默认提供这个功能。

4、runtime

上一节介绍了offline-plugin在webpack当中的配置,这一节将介绍runtime的一些用法。
若要使offline-plugin生效,用户必须在入口js文件中经过runtime进行初始化操做:

// 经过AMD方式
require('offline-plugin/runtime').install();

// 或者经过ES6/Babel/TypeScript方式

import * as OfflinePluginRuntime from 'offline-plugin/runtime';
OfflinePluginRuntime.install();

OfflinePluginRuntime对象提供了下列三个方法:

  • install(options: Object)
    开启ServiceWorker/AppCache的安装流程。这个方法是安全的,而且必须在页面初始化的时候就被调用。另外请勿把它放在任何的条件语句以内。(这句话不全对,在后面的降级方案里面会详细介绍)
  • applyUpdate()
    接受当前所安装的sw的更新信息。
  • update()
    检查新版本的ServiceWorker/AppCache的更新信息。

runtime.install()方法接受一个配置对象参数,用于处理sw各个生命周期里面的事件:

  • onInstalled

    当ServiceWorker/AppCache被install时执行,可用于展现“APP已经支持离线访问”。
  • onUpdating

    AppCache不支持该方法
    当更新信息被获取且浏览器正在进行资源更新时触发。在这个时刻,一些资源正在被下载。
  • onUpdateReady

    当`onUpdating`事件完成时触发。这时,全部资源都已经下载完毕。
    经过调用`runtime.applyUpdate()`方法来触发更新。
  • onUpdateFailed

    当`onUpdating`事件由于某些缘由失败时触发。
    这时没有任何资源被下载,同时全部的资源更新进程都应该被取消或跳过。
  • onUpdated

    当更新被接受时触发。

5、降级方案

当某些时候咱们须要撤掉sw进行降级的时候,咱们须要主动注销sw。然而offline-plugin默认没有提供注销sw的unregister()方法,因此咱们须要本身实现。

其实要主动注销sw很是简单,咱们能够直接调用ServiceWorkerContainer.getRegistrations()方法来拿到registration实例,而后调用registration.unregister()方法便可,具体代码以下:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.getRegistration().then((registration) => {
    registration && registration.unregister().then((boolean) => {
      boolean ? alert('注销成功') : alert('注销失败')
    });
  })
}

在调用该方法后,sw已经被注销,刷新一下页面就能看到资源是从新从网络获取的了。

在真实的生产环境中,咱们能够经过调用接口,来决定是否使用降级方案:

fetch(URL).then((switch) => {
  if (switch) {
    OfflinePluginRuntime.install()
  } else {
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.getRegistration().then((registration) => {
        registration && registration.unregister().then((boolean) => {
          boolean ? alert('注销成功') : alert('注销失败')
        })
      })
    }
  }
})

6、遇到的坑

在具体实践中,遇到一个比较大的坑,就是sw.js文件的更新。

在service worker的设计中,浏览器每一次加载站点的URL,都会从新请求一遍sw.js。若发现这一次的sw.js内容和上一次的不同,就会断定为资源更新,从新触发sw的生命周期。然而,sw.js也是一个普通的js资源文件,会默认使用服务器设置的expired时间,也就是它的max-age。在理解了service worker的设计后,咱们不难发现,sw.jsmax-age应该尽量短,以便浏览器可以及时更新资源列表。

这也是我在研究阶段直接使用http-server时所发现的问题。后来在官方的例子中,我发现npm script里面是这么写的:

"start": "http-server ./dist -p 7474 -c no-cache"

直接指定了全部资源都不使用缓存,这一点值得咱们注意。

另外,webpack-dev-server里没法正常使用offline-plugin,由于它须要具体的文件去生成sw.js,可是经过webpack-dev-server构建的项目,其文件是存放在内存中的,因此没法和offline-plugin正常搭配使用。建议仅在生产模式内使用offline-plugin

7、添加到主屏

手机浏览器都提供了“添加到主屏”的功能,但普通的网站添加到主屏,仅仅是把网站的书签放到桌面。若是要想把网站以PWA的形式添加到主屏,咱们须要一个manifest.json文件

{
  "name": "offline-plugin",
  "icons": [
    {
      "src": "/android-chrome-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "theme_color": "#181743",
  "background_color": "#181743",
  "start_url": "/",
  "display": "standalone"
}

而后,把这个manifest.json和其余静态资源一并打包到网站根目录便可:

clipboard.png

示例地址:

clipboard.png

打开chrome开发者工具,进入到Application一列,选择Manifest,就能够看到效果了:

clipboard.png

截止到目前(2017年8月15日),我所使用的iOS10.3.2版本的iPhone7手机,已经支持PWA了,效果以下:
通过查阅大量的资料,到目前为止,iOS并不支持PWA,可是能够经过在html里面添加几个标签,实现web页面和原生APP类似的体验效果:

应用图标:
<link rel="apple-touch-icon" href=“/custom_icon.png">

启动画面:
<link rel="apple-touch-startup-image" href="/launch.png">

应用名称:
<meta name="apple-mobile-web-app-title" content="AppTitle">

全屏效果:
<meta name="apple-mobile-web-app-capable" content="yes">

设置状态栏颜色:
<meta name="apple-mobile-web-app-status-bar-style" content="black">

使用safari打开
clipboard.png

添加到主屏后打开
clipboard.png

离线后从主屏打开

clipboard.png

打开任务管理器

clipboard.png

能够看到,PWA不管从表现仍是功能,都像一个独立的APP那样存在。

8、尾声

原来一直觉得苹果对PWA支持很差,但经过此次实践,能够知道其实PWA也取得了极大的推动,开发者们能够开心地搭建本身的PWA啦!结论不能下太早。。。

相关文章
相关标签/搜索