From 7e3aa09510e733ce7c53c2e961dca97dbc06a91e Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Wed, 25 Sep 2024 22:00:28 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20perf:=20refactor=20pwa=20i?= =?UTF-8?q?mplement=20to=20have=20better=20performance=20(#4124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test remove serwist * 尝试优化 pwa install 实现 * Update next.config.mjs * fix lint * delay the service worker register * only enabled on prod * when isShowPWAGuide update, trigger guide too --- .gitignore | 4 + next.config.mjs | 12 ++- package.json | 3 +- .../ChatInput/Desktop/TextArea.test.tsx | 1 + src/app/sw.ts | 26 ++++++ .../FileViewer/Renderer/Image/index.tsx | 1 + src/features/PWAInstall/Install.tsx | 80 +++++++++++++++++++ src/features/PWAInstall/index.tsx | 67 ++-------------- src/server/services/discover/index.ts | 2 +- src/services/message/server.ts | 2 +- src/services/session/server.ts | 4 +- src/store/chat/slices/plugin/action.ts | 4 +- tsconfig.json | 6 +- 13 files changed, 135 insertions(+), 77 deletions(-) create mode 100644 src/app/sw.ts create mode 100644 src/features/PWAInstall/Install.tsx diff --git a/.gitignore b/.gitignore index af5abc9fad..9893f27bef 100644 --- a/.gitignore +++ b/.gitignore @@ -61,5 +61,9 @@ bun.lockb sitemap*.xml robots.txt +# Serwist +public/sw* +public/swe-worker* + *.patch *.pdf diff --git a/next.config.mjs b/next.config.mjs index d7e87cb7bf..a3d6fa860f 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,6 @@ -import nextPWA from '@ducanh2912/next-pwa'; import analyzer from '@next/bundle-analyzer'; import { withSentryConfig } from '@sentry/nextjs'; +import withSerwistInit from '@serwist/next'; const isProd = process.env.NODE_ENV === 'production'; const buildWithDocker = process.env.DOCKER === 'true'; @@ -192,12 +192,10 @@ const noWrapper = (config) => config; const withBundleAnalyzer = process.env.ANALYZE === 'true' ? analyzer() : noWrapper; const withPWA = isProd - ? nextPWA({ - dest: 'public', - register: true, - workboxOptions: { - skipWaiting: true, - }, + ? withSerwistInit({ + register: false, + swDest: 'public/sw.js', + swSrc: 'src/app/sw.ts', }) : noWrapper; diff --git a/package.json b/package.json index c185d09a29..104a865c4a 100644 --- a/package.json +++ b/package.json @@ -127,6 +127,7 @@ "@next/third-parties": "^14.2.6", "@react-spring/web": "^9.7.3", "@sentry/nextjs": "^7.119.0", + "@serwist/next": "^9.0.8", "@t3-oss/env-nextjs": "^0.11.0", "@tanstack/react-query": "^5.52.1", "@trpc/client": "next", @@ -232,7 +233,6 @@ }, "devDependencies": { "@commitlint/cli": "^19.4.0", - "@ducanh2912/next-pwa": "^10.2.8", "@edge-runtime/vm": "^4.0.2", "@lobehub/i18n-cli": "^1.19.1", "@lobehub/lint": "^1.24.4", @@ -288,6 +288,7 @@ "remark-cli": "^11.0.0", "remark-parse": "^10.0.2", "semantic-release": "^21.1.2", + "serwist": "^9.0.8", "stylelint": "^15.11.0", "supports-color": "8", "tsx": "^4.17.0", diff --git a/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.test.tsx b/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.test.tsx index 3fa5f18655..524ecbe181 100644 --- a/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.test.tsx +++ b/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.test.tsx @@ -150,6 +150,7 @@ describe('', () => { const beforeUnloadHandler = vi.fn(); addEventListenerSpy.mockImplementation((event, handler) => { + // @ts-ignore if (event === 'beforeunload') { beforeUnloadHandler.mockImplementation(handler as any); } diff --git a/src/app/sw.ts b/src/app/sw.ts new file mode 100644 index 0000000000..36c91f23b3 --- /dev/null +++ b/src/app/sw.ts @@ -0,0 +1,26 @@ +import { defaultCache } from '@serwist/next/worker'; +import type { PrecacheEntry, SerwistGlobalConfig } from 'serwist'; +import { Serwist } from 'serwist'; + +// This declares the value of `injectionPoint` to TypeScript. +// `injectionPoint` is the string that will be replaced by the +// actual precache manifest. By default, this string is set to +// `"self.__SW_MANIFEST"`. +declare global { + interface WorkerGlobalScope extends SerwistGlobalConfig { + __SW_MANIFEST: (PrecacheEntry | string)[] | undefined; + } +} + +// eslint-disable-next-line no-undef +declare const self: ServiceWorkerGlobalScope; + +const serwist = new Serwist({ + clientsClaim: true, + navigationPreload: true, + precacheEntries: self.__SW_MANIFEST, + runtimeCaching: defaultCache, + skipWaiting: true, +}); + +serwist.addEventListeners(); diff --git a/src/features/FileViewer/Renderer/Image/index.tsx b/src/features/FileViewer/Renderer/Image/index.tsx index e500abc28b..44adf626bb 100644 --- a/src/features/FileViewer/Renderer/Image/index.tsx +++ b/src/features/FileViewer/Renderer/Image/index.tsx @@ -6,6 +6,7 @@ const ImageRenderer: DocRenderer = ({ mainState: { currentDocument } }) => { return (
+ {/* eslint-disable-next-line @next/next/no-img-element */} {fileName} import('@khmyznikov/pwa-install/dist/pwa-install.react.js'), { + ssr: false, +}); + +const PWAInstall = memo(() => { + const { t } = useTranslation('metadata'); + + const { install, canInstall } = usePWAInstall(); + + const isShowPWAGuide = useUserStore((s) => s.isShowPWAGuide); + const [hidePWAInstaller, updateSystemStatus] = useGlobalStore((s) => [ + systemStatusSelectors.hidePWAInstaller(s), + s.updateSystemStatus, + ]); + + // we need to make the pwa installer hidden by default + useLayoutEffect(() => { + sessionStorage.setItem('pwa-hide-install', 'true'); + }, []); + + const pwaInstall = + // eslint-disable-next-line unicorn/prefer-query-selector + typeof window === 'undefined' ? undefined : document.getElementById(PWA_INSTALL_ID); + + // add an event listener to control the user close installer action + useEffect(() => { + if (!pwaInstall) return; + + const handler = (e: Event) => { + const event = e as CustomEvent; + + // it means user hide installer + if (event.detail.message === 'dismissed') { + updateSystemStatus({ hidePWAInstaller: true }); + } + }; + + pwaInstall.addEventListener('pwa-user-choice-result-event', handler); + return () => { + pwaInstall.removeEventListener('pwa-user-choice-result-event', handler); + }; + }, [pwaInstall]); + + // trigger the PWA guide on demand + useEffect(() => { + if (!canInstall || hidePWAInstaller) return; + + // trigger the pwa installer and register the service worker + if (isShowPWAGuide) { + install(); + if ('serviceWorker' in navigator && window.serwist !== undefined) { + window.serwist.register(); + } + } + }, [canInstall, hidePWAInstaller, isShowPWAGuide]); + + return ( + + ); +}); + +export default PWAInstall; diff --git a/src/features/PWAInstall/index.tsx b/src/features/PWAInstall/index.tsx index 23830198b8..99b9c8eb09 100644 --- a/src/features/PWAInstall/index.tsx +++ b/src/features/PWAInstall/index.tsx @@ -1,79 +1,24 @@ 'use client'; import dynamic from 'next/dynamic'; -import { memo, useEffect, useLayoutEffect } from 'react'; -import { useTranslation } from 'react-i18next'; +import { memo } from 'react'; -import { BRANDING_NAME } from '@/const/branding'; -import { PWA_INSTALL_ID } from '@/const/layoutTokens'; -import { usePWAInstall } from '@/hooks/usePWAInstall'; import { usePlatform } from '@/hooks/usePlatform'; -import { useGlobalStore } from '@/store/global'; -import { systemStatusSelectors } from '@/store/global/selectors'; import { useUserStore } from '@/store/user'; -// @ts-ignore -const PWA: any = dynamic(() => import('@khmyznikov/pwa-install/dist/pwa-install.react.js'), { +const Install: any = dynamic(() => import('./Install'), { ssr: false, }); const PWAInstall = memo(() => { - const { t } = useTranslation('metadata'); const { isPWA } = usePlatform(); - - const { install, canInstall } = usePWAInstall(); - const isShowPWAGuide = useUserStore((s) => s.isShowPWAGuide); - const [hidePWAInstaller, updateSystemStatus] = useGlobalStore((s) => [ - systemStatusSelectors.hidePWAInstaller(s), - s.updateSystemStatus, - ]); - // we need to make the pwa installer hidden by default - useLayoutEffect(() => { - sessionStorage.setItem('pwa-hide-install', 'true'); - }, []); + if (isPWA || !isShowPWAGuide) return null; - const pwaInstall = - // eslint-disable-next-line unicorn/prefer-query-selector - typeof window === 'undefined' ? undefined : document.getElementById(PWA_INSTALL_ID); - - // add an event listener to control the user close installer action - useEffect(() => { - if (!pwaInstall) return; - - const handler = (e: Event) => { - const event = e as CustomEvent; - - // it means user hide installer - if (event.detail.message === 'dismissed') { - updateSystemStatus({ hidePWAInstaller: true }); - } - }; - - pwaInstall.addEventListener('pwa-user-choice-result-event', handler); - return () => { - pwaInstall.removeEventListener('pwa-user-choice-result-event', handler); - }; - }, [pwaInstall]); - - // trigger the PWA guide on demand - useEffect(() => { - if (!canInstall || hidePWAInstaller) return; - - if (isShowPWAGuide) { - install(); - } - }, [canInstall, hidePWAInstaller, isShowPWAGuide]); - - if (isPWA) return null; - return ( - - ); + // only when the user is suitable for the pwa install and not install the pwa + // then show the installation guide + return ; }); export default PWAInstall; diff --git a/src/server/services/discover/index.ts b/src/server/services/discover/index.ts index 6ca45e4873..e4001ee1d6 100644 --- a/src/server/services/discover/index.ts +++ b/src/server/services/discover/index.ts @@ -212,7 +212,7 @@ export class DiscoverService { // Providers // eslint-disable-next-line @typescript-eslint/no-unused-vars - getProviderList = async (locale: Locales): Promise => { + getProviderList = async (_locale: Locales): Promise => { const list = DEFAULT_MODEL_PROVIDER_LIST.filter((item) => item.chatModels.length > 0); return list.map((item) => { const provider = { diff --git a/src/services/message/server.ts b/src/services/message/server.ts index dfb6239cdf..6562b4ec3e 100644 --- a/src/services/message/server.ts +++ b/src/services/message/server.ts @@ -79,7 +79,7 @@ export class ServerService implements IMessageService { return lambdaClient.message.updatePluginState.mutate({ id, value }); } - bindMessagesToTopic(topicId: string, messageIds: string[]): Promise { + bindMessagesToTopic(_topicId: string, _messageIds: string[]): Promise { throw new Error('Method not implemented.'); } diff --git a/src/services/session/server.ts b/src/services/session/server.ts index f4f73de5bc..d68d27c13f 100644 --- a/src/services/session/server.ts +++ b/src/services/session/server.ts @@ -91,7 +91,7 @@ export class ServerService implements ISessionService { return lambdaClient.session.updateSessionChatConfig.mutate({ id, value }, { signal }); } - getSessionsByType(type: 'agent' | 'group' | 'all' = 'all'): Promise { + getSessionsByType(_type: 'agent' | 'group' | 'all' = 'all'): Promise { // TODO: need be fixed // @ts-ignore return lambdaClient.session.getSessions.query({}); @@ -121,7 +121,7 @@ export class ServerService implements ISessionService { return lambdaClient.sessionGroup.getSessionGroup.query(); } - batchCreateSessionGroups(groups: SessionGroups): Promise { + batchCreateSessionGroups(_groups: SessionGroups): Promise { return Promise.resolve({ added: 0, ids: [], skips: [], success: true }); } diff --git a/src/store/chat/slices/plugin/action.ts b/src/store/chat/slices/plugin/action.ts index b0c9cd244b..4ad9844647 100644 --- a/src/store/chat/slices/plugin/action.ts +++ b/src/store/chat/slices/plugin/action.ts @@ -141,7 +141,9 @@ export const chatPlugin: StateCreator< try { content = JSON.parse(data); - } catch {} + } catch { + /* empty block */ + } if (!content) return; diff --git a/tsconfig.json b/tsconfig.json index e53c40e6d4..3ba53989b8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "target": "ESNext", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": ["dom", "dom.iterable", "esnext", "webworker"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -16,7 +16,7 @@ "jsx": "preserve", "incremental": true, "baseUrl": ".", - "types": ["vitest/globals"], + "types": ["vitest/globals", "@serwist/next/typings"], "paths": { "@/*": ["./src/*"], "~test-utils": ["./tests/utils.tsx"] @@ -27,7 +27,7 @@ } ] }, - "exclude": ["node_modules"], + "exclude": ["node_modules", "public/sw.js"], "include": [ "next-env.d.ts", "vitest.config.ts",