可安装的 Web 应用: 用 JavaScript 与 Node.js 创建 PWAs 实践

翻译自 Peter Mbanugo 的文章,原文链接: nstallable Web Apps: A Practical Introduction To PWAs with JavaScript and Node.js

渐进式 Web 应用 (PWAs) 可以为用户在不稳定网络链接条件下提供更好的操控性,像离线 (offline-first) Web App 就可以在本地保存购物列表 (示例购物车)。这个示例 PWA 使用了 Hoodie 并添加 service worker 使 App 在离线状态下完成加载,当然我们也可以添加更多功能使其变得更好用。

在文本中,我们将会复制这个渐进式 Web 应用并且让这个应用可以被安装。可以被安装那就意味这这个应用需要添加到用户的开始屏幕并且像原生 App 一样的被启动。要让他可以被安装,我们在构建步骤中添加 manifest 文件和 Workbox 以便自动生成 service worker。

准备工作

为了能按照作者的思路来模仿,你需要以下环境和代码:

  1. NodeJS 版本 6.6.0 (或更高)
  2. npm 版本 6.6.0 (或更高)
  3. 示例购物车 PWA 的源代码 Github

如果你已经下载了源代码,那就在命令行中使用 npm install 命令去安装依赖。代码中已经包含了 service worker 并且使用 Cache API 来保存此 App 的资源。

Service worker 是一个可以编程的网络代理,它运行于独立的浏览器线程上,并允许你按照自己的要求来处理拦截的网络请求。

项目中的 service worker 文件位于 public/sw.js,并且已经有了以下内容:


//file -> public/sw.js

const CACHE_NAME = "cache-v1";
const assetToCache = [
  "/index.html",
  "/",
  "/history.html",
  "/manifest.json",
  "/resources/mdl/material.indigo-pink.min.css",
  "/resources/mdl/material.min.js",
  "/resources/mdl/MaterialIcons-Regular.woff2",
  "/resources/mdl/material-icons.css",
  "/css/style.css",
  "/resources/dialog-polyfill/dialog-polyfill.js",
  "/resources/dialog-polyfill/dialog-polyfill.css",
  "/resources/system.js",
  "/js/transpiled/index.js",
  "/js/transpiled/history.js",
  "/js/transpiled/shared.js",
  "/hoodie/client.js"
];
self.addEventListener("install", function(event) {
  event.waitUntil(
    caches
      .open(CACHE_NAME)
      .then(function(cache) {
        return cache.addAll(assetToCache);
      })
      .catch(console.error)
  );
});
self.addEventListener("fetch", function(event) {
  event.respondWith(
    caches.match(event.request).then(function(response) {
      if (response) {
        return response;
      }
      return fetch(event.request);
    })
  );
});

由上段代码可知,当我们需要添加或修改资源列表时,我们需要更改 CACHE_NAME 的值来使旧的资源失效并且重新安装 service worker 脚本。这个过程很费事。相反,我们可以在资源发生变化时通过一些配置文件自动生成 service worker 脚本并且自动更新缓存。我们将使用 Workbox 来做这件事情。

使用 Workbox 生成 service worker 脚本

预缓存的资源是那些在被使用签就保存起来的资源。我们的 service worker 虽然预缓存着资源,但是当其中的一些资源发生变化时,service worker 就会删掉所有的旧的缓存资源并且重新下载所有的资源。而 Workbox 会自动产生一个 service worker 脚本,这个脚本只更新变更的缓存资源,不会刷新无关的缓存资源,这样刷新缓存资源会变得更容易。这也是一个更明知的刷新缓存资源的选择,这样做会使我们的 App 运行的更快并且节省网络带宽。

安装 Workbox

因为我们的构建过程简单,我们会让 Workbox 生成整个 service worker 脚本。

运行以下命令来安装 workbox-cli:


npm install -D workbox-cli@2.1.2

将 workbox-cli 添加到构建过程

在项目根目录创建一个名为 workbox-cli-config.js 的文件,这个文件将会自动被 Workbox 使用并生成最终的 service worker 脚本。

将以下内容添加到

workbox-cli-config.js 文件中:


module.exports = {
  globDirectory: "public/",
  globPatterns: ["**/*.{css,ico,html,png,js,json,woff2}"],
  swDest: "./public/sw.js",
  globIgnores: ["icons/*", "js/src/*", "sw.old.js"],
  skipWaiting: true,
  clientsClaim: true,
  templatedUrls: {
    "/hoodie/client.js": "../.hoodie/cleint.js"
  }
};

