Browse Source

:sparkles: 自动更新

Ritchie 2 months ago
parent
commit
c1aaa65c3b
6 changed files with 220 additions and 13 deletions
  1. 39 0
      generate-version.js
  2. 2 1
      package.json
  3. 82 0
      public/sw.js
  4. 61 0
      src/main.js
  5. 36 12
      src/router/index.js
  6. 0 0
      vue.config.cjs

+ 39 - 0
generate-version.js

@@ -0,0 +1,39 @@
+import fs from 'fs/promises'
+import path from 'path'
+import { fileURLToPath } from 'url'
+import dayjs from 'dayjs'
+import utc from 'dayjs/plugin/utc.js'
+import timezone from 'dayjs/plugin/timezone.js'
+
+dayjs.extend(utc)
+dayjs.extend(timezone)
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = path.dirname(__filename)
+
+async function generateVersion() {
+  try {
+    const pkgBuffer = await fs.readFile(path.resolve(__dirname, 'package.json'))
+    const pkg = JSON.parse(pkgBuffer.toString())
+    
+    const now = dayjs().tz("Asia/Shanghai")
+    
+    const versionData = {
+      version: pkg.version,
+      timestamp: now.format("YYYY-MM-DDTHH:mm:ss.SSS"),
+      commitHash: process.env.GIT_COMMIT_HASH || 'dev-build'
+    }
+    
+    await fs.writeFile(
+      path.resolve(__dirname, 'public', 'version.json'),
+      JSON.stringify(versionData, null, 2)
+    )
+    
+    console.log('✅ 版本文件已生成:', versionData)
+  } catch (error) {
+    console.error('❌ 版本生成失败:', error)
+    process.exit(1)
+  }
+}
+
+generateVersion()

+ 2 - 1
package.json

