Dan*_*515 13 firebase typescript firebase-cloud-messaging
I have a project built with SolidJS, typescript and Vite. I already have a service worker working using vite PWA plugin, using the generate service worker strategy. Now I want to add notifications using firebase cloud messaging (FCM) and their documentation instructs you to create a very simple file that is meant to be bundled as a service worker. This presents a challenge because Vite is not really meant to be used with several entry-points, which is somewhat required to properly include this file.
I tried several approaches, and I am not happy with any of them. All feel hacky and seem to be suboptimal. Here are the things I tried, ordered from more to less success.
This is the current strategy I'm using because it has the best balance between convenience and being a working solution. There is no single line in the vite-pwa-plugin documentation that says that using several instances of the plugin is possible or encouraged, but there is not anything against it either. So what I did was to instantiate the plugin two times, one for the "legit/real" service worker that my app uses, and another with the inject manifest strategy to bundle the required firebase-messaging-sw.js service worker:
Vite config:
export default defineConfig({
plugins: [
// Abuse the plugin API to bundle the messaging service worker
VitePWA({
strategies: 'injectManifest',
srcDir: 'src',
filename: 'firebase-messaging-sw.ts',
workbox: {
globPatterns: [],
globIgnores: ['*'],
},
}),
// Real legit service worker for my app
VitePWA({
registerType: 'autoUpdate',
devOptions: { enabled: true },
// minimum PWA
includeAssets: ['favicon.ico', 'robots.txt', '*.svg', '*.{png,ico}'],
workbox: {
// bla bla bla
Run Code Online (Sandbox Code Playgroud)
Service worker file firebase-messaging-sw.js:
// Import and configure the Firebase SDK
import { initializeApp } from 'firebase/app';
import { getMessaging } from 'firebase/messaging/sw';
import firebaseConfig from './firebase-config.json';
// Line below makes typescript happy by importing the definitions required for ServiceWorkerGlobalScope
import { precacheAndRoute as _ } from 'workbox-precaching';
declare let self: ServiceWorkerGlobalScope;
const firebaseApp = initializeApp(firebaseConfig);
getMessaging(firebaseApp);
console.info('Firebase messaging service worker is set up');
// If we don't include a point to inject the manifest the plugin will fail.
// Using just a variable will not work because it is tree-shaked, we need to make it part of a side effect to prevent it from being removed
console.log(self.__WB_MANIFEST);
Run Code Online (Sandbox Code Playgroud)
As you can see, this approach seems to be abusing the plugin API and has some nasty side-effects that fulfills no purpose other than preventing the bundling from failing. However, it works, doesn't require a separate build file or configuration and it's everything within a single vite config. Nasty, but convenient
I tried creating a local plugin to handle the imports of the firebase-messaging-sw.js file and emitting it as a separate chunk. However, when the file is registered as a service worker I get errors because the file is bundled as if it was a chunk that is part of the application, therefore it relies on bundle features (like import) that are not available on the service worker environment. The plugin code looks something like this:
import { Plugin } from 'vite';
const print = (obj) => console.dir(obj, { depth: 8 });
export function renameFirebaseSw(): Plugin {
const virtualId = 'virtual:firebase-messaging-sw';
const resolvedVirtualModuleId = '\0' + virtualId;
return {
name: 'vite:rename-firebase-sw',
// enforce: 'post',
resolveId(id) {
if (id === virtualId) return resolvedVirtualModuleId;
},
buildStart() {
print('start');
},
load(id) {
if (id === resolvedVirtualModuleId) {
const ref1 = this.emitFile({
type: 'chunk',
fileName: 'firebase-messaging-sw.js',
id: './src/firebase-messaging-sw.ts',
});
console.log({ ref1 });
return `export default import.meta.ROLLUP_FILE_URL_${ref1}`;
}
},
};
}
Run Code Online (Sandbox Code Playgroud)
然后,您从应用程序中的任何位置导入虚拟模块,插件将拦截它并使用消息传递服务工作文件发出一个单独的块。正如我之前提到的,这不会正确捆绑服务工作线程代码并且会失败,更不用说它不适用于开发,只有在构建生产时才适用
最后,您可以有一个单独的 vite 配置,仅用于捆绑 Service Worker 文件。这种方法似乎是最干净的,因为您只是将服务工作线程代码捆绑在一起,就像它是一个单独的应用程序一样(确实如此)。问题是您需要确保输出名称是适当的(没有哈希,并且导入/注册它的应用程序使用相同的名称),以确保您始终运行主应用程序构建步骤并且Service Worker 构建步骤并在开发模式下并行运行它们。说实话,这不是我想要维护的东西。
是否有任何干净且方便的方法来包含云消息传递所需的服务工作线程,并且没有提到的任何妥协?我做了很多研究和调查,但除了解决方法之外什么也没找到。
感谢您的详细撰写,在阅读您的问题之前我几乎放弃了 VitePWA。还有另一种方法,对我来说感觉恰到好处。也许自从您发布以来,图书馆已经发生了变化。这里的关键是明确告诉 Firebase 使用现有的(自定义)Service Worker 并放弃约定firebase-messaging-sw.js:
对于值得注意的 VitePWA 选项,我设置injectRegister为 null,因为我将手动注册自己的自定义服务工作线程。我还启用了开发模式和类型module以允许 es6 导入。在生产中,VitePWA 会将其转换为classic最大兼容性。
// vite.config.js
plugins: [
VitePWA({
strategies: 'injectManifest',
injectRegister: null,
registerType: 'autoUpdate',
devOptions: {
enabled: true,
type: 'module',
navigateFallback: 'index.html'
},
workbox: {
sourcemap: true
}
})
]
Run Code Online (Sandbox Code Playgroud)
注册部分记录在VitePWA开发页面。这里需要注意的另一件事是serviceWorkerRegistrationFirebase 的getToken. 您可以在这里告诉 Firebase 使用现有的自定义 Service Worker。
import { initializeApp } from "firebase/app";
import { getMessaging, getToken, onMessage } from "firebase/messaging";
const firebaseApp = initializeApp(<your-firebase-config>);
const messaging = getMessaging(firebaseApp);
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register(
import.meta.env.MODE === 'production' ? '/sw.js' : '/dev-sw.js?dev-sw',
{ type: import.meta.env.MODE === 'production' ? 'classic' : 'module' }
)
.then((registration) => {
getToken(messaging, {
vapidKey: '<your-vapidkey>',
serviceWorkerRegistration : registration
})
.then((currentToken) => {
// do something
});
});
}
Run Code Online (Sandbox Code Playgroud)
使用 Service Worker 进行一些操作,其中也包含一些 Firebase 部分。
// public/sw.js
import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching';
import { clientsClaim } from 'workbox-core';
import { NavigationRoute, registerRoute } from 'workbox-routing';
import { initializeApp } from "firebase/app";
import { getMessaging } from "firebase/messaging/sw";
// self.__WB_MANIFEST is default injection point
precacheAndRoute(self.__WB_MANIFEST);
// clean old assets
cleanupOutdatedCaches();
let allowlist;
if (import.meta.env.DEV) {
allowlist = [/^\/$/];
}
// to allow work offline
registerRoute(new NavigationRoute(
createHandlerBoundToURL('index.html'),
{ allowlist },
));
const firebaseApp = initializeApp(<your-firebase-config>);
const messaging = getMessaging(firebaseApp);
self.skipWaiting();
clientsClaim();
Run Code Online (Sandbox Code Playgroud)
您可以按照在开发模式下通常的方式运行 Vite,并且您的自定义 Service Worker 应该与 Firebase Cloud Messaging 配合使用。
我希望这可以帮助别人。我自己正处于起步阶段,如果我想出更好的东西或遇到更多问题,我会更新。但到目前为止,效果很好!
小智 5
好的。因此,我花了大约一周的时间来了解 Vite 和 Service Worker,以便了解如何将 Firebase 与默认的 Vite PWA Service Worker 一起使用。您无需创建单独的 Service Worker 即可添加 Firebase。您可以使用“注入清单”策略。
通过注入清单策略,Vite 将采用您的自定义服务工作线程模板并将预加载的资源(通常称为预缓存清单)添加到您的自定义服务工作线程模板中。
知道这一点后,您可以配置 vite.config.ts 文件来使用此策略。您仍然需要处理资源缓存和设置工作箱。让我们深入研究一下。
配置您的 Vite PWA 以使用注入清单策略(请参阅此处了解如何执行此操作)。确保按照文档中的说明指定 srcDir 和文件名。
您将使用 .ts 扩展名以及工作箱依赖项创建一个服务工作人员。为了使您的 ts Service Worker 成功编译成公共目录中的 js 等效项,指定 rollup 使用的格式非常重要。此格式是“iife”格式。可以在此处找到有关如何执行此操作的说明。如果您不这样做,.ts 文件中的工作箱依赖项将导致您的 Service Worker 出现错误并阻止其在浏览器中运行。
此时剩下的就是创建 custom.ts 文件。确保您使用的路径与您提供给 Vite PWA srcDir 和文件名的路径匹配。我使用路径 src/sw.ts 创建了我的 Service Worker,这意味着我的 Vite.config.ts 看起来像这样
VitePWA({
strategies: 'injectManifest',
injectManifest: {
rollupFormat: 'iife',
},
registerType: 'autoUpdate',
srcDir: 'src',
filename: 'sw.ts',
devOptions: {
enabled: true,
type: "module"
},
})
Run Code Online (Sandbox Code Playgroud)
为了简洁起见,我删除了清单。现在我们已经建立了 sw.ts 文件,现在我们需要创建它。您可以在此处找到创建 .ts 文件所需的碎片代码。不幸的是,这些文件的分割方式对初学者来说不太友好。我已经能够重新生成和研究代码,同时添加注释来解释代码每个部分的作用。我还在评论中概述了如何进行 Firebase 集成。
这是代码!
//Import Cache Names and ClientClaim module for providing the cache name and taking control of all pages immediately
import { cacheNames, clientsClaim } from 'workbox-core'
//Import routing modules for registering routes and setting default and catch handlers
import { registerRoute, setCatchHandler, setDefaultHandler } from 'workbox-routing'
//Import caching modules for caching strategies
import {
NetworkFirst, //Cache the network response first and return it if it succeeds, otherwise return the cached response
NetworkOnly, //Fetch the resource from the network and don't cache it
Strategy, //Base class for caching strategies
StrategyHandler //Base class for caching strategy handlers
} from 'workbox-strategies'
//Import module for caching precached assets
import type { ManifestEntry } from 'workbox-build'
//Firebase
// declare let firebase: any;
// importScripts('https://www.gstatic.com/firebasejs/9.6.8/firebase-app-compat.js');
import { initializeApp } from 'firebase/app';
// importScripts('https://www.gstatic.com/firebasejs/9.6.8/firebase-messaging-compat.js');
import { getMessaging, onBackgroundMessage } from 'firebase/messaging/sw';
//Extend the ServiceWorkerGlobalScope to include the __WB_MANIFEST property
interface MyServiceWorkerGlobalScope extends ServiceWorkerGlobalScope {
__WB_MANIFEST: any;
}
// Give TypeScript the correct global.
declare let self: MyServiceWorkerGlobalScope
// Declare type for ExtendableEvent to use in install and activate events
declare type ExtendableEvent = any
const data = {
race: false, //Fetch first, if it fails, return a previously cached response
debug: false, //Don't log debug messages for intercepted requests and responses
credentials: 'same-origin', //Only request resources from the same origin
networkTimeoutSeconds: 0, //Timout in seconds for network requests; 0 means no timeout
fallback: 'index.html' //Fallback to index.html if the request fails
}
// Access the pre-defined cache names for use in this app
const cacheName = cacheNames.runtime
//Configure the strategy for handling all requests based on the data object
const buildStrategy = (): Strategy => {
//If race condition is set to true, begin a race condition between fetching from Network and Cache
if (data.race) {
class CacheNetworkRace extends Strategy {
//Handle method for the race condition exists on the Strategy Calass
_handle(request: Request, handler: StrategyHandler): Promise<Response | undefined> {
const fetchAndCachePutDone: Promise<Response> = handler.fetchAndCachePut(request)
const cacheMatchDone: Promise<Response | undefined> = handler.cacheMatch(request)
//Return Promise with race conditions where the first to resolve wins
return new Promise((resolve, reject) => {
fetchAndCachePutDone.then(resolve).catch((e) => {
if (data.debug)
console.log(`Cannot fetch resource: ${request.url}`, e)
})
cacheMatchDone.then(response => response && resolve(response))
// Reject if both network and cache error or find no response.
Promise.allSettled([fetchAndCachePutDone, cacheMatchDone]).then((results) => {
const [fetchAndCachePutResult, cacheMatchResult] = results
//fetchAndCachePutResult will be rejected if the network request fails and cacheMatchResult will be
//undefined if the cache is empty since the cachematch promise has no way to be rejected
if (fetchAndCachePutResult.status === 'rejected' && cacheMatchResult.status !== 'fulfilled')
reject(fetchAndCachePutResult.reason)
})
})
}
}
return new CacheNetworkRace()
}
else {
if (data.networkTimeoutSeconds > 0)
return new NetworkFirst({ cacheName, networkTimeoutSeconds: data.networkTimeoutSeconds })
else
return new NetworkFirst({ cacheName })
}
}
//Retrieve the manifest. First define asynchronus function to retrieve the manifest
// This is also required for the injection of the manifest into the service worker by workbox
// So despite it being outdate, Your application will not build without it
const manifest = self.__WB_MANIFEST as Array<ManifestEntry>
//Array for resources that have been cached by the service worker
const cacheEntries: RequestInfo[] = []
//Run through the manifest and cache all resources
const manifestURLs = manifest.map(
(entry) => {
//Create a new url using the service worker location and the manifest entry url
const url = new URL(entry.url, self.location.href)
cacheEntries.push(new Request(url.href, {
credentials: data.credentials as any
}))
return url.href
}
)
// Cache resources when the service worker is first installed
self.addEventListener('install', (event: ExtendableEvent) => {
// The browser will wait until the promise is resolved
event.waitUntil(
// Open the cache and cache all the resources in it. This may include resources
// that are not in the manifest
caches.open(cacheName).then((cache) => {
return cache.addAll(cacheEntries)
})
)
})
// Upon activating the service worker, clear outdated caches by removing caches associated with
// URL resources that are not in the manifest URL array
self.addEventListener('activate', (event: ExtendableEvent) => {
// - clean up outdated runtime cache
event.waitUntil(
caches.open(cacheName).then((cache) => {
// clean up those who are not listed in manifestURLs since the manifestURLs are the only
// resources that are unlikely to be outdated
cache.keys().then((keys) => {
keys.forEach((request) => {
data.debug && console.log(`Checking cache entry to be removed: ${request.url}`)
//If the manifest does not include the request url, delete it from the cache
if (!manifestURLs.includes(request.url)) {
cache.delete(request).then((deleted) => {
if (data.debug) {
if (deleted)
console.log(`Precached data removed: ${request.url || request}`)
else
console.log(`No precache found: ${request.url || request}`)
}
})
}
})
})
})
)
})
//Register all URLs that are found in the manifest and use the buildStrategy function to cache them
registerRoute(
({ url }) => manifestURLs.includes(url.href),
buildStrategy()
)
// Inform the service worker to send all routes that are not recognized to the network to be fetched
setDefaultHandler(new NetworkOnly())
// This method is called when the service worker is unable to fetch a resource from the network
setCatchHandler(
({ event }: any): Promise<Response> => {
switch (event.request.destination) {
case 'document':
return caches.match(data.fallback).then((r) => {
return r ? Promise.resolve(r) : Promise.resolve(Response.error())
})
default:
return Promise.resolve(Response.error())
}
}
)
// this is necessary, since the new service worker will keep on skipWaiting state
// and then, caches will not be cleared since it is not activated
self.skipWaiting()
clientsClaim()
//Firebase config. You can init here by pasting the details. Don't worry it's not a security risk
//as the config is used to connect to the firebase project for listening and not to access the project's admin console
const firebaseConfig = {
apiKey: //Enter your API key here,
authDomain: //Enter your auth domain,
projectId: //Enter your project ID,
storageBucket: //Enter your storage Bucket,
messagingSenderId: //Enter the messaging sender Id,
appId: //Enter your app id,
}
//Initialize Firebase and get the messaging module
const firebaseApp = initializeApp(firebaseConfig)
const messaging = getMessaging(firebaseApp)
// //Handle Background Firebase Messages that come in while the app is closed
onBackgroundMessage(messaging, (payload: any) => {
console.log('Received background message ', payload)
})
Run Code Online (Sandbox Code Playgroud)
这样,您应该让 Service Worker 能够监听您的 Firebase 应用程序实例!如果我留下了什么,请随时发表评论。如果还有什么不明白的地方,也欢迎大家留言评论。尝试阅读 vite 文档。它可能有点脱节,但您需要的大部分内容都在那里。
一切顺利!!
小智 0
我想要在我的项目中使用两个服务人员,其中之一是 firebase-messaging-sw.js,它与 VitePWA 配合得不好,我想用打字稿编写它们,所以我最终制作了一个像您一样的插件来转译它们,将它们放入 dist/src 目录中,以便它们与 HMR 一起工作并进行开发。仍然不是一个令人惊奇的解决方案,但它对我有用。https://github.com/x8BitRain/vite-plugin-ts-sw-hmr
至于导入部分,我最终只是使用以下内容导入必要的脚本以使其正常工作,而不是捆绑本地依赖项。
importScripts('https://www.gstatic.com/firebasejs/9.5.0/firebase-app-compat.js')
importScripts(
'https://www.gstatic.com/firebasejs/9.5.0/firebase-messaging-compat.js'
)
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
4529 次 |
| 最近记录: |