让我们了解以下配置参数:

  • globDirectory 表示监控变化并且获取资源文件的目录
  • globPatterns 表示那些文件将会为缓存。我们使用通配符来表示所有的具有指定扩展名的文件(包含子目录的文件)都会被缓存
  • swDest 表示输出 service work 脚本文件的位置
  • skipWaiting 表示 service worker 进入等待阶段时尽快的激活,如果我们没有选择 Workbox, 那么我们会等同的在安装实践处理函数中添加 self.skipWaiting();
  • clientsClaim 表示新的 service worker 是否在激活后立即控制所有客户端
  • templatedUrls 用于存放一些基于服务端逻辑的 URL。当请求 /hoodie/client.js 可以在项目中找到相应的文件。

如果这个文件发生变化,当启动构建过程或者刷新 App 时,service worker 会按照配置更新缓存。

好了,现在更新 package.json 通过在 “build” 这一行中添加 && workbox generate:sw 以在构建过程的最后一步调用 workbox 。


"build": "babel public/js/src --out-dir 
public/js/transpiled && workbox generate:sw"

运行以下命令来启动构建:


npm run build

已经存在的 sw.js 文件将会被刷新,内容和以下代码相似:


/**
 * DO NOT EDIT THE FILE MANIFEST ENTRY
 *
 * The method precache() does the following:
 * 1. Cache URLs in the manifest to a local cache.
 * 2. When a network request is made for any of these URLs the response
 *    will ALWAYS comes from the cache, NEVER the network.
 * 3. When the service worker changes ONLY assets with a revision change are
 *    updated, old cache entries are left as is.
 *
 * By changing the file manifest manually, your users may end up not receiving
 * new versions of files because the revision hasn't changed.
 *
 * Please use workbox-build or some other tool / approach to generate the file
 * manifest which accounts for changes to local files and update the revision
 * accordingly.
 */
const fileManifest = [
  {
    "url": "css/style.css",
    "revision": "99559afa2b600e50f33cebcb12bd35e6"
  },
  {
    "url": "favicon.ico",
    "revision": "2ec6120d215494c24e7c808d0d5abf56"
  },
  {
    "url": "history.html",
    "revision": "240e2a52b8580117383162e8ec15fc00"
  },
  {
    "url": "index.html",
    "revision": "4a215dad3782fb0715224df00149cee9"
  },
  {
    "url": "js/transpiled/history.js",
    "revision": "f5d6af7aff37147b0c82043fe3153828"
  },
  {
    "url": "js/transpiled/index.js",
    "revision": "3b5384eca25ad783829434ee190ecb58"
  },
  {
    "url": "js/transpiled/shared.js",
    "revision": "38039d6e28ad31c85c4adc0c4bab2dc9"
  },
  {
    "url": "manifest.json",
    "revision": "cfada03439f24ccdb59dae8d4f6370d1"
  },
  {
    "url": "resources/dialog-polyfill/dialog-polyfill.css",
    "revision": "24599b960cd01b8e5dd86eb5114a1bcb"
  },
  {
    "url": "resources/dialog-polyfill/dialog-polyfill.js",
    "revision": "a581e4aa2ea7ea0afd4b96833d2e527d"
  },
  {
    "url": "resources/mdl/material-icons.css",
    "revision": "35ac69ce3f79bae3eb506b0aad5d23dd"
  },
  {
    "url": "resources/mdl/material.indigo-pink.min.css",
    "revision": "6036fa3a8437615103937662723c1b67"
  },
  {
    "url": "resources/mdl/material.min.js",
    "revision": "713af0c6ce93dbbce2f00bf0a98d0541"
  },
  {
    "url": "resources/mdl/MaterialIcons-Regular.woff2",
    "revision": "570eb83859dc23dd0eec423a49e147fe"
  },
  {
    "url": "resources/system.js",
    "revision": "c6b00872dc6e21c1327c08b0ba55e275"
  },
  {
    "url": "sw1.js",
    "revision": "0a3eac47771ce8e62d28908ee47a657f"
  },
  {
    "url": "/hoodie/client.js",
    "revision": "1d95959fa58dcb01884b0039bd16cc6d"
  }
];
const workboxSW = new self.WorkboxSW({
  "skipWaiting": true,
  "clientsClaim": true
});
workboxSW.precache(fileManifest);