@@ -1,11 +1,12 @@
 {
   "name": "shenze-vue3-app",
-  "version": "0.1.0",
+  "version": "1.2.3",
   "private": true,
   "scripts": {
     "start": "vue-cli-service serve",
     "dev": "vue-cli-service serve --port 4200",
     "serve": "vue-cli-service serve",
+    "prebuild": "node generate-version.js",
     "build": "vue-cli-service build --mode production",
     "buildDev": "vue-cli-service build --mode development",
     "lint": "vue-cli-service lint"

+ 82 - 0
public/sw.js

@@ -0,0 +1,82 @@
+const CACHE_NAME = 'shenze-cache-v1'; // 更新版本号强制更新
+const DYNAMIC_CACHE = 'shenze-dynamic-v1';
+
+const STATIC_ASSETS = [
+  '/shenze/index.html',
+  '/shenze/favicon.ico',
+  '/shenze/manifest.json',
+  // 避免缓存 version.json
+];
+
+self.addEventListener('install', (event) => {
+  event.waitUntil(
+    caches.open(CACHE_NAME)
+      .then(cache => cache.addAll(STATIC_ASSETS))
+      .then(() => self.skipWaiting())
+  );
+});
+
+self.addEventListener('activate', (event) => {
+  event.waitUntil(
+    caches.keys().then(cacheNames => {
+      return Promise.all(
+        cacheNames.map(cacheName => {
+          if (cacheName !== CACHE_NAME && cacheName !== DYNAMIC_CACHE) {
+            return caches.delete(cacheName);
+          }
+        })
+      );
+    }).then(() => self.clients.claim())
+  );
+});
+
+// fetch 事件:合理区分缓存策略
+self.addEventListener('fetch', (event) => {
+  const url = new URL(event.request.url);
+
+  // version.json 永远走网络
+  if (url.pathname === '/shenze/version.json') {
+    event.respondWith(
+      fetch(event.request)
+        .then(response => response)
+        .catch(() => new Response('{}', { headers: { 'Content-Type': 'application/json' } }))
+    );
+    return;
+  }
+
+  // index.html 及所有 HTML 页面,网络优先
+  if (url.pathname === '/shenze/' || url.pathname.endsWith('.html')) {
+    event.respondWith(
+      fetch(event.request)
+        .then(response => {
+          // 更新缓存
+          const copy = response.clone();
+          caches.open(CACHE_NAME).then(cache => cache.put(event.request, copy));
+          return response;
+        })
+        .catch(() => caches.match(event.request))
+    );
+    return;
+  }
+
+  // 其他静态资源(js/css/img/font等),缓存优先
+  event.respondWith(
+    caches.match(event.request).then(cachedResponse => {
+      return cachedResponse || fetch(event.request).then(networkResponse => {
+        if (event.request.method === 'GET' && networkResponse && networkResponse.status === 200) {
+          const copy = networkResponse.clone();
+          caches.open(DYNAMIC_CACHE).then(cache => cache.put(event.request, copy));
+        }
+        return networkResponse;
+      }).catch(() => {
+        return undefined;
+      });
+    })
+  );
+});
+
+self.addEventListener('message', event => {
+  if (event.data === 'skip-waiting') {
+    self.skipWaiting();
+  }
+});

+ 61 - 0
src/main.js

@@ -17,6 +17,67 @@ import '@vant/touch-emulator';
 // 导入语言
 import i18n from './utils/i18n';
 
+async function checkVersion() {
+  // 开发环境跳过检测
+  if (process.env.NODE_ENV !== 'production') return;
+  try {
+    // 添加时间戳避免 version.json 缓存
+    const response = await fetch('/shenze/version.json?t=' + Date.now());
+    const serverVersion = await response.json();
+    const localVersion = localStorage.getItem('appVersion') || '';
+
+    if (serverVersion.version !== localVersion) {
+      showUpdateDialog(serverVersion.version); // 触发更新提示
+    } else if (!localVersion) {
+      localStorage.setItem('appVersion', serverVersion.version);
+    }
+  } catch (error) {
+    console.error('版本检测失败:', error);
+  }
+}
+
+// 启动定时检测(每 30 秒)
+function startVersionPolling() {
+  checkVersion(); // 初始检测
+  setInterval(checkVersion, 30000); // 30 秒轮询一次
+}
+
+// 在应用初始化时启动轮询
+startVersionPolling();
+
+function showUpdateDialog(newVersion) {
+  Dialog.confirm({
+    title: '版本更新提示',
+    message: `检测到新版本 ${newVersion},是否立即刷新?`,
+    confirmButtonText: '立即刷新',
+    cancelButtonText: '稍后刷新',
+  }).then(() => {
+    localStorage.setItem('appVersion', newVersion);
+    window.location.reload();
+  }).catch(() => {
+    // 用户取消
+    console.log('用户推迟了更新');
+  });
+}
+
+// 注册Service Worker
+if ('serviceWorker' in navigator && process.env.NODE_ENV === 'production') {
+  window.addEventListener('load', () => {
+    navigator.serviceWorker.register('/shenze/sw.js').then(registration => {
+      console.log('ServiceWorker 注册成功:', registration.scope);
+
+      navigator.serviceWorker.addEventListener('controllerchange', () => {
+        window.location.reload();
+      });
+    }).catch(error => {
+      console.log('ServiceWorker 注册失败:', error);
+    });
+  });
+}
+
+// 在应用初始化前执行检测
+checkVersion();
+
 const app = createApp(App) // 创建实例
 // 全局过滤器
 app.config.globalProperties.$filters = {

+ 36 - 12
src/router/index.js

@@ -690,19 +690,43 @@ const router = createRouter({
     },
   ],
 });
+
+let isVersionChecked = false;
+const MAX_RETRY_COUNT = 3;
+let retryCount = 0;
+
 // 路由守卫处理
-router.beforeEach((to, from, next) => {
-  // 从账户操作页面回到home页面
-  // if (from.name === "accountOperation" && to.name === "home") {
-  //   // router.push("/home");
-  //   location.reload();
-  //   // 阻止重定向回 账户操作页
-  //   if (to.path === '/accountOperation') {
-  //     next(false);
-  //   } else {
-  //     next();
-  //   }
-  // }
+router.beforeEach(async (to, from, next) => {
+  // 只在首次路由跳转时检查版本(生产环境)
+  if (!isVersionChecked && process.env.NODE_ENV === 'production') {
+    isVersionChecked = true;
+    try {
+      const response = await fetch(`/shenze/version.json?t=${Date.now()}`, {
+        cache: 'no-store'
+      });
+      const serverVersion = await response.json();
+      const localVersion = localStorage.getItem('appVersion') || '';
+
+      if (serverVersion.version !== localVersion) {
+        // 版本不一致,提示用户刷新
+        if (retryCount < MAX_RETRY_COUNT) {
+          retryCount++;
+          console.log(`检测到新版本(${serverVersion.version}),第${retryCount}次尝试刷新...`);
+          localStorage.setItem('appVersion', serverVersion.version);
+          window.location.reload();
+          return; // 中断当前路由导航
+        } else {
+          console.log('已达到最大重试次数,继续使用旧版本');
+          retryCount = 0; // 重置计数器
+        }
+      } else if (!localVersion) {
+        localStorage.setItem('appVersion', serverVersion.version);
+      }
+    } catch (error) {
+      console.error('版本检测失败:', error);
+      // 继续路由导航,即使版本检测失败
+    }
+  }
 
   // 页面带有不需要识别登录状态的跳过登录验证
   if (to.meta.noLogin) {

vue.config.js → vue.config.cjs