mirror of
https://github.com/jason5ng32/MyIP.git
synced 2026-11-04 08:55:47 +00:00
Refactor(cache): drop service worker, add explicit Cache-Control on static assets
Service-worker offline caching had no real value (every detection in this
app needs the network) and conflicted with Cloudflare challenge pages —
the SW would NetworkFirst-timeout and serve a stale index.html, then the
hashed chunk requests came back as a wall of 403s from CF.
Replace with conventional HTTP cache headers in frontend-server.js:
- dist/assets/** + dist/fonts/** → 1y immutable (content-addressed)
- top-level images → 24h
- index.html + manifest.webmanifest → no-store (so deploys propagate
cleanly to hashed chunks)
- everything else → 1h
Existing prod clients still have the old SW + ~5 MB precache installed,
so unregister-service-worker.js does a one-shot cleanup on first load
after this deploy: unregister all SW registrations, drop every cache.
Safe to delete the file and its main.js call after a few release cycles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
9c37dbcd87
commit
9c5b655f4d
9 files changed
+51
-97
No files matched your search
@@ -31,7 +31,7 @@ Single repo, two halves: a Vue 3 SPA front-end and an Express 5 back-end API, se
|
||||
| Backend | Express 5 |
|
||||
| Logger | `pino` + `pino-pretty` (dev) + `pino-http` (request logs) — singleton at `common/logger.js` |
|
||||
| Auth | Firebase Auth (optional, env-gated) |
|
||||
| PWA | Serwist |
|
||||
| PWA install | `manifest.webmanifest` only (installable but online-only — no service worker) |
|
||||
| Tests | Node built-in test runner (`node --test`) |
|
||||
| Runtime libs | chart.js · svgmap · @cloudflare/speedtest · maxmind · whoiser · thumbmarkjs · ua-parser-js · detect-gpu · circle-progress.vue · @vueuse/core (used by shadcn-vue primitives) |
|
||||
|
||||
|
||||
+27
-2
@@ -21,8 +21,33 @@ frontendApp.use('/api', createProxyMiddleware({
|
||||
changeOrigin: true
|
||||
}));
|
||||
|
||||
// Set static file directory
|
||||
frontendApp.use(express.static(path.join(__dirname, './dist')));
|
||||
// Set static file directory.
|
||||
// Cache-Control is set per-asset class so the static layer behaves well
|
||||
// even when no CDN sits in front of it (CF in production sets its own
|
||||
// Browser TTL on top of these, so the longer values are upper bounds):
|
||||
// - dist/assets/** Vite-hashed JS/CSS/images — content-addressed → 1y immutable
|
||||
// - dist/fonts/** non-hashed but essentially never change → 1y immutable
|
||||
// - top-level images favicon / logos / achievements / … → 24h
|
||||
// - index.html + manifest never cache — otherwise stale HTML keeps pointing
|
||||
// at a hashed chunk that no longer exists post-deploy
|
||||
// - everything else (robots.txt, …) → 1h
|
||||
const distDir = path.join(__dirname, './dist');
|
||||
|
||||
function setStaticHeaders(res, filePath) {
|
||||
const rel = path.relative(distDir, filePath).replaceAll(path.sep, '/');
|
||||
|
||||
if (rel.startsWith('assets/') || rel.startsWith('fonts/')) {
|
||||
res.setHeader('Cache-Control', `public, max-age=${24 * 60 * 60 * 365}, immutable`);
|
||||
} else if (/\.(png|jpg|jpeg|webp|svg|ico)$/i.test(rel)) {
|
||||
res.setHeader('Cache-Control', `public, max-age=${24 * 60 * 60}`);
|
||||
} else if (rel.endsWith('.html') || rel === 'manifest.webmanifest') {
|
||||
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||
} else {
|
||||
res.setHeader('Cache-Control', `public, max-age=${60 * 60}`);
|
||||
}
|
||||
}
|
||||
|
||||
frontendApp.use(express.static(distDir, { setHeaders: setStaticHeaders }));
|
||||
|
||||
// Start static file server
|
||||
frontendApp.listen(frontEndPort, () => {
|
||||
|
||||
@@ -15,7 +15,6 @@ frontend/
|
||||
├── main.js ← app bootstrap + global init
|
||||
├── store.js ← Pinia main store
|
||||
├── firebase-init.js ← Firebase Auth env-gated init
|
||||
├── sw.js ← Serwist service worker
|
||||
├── router/ ← Advanced Tools subroutes (open inside a Drawer)
|
||||
├── locales/ ← i18n copy (en / zh / fr / tr) + security-checklist data
|
||||
├── style/style.css ← Tailwind v4 entry + design tokens
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ import App from './App.vue'
|
||||
import i18n from './locales/i18n';
|
||||
import router from './router';
|
||||
import { analytics } from './utils/use-analytics';
|
||||
import { registerServiceWorker } from './utils/register-service-worker';
|
||||
import { unregisterLegacyServiceWorker } from './utils/unregister-service-worker';
|
||||
|
||||
import { detectOS } from './utils/system-detect';
|
||||
import './style/style.css'
|
||||
@@ -45,7 +45,7 @@ window.addEventListener('resize', handleResize);
|
||||
|
||||
// Start Google Analytics
|
||||
analytics.page();
|
||||
registerServiceWorker();
|
||||
unregisterLegacyServiceWorker();
|
||||
|
||||
// Check Firebase environment
|
||||
store.checkFirebaseEnv();
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
// Service Worker configuration
|
||||
|
||||
import { CacheFirst, ExpirationPlugin, NetworkFirst, Serwist, StaleWhileRevalidate } from 'serwist';
|
||||
|
||||
const serwist = new Serwist({
|
||||
precacheEntries: self.__SW_MANIFEST,
|
||||
precacheOptions: {
|
||||
cleanupOutdatedCaches: true,
|
||||
},
|
||||
skipWaiting: true,
|
||||
clientsClaim: true,
|
||||
runtimeCaching: [
|
||||
{
|
||||
matcher: ({ request, url }) => request.mode === 'navigate' || url.pathname.endsWith('.html'),
|
||||
handler: new NetworkFirst({
|
||||
cacheName: 'html-cache',
|
||||
networkTimeoutSeconds: 3,
|
||||
plugins: [
|
||||
new ExpirationPlugin({
|
||||
maxEntries: 5,
|
||||
maxAgeSeconds: 60 * 60,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
matcher: /\/(sw\.js|manifest\.webmanifest)$/,
|
||||
handler: new NetworkFirst({
|
||||
cacheName: 'critical-assets',
|
||||
plugins: [
|
||||
new ExpirationPlugin({
|
||||
maxEntries: 3,
|
||||
maxAgeSeconds: 4 * 60 * 60,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
matcher: /\.(?:png|jpg|jpeg|svg|webp|woff|woff2)$/,
|
||||
handler: new CacheFirst({
|
||||
cacheName: 'images',
|
||||
plugins: [
|
||||
new ExpirationPlugin({
|
||||
maxEntries: 60,
|
||||
maxAgeSeconds: 7 * 24 * 60 * 60,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
matcher: /\.(?:js|css)$/,
|
||||
handler: new StaleWhileRevalidate({
|
||||
cacheName: 'assets',
|
||||
plugins: [
|
||||
new ExpirationPlugin({
|
||||
maxEntries: 30,
|
||||
maxAgeSeconds: 3 * 24 * 60 * 60,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
serwist.addEventListeners();
|
||||
@@ -1,14 +0,0 @@
|
||||
export function registerServiceWorker() {
|
||||
if (!import.meta.env.PROD || !('serviceWorker' in navigator)) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', async () => {
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.register('/sw.js');
|
||||
await registration.update();
|
||||
} catch (error) {
|
||||
console.warn('Service worker registration failed:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// One-shot cleanup for the legacy Serwist service worker.
|
||||
// Existing prod clients still have it registered and a ~5 MB precache cached,
|
||||
// so first visit after this deploy unregisters the worker and drops every
|
||||
// cache it ever owned. Safe to delete this file (and its main.js call) after
|
||||
// a few release cycles, once stale clients have rotated.
|
||||
|
||||
export function unregisterLegacyServiceWorker() {
|
||||
if (!import.meta.env.PROD || typeof navigator === 'undefined' || !('serviceWorker' in navigator)) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.serviceWorker.getRegistrations()
|
||||
.then((regs) => Promise.all(regs.map((r) => r.unregister())))
|
||||
.catch(() => {});
|
||||
|
||||
if (typeof caches !== 'undefined') {
|
||||
caches.keys()
|
||||
.then((keys) => Promise.all(keys.map((k) => caches.delete(k))))
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
@@ -55,12 +55,10 @@
|
||||
"whoiser": "^1.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@serwist/vite": "^9.5.7",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@vitejs/plugin-vue": "^6.0.6",
|
||||
"code-inspector-plugin": "^1.5.1",
|
||||
"nodemon": "^3.1.14",
|
||||
"serwist": "^9.5.7",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"vite": "^8.0.8"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import dotenv, { parse } from 'dotenv';
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { serwist } from '@serwist/vite'
|
||||
import { CodeInspectorPlugin } from 'code-inspector-plugin';
|
||||
|
||||
dotenv.config();
|
||||
@@ -76,15 +75,6 @@ export default defineConfig({
|
||||
}
|
||||
}),
|
||||
tailwindcss(),
|
||||
serwist({
|
||||
swSrc: 'frontend/sw.js',
|
||||
swDest: 'sw.js',
|
||||
globDirectory: 'dist',
|
||||
globPatterns: [
|
||||
'**/*.{js,css,woff,woff2}',
|
||||
'*.{js,css,png,svg,jpg,webp}',
|
||||
],
|
||||
}),
|
||||
CodeInspectorPlugin({
|
||||
bundler: 'vite',
|
||||
hideDomPathAttr: true,
|
||||
|
||||
Reference in new issue
Block a user