文件第一行 importScripts('workbox-sw.prod.v2.1.2.js') 会导入 Workbox 的 service worker 库。你会注意到项目目录中出现了同名的文件。如果你不小心删掉了它,请不要担心。当你在命令行中运行 generate:sw 时也会创建同样的文件。

有了这些新的设置,在命令行中运行 ‘npm start’ 并在浏览器中打开 localhost:8080 并打开控制台来查看 service worker 诗如歌更新的,允许在进入等待阶段是跳过等待阶段 (waiting phase) 。

添加 manifest 文件

manifest 是一个 JSON 文件,用于提供应用信息(比如, 应用的名字和图标等)和控开始屏幕下的外观 (比如智能手机开始屏幕,Windows 10 开始屏幕之类的). manifest 同样定义着当 App 启动时显示启动画面和启动后显示的页面或 URL。

我们需要添加一个 manifest.json 文件来为用户提供应用的元数据。在 public 目录下添加一个名为 manifest.json 的文件,并包含以下内容:


{
  "name": "Shopping List",
  "short_name": "ShoppingList",
  "theme_color": "#00aba9",
  "background_color": "#00aba9",
  "display": "standalone",
  "orientation": "landscape",
  "scope": "/",
  "start_url": "/",
  "icons": null
}
  • name 为 app 的名字,这个名字将会在安装弹窗中出现
  • short_name 会出现在安装后的 app 图标下面
  • theme_color 应用开打后 App bar 的颜色
  • background_color 启动状态和加载状态之间是显示的启动页面的背景颜色
  • display 定义应用的显示模式,Standalone 表示呈现为一个单独安装的应用
  • orientation 定义应用的显示的横竖屏设置
  • scope 用于定义应用的范围,当导航页面超过这个范围后,将会以正常的网页形式显示
  • start_url 表明启动页的位置
  • icons 用户定义不同尺寸下的图标以适配不同设备的屏幕尺寸

注意我们虽然将 icons 设置为 null。 但是我们需要 icon 用于图钉,推送通知,安装横幅和启动画面。从这里下载 一个 512×512 尺寸的 icon , 在浏览器中打开 https://app-manifest.firebaseapp.com 并上传刚下好的 icon,这个网页将会为你生成应用所需各种尺寸的 icon。

生成图标

点击 “Generate .ZIP” 按钮即可下载 manifest 文件以及所需的图标,既然我们已经有了 manifest 文件,我们只需要 zip 文件中的图标。解压 zip 文件,复制 icons 目录于项目 public 目录下,替换项目 manifest.json 文件 icons 属性的值为:


"icons": [
    {
      "src": "/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
]

安装

我们把之前创建的 manifest.json 文件,链接到 public 目录中的 index.htmlhistory.html 文件中后,应用就可以安装了!你可以通过运行 npm run build && npm start 再次构建并且启动它。在浏览器中打开你的应用,浏览器将会安装最新的 service worker。然后尝试把应用添加到开始屏幕或者桌面上。

使用 Chrome 添加 App 到桌面上:

  1. 在开发者工具栏中打开 Applications 选项卡
  2. 在侧边栏中选择 “Manifest”
  3. 点击 “Add to homescreen”。你将会在地址栏上看到一个弹窗,点击 “Add” 并且安装到桌面 App 中

Chrome 安装

打包

我们提到了如何使用 Workbox 来生成 service worker 脚本、manifest 的概念及如何提供 App 安装过程中所需信息。我们还使用 https://app-manifest.firebaseapp.com 来生成不同尺寸的图标,当然你也可以添加其他信息一并生成一个的 manifest.json 文件。

现在,你的应用已经可以安装了,你可以部署到你偏爱的主机服务上并让所有人都可以安装。

你可以在 github 上找到本项目的代码。

以下优质资源可以作为参考:
Web App Manifest
Web App Manifest Generator
Workbox
Offline-First with Node.js and Hoodie: A Practical Introduction to Progressive Web Apps

关于作者

Peter Mbanugo 专注于离线应用,持续研究更好的方式来构建更快更轻量级更高效的 Web 应用服务。
任何时间可以通过 p.mbanugo@yahoo.com 和 Twitter (@p_mbanugo) 与作者取得联系。

共有 0 条评论

123