构建离线内容

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-02-26

构建离线应用

离线应用的优点:

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

Service Workers

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

使用

Service Workers 在注册成功后会在其生命周期中派发一些事件,通过监听对应的事在特点的时间节点上做一些事情。Service Workers脚本中引入了新的关键字self,代表当前的Service Workers 实例。

代码示例:

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

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

// 需要被缓存的文件的 URL 列表
//(使用插件完成,因为每一次的hash都可能不一样,所以不能写死借助插件完成)
var cacheFileList = global.serviceWorkerOption.assets;

// 监听 install 事件
self.addEventListener('install', function (event) {
  // 注册成功后会触发install事件 在这个事件进行资源缓存
  // 等待所有资源缓存完成时,才可以进行下一步
  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 的网页时,浏览器都会重新下载Service Workers脚本文件
// 如果发现和当前已经注册过的文件存在字节差异,就将其视为“新服务工作线程”

// 新的Service Workers 线程将会启动,且将会触发其install 事件
// 当网站上当前打开的页面关闭时,旧的Service Workers 线程将会被终止,新的Service Workers 线程将会取得控制权。

// 新 Service Workers 线程取得控制权后,将会触发其 activate 事件
// 在触发activate事件后,删除旧的缓存
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);
          }
        })
      );
    })
  );
});

接入webpack

serviceworker-webpack-plugin插件:会在全局注册一个变量global.serviceWorkerOption.assets,存储正确的带有当前hash值的资源列表,然后打包的时候会在sw.js的头部就会插入这样一段代码

var serviceWorkerOption = {
  "assets":[
    "./app_e26c96a9.js",
    "./app_028dcf98.css",
    "./index.html"
  ]
};

Webpack的配置文件:

// webpack.config.js
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/,
        // 提取出 Chunk 中的 CSS 代码到单独的文件中
        use: ExtractTextPlugin.extract({
          use: ['css-loader']
        }),
      },
    ]
  },
  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'),
    }),
  ],
  // Service Workers 依赖 HTTPS, 开发环境可以使用http
};

template.html模板文件:

<html>
<head>
  <meta charset="UTF-8">
  <!--导入 Chunk app 中的 CSS-->
  <link rel="stylesheet" href="app">
</head>
<body>
<div id="app"></div>
<!--导入 Chunk app 中的 JS-->
<script src="app"></script>
</body>
</html>

入口文件main.js

require('./main.css');

if (navigator.serviceWorker) {
  window.addEventListener('DOMContentLoaded',function() {
    // 调用 serviceWorker.register 注册,参数 /sw.js 为脚本文件所在的 URL 路径
    navigator.serviceWorker.register('sw.js');
  });
}

window.document.getElementById('app').innerText = 'Hello,webpack!!!';

用于测试的css文件

/* main.css */
div {
  color: red;
}

再加上package.json,与上面的main.jsmain.csssw.jswebpack.config.jstemplate.html一起就是完整的demo了。

{
  "name": "dive-into-webpack",
  "version": "1.0.0",
  "scripts": {
    "dev": "webpack-dev-server",
    "build": "webpack --config webpack_dist.config.js"
  },
  "dependencies": {},
  "devDependencies": {
    "css-loader": "^0.28.5",
    "extract-text-webpack-plugin": "^3.0.0",
    "serviceworker-webpack-plugin": "^0.2.3",
    "style-loader": "^0.18.2",
    "web-webpack-plugin": "^1.10.0",
    "webpack": "^3.4.0",
    "webpack-dev-server": "^2.7.1"
  }
}

此时执行npm run dev,就能启动本地服务了,使用chrome打开https://localhost:8080/index.html就能访问接入了Service Workers离线缓存页面了。

打开开发者工具的Application-Service Workers一栏就能看到当前页面的Service Workers了。

打开开发者工具的Application-Cache-Cache Storage一栏就能当前页面缓存的资源列表了。

通过开发者工具中的Application-Service Workers中的offline选项禁用网络,就能看到缓存是否生效了。

老版本的chrome禁止了localhosthttps证书,解决办法如下:使用命令行打开chrome,这种方法可以在本地查看Service Workers的相关操作是否生效了,/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --user-data-dir=./tmp --ignore-certificate-errors --unsafely-treat-insecure-origin-as-secure=https://localhost:8080

关于这个demo打包的配置

// webpack_dist.config.js
const path = require('path');
const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
const DefinePlugin = require('webpack/lib/DefinePlugin');
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]_[chunkhash:8].js',// 给输出的文件名称加上 hash 值
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.css/,
        // 提取出 Chunk 中的 CSS 代码到单独的文件中
        use: ExtractTextPlugin.extract({
          use: ['css-loader?minimize'] // 压缩 CSS 代码
        }),
      },
    ]
  },
  plugins: [
    // 使用本文的主角 WebPlugin,一个 WebPlugin 对应一个 HTML 文件
    new WebPlugin({
      template: './template.html', // HTML 模版文件所在的文件路径
      filename: 'index.html' // 输出的 HTML 的文件名称
    }),
    new ExtractTextPlugin({
      filename: `[name]_[contenthash:8].css`,// 给输出的 CSS 文件名称加上 hash 值
    }),
    new DefinePlugin({
      // 定义 NODE_ENV 环境变量为 production 去除 react 代码中的开发时才需要的部分
      'process.env': {
        NODE_ENV: JSON.stringify('production')
      }
    }),
    // 压缩输出的 JS 代码
    new UglifyJsPlugin({
      // 最紧凑的输出
      beautify: false,
      // 删除所有的注释
      comments: false,
      compress: {
        // 在UglifyJs删除没有用到的代码时不输出警告
        warnings: false,
        // 删除所有的 `console` 语句,可以兼容ie浏览器
        drop_console: true,
        // 内嵌定义了但是只用到一次的变量
        collapse_vars: true,
        // 提取出出现多次但是没有定义成变量去引用的静态值
        reduce_vars: true,
      }
    }),
    new ServiceWorkerWebpackPlugin({
      entry: path.join(__dirname, 'sw.js'),
      publicPath: '',
    }),
  ],
};