你知道现在流行的微信小程序实际上也是 PWA 的一种吗?

前言

  PWA(Progressive Web App),中文称为“渐进式网络应用程序”,利用一系列现代的 Web 技术来增强 Web App 的功能,从而实现应用程序一样的用户体验。事实上,任何的网站都可以做成 PWA。从这个概念上来说,PWA 实际上是一种理念。根据这种理念可以创造出各式各样的东西,比如说微信小程序、百度小程序、支付宝小程序等等(😂 虽然它们都不承认自己是和 PWA 一脉相承的)。

为何有 PWA

  随着网络和智能设备的飞速发展及提升,越来越多的原生应用程序占据了智能设备的空间和资源。曾几何时,一个微博应用程序还只有 100M 左右,现在一安装一登录就要到快 1G。再没事刷一刷新闻或者视频,可能就要变成 1.5G,甚至更多。类似微博这样,我们日常可能常用的淘宝、微信、QQ、知乎等等几乎都是如此。虽然这样的空间和运行内存资源占用能给我们带来很好的用户体验,但实际上还是在一点点榨干智能设备的资源。最后智能设备越来越卡,只能依靠时常清理、重置或者升级硬件来焕发新机。

  相比原生应用开发而言,Web 应用程序的开发和更新维护则显得不要太轻松,真正实现了“一遍更新所有客户端适用”。如果要从性能和可调用设备资源来看,可能以前原生应用要占上风。但是随着 HTML5 和 CSS3 的发展,Web 应用程序在性能和设备资源调用上已经与原生应用差别无二了。所以越来越多的基于 Web 技术的 Hybrid 客户端或者纯 Web 客户端开始涌现,比如非常知名的网易云客户端,无论是 PC 客户端还是移动客户端都已经切换到了 Web 技术栈。还有现在非常流行的代码编辑器 VS Code,是基于 Electron 框架进行二次开发,其简易的操作逻辑和丰富的插件为开发者带来了无限的可能。

代码编辑器小知识

  代码编辑器可以从本身的复杂程度分成两种:一种是以 Eclipse、Visual Studio、*Storm(PHPStorm 等)、 Xcode、IDEA 为代表的大而全的“巨无霸”代码编辑器,另一种是以 Sublime Text、Notepad++、Atom、VS Code、Brackets 为代表的小而实用的“简洁”代码编辑器。
  “巨无霸”代码编辑器也有其好处,能够提供保姆式的界面交互,可能看着省心一点。但这也是一个缺点,你需要先学习如何使用代码编辑器,才能用它来编写你想要的程序。“简洁”代码编辑器只提供必要的组件,比如文件列表、终端、搜索替换、高亮等。如果需要更多的功能,可以通过安装插件的方式来扩展支持。当然,“简洁”代码编辑器会要求你对“一切皆文件”的 Unix/Linux 哲学有比较好的认识,因为你需要通过手动修改文件配置内容的方式来设置各种各样的东西,而极少有交互界面提供。

  当我们把原生应用和 Web 应用放在一起来考虑时,我们就会想是否能有一种方式可以结合两者的优点来为用户提供更好的服务呢?PWA 于是应运而生

其他方案

  除了 PWA 之外,其实还是其他的方案出现。比如像 Hybrid 应用、React Native、Flutter 等,具体可以阅读参考资料了解更多。

什么是渐进式

  所谓的“渐进式”有两个含义:

  • 一是 Web 应用渐进式接近原生应用:通过各种 Web 技术实现与原生应用相近的用户体验。
  • 二是给支持 PWA 的浏览器用户带来更好的体验:由于不同浏览器支持的 HTML/CSS 的情况有所差别,可以利用 PWA 来分步骤、分阶段在不同浏览器上新增特性。
渐进式应用

  一个渐进式应用首先是一个网页,通过各种 Web 技术编写出的一个网页应用。之后通过添加应用 Manifest 实现添加到主屏幕,通过 Service Worker 来实现离线缓存和消息推送等功能。

  正是由于这些因素,PWA 至少可以给你的站点带来以下好处:

  • 更快、更安全的用户体验
  • 更好的搜索排名(尤其对于 Google)
  • 更好的可用性
  • 更好的性能
  • 离线访问
  • 手机屏幕上的快捷方式像是原生应用

