在静态网站服务工作者示例中实现渐进式 Web 应用程序 (PWA)

2025-06-07

在静态网站中实现渐进式 Web 应用程序 (PWA)

Service Worker 示例

在静态网站中实现渐进式 Web 应用程序 (PWA)

我迁移到 Hugo 的最后一步是实现一个渐进式 Web 应用(简称 PWA)。我想要实现 PWA 的原因有几个:

  1. 它允许用户(并提示他们)将网站作为应用程序安装到他们的移动设备上。
  2. 将来,我可以使用推送通知来告知用户新内容。
  3. 它支持离线模式,因此用户在互联网中断时仍然可以浏览和阅读。
  4. 它缓存内容以提供更快、响应更灵敏的体验。

终身应用开发者
终身应用开发者

如果您对这些功能感兴趣,那么 PWA 可能就是您正在寻找的!

什么是 PWA?

PWA最初是为移动设备设计的(之所以说最初,是因为现在已经支持桌面 PWA),它是一种使用 HTML、CSS 和 JavaScript 等传统 Web 技术构建的特殊移动应用。所有现代浏览器都支持 PWA。它们之所以被称为“渐进式”,是因为它们本质上就像浏览器中的普通网页一样,但一旦安装,就可以逐步添加新功能,例如与硬件交互和管理推送通知。PWA 的最低要求是包含清单和一个 Service Worker。

宣言

这是终身开发者的宣言。

{
    "name": "Developer for Life",
    "short_name": "dev4life",
    "icons": [
        {
            "src": "/appicons/favicon-128.png",
            "sizes": "128x128",
            "type": "image/png"
        },
        {
            "src": "/appicons/apple-touch-icon-144x144.png",
            "sizes": "144x144",
            "type": "image/png"
        },
        {
            "src": "/appicons/apple-touch-icon-152x152.png",
            "sizes": "152x152",
            "type": "image/png"
        },
        {
            "src": "/appicons/favicon-196x196.png",
            "sizes": "196x196",
            "type": "image/png"
        },
        {
            "src": "/appicons/splash.png",
            "sizes": "512x512",
            "type": "image/png"
        }
    ],
    "start_url": "/",
    "display": "standalone",
    "orientation": "portrait",
    "background_color": "#FFFFFF",
    "theme_color": "#FFFFFF"
}
Enter fullscreen mode Exit fullscreen mode

它包含一些基本信息,例如应用安装时显示的图标、使用的颜色、起始页以及默认方向。它安装在您网站的根目录下。此链接将下载 Developer for Life 的清单:manifest.json

安装清单后,您可以在“应用程序”选项卡下的开发人员工具中查看它。

显现
显现

为了生成图标,我使用了免费的在线工具favicomatic.com

服务人员

PWA 最关键的部分是相关的Service Worker。这是一个特殊的 JavaScript 应用,由浏览器或移动设备注册,用于管理网站。出于安全考虑,Service Worker 的作用域仅限于其所在的域。您无法为 Service Worker 引用其他域的 JavaScript,并且 Service Worker 被阻止直接修改页面。相反,它们充当代理来帮助编组请求。如果您将 Service Worker 放置在 ,mydomain.com/serviceworker/code.js它将只能访问在其下提供服务的页面mydomain.com/serviceworker。因此,它通常安装在根目录下。

我创建了一个部分模板,并在页脚中引用。它包含以下代码:

if ('serviceWorker' in navigator) {
    navigator.serviceWorker
        .register('/sw.js', { scope: '/' })
        .then(() => {
            console.info('Developer for Life Service Worker Registered');
        }, err => console.error("Developer for Life Service Worker registration failed: ", err));
    navigator.serviceWorker
        .ready
        .then(() => {
            console.info('Developer for Life Service Worker Ready');
        });
}
Enter fullscreen mode Exit fullscreen mode

JavaScript 会注册 Service Worker 的源代码 ( sw.js ),并在准备就绪时发出控制台消息。我实现的 Service Worker 主要充当网络代理。它主要完成以下几项任务:

  1. 它获取内容并将其存储在浏览器的缓存中。这用于在在线时提供内容以加快连接速度,以及使内容在离线时可用。
  2. 当您尝试在没有连接互联网的情况下访问非缓存内容时,它会提供一个特殊的离线页面。
  3. 它根据生存时间 (TTL)设置刷新内容。
  4. 如果检测到新版本,它会清除旧缓存并重新开始。

我根据“离线优先服务工作者”编写了以下源代码:

GitHub 徽标 wildhaber /离线优先-sw

服务工作者示例,具有 404 处理、自定义离线页面和特定文件类型的最大 TTL。

Service Worker 示例

线下首张海报

