一次性完整学完搭建PWA项目

2024-10-09
351912 分钟
...

最近在开发一个简单的个人记录网站,技术栈是使用 Vite + Vue3,由于使用的单台服务器,有时候服务器会被限制带宽,所以平时都会比较访问比较慢。所以想实现一个离线应用,而 PWA 应用则是目前最佳方案。

本文涉及知识点如下:

  • PWA 的概念
  • Service Worker使用
  • 用构建工具搭建 PWA 应用

渐进式 Web 应用(Progressive Web App,PWA)是一个使用 web 平台技术构建的应用程序,但它提供的用户体验就像一个特定平台的应用程序。 ——MDN 渐进式 Web 应用(PWA)

正如上文所描述一样 PWA 最终目的让你的 web 网站可以像 app 应用一样可以给到用户去离线体验,简单点说,就是没有网络,你也可以正常访问该网站的一些资源。

PWA从技术上分为三个部分:

  • 主应用,就是平时我们开发网站所包含的内容,有:html,js,css等
  • Web app manifests,主要为manifest.json,提供浏览器安装 PWA 所需的信息,例如应用程序名称和图标等
  • Service Worker,主要为js文件,提供基本的离线缓存资源能力

manifest.json

manifest.json描述web网站的信息(如名称,作者,图标和描述)的JSON文件,具体例子如下所示。

manifest.json是需要在网站中html文件中 head中引用,如下:

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

完整的manifest.json示例:

{
  "name": "网站完整名称", 
  "short_name": "网站简称", 
  "start_url": ".", 
  "display": "standalone", 
  "background_color": "#fff", 
  "description": "网站描述",
  "icons": [ 
    {
      "src": "images/touch/homescreen48.png",
      "sizes": "48x48",
      "type": "image/png"
    },
    {
      "src": "images/touch/homescreen72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "images/touch/homescreen96.png",
      "sizes": "96x96",
      "type": "image/png"
    },
    {
      "src": "images/touch/homescreen144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "images/touch/homescreen168.png",
      "sizes": "168x168",
      "type": "image/png"
    },
    {
      "src": "images/touch/homescreen192.png",
      "sizes": "192x192",
      "type": "image/png"
    }
  ],
}

了解基本的描述文件,下面我们将进入Service Worker作为其中整个控制中心,我们将在下面进行着重了解。

是什么

我们先来看看官方定义:

Service worker 是一个注册在指定源和路径下的事件驱动 worker。它采用 JavaScript 文件的形式,控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。你可以完全控制应用在特定情形(最常见的情形是网络不可用)下的表现。 —— MDN Service Worker

进行简单总结一下 Service Woker是什么:

  • 是一个区别于主 JavaScript 线程,运行在其他单独线程,但是必须要注册到主 JavaScript 线程中
  • 是用JavaScript编写的
  • 可以拦截并修改访问和资源请求,从而实现资源缓存

出于安全考量,Service worker 只能由 HTTPS 承载,毕竟修改网络请求的能力暴露给中间人攻击会非常危险,如果允许访问这些强大的 API,此类攻击将会变得很严重。

生命周期

Service Woker的生命周期如下:

  1. 注册,使用 ServiceWorkerContainer.register() 方法首次注册 service worker
  2. 下载,页面首次加载后会下载ServiceWorker或者过去 24 小时没有被下载会再次下载
  3. 安装,首次启用 service worker,页面会首先尝试安装,如果现有 service worker 已启用,新版本会在后台安装,但仍不会被激活——这个时序称为 worker in waiting。
  4. 激活,首次启用 service worker,安装结束后会直接激活,新版本的service worker会直到所有已加载的页面不再使用旧的 service worker 才会激活新的 service worker,但是可以通过ServiceWorkerGlobalScope.skipWaiting() 可以更快地进行激活。

Service Worker提供几个事件用来监听生命周期的变化,如下:

  • self.addEventListener("install") 该事件触发时的标准行为是准备 service worker 用于使用,例如使用内建的 storage API 来创建缓存,并且放置应用离线时所需资源。
  • self.addEventListener("activate") 事件触发的时间点通常是清理旧缓存以及其他与你的 service worker 的先前版本相关的东西。
  • self.addEventListener("fetch") 事件触发的时间点是每次获取 service worker 控制的资源时,都会触发 fetch 事件