为 Jekyll 配置 PWA

  从上面列举的 PWA 的好处,我们可以很显然知道:PWA 的离线缓存、原生式体验、搜索引擎友好这些优点都能用于实现站点加速。通常的静态站点加速方法可能是:减少请求、缩减请求的大小、CDN 加速等等,但是 PWA 本身的特性对于用户体验来说也是有一些加速效果的,毕竟被访问过一次就会缓存下来。这与 CDN 的缓存有些类似,只不过 CDN 有很多缓存节点,而 PWA 是把本机作为唯一的缓存节点。

  经过一番对于 Jekyll 上 PWA 支持的调查后,发现的确也存在一些号称可以很方便、简单地使用的 PWA 插件。但是尝试了一下觉得有点复杂且插件看起来毫无用处。根据上面提到的 PWA 实现方式来看,只要我们配置好 Manifest 和 Service Worker,就可以把一个站点全部变成 PWA 应用。根本上与是否 Jekyll 或其他应用无关,因此使用 PWA 插件的必要性不大。

  接下来就让我们来尝试一下徒手配置 PWA。

配置 Manifest

元信息

  Manifest 实际上是一个声明了 PWA 应用的所有元信息的 JSON 配置文件,如下所示。我们需要定义 PWA 应用的名称、语言、缩写、图标、主题颜色、背景颜色、起始路径。

{
    "lang": "{{ site.language }}",
    "dir": "{{ site.baseurl }}",
    "name": {{ site.title | smartify | jsonify }},
    "short_name": {{ site.pwa.short_name | smartify | jsonify }},
    "icons": [
      {
          "src": "{{ "/assets/img/touch/android-chrome-192x192.png" | prepend: site.baseurl }}",
          "sizes": "192x192",
          "type": "image/png"
      },
      {
          "src": "{{ "/assets/img/touch/android-chrome-512x512.png" | prepend: site.baseurl }}",
          "sizes": "512x512",
          "type": "image/png"
      },
      {
        "src": "{{ "/assets/img/touch/android-chrome-maskable-192x192.png" | prepend: site.baseurl }}",
        "sizes": "192x192",
        "type": "image/png",
        "purpose": "maskable"
      },
      {
        "src": "{{ "/assets/img/touch/android-chrome-maskable-512x512.png" | prepend: site.baseurl }}",
        "sizes": "512x512",
        "type": "image/png",
        "purpose": "maskable"
      }
    ], 
    "theme_color": "{{ site.pwa.color }}",
    "background_color": "{{ site.pwa.color }}",
    "start_url": "{{ "/" | prepend: site.baseurl }}",
    "display": "standalone",
    "orientation": "natural"
}

  这里大部分的元信息都可以通过 Jekyll 的 _config.yml 全局配置文件来动态配置。其中,由于图标需要适用于不同的设备、屏幕分辨率,所以需要准备不同尺寸、分辨率的站点图标。这里可以使用 cthedot 开发的 cthedot/icongen 工具来一键生成,非常简单方便。生成后将图标文件放置在上面设置好的位置(可自行对应修改)。

icongen 镜像站

  由于 icongen 源站在德国,亚洲访问速度有时可能不大好,所以笔者使用 icongen 开源代码在 Cloudflare 上部署了一个镜像站点 icongen,欢迎使用。当然,由于 icongen 是一个静态页面,无须任何服务器托管也可使用,大家也可下载项目源代码,用浏览器打开源代码目录下的 app/index.html 文件也可正常使用。

模板化

  由于 manifest.json 文件我们不想要每个用户都来复制一遍,所以可以将这个文件内容模板化,即把 manifest.json 文件放置在 _layouts 文件夹中。这样一来,用户可以非常简单地在源码的主目录下建立一个新的 manifest.json 文件,内容如下所示:

---
layout: manifest
---

  另外,在全局配置文件 _config.yml 中需确保有如下配置:

language: 'zh'
baseurl: ''
name: 'Blog'

# PWA
pwa:
  color: '#81BBFF'
  short_name: 'lisz'

链接到页面

  当上面的内容都设置好后,我们还需将 manifest.json 的声明配置加入到网站的所有页面,内容如下所示。一般来说,Jekyll 主题只需要在 head.html 和 post-head.html 两个头文件模块中加入下面内容即可(一个是普通页面,一个是文章页面)。

<link rel="manifest" href="manifest.json">

配置 Service Worker

注册 Service Worker

  配置完 Manifest 之后,只是能告诉浏览器你的应用是一个 PWA 应用,而实际的 PWA 的离线缓存等特性都还没有实现,这些都是在 Service Worker 中实现的。首先在 _includes 目录中创建 pwa.html 文件,用于注册 PWA 应用,内容如下:

<!-- pwa.html -->
<script>
    if ("serviceWorker" in navigator) {
        if (navigator.serviceWorker.controller) {
            console.log("An active service worker found, no need to register");
        } else {
            // Register the service worker
            navigator.serviceWorker
            .register("{{ "/sw.js" | prepend: site.baseurl }}", {
                scope: "{{ "/" | prepend: site.baseurl }}"
            })
            .then(function (reg) {
                console.log("Service worker has been registered for scope: " + reg.scope);
            });
        }
    }
</script>

  并且为了启用 pwa.html 小插件,应该在 page.html 和 post.html 模板的 body 末尾添加上对小插件的引用,如下所示:

...
<body>
...
  {% include pwa.html %}
...
</body>
</html>

离线缓存

  Service Worker 功能定义文件实际上也是一个 JS 文件(这里命名为 sw.js),参考内容如下:

// sw.js
const CACHE = "pwabuilder-offline";
  
const offlineFallbackPage = "index.html";

// Install stage sets up the index page (home page) in the cache and opens a new cache
self.addEventListener("install", function (event) {
console.log("Install Event processing");

event.waitUntil(
    caches.open(CACHE).then(function (cache) {
    console.log("Cached offline page during install");

    if (offlineFallbackPage === "ToDo-replace-this-name.html") {
        return cache.add(new Response("Update the value of the offlineFallbackPage constant in the serviceworker."));
    }

    return cache.add(offlineFallbackPage);
    })
);
});

// If any fetch fails, it will look for the request in the cache and serve it from there first
self.addEventListener("fetch", function (event) {
if (event.request.method !== "GET") return;

event.respondWith(
    fetch(event.request)
    .then(function (response) {
        console.log("Add page to offline cache: " + response.url);

        // If request was success, add or update it in the cache
        event.waitUntil(updateCache(event.request, response.clone()));

        return response;
    })
    .catch(function (error) {        
        console.log("Network request Failed. Serving content from cache: " + error);
        return fromCache(event.request);
    })
);
});

function fromCache(request) {
// Check to see if you have it in the cache
// Return response
// If not in the cache, then return error page
return caches.open(CACHE).then(function (cache) {
    return cache.match(request).then(function (matching) {
    if (!matching || matching.status === 404) {
        return Promise.reject("no-match");
    }

    return matching;
    });
});
}

function updateCache(request, response) {
    return caches.open(CACHE).then(function (cache) {
        return cache.put(request, response);
    });
}

  sw.js 文件的内容主要定义了第一次访问时将文件离线缓存下来,再次请求是从缓存中加载。如果首次从缓存中没有找到想要的文件,则会请求更新缓存获取该文件。

模板化

  为了在 Jekyll 主题中能更方便用户使用,这里也应该像 Manifest 那样模板化,即在 _layouts 目录下创建以上内容的 sw.js 文件。而用户只需要在源代码主目录下创建如下内容的 sw.js 文件即可:

---
layout: sw
---

验证 PWA

  当我们设置好 Manifest 和 Service Worker,并且再次生成 Jekyll 静态页面并托管到服务器之后,用 Chrome 浏览器访问主页就会发现地址栏的右边会出现一个新的图标,如下图所示。这个图标就是在提示你所访问的页面是 PWA 应用,支持安装快捷方式。点击该图标,会有一个像下面这样的小弹窗提示,点击安装即可安装到主屏幕,PC 端、移动端均可。

安装提示 Install tips

  除此之外,我们还可以在 PC 端打开浏览器的控制台。如下图所示,我们可以发现一些来自 sw.js 的打印信息。这些信息告诉我们 Service Worker 已经注册成功了,并且有哪些文件已经离线缓存了。

控制台提示 Command line tips

  在浏览器开发工具中,我们还可以通过查看应用Service Workers 来再次确认 PWA 的情况。如下图所示,我们可以看到 sw.js 从 Initial 到 Wait 到 Activate 的过程,

应用提示 App tips

后记

  从上面的步骤来看,Web 应用 PWA 化似乎也不是那么难。只要有 Manifest 和 Service Worker 就能成功把 Web 应用 PWA 化。当然,也有一些更加简单 PWA 化的方法,比如说 pwabuilder。不过,pwabuilder 更擅长帮你优化 PWA 应用并且转成 Windodws、Apple 和 Android 三个平台的应用。如果你想要把你的 PWA 应用上传到应用商店,那么不妨试试。

  除了 Jekyll 之外,其他的静态生成器比如 Hexo、Next.js 等等或者动态网站都可以 PWA 化,而且和 CDN 加速一起使用也非常合适。

推荐一款 PWA 应用

  当年 Google 为了推广 PWA 曾开发了一款非常好用的 PWA 应用 – Squoosh。这就是本地版的图片格式转换、图片压缩的免费工具,完全可以替代大部分其他工具。如果你喜欢终端的话,你也可以使用 squoosh cli

相关补充

PWA 空间占用及流量消耗

(2022年6月8日)

  前两天刚发布本文时,有位叫 小灰灰灰灰 的网友向我提了一个关于“PWA 空间占用及流量消耗”的问题,我也回答了一些自己的想法(可以在博客主节点 lisz.me 的评论区看到具体内容)。首先非常感谢 小灰灰灰灰 的热心提问,其次他的这个问题也让我觉得可能我需要再进一步了解一下,否则这篇博文的标题中的加速可能需要打上引号了。

  虽然从我自身的安卓手机 PWA 应用空间占用查询来看,Chrome 的空间设置中的空间占用量有点大(最高 4.9 GB,首次访问 480 MB,如下图所示),但查询手机系统中的空间管理却没有发现 PWA 应用或者 Chrome 占用了所谓的“4.9 GB”空间(PWA 应用 230K 左右,Chrome 1.4 GB 左右)。

Chrome 显示存储用量 Storage

  正如 小灰灰灰灰 所说,Chrome 显示的空间占用量存在水分,毕竟不可能只访问一个页面就需要下载 400 MB 的东西吧。而且,根本就没有那么多文件让 PWA 应用下载。据个人不完全统计,本站目前所有代码加上所有图床托管图片的总体大小在 15 MB 左右。即使全站都缓存下来,也不可能用到 400 MB。小灰灰灰灰 用在 Stack Overflows 上查到的命令在 Chrome 的控制台中输出了类似 “Using 23K out of 270M”的结果,因此他猜测可能是预申请了过百兆的空间,而实际使用量只有几十 K 左右。对此,我表示赞同。从实际的源站流量监控来看,与所谓的 400 MB 也不匹配。从上月 26 日到今天(共 14 天)为止消耗了 323.2 MB 流量,本月截止到今天(共 8 天)图床托管消耗了 131 MB 流量。

  综合以上的信息来看,一个像本站一样的静态博客 PWA 化之后对于智能终端空间占用量或者服务提供的流量消耗影响不会太大。如果是其他非常复杂的站点或者有大量未经优化图片的站点,可能全部 PWA 化会在空间占用了和流量消耗上面有一些比较明显的影响。这一点可以查看参考资料《PWA 初探》一文了解更多。其实我们还是应该回到我们使用 PWA 的初衷上来,PWA 是用来加速和优化提升用户体验的,因此我们可以选择某些部分 PWA 化而非整体。

  对于博客或者静态站点 PWA 化,个人有几点小建议可以在一定程度上降低 PWA 化对空间占用和流量消耗的影响:

  • 图片 webp 化:压缩图片大小、保留大部分的质量,从而加快所有图片加载时间。
  • 图床托管图片:图片交由图床来管理和存储,这样可以分担一部分的源站流量,毕竟也是有很多可以免费使用的图床的。
  • 懒加载和骨架屏:懒加载可以只加载可视区域内的若干张图片,只有在页面滑动时才会陆续加载将要看到的区域内的图片,减少初次渲染页面的时间;骨架屏能够在元素尚未完全加载前填充元素的空间,不会突然冒出元素占用(Vue 用得不好的时候会有这种情况),从视觉上给用户更加友好的体验。
  • CDN 化:虽然 PWA 应用有缓存能力,但是只是本地缓存,对于大多数第一次请求缓存的时间还是取决于用户客户端到源站之间的网络,CDN 能够很好地弥补这一点。

参考资料

版权声明:如无特别声明,本文版权归 Mr Li 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:《 PWA:可能是成本最低的站点加速方式 》

本文链接:https://lisz.me/tech/webmaster/pwa.html