特征

  • 自定义离线页面
  • 自定义 404 页面
  • 缓存始终来自网络的资源的黑名单规则
  • 为不同文件扩展名设置单独的 TTL,以实现离线优先的滚动缓存刷新
  • 轻松定制以满足您的特定需求
  • 更新时清理旧缓存
  • 自动缓存相关内容的定义<link rel='index|next|prev|prefetch'>

安装与使用

安装 Service Worker

只需将sw.js复制到您的根目录中:

# simple wget-snippet or do it manually
# cd /your-projects-root-directory/
wget https://raw.githubusercontent.com/wildhaber/offline-first-sw/master/sw.js
Enter fullscreen mode Exit fullscreen mode

并使用以下代码片段启动 Service Worker:

<script>
    if('serviceWorker' in navigator) {
        /**
         * Define if <link rel='next|prev|prefetch'> should
         * be preloaded when accessing this page
         */
        const PREFETCH = true;

        /**
         * Define which link-rel's should be preloaded if enabled.
         */
        const PREFETCH_LINK_RELS = ['index','next', 'prev', 'prefetch'];

        /**
         * prefetchCache
         */
        function prefetchCache() {
Enter fullscreen mode Exit fullscreen mode

从上到下,代码的细分如下:

const CACHE_VERSION = 2.3;
Enter fullscreen mode Exit fullscreen mode

当代码更改时,我会更新此设置,以强制刷新缓存。每当sw.js文件更改时,浏览器都会将 Service Worker 更新到新版本。

const BASE_CACHE_FILES = [
    '/',
    '/js/jquery-3.3.1.min.js',
    '/404.html',
    '/offline',
    '/css/medium.css',
    '/css/bootstrap.min.css',
    '/css/additional.css',
    '/css/custom.css',
    '/manifest.json',
    '/images/logo.png',
    '/images/jumbotron.jpg',
    '/js/mediumish.js',
    '/blog',
    '/blog/2017-08-17_upcoming-talks/',
    '/static/about',
    '/privacy'
];
const OFFLINE_CACHE_FILES = [
    '/offline/'
];
const NOT_FOUND_CACHE_FILES = [
    '/404.html'
];
const OFFLINE_PAGE = '/offline/';
const NOT_FOUND_PAGE = '/404.html';
Enter fullscreen mode Exit fullscreen mode

这些文件被分组到需要预缓存的资源中,也就是说,即使用户没有访问这些页面,这些资源也会被获取并安装。这提供了基本的离线体验。此外,还有一个专门用于离线模式和页面未找到情况的缓存。我选择了一些资源来渲染主页和导航栏中可用的顶级页面。

const CACHE_VERSIONS = {
    assets: 'assets-v' + CACHE_VERSION,
    content: 'content-v' + CACHE_VERSION,
    offline: 'offline-v' + CACHE_VERSION,
    notFound: '404-v' + CACHE_VERSION,
};
// Define MAX_TTL's in SECONDS for specific file extensions
const MAX_TTL = {
    '/': 3600,
    html: 43200,
    json: 43200,
    js: 86400,
    css: 86400,
};
Enter fullscreen mode Exit fullscreen mode

此代码建立了四个独立的缓存,分别用于保存资源(图片、CSS 文件、脚本)、内容(实际页面)、离线页面和“未找到”页面。它还设置了默认的“生存时间”(以秒为单位)。您可以在开发者工具中查看这些缓存:

缓存
缓存

您还可以深入了解每个缓存的内容。这是我的内容缓存:

缓存内容
缓存内容

接下来的几个方法是内部实用程序,用于执行诸如确定文件扩展名和确定缓存是否已过期之类的操作。一个重要的设置是CACHE_BLACKLIST。我将其实现为一个简单的函数。

const CACHE_BLACKLIST = [
   (str) => !str.startsWith('https://blog.jeremylikness.com')
];
Enter fullscreen mode Exit fullscreen mode

这确保我不会缓存非我自己网站提供的内容。我基本上禁止了所有非我域名提供的内容。这意味着像外部广告这样的内容在离线模式下无法播放,这完全没问题。

⭐ 提示:您可以添加localhost允许,但我建议仅在测试时这样做。如果您永久保留此设置,则在编辑新文章时必须手动刷新页面。自动刷新将提供缓存页面,而不是您最新的编辑。CTRL+F5强制刷新很容易,但我更喜欢localhost在测试完成后直接删除。

安装方法只是将文件预加载到各自的缓存中:

function installServiceWorker() {
    return Promise.all(
        [caches.open(CACHE_VERSIONS.assets).then((cache) => {
            return cache.addAll(BASE_CACHE_FILES);
        }
            , err => console.error(`Error with ${CACHE_VERSIONS.assets}`, err)),
        caches.open(CACHE_VERSIONS.offline).then((cache) => {
            return cache.addAll(OFFLINE_CACHE_FILES);
        }
            , err => console.error(`Error with ${CACHE_VERSIONS.offline}`, err)),
        caches.open(CACHE_VERSIONS.notFound).then((cache) => {
            return cache.addAll(NOT_FOUND_CACHE_FILES);
        }
            , err => console.error(`Error with ${CACHE_VERSIONS.notFound}`, err))]
    )
        .then(() => {
            return self.skipWaiting();
        }, err => console.error("Error with installation: ", err));
}
Enter fullscreen mode Exit fullscreen mode

cleanupLegacyCache当检测到新版本时,会调用该方法。它会查找较旧的缓存并将其删除。

function cleanupLegacyCache() {
    let currentCaches = Object.keys(CACHE_VERSIONS).map((key) => {
        return CACHE_VERSIONS[key];
    });
    return new Promise(
        (resolve, reject) => {
            caches.keys().then((keys) => {
                return legacyKeys = keys.filter((key) => {
                    return !~currentCaches.indexOf(key);
                });
            }).then((legacy) => {
                if (legacy.length) {
                    Promise.all(legacy.map((legacyKey) => {
                        return caches.delete(legacyKey)
                    })
                    ).then(() => {
                        resolve()
                    }).catch((err) => {
                        console.error("Error in legacy cleanup: ", err);
                        reject(err);
                    });
                } else {
                    resolve();
                }
            }).catch((err) => {
                console.error("Error in legacy cleanup: ", err);
                reject(err);
            });
        });
}
Enter fullscreen mode Exit fullscreen mode

最复杂的代码是 Service Worker 的核心。该应用基本上会拦截fetch浏览器加载内容的事件,并将其替换为 JavaScript 代理。以下伪代码解释了它的工作原理。

Intercept request for content
Is content in cache?
Yes, is content expired?
Yes, fetch fresh content.
If fetch was successful, store it in cache and return it
If fetch was not successful, just serve cached content
No, serve cached content
No, fetch the content for the first time
If fetch had OK status, store in cache and return
Otherwise show and store "not found" page
If fetch throws exception, show offline page
Done.
Enter fullscreen mode Exit fullscreen mode

这是离线优先策略,对于不经常更改的内容非常有效。我见过的另一种流行实现是始终获取最新内容,即使内容已在缓存中。缓存内容会立即提供,以提高响应速度;而最新内容则会存储起来,以便下次访问时页面保持最新状态。

根据我目前分享的内容,你大概已经知道如何直接访问该网站/PWA的离线页面,或者在断开网络连接的情况下让它显示出来。我尝试加入等离子和闪电技术,让它变得更有趣。

故障排除

您可能会发现(就像我一样)首次设置时需要进行大量的故障排除。大多数浏览器都会在开发者工具中提供清单视图以及 Service Worker 的相关信息。它通常位于某个application标签页下。

服务人员状态
服务人员状态

您可以使用此功能强制更新、取消注册等。您可以浏览缓存并手动删除以重新开始。最后,Service Worker 代码本身会显示在源列表中,您可以像其他 JavaScript 代码一样设置断点进行调试。有时,单步执行以观察页面逻辑的执行情况会很有用。我遇到的最大问题是输入了预缓存文件的路径错误,这会导致注册问题并最终破坏功能。

灯塔

Lighthouse是一款开源工具,可帮助提供有关您网站的反馈,从而提升网页质量。它会评估性能、可访问性和 SEO 就绪性等指标。它还可以评估您的 PWA。您可以在auditsChrome 和 Edge Insider 浏览器的开发者工具标签页中访问 Lighthouse。我发现它在设置 PWA 并确保满足所有要求方面非常有帮助。它会自动测试一系列功能,并提供一些您可以自行执行的手动检查。

Lighthouse PWA 审核
Lighthouse PWA 审核

请注意,某些要求可能会在本地失败,例如强制使用 HTTPS。我用它在本地机器上完成了 80% 的测试,然后在实际的安全域上首次部署后完成了测试。

概括

PWA 让终端用户更容易访问内容。如果做得好,它们能带来更快速、更流畅的体验。我仍然会惊讶地发现,当我在服务器关闭的情况下意外导航到本地页面时,看到的不是“页面未找到”,而是离线页面。希望这些步骤能帮助你清晰地实现自己的 PWA。接下来,我将研究通知功能,以便在新博客文章发布时提醒用户。在此之前,希望你喜欢这个系列!

问候,

杰里米·莱克尼斯

文章来源:https://dev.to/jeremylikness/implement-a-progressive-web-app-pwa-in-your-static-website-h44
PREV
停止限制开源库的潜力
NEXT
2020 年如何配置 .NET 开发环境