From 51d9e6e7fe291bd9b04bd5539751124f0a0fb2f1 Mon Sep 17 00:00:00 2001 From: D-Sketon <2055272094@qq.com> Date: Sat, 23 Nov 2024 15:15:46 +0800 Subject: [PATCH] feat: service worker --- assets/css/main.scss | 4 ++ assets/css/partials/notification.scss | 53 +++++++++++++++ assets/js/service_worker.ts | 52 +++++++++++++++ assets/js/sw.ts | 95 +++++++++++++++++++++++++++ assets/js/tsconfig.json | 2 +- config/_default/params.yml | 4 ++ layouts/partials/afterFooter.html | 27 ++++++-- layouts/partials/head/config.html | 5 ++ layouts/partials/helpers/ts.html | 21 +++++- layouts/partials/loader.html | 11 ++++ 10 files changed, 264 insertions(+), 10 deletions(-) create mode 100644 assets/css/partials/notification.scss create mode 100644 assets/js/service_worker.ts create mode 100644 assets/js/sw.ts diff --git a/assets/css/main.scss b/assets/css/main.scss index ed1ceee..9acf4d0 100644 --- a/assets/css/main.scss +++ b/assets/css/main.scss @@ -186,6 +186,10 @@ a, .main-nav-icon, .popup-btn-close { @import "partials/sidebar"; {{ end }} +{{ if $params.service_worker.enable }} +@import "partials/notification"; +{{ end }} + [data-theme="dark"] { ::-webkit-scrollbar, ::-webkit-scrollbar-track { background-color: #616161; diff --git a/assets/css/partials/notification.scss b/assets/css/partials/notification.scss new file mode 100644 index 0000000..4615054 --- /dev/null +++ b/assets/css/partials/notification.scss @@ -0,0 +1,53 @@ +.notification-wrapper { + position: fixed; + z-index: 1000; + top: 20px; + right: 20px; + background: var(--color-header-background); + box-shadow: 0 0 10px var(--color-hover-shadow); + border-radius: 5px; + padding: 10px 20px; + margin-left: 20px; + opacity: 0; + visibility: hidden; + transition: .5s; + color: var(--grey-7); + + &.show { + opacity: 1; + visibility: visible; + } + + h1 { + padding: 10px 0; + } + + p { + padding: 10px 0; + } + + .notification-btn { + display: flex; + justify-content: flex-end; + gap: 15px; + padding: 10px 0; + } + + button { + background: #ff7777; + border-radius: 5px; + color: #fff; + padding: 5px 10px; + font-family: 'Noto Serif SC', 'Noto Serif JP', -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif; + border: none; + transition: .2s; + + &:hover { + background: rgb(255, 77, 77); + } + + &:active { + background: #ff2e2e; + } + } +} \ No newline at end of file diff --git a/assets/js/service_worker.ts b/assets/js/service_worker.ts new file mode 100644 index 0000000..cb29e8b --- /dev/null +++ b/assets/js/service_worker.ts @@ -0,0 +1,52 @@ +if ("serviceWorker" in navigator && window.siteConfig.swPath) { + _$("#notification-update-btn").onclick = () => { + try { + navigator.serviceWorker.getRegistration().then((reg) => { + reg.waiting.postMessage("skipWaiting"); + }); + } catch (e) { + window.location.reload(); + } + }; + + _$("#notification-close-btn").onclick = () => { + _$(".notification-wrapper").classList.remove("show"); + } + + function emitUpdate() { + _$(".notification-wrapper").classList.add("show"); + } + + navigator.serviceWorker + .register(siteConfig.swPath) + .then((registration) => { + console.log("Service Worker 注册成功: ", registration); + if (registration.waiting) { + emitUpdate(); + return; + } + registration.onupdatefound = () => { + console.log("Service Worker 更新中..."); + const installingWorker = registration.installing; + installingWorker.onstatechange = () => { + if (installingWorker.state === "installed") { + if (navigator.serviceWorker.controller) { + emitUpdate(); + } + } + }; + }; + }) + .catch((error) => { + console.log("Service Worker 注册失败: ", error); + }); + + let refreshing = false; + navigator.serviceWorker.addEventListener("controllerchange", () => { + if (refreshing) { + return; + } + refreshing = true; + window.location.reload(); + }); +} diff --git a/assets/js/sw.ts b/assets/js/sw.ts new file mode 100644 index 0000000..e5bb80b --- /dev/null +++ b/assets/js/sw.ts @@ -0,0 +1,95 @@ +const VERSION = "{{ time.Now.Unix }}"; + +const preCache = [ + '{{ "images/taichi.png" | relURL }}', + '{{ "images/taichi-fill.png" | relURL }}', + '{{ "css/loader.css" | relURL }}', + '{{ "css/main.css" | relURL }}', + '{{ "js/main.js" | relURL }}', + '{{ .Site.Params.banner | relURL }}', +]; + +const cacheDomain = [ + "fonts.googleapis.com", + "npm.webcache.cn", + "unpkg.com", + "fastly.jsdelivr.net", + "cdn.jsdelivr.net", +]; + +// 安装时预加载必要内容 +self.addEventListener("install", (event: ExtendableEvent) => { + console.log(`Service Worker ${VERSION} installing.`); + event.waitUntil(caches.open(VERSION).then((cache) => cache.addAll(preCache))); +}); + +async function cacheRequest(request, options?) { + try { + const responseToCache = await fetch(request); + const cache = await caches.open(VERSION); + if (!/^https?:$/i.test(new URL(request.url).protocol)) + return responseToCache; + cache.put(request, responseToCache.clone()); + return responseToCache; + } catch (e) { + const responseToCache = await fetch(request, options); + const cache = await caches.open(VERSION); + if (!/^https?:$/i.test(new URL(request.url).protocol)) + return responseToCache; + cache.put(request, responseToCache.clone()); + return responseToCache; + } +} + +async function respondRequest(request, options?) { + const response = await caches.match(request); + if (response) { + return response; + } + return cacheRequest(request, options); +} + +self.addEventListener("fetch", (event) => { + const url = new URL(event.request.url); + // 检查请求的域名是否在 CacheDomain 中 + if (cacheDomain.includes(url.hostname)) { + event.respondWith(respondRequest(event.request)); + } else { + // 检查请求是否为 POST 或带有查询参数的 GET 这样可避免错误缓存 + if ( + event.request.method === "POST" || + (event.request.method === "GET" && url.search) + ) { + try { + event.respondWith(fetch(event.request)); + } catch (e) { + event.respondWith(fetch(event.request, { mode: "no-cors" })); + } + } else { + event.respondWith(respondRequest(event.request, { mode: "no-cors" })); + } + } +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (VERSION !== cacheName) { + console.log(`Service Worker: deleting old cache ${cacheName}`); + return caches.delete(cacheName); + } + }) + ); + }) + ); + console.log(`Service Worker ${VERSION} activated.`); +}); + +self.addEventListener("message", (event) => { + console.log("Service Worker: message received"); + if (event.data === "skipWaiting") { + (self as unknown as ServiceWorkerGlobalScope).skipWaiting(); + } +}); diff --git a/assets/js/tsconfig.json b/assets/js/tsconfig.json index 88e50e1..9f45323 100644 --- a/assets/js/tsconfig.json +++ b/assets/js/tsconfig.json @@ -4,7 +4,7 @@ "target": "ES2018", "declaration": true, "esModuleInterop": true, - "lib": ["dom", "ESNext", "DOM.Iterable"] + "lib": ["dom", "ESNext", "DOM.Iterable", "WebWorker"] }, "include": ["./**/*"], "exclude": ["node_modules"] diff --git a/config/_default/params.yml b/config/_default/params.yml index c63171d..8c92a40 100644 --- a/config/_default/params.yml +++ b/config/_default/params.yml @@ -292,6 +292,10 @@ quicklink: # Only support string ignores: [] +# Experimental +service_worker: + enable: false + # Experimental live2d: enable: false diff --git a/layouts/partials/afterFooter.html b/layouts/partials/afterFooter.html index 519321e..2d9c849 100644 --- a/layouts/partials/afterFooter.html +++ b/layouts/partials/afterFooter.html @@ -6,10 +6,10 @@ {{ partial "helpers/js.html" (dict "src" (index (partial "helpers/vendorCdn.html" (dict "item" $js.lazysizes "ctx" $site)) 0) "integrity" (index (partial "helpers/vendorCdnIntegrity.html" (dict "item" $js.lazysizes)) 0)) }} {{ partial "helpers/js.html" (dict "src" (index (partial "helpers/vendorCdn.html" (dict "item" $js.clipboard "ctx" $site)) 0) "integrity" (index (partial "helpers/vendorCdnIntegrity.html" (dict "item" $js.clipboard)) 0)) }} -{{ partial "helpers/ts.html" (dict "source" "js/main.ts") }} +{{ partial "helpers/ts.html" (dict "source" "js/main.ts" "ctx" .) }} {{ if $params.animation.enable }} - {{ partial "helpers/ts.html" (dict "source" "js/aos.ts") }} + {{ partial "helpers/ts.html" (dict "source" "js/aos.ts" "ctx" .) }} {{ end }} -{{ partial "helpers/ts.html" (dict "source" "js/pjax_main.ts" "pjax" true) }} +{{ partial "helpers/ts.html" (dict "source" "js/pjax_main.ts" "pjax" true "ctx" .) }} {{ if $params.algolia_search.enable }} {{ partial "helpers/js.html" (dict "src" (index (partial "helpers/vendorCdn.html" (dict "item" $js.algolia "ctx" $site)) 0) "defer" true "integrity" (index (partial "helpers/vendorCdnIntegrity.html" (dict "item" $js.algolia)) 0)) }} {{ partial "helpers/js.html" (dict "src" (index (partial "helpers/vendorCdn.html" (dict "item" $js.instantsearch "ctx" $site)) 0) "defer" true "integrity" (index (partial "helpers/vendorCdnIntegrity.html" (dict "item" $js.instantsearch)) 0)) }} - {{ partial "helpers/ts.html" (dict "source" "js/algolia_search.ts") }} + {{ partial "helpers/ts.html" (dict "source" "js/algolia_search.ts" "ctx" .) }} {{ end }} {{ if $params.firework.enable }} @@ -136,7 +136,7 @@ cacheBust: false, }); - {{ partial "helpers/ts.html" (dict "source" "js/pjax.ts") }} + {{ partial "helpers/ts.html" (dict "source" "js/pjax.ts" "ctx" .) }} {{ end }} {{ if $params.live2d.enable }} @@ -240,7 +240,7 @@ {{ end }} {{ if not .IsHome }} - {{ partial "helpers/ts.html" (dict "source" "js/insert_highlight.ts" "pjax" true) }} + {{ partial "helpers/ts.html" (dict "source" "js/insert_highlight.ts" "pjax" true "ctx" .) }} {{ $photoswipe_lightbox := index (partial "helpers/vendorCdn.html" (dict "item" $js.photoswipe_lightbox "ctx" $site)) 0 }} {{ $photoswipe_lightbox_integrity := index (partial "helpers/vendorCdnIntegrity.html" (dict "item" $js.photoswipe_lightbox)) 0 }} {{ $photoswipe := index (partial "helpers/vendorCdn.html" (dict "item" $js.photoswipe "ctx" $site)) 0 }} @@ -325,6 +325,21 @@ {{ partial "helpers/js.html" (dict "src" (index (partial "helpers/vendorCdn.html" (dict "item" $js.busuanzi "ctx" $site)) 0) "async" true "integrity" (index (partial "helpers/vendorCdnIntegrity.html" (dict "item" $js.busuanzi)) 0)) }} {{ end }} +{{ if $params.service_worker.enable }} + {{ partial "helpers/ts.html" (dict "source" "js/sw.ts" "ctx" . "inline" false "target" "sw.js") }} + {{ partial "helpers/ts.html" (dict "source" "js/service_worker.ts" "ctx" .) }} +{{ else }} + +{{ end }} + +{{ if .Site.Params.service_worker.enable }} + +{{ end }} diff --git a/layouts/partials/helpers/ts.html b/layouts/partials/helpers/ts.html index 8fd8122..f1275c8 100644 --- a/layouts/partials/helpers/ts.html +++ b/layouts/partials/helpers/ts.html @@ -1,15 +1,30 @@ {{ $source := .source }} +{{ $temp := .source }} {{ $pjax := .pjax }} +{{ $target := .target }} +{{ $inline := .inline | default true }} +{{ $ctx := .ctx }} {{- with resources.Get $source }} {{- if eq hugo.Environment "development" }} - {{- with . | js.Build }} + {{ $ts := . | resources.ExecuteAsTemplate $temp $ctx }} + {{- $opts := dict "targetPath" $target }} + {{- with $ts | js.Build $opts }} + {{ if $inline }} + {{ else }} + + {{ end }} {{- end }} {{- else }} - {{- $opts := dict "minify" true }} - {{- with . | js.Build $opts | fingerprint }} + {{ $ts := . | resources.ExecuteAsTemplate $temp $ctx }} + {{- $opts := dict "minify" true "targetPath" $target }} + {{- with $ts | js.Build $opts }} + {{ if $inline }} + {{ else }} + + {{ end }} {{- end }} {{- end }} {{- end }} diff --git a/layouts/partials/loader.html b/layouts/partials/loader.html index 56195ca..1335afd 100644 --- a/layouts/partials/loader.html +++ b/layouts/partials/loader.html @@ -42,3 +42,14 @@
{{ i18n "service_worker.content" }}
+