build: migrate PWA to Serwist and Vite 8

This commit is contained in:
jason5ng32 committed 2026-04-14 12:23:07 +08:00
1 parent 679cd6d256
commit 2c25229762
8 files changed
+212 -127

No files matched your search

+13 -1
View File
@@ -46,6 +46,7 @@ const showPWA = () => {
pwaInstall.isAppleMobilePlatform = isIosSafari.value;
pwaInstall.isAppleDesktopPlatform = isMacSafari.value;
pwaInstall.externalPromptEvent = window.ipcheckInstallPromptEvent || pwaInstall.externalPromptEvent;
if (!pwaInstall.isUnderStandaloneMode && pwaInstall.isInstallAvailable) {
pwaInstall.showDialog(true);
@@ -62,6 +63,17 @@ const showPWA = () => {
onMounted(() => {
getBrowser();
const pwaInstall = document.getElementsByTagName('pwa-install')[0];
if (pwaInstall && window.ipcheckInstallPromptEvent) {
pwaInstall.externalPromptEvent = window.ipcheckInstallPromptEvent;
}
window.addEventListener('beforeinstallprompt', event => {
if (pwaInstall) {
pwaInstall.externalPromptEvent = event;
}
});
if (popupCount() < 2) {
setTimeout(() => {
showPWA();
@@ -70,4 +82,4 @@ onMounted(() => {
});
</script>
<style scoped></style>
<style scoped></style>
+2
View File
@@ -6,6 +6,7 @@ import i18n from './locales/i18n';
import router from './router';
import 'bootstrap';
import { analytics } from './utils/use-analytics';
import { registerServiceWorker } from './utils/register-service-worker';
import { Tooltip } from 'bootstrap';
import { detectOS } from './utils/system-detect';
@@ -42,6 +43,7 @@ window.addEventListener('resize', handleResize);
// 启动 Google Analytics
analytics.page();
app.config.globalProperties.$analytics = analytics;
registerServiceWorker();
// 注册全局事件跟踪函数,改造完程序后移除
app.config.globalProperties.$trackEvent = function (category, action, label) {
+63
View File
@@ -0,0 +1,63 @@
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();
+14
View File
@@ -0,0 +1,14 @@
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);
}
});
}
+8 -1
View File
@@ -12,6 +12,7 @@
<meta name="theme-color" content="#f8f9fa">
<meta name="background-color" content="#ffffff">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="manifest" href="/manifest.webmanifest">
<link rel="apple-touch-icon" href="logos/ios-logo-512.png">
<title>IPCheck.ing - Check My IP Address and Geolocation - Check WebRTC Connection IP - DNS Leak Test - Speed Test -
Ping/MTR Test - Jason Ng Open Source </title>
@@ -19,6 +20,12 @@
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<script>
window.ipcheckInstallPromptEvent = null;
window.addEventListener('beforeinstallprompt', (event) => {
event.preventDefault();
window.ipcheckInstallPromptEvent = event;
});
setTimeout(() => {
document.getElementById('waiting-too-long').innerHTML = `
<p>Waiting too long? Try to use <a href="https://lite.ipcheck.ing/">IPCheck.ing Lite</a>.<p>
@@ -88,4 +95,4 @@
<script type="module" src="/frontend/main.js"></script>
</body>
</html>
</html>
+16 -15
View File
@@ -12,38 +12,39 @@
"start": "concurrently \"npm run start-frontend\" \"npm run start-backend\""
},
"dependencies": {
"@cloudflare/speedtest": "^1.7.0",
"@cloudflare/speedtest": "^1.8.1",
"@khmyznikov/pwa-install": "^0.6.3",
"@thumbmarkjs/thumbmarkjs": "^1.7.5",
"@thumbmarkjs/thumbmarkjs": "^1.7.7",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1",
"chart.js": "^4.5.1",
"circle-progress.vue": "^3.4.1",
"code-inspector-plugin": "^1.4.5",
"code-inspector-plugin": "^1.5.1",
"concurrently": "^9.2.1",
"country-code-lookup": "^0.1.3",
"country-code-lookup": "^0.1.5",
"detect-gpu": "^5.0.70",
"dotenv": "^17.3.1",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"express-rate-limit": "^8.3.1",
"express-rate-limit": "^8.3.2",
"express-slow-down": "^3.1.0",
"firebase": "^12.11.0",
"firebase": "^12.12.0",
"flag-icons": "^7.5.0",
"http-proxy-middleware": "^3.0.5",
"maxmind": "^5.0.5",
"maxmind": "^5.0.6",
"nodemon": "^3.1.14",
"pinia": "^3.0.4",
"svgmap": "2.19.2",
"svgmap": "2.19.3",
"ua-parser-js": "^2.0.9",
"vue": "^3.5.30",
"vue-i18n": "^11.3.0",
"vue": "^3.5.32",
"vue-i18n": "^11.3.2",
"vue-markdown-render": "^2.3.0",
"vue-router": "^5.0.4",
"whoiser": "^1.18.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.5",
"vite": "^7.3.1",
"vite-plugin-pwa": "^1.2.0"
"@serwist/vite": "^9.5.7",
"@vitejs/plugin-vue": "^6.0.6",
"serwist": "^9.5.7",
"vite": "^8.0.8"
}
}
}
+30
View File
@@ -0,0 +1,30 @@
{
"name": "IPCheck.ing",
"short_name": "IPCheck.ing",
"description": "All in one IP Toolbox",
"id": "com.jasonng.myip",
"start_url": "/",
"scope": "/",
"display": "standalone",
"theme_color": "#f8f9fa",
"background_color": "#ffffff",
"orientation": "portrait",
"icons": [
{
"src": "/logos/logo-192.webp",
"sizes": "192x192",
"type": "image/webp",
"purpose": "maskable"
},
{
"src": "/logos/ios-logo-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/logos/ios-logo-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
+66 -110
View File
@@ -1,13 +1,69 @@
import dotenv, { parse } from 'dotenv';
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { VitePWA } from 'vite-plugin-pwa'
import { serwist } from '@serwist/vite'
import { CodeInspectorPlugin } from 'code-inspector-plugin';
dotenv.config();
const backEndPort = parseInt(process.env.BACKEND_PORT || 11966, 10);
const frontEndPort = parseInt(process.env.FRONTEND_PORT || 18966, 10);
const nodeModuleChunkGroups = {
vendor: ['vue', 'vue-router', 'vue-i18n'],
chart: ['chart.js'],
speedtest: ['@cloudflare/speedtest'],
svgmap: ['svgmap'],
'browser-detect': ['@thumbmarkjs/thumbmarkjs', 'detect-gpu', 'ua-parser-js'],
};
const sourceChunkGroups = {
'utils-getips': [
'/frontend/utils/getips/index',
'/frontend/utils/valid-ip',
'/frontend/utils/transform-ip-data',
'/frontend/utils/masked-info'
],
'utils-data': [
'/frontend/utils/country-name',
'/frontend/utils/speedtest-colos'
],
'utils-auth': [
'/frontend/utils/authenticated-fetch'
],
'utils-analytics': [
'/frontend/utils/use-analytics'
]
};
function isNodePackage(normalizedId, packageName) {
const nodeModulesPath = '/node_modules/';
const nodeModulesIndex = normalizedId.lastIndexOf(nodeModulesPath);
if (nodeModulesIndex === -1) {
return false;
}
const packagePath = normalizedId.slice(nodeModulesIndex + nodeModulesPath.length);
return packagePath === packageName || packagePath.startsWith(`${packageName}/`);
}
function manualChunks(id) {
const normalizedId = id.replaceAll('\\', '/');
for (const [chunkName, packages] of Object.entries(nodeModuleChunkGroups)) {
if (packages.some((packageName) => isNodePackage(normalizedId, packageName))) {
return chunkName;
}
}
for (const [chunkName, modules] of Object.entries(sourceChunkGroups)) {
if (modules.some((moduleName) => normalizedId.includes(moduleName))) {
return chunkName;
}
}
return undefined;
}
export default defineConfig({
plugins: [
@@ -18,92 +74,14 @@ export default defineConfig({
}
}
}),
VitePWA({
registerType: 'autoUpdate',
injectRegister: 'auto',
workbox: {
globPatterns: [
'**/*.{js,css,woff,woff2}',
'*.{js,css,png,svg,jpg,webp}',
],
navigateFallback: null, // 禁用默认 navigateFallback,使用自定义 HTML 缓存策略
runtimeCaching: [
{
urlPattern: ({ request, url }) => {
// 匹配 HTML 文档请求(导航请求)
return request.mode === 'navigate' || url.pathname.endsWith('.html');
},
handler: 'NetworkFirst', // 优先从网络获取最新版本
options: {
cacheName: 'html-cache',
expiration: {
maxEntries: 5,
maxAgeSeconds: 60 * 60, // 1 小时,较短的缓存时间
},
networkTimeoutSeconds: 3, // 3秒网络超时后使用缓存
},
},
{
urlPattern: /\/(sw\.js|registerSW\.js|manifest\.webmanifest)$/, // sw 文件
handler: 'NetworkFirst',
options: {
cacheName: 'critical-assets',
expiration: {
maxEntries: 3,
maxAgeSeconds: 4 * 60 * 60, // 4 小时
},
},
},
{
urlPattern: /\.(?:png|jpg|jpeg|svg|webp|woff|woff2)$/, // 图片文件
handler: 'CacheFirst',
options: {
cacheName: 'images',
expiration: {
maxEntries: 60,
maxAgeSeconds: 7 * 24 * 60 * 60, // 7 天
},
},
},
{
urlPattern: /\.(?:js|css)$/, // JS 和 CSS 文件
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'assets',
expiration: {
maxEntries: 30,
maxAgeSeconds: 3 * 24 * 60 * 60, // 3 天
},
},
},
]
},
manifest: {
name: 'IPCheck.ing',
short_name: 'IPCheck.ing',
theme_color: '#f8f9fa',
orientation: "portrait",
id: 'com.jasonng.myip',
description: 'All in one IP Toolbox',
icons: [
{
src: '/logos/logo-192.webp',
sizes: '192x192',
type: 'image/webp',
purpose: 'maskable'
},
{
src: '/logos/ios-logo-192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: '/logos/ios-logo-512.png',
sizes: '512x512',
type: 'image/png',
},
],
},
serwist({
swSrc: 'frontend/sw.js',
swDest: 'sw.js',
globDirectory: 'dist',
globPatterns: [
'**/*.{js,css,woff,woff2}',
'*.{js,css,png,svg,jpg,webp}',
],
}),
CodeInspectorPlugin({
bundler: 'vite',
@@ -121,29 +99,7 @@ export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor': ['vue', 'vue-router', 'vue-i18n'],
'chart': ['chart.js/auto'],
'speedtest': ['@cloudflare/speedtest'],
'svgmap': ['svgmap'],
'browser-detect': ['@thumbmarkjs/thumbmarkjs', 'detect-gpu', 'ua-parser-js'],
'utils-getips': [
'@/utils/getips/index',
'@/utils/valid-ip',
'@/utils/transform-ip-data',
'@/utils/masked-info'
],
'utils-data': [
'@/utils/country-name',
'@/utils/speedtest-colos'
],
'utils-auth': [
'@/utils/authenticated-fetch'
],
'utils-analytics': [
'@/utils/use-analytics'
]
},
manualChunks,
assetFileNames: (assetInfo) => {
if (assetInfo.fileName && (assetInfo.fileName.endsWith('.woff') || assetInfo.fileName.endsWith('.woff2'))) {
return 'fonts/[name][extname]';