跳到主要内容

为应用组件添加 PWA 集成

在目前的版本中,PWA 的集成仍处于早期阶段,我们将持续优化 PWA 在应用中的集成体验确保 @blocklet/cli 的版本大于 1.16.38-beta-20250115-235439-bb5a1c1b 在文章的开头,我们需要先了解什么是 PWA (Progressive Web App)

在目前的版本中,PWA 的集成仍处于早期阶段,我们将持续优化 PWA 在应用中的集成体验 确保 @blocklet/cli 的版本大于 1.16.38-beta-20250115-235439-bb5a1c1b

在文章的开头,我们需要先了解什么是 PWA (Progressive Web App) ,请参考下面的链接进行了解

Progressive web apps | MDN

在有了上述的基础后,接下来可以了解一下如何在 Blocklet 中添加 PWA

在 Blocklet 中添加 PWA 与普通的应用有何不同

由于 Blocklet 强大的可组合性,在一个应用中,往往会存在多个组件,不同的组件有自己独立的代码,也就意味着不同的组件会有自己不同的 Service Worker 逻辑。

在这样的前提下,我们必须有一个良好的机制,能够隔离各个组件的 Service Worker 逻辑。

那么接下来看看,如何将一个现有的 Blocklet 组件改造为带有 Service Worker 能力的组件。

为组件添加 Service Worker 代码

以一个简单的应用为例,本文将逐步展示在组件中集成 Service Worker 的流程。

GitHub - blocklet/profile-demo

添加 vite-plugin-pwa 插件,并添加以下代码到 vite.config.js

javascript
import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';

export default defineConfig(() => {

return {

// other configs

plugins: [

  // other plugins

VitePWA({

  manifest: false,

  injectRegister: false,

  registerType: 'autoUpdate',

  strategies: 'injectManifest',

  srcDir: 'src',

  filename: 'service-worker.js',

  injectManifest: {

    maximumFileSizeToCacheInBytes: 4194304,

  },

})

]

};
});

添加 src/service-worker.js 文件,并编写适用于当前项目的逻辑

javascript
const { strategies, precaching, core, routing } = self.blocklet.workbox;

precaching.precacheAndRoute(
  (self.__WB_MANIFEST || []).filter((x) => x.url !== 'index.html').map(self.blocklet.fixRouteUrl),
);

routing.registerRoute(
  new routing.Route(
    ({ request, url }) => {
      if (!self.blocklet.canCache({ url })) return false;
      return ['image', 'font', 'style', 'script'].includes(request.destination);
    },
    new strategies.CacheFirst({
      cacheName: `static-resources-${self.registration.scope}`,
    }),
  ),
);

routing.registerRoute(
  new routing.Route(
    ({ request, url }) => {
      const whitelist = ['/.well-known/service/api/user/privacy/config'];
      if (whitelist.includes(url.pathname)) return true;
      if (!self.blocklet.canCache({ url })) return false;
      return request.method === 'GET';
    },
    new strategies.NetworkFirst({
      cacheName: `others-${self.registration.scope}`,
    }),
  ),
);

self.skipWaiting();
core.clientsClaim();
  • blocklet-service 中提供了一组非常有用的函数和属性,均注入在 self.blocklet 中。所以自定义的 service-worker 第一行都建议导入该公共模块 importScripts('/.well-known/service/static/share/shared-service-worker.js');
  • 接下来会使用 self.__WB_MANIFEST 来获取编译时的静态资源文件,该转换由 vite-plugin-pwa 来提供
    • 在项目完成编译后,该代码片段会变成 [{"revision":null,"url":"assets/index-BRdNHKF-.js"},{"revision":null,"url":"assets/index-CnoUf9Rp.css"},{"revision":"dc80b26617d2f88aacce576b0dd81d8a","url":"index.html"}] 这样的结果,它代表了当前应用的所有静态资源
  • 接着需要使用 workbox.precaching.precacheAndRoute 来将上面获得的所有静态资源进行预缓存
    • workbox 已经注入到 self.blocklet.workbox 中了,可以直接使用,无需自己再进行导入
    • 需要注意的是:Blocklet 组件的静态资源将由 .blocklet/proxy/xxx 进行代理,所以在真正进行资源预缓存时,需要将静态资源的地址进行一次转换,可以直接使用 self.blocklet.fixRouteUrl 来完成
  • 再接下来就是组件可以根据自身的特点来进行逻辑的处理
    • 在这个步骤中,可以实现动态的资源/数据缓存,也可以实现 service-worker 与页面的通信,还可以实现 background-sync 等能力
    • 在这个步骤中,大概率会使用到 self.blocklet.canCache 这个函数,它是用于判断当前请求的资源是否应该属于当前 service-worker 作用域的,通过它,可以轻松实现不同组件的资源隔离

启用/禁用组件的 service worker

也许你会想问:等等,代码中还没有添加 service-worker 注册部分的代码吧?

是的,确实还没添加,但答案同时也是,并不需要为应用添加

只需要在应用的配置中,添加一个环境变量 ENABLED_SERVICE_WORKER ,并设置为 true 即可

image.png

image.png

重启当前组件,即可在页面中自动进行 service-worker 的注册

如果你需要注销某个组件的 service-worker,只需要将 ENABLED_SERVICE_WORKER 的值该为 false 即可

额外的提示

对于一些组件来说,它可能本身只是用于提供一个服务,而自身是不需要对外提供可访问页面的

比如 Media Kit,它的作用就是为其他组件提供文件上传和浏览的能力,常常会作为图床来使用

基于上面的组件隔离策略,Media Kit 中的资源则不会被当前的 Service Worker 进行缓存,那么此时就需要为 Media Kit 设置一个特殊的环境变量,SERVICE_WORKER_SHARING_ENABLED 将值设置为 true,表示允许在 Service Worker 进行共享缓存,此时该组件的资源就不会被 self.blocklet.canCache 所拦截

image.png

备忘单

typescript
type MountPoint = string;
type DID = string;
type BlockletServiceWorker = {

// property

workbox: Workbox;

did: DID;

prefix: string;

mountPoints: Array<MountPoint>;

sharedMountPoints: Array<MountPoint>;

// function

canCache: (options: { url: string }) => boolean;

getPrefix: () => string;

getDid: () => string;

getMountPoints: () => Array<MountPoint>;

getSharedMountPoints: () => Array<MountPoint>;

fixRouteUrl: (string) => string;
};
  • workbox: 目前使用的是 workbox-cdn ,查询 https://developer.chrome.com/docs/workbox/modules/workbox-sw 获取更多相关信息
  • did: 当前访问的页面对应的 Blocklet 组件的 did
  • prefix: 当前访问的页面对应的 Blocklet 组件的 mountPoint
  • mountPoints: 当前应用中所有组件的 mountPoint
  • sharedMountPoints: 当前应用中所有开启了 SERVICE_WORKER_SHARING_ENABLED 组件的 mountPoint
  • canCache: 检查一个 url 是否建议被当前组件的 service worker