这里的this代表的是 Service Worker 本身对象。

常用API

了解完后,我们需要知道 Service Worker 有哪些常用的 API接口,或者当我们需要去实现一个 PWA 会用到哪些 API 接口,具体如下:

  • navigator.serviceWorker.register() 主 JavaScript 线程注册 Service Worker 方法
  • CacheCacheStorage 用来控制缓存

尝鲜使用

第一步 写个 demo站点

我们肯定需要有一个站点,里面有 html/css/js文件,代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="manifest" href="./manifest.json" />
    <title>Service Worker测试页面</title>
    <link rel="stylesheet" href="./test.css">
</head>
<body>
    <h1>测试 Service Worker</h1>
    <script src="./test.js"></script>
    <script>
        
    </script>
</body>
</html>

第二步 注册 Service Worker

这一步有两个 事情:

  • 写Service Worker的相关逻辑的js文件 (且叫sw.js)
  • sw.js注册到html文件中 具体代码如下:
if ('serviceWorker' in window.navigator) {
    const registerServiceWorker = async () => {
        if ("serviceWorker" in navigator) {
            try {
                const registration = await navigator.serviceWorker.register("./sw.js", {
                    scope: "/",
                });
                if (registration.installing) {
                    console.log("正在安装 Service worker");
                } else if (registration.waiting) {
                    console.log("已安装 Service worker installed");
                } else if (registration.active) {
                    console.log("激活 Service worker");
                }
            } catch (error) {
                console.error(`注册失败:${error}`);
            }
        }
    };

    registerServiceWorker();
}


self.addEventListener('install', function(event) {
    console.log('install');
    
});

self.addEventListener('activate', function(event) {
    console.log('activate');
    
});

第三步 缓存管理

缓存管理包括两部分,一个是缓存资源,另外一个同步更新资源,在 ServiceWorker 代码中是通过CacheCacheStorage去控制,代码如下:

self.addEventListener('install', function(event) {
    
    event.waitUntil(
        
        caches.open('v1').then(function(cache) {
            cache.addAll([
                './index.html', 
            ]);
        })
    );
});


const cacheFirst = async (request) => {
    
    const responseFromCache = await caches.match(request);
    console.log('responseFromCache', responseFromCache);
    if (responseFromCache) {
        return responseFromCache
    }
    return fetch(request);
}

self.addEventListener("fetch", (event) => {
    
    console.log('caches match',);
    console.log('fetch', event.request.url);
    event.respondWith(cacheFirst(event.request));
});

动态缓存

当然,上面是将固定的资源进行缓存,如果是需要对整个页面请求资源进行缓存管理,那么可以通过fetch事件拦截请求实现动态缓存,代码如下:

 * 缓存优先
 * @param {*} request 
 * @returns 
 */
const cacheFirst = async (request) => {
    
    const responseFromCache = await caches.match(request);
    console.log('responseFromCache', responseFromCache);
    if (responseFromCache) {
        return responseFromCache
    }
    
    const responseFromServer = await fetch(request);
    const cache = await caches.open(cacheName);
    
    cache.put(request, responseFromServer.clone());
    return responseFromServer;
}

self.addEventListener("fetch", (event) => {
    
    console.log('caches match',);
    console.log('fetch', event.request.url);
    event.respondWith(cacheFirst(event.request));
});

缓存成功后,可以在 DevTools找到 网络请求状态,会标识是从 Service Worker 获取资源,具体如下图:

第四步 更新缓存池

当你的Service Worker js文件有更新,需要删除旧的缓存,同时启动新的 Service Worker cache,代码如下:

const deleteCache = async (key) => {
  await caches.delete(key);
};

const deleteOldCaches = async () => {
  const cacheKeepList = ["v2"];
  const keyList = await caches.keys();
  const cachesToDelete = keyList.filter((key) => !cacheKeepList.includes(key));
  await Promise.all(cachesToDelete.map(deleteCache));
};

self.addEventListener("activate", (event) => {
  event.waitUntil(deleteOldCaches());
});

讲完了这些,可能还需要实际体验一把,可以访问在线Service Worker Demo,源码在这里Github qborfy/service worker

上面讲述了 Service Worker 的概念和使用,但是在实际项目中,如果要按照这一套去实现,会遇到很多问题,如:经过打包后我们的 js , css等文件是动态生成的,从而导致每次都需要更新 Service Worker的 Cache 版本池。

所以需要结合构建工具去让项目更快支持 PWA应用开发,具体有以下几个。

Vite构建

Vite官方推荐使用插件vite-plugin-pwa,使用如下:

注意: vite版本需要 3+

npm i vite-plugin-pwa -D

调整vite的配置文件vite.config.js,最小配置如下:

import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
  plugins: [
    VitePWA({
      registerType: 'autoUpdate', 
      injectRegister: 'auto', 
      manifest: { 
        name: 'qborfy study website',
        short_name: 'qborfyStudy',
        description: 'qborfy study website',
        theme_color: '#ffffff',
        icons: [
          {
            src: 'favicon.png',
            sizes: '192x192',
            type: 'image/png'
          },
          {
            src: 'favicon.png',
            sizes: '512x512',
            type: 'image/png'
          }
        ]
      }
    }),
  ]
})

最终会在 npm run build后,完成以下几个事情:

  • 生成registerSW.js,用来注册Service Workersw.js文件
  • 生成sw.js文件,在 index.html引入
  • 生成manifest.webmanifest,在 index.html引入,声明网站的信息,可以在manifest配置项调整
  • 生成workbox.xxx.js,用来管理缓存使用策略的代码,可以通过strategies去配置

其他更多帮助文档可以到官方文档查看, vite-plugin-pwa官方文档

Webpack构建

Webpack作为前端最主流的构建工具,当然也有对应插件去实现,那就是workbox-webpack-plugin插件,其实是Chrome自己开源的workbox工具库中支持的插件之一。

具体用法如下:

  1. 安装依赖
npm install workbox-webpack-plugin --save-dev
  1. webpack.config.js增加插件配置
const WorkboxPlugin = require('workbox-webpack-plugin')
module.exports = {
    ...,
    plugins: [
        new WorkboxPlugin.GenerateSW({
            clientsClaim: true, 
            skipWaiting: true
        }),
    ]
};
  1. 在index.html注册 service worker
if ('serviceWorker' in navigator) {
    window.addEventListener('load', async () => {
        console.log('page load...');
        let res = await navigator.serviceWorker.register('/service-worker.js');
        console.log(res, 'serviceWorker res');
        if (res) {
            console.log('register success!');
        } else {
            console.log('register fail!');
        }
    });
}

更多帮助可以到workbox 官方文档中查看

workbox工具库

其实上面两个插件都是基于 Chrome 开源的 workbox工具库去做二次封装实现的,接下来我们对workbox.js做一个简单的了解,方便后续如果我们需要自己去开发符合项目的 service worker控制。

Service Worker有很多抽象的概念和 API,如:网络请求!缓存策略!缓存管理!预缓存!等等, Workbox的作用就是将复杂的 API 进行抽象,使更易于使用。

Workbox 是一组简化常见服务工作线程路由和缓存的模块。每个可用模块都解决 Service Worker 开发的特定方面。 Workbox 旨在使 Service Worker 的使用尽可能简单,同时允许在需要时灵活地满足复杂的应用程序要求。

如何使用Workbox,官方提供两种方式:

  • 结合构建工具使用,如上面的 Vite 或者 Webpack
  • 没有构建工具,官方提供了workbox-sw,让你可以利用 workbox api去实现自己的 service worker策略

这里简单使用一下,代码如下:

importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js');



workbox.routing.registerRoute(
    ({ request }) => request.destination === 'image',
    new workbox.strategies.CacheFirst()
);

其他使用说明文档可以到workbox 官方文档中查看。

这里我还收集了一些开发 PWA 后续可能会用到的点,大家可以看看。

Service Worker其他

本文主要是想通过 PWA 去优化个人网站的访问速度,PWA 不仅仅只能做缓存优化,还包括一下几点:

  • 通知 Notification,可以在后台接受服务器通知,然后告知用户
  • 通讯 Message,可以和主 JS 线程通讯
  • 后台更新,可以在用户没有访问页面的时候进行后台定时更新

如何发布一个 PWA 应用

注意事项

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
pwa2024-10-09
PWA - 微前端项目实践主要目的: 解决资源加载慢问题
背景 平台各个子应用静态资源加载速度慢,用户体验有待提升 主要目的: 解决资源加载慢问题,提升体验 支持应用自启动(原生 体验) 项目使用到的技术栈为: PWA 介绍 简介 PWA 它不是特指某一项技术,而是应用多项技术来改善用户体验的 ,其核心技术包括 , 等,用户体验才是 的核心。 特点: 可靠 即使在网络不稳定甚至断网的环境下,也能瞬间加载并展现 用户体验 快速响应,具有平滑的过渡动画及用户操作的反馈 用户黏性 和 一样,可以被添...
学习
pwa2024-10-09
通过PWA,我的博客也能离线阅读啦
前言 我经常看到有些博客站点拥有离线观看的功能,就算是没有网络,刷新页面依然可以获得数据。我对这一功能甚为好奇,了解后才发现这一技术是PWA。 何为PWA PWA又为渐进式Web应用,在MDN上是这样说的, 如何实现 构建一个PWA程序主要是用到 技术,这一项技术在现代浏览器中默认开启。 以上是一张官方的图,讲解了 的工作流程,简单来说就是 注册程序 安装程序 激活程序 获取数据 版本更新后再次获取数据 。 注册程序 安装程序 定义版本...
学习
AI2026-09-01
Deep Agents 01:何为 Agent Harness,以及如何开始
1、本篇任务:完成一份多步骤、带证据的技术调研 普通客服 Agent 的问题短、工具少、输出即时。技术调研或编码任务会持续很久,产生计划、搜索结果、文件和中间结论。Deep Agents 在 LangChain/LangGraph 之上预装规划、虚拟文件系统、上下文压缩和子 Agent,适合这类开放任务。 本课让 Agent 比较两种向量数据库,并交付一份可验证报告。 2、什么时候需要 Deep Agent 满足以下两项以上再考虑:任务...
学习
AI2026-09-01
Deep Agents 02:子 Agent、虚拟文件系统与长期记忆
1、本篇任务:让主管只看结论,让子 Agent 处理细节 技术调研会产生几十次搜索和大量文件。如果全部进入主管上下文,真正的目标会被噪音淹没。本课用两个子 Agent: 收集证据, 检查结论;主管负责计划与最终合成。 2、什么时候委派,什么时候直接调用工具 适合委派:子任务有多步;需要专门提示或工具;会产生大量中间结果;只需返回有限结论。不适合:一步查询;主管需要全部中间上下文;协调成本超过任务本身。 3、配置专门子 Agent Pyt...
学习
AI2026-09-01
Deep Agents 03:生产化、Sandbox、权限与上线验收
1、本篇任务:让 Deep Agent 在隔离环境中分析代码 只读研究 Agent 风险有限;编码 Agent 需要读写文件、安装依赖和执行测试。本课不讲如何让模型写更漂亮的代码,只讲执行环境、权限、恢复和上线验收。 2、先做威胁模型 资产包括源代码、用户文件、云凭证、生产网络和发布权限;攻击入口包括用户消息、仓库内容、网页、依赖包、MCP 返回和命令输出。 Prompt injection 不是靠一句 system prompt 解决...
学习
AI2026-09-01
LangChain 01:全景、原理与学习路线
1、本篇学完要得到什么 这一篇只解决三个问题:LangChain 到底负责什么;它与 LangGraph、Deep Agents、LangSmith 是什么关系;后面应按什么顺序学习。 贯穿整套课程的项目是“退款政策与订单助手”。它最终能够:回答知识库中的退款规则;查询当前用户的订单;生成结构化答复;对真正的退款操作进行人工审批;断线后恢复;通过评测后发布。 先记住一句话: 模型负责理解与生成,应用负责数据、权限、状态和副作用。 如果把...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录