教你如何构建一个离线应用?

作者:麦乐

什么是离线应用?

离线应用是指通过离线缓存技术,让资源在第一次被加载后缓存在本地,下次访问它时就直接返回本地的文件,就算没有网络连接。

优点:

  • 在没有网络的情况下也能打开网页。
  • 由于部分被缓存的资源直接从本地加载,对用户来说可以加速网页加载速度,对网站运营者来说可以减少服务器压力以及传输流量费用。

离线应用的核心是离线缓存技术,目前比较成熟Service Workers。它可以通过 JavaScript 代码去控制缓存的逻辑。也就是说,这个过程可以交给前端来处理。

为什么需要?

离线应用其实就是一种缓存技术,那有的人可能会想,HTTP的缓存策略已经发展的相当成熟了,为什么还需要Service Workers?前端的开发者会觉得:“浏览器的事情,为什么要依赖于后端呢?后端就好好提供数据就行了,缓存这种事情我想自己控制”。

什么是Service Workers

Service Workers 是一个在浏览器后台运行的脚本,它生命周期完全独立于网页。它无法直接访问 DOM,但可以通过 postMessage 接口发送消息来和 UI 进程通信。 拦截网络请求是 Service Workers 的一个重要功能,通过它能完成离线缓存、编辑响应、过滤响应等功能。

Service Worker并非专门为缓存而设计,它还可以解决Web应用推送、后台长计算等问题。

判断浏览器是否支持 Service Workers 的最简单的方法是通过以下代码:

// 如果 navigator 对象上存在 serviceWorker 对象,就表示支持
if (navigator.serviceWorker) {
  // 通过 navigator.serviceWorker 使用
}

注册 Service Workers

要给网页接入 Service Workers,需要在网页加载后注册一个描述 Service Workers 逻辑的脚本。 通常它在 index.html中被注册,代码如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
   
    </head>
    <body>
        <script>
            if ('serivceWorker' in navigator) {
                navigator.serviceWorker.register('/sw.js')
                    .then(reg => console.log('Service worker registered successfully!'))
                    .catch(err => console.log('Service worker failed to register!'));
            }
        </script>
    </body>
</html>

第一次打开该网页时 Service Workers 的逻辑不会生效,因为脚本还没有被加载和注册,但是以后再次打开该网页时脚本里的逻辑将会生效。

在 Chrome 中可以通过打开网址 chrome://inspect/#service-workers 来查看当前浏览器中所有注册了的 Service Workers。

实现离线缓存

Service Workers 在注册成功后会在其生命周期中派发出一些事件,通过监听对应的事件在特点的时间节点上做一些事情。

在 Service Workers 脚本中,引入了新的关键字 self 代表当前的 Service Workers 实例。

在 Service Workers 安装成功后会派发出 install 事件,需要在这个事件中执行缓存资源的逻辑,实现代码如下:

sw.js

// 当前缓存版本的唯一标识符,用当前时间代替
var cacheKey = new Date().toISOString();

// 需要被缓存的文件的 URL 列表
var cacheFileList = [
  '/index.html',
  '/app.js',
  '/app.css'
];

// 监听 install 事件
self.addEventListener('install', function (event) {
  // 等待所有资源缓存完成时,才可以进行下一步
  event.waitUntil(
    caches.open(cacheKey).then(function (cache) {
      // 要缓存的文件 URL 列表
      return cache.addAll(cacheFileList);
    })
  );
});

接下来需要监听网络请求事件去拦截请求,复用缓存,代码如下:

self.addEventListener('fetch', function(event) {
  event.respondWith(
    // 去缓存中查询对应的请求
    caches.match(event.request).then(function(response) {
        // 如果命中本地缓存,就直接返回本地的资源
        if (response) {
          return response;
        }
        // 否则就去用 fetch 下载资源
        return fetch(event.request);
      }
    )
  );
});

以上代码已经实现了缓存功能,但是如果代码发生了改变,我们还需要去下载新的资源,更新缓存资源。

浏览器针对 Service Workers 有如下机制:

  1. 每次打开接入了 Service Workers 的网页时,浏览器都会去重新下载 Service Workers 脚本文件(所以要注意该脚本文件不能太大),如果发现和当前已经注册过的文件存在字节差异,就将其视为“新服务工作线程”。
  2. 新 Service Workers 线程将会启动,且将会触发其 install 事件。
  3. 当网站上当前打开的页面关闭时,旧 Service Workers 线程将会被终止,新 Service Workers 线程将会取得控制权。
  4. 新 Service Workers 线程取得控制权后,将会触发其 activate 事件。

我们就可以在activate事件中删除原来的缓存。

// 当前缓存白名单,在新脚本的 install 事件里将使用白名单里的 key 
var cacheWhitelist = [cacheKey];

self.addEventListener('activate', function(event) {
  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          // 不在白名单的缓存全部清理掉
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            // 删除缓存
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

这样就完成了更新。

Vue中实现离线缓存

我们目前的项目大部分都是根据脚手架集成的,比如收vue,发布以前并不确定会打包出来多少个文件,假如构建输出的文件目录结构为:

├── app_4c3e186f.js
├── app_7cc98ad0.css
└── index.html

那么 sw.js 文件中 cacheFileList 的值应该是:

var cacheFileList = [
  '/index.html',
  'app_4c3e186f.js',
  'app_7cc98ad0.css'
];

但是这些文件的名字往往是不固定的,比如有些使用hash来命名的,这种情况下如何确定需要缓存的文件路径列表呢?

这个需要借助一个插件在webpack中去实现:

const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const { WebPlugin } = require('web-webpack-plugin');
const ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin');

module.exports = {
  entry: {
    app: './main.js'// Chunk app 的 JS 执行入口文件
  },
  output: {
    filename: '[name].js',
    publicPath: '',
  },
  module: {
    rules: [
      {
        test: /\.css$/,// 增加对 CSS 文件的支持
        // 提取出 Chunk 中的 CSS 代码到单独的文件中
        use: ExtractTextPlugin.extract({
          use: ['css-loader'] // 压缩 CSS 代码
        }),
      },
    ]
  },
  plugins: [
    // 一个 WebPlugin 对应一个 HTML 文件
    new WebPlugin({
      template: './template.html', // HTML 模版文件所在的文件路径
      filename: 'index.html' // 输出的 HTML 的文件名称
    }),
    new ExtractTextPlugin({
      filename: `[name].css`,// 给输出的 CSS 文件名称加上 Hash 值
    }),
    new ServiceWorkerWebpackPlugin({
      // 自定义的 sw.js 文件所在路径
      // ServiceWorkerWebpackPlugin 会把文件列表注入到生成的 sw.js 中
      entry: path.join(__dirname, 'sw.js'),
    }),
  ],
  devServer: {
    // Service Workers 依赖 HTTPS,使用 DevServer 提供的 HTTPS 功能。
    https: true,
  }
};

需要修改上面的 sw.js 文件中写成了静态值的 cacheFileList 为如下:

// 需要被缓存的文件的 URL 列表
var cacheFileList = global.serviceWorkerOption.assets;

注意需要使用https协议。

验证结果

为了验证 Service Workers 和缓存生效了,需要通过 Chrome 的开发者工具来查看。

通过打开开发者工具的 Application-Service Workers 一栏,就能看到当前页面注册的 Service Workers。

图3.12.1 查看当前页面注册的 Service Workers

{{o.name}}
{{m.name}}

猜你喜欢

转载自my.oschina.net/u/3620858/blog/5514292