mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
Merge pull request #813 from xinnan-tech/fix/manager-web-cdn
Fix/manager web cdn
This commit is contained in:
@@ -1 +1,3 @@
|
||||
VUE_APP_API_BASE_URL=/xiaozhi
|
||||
VUE_APP_API_BASE_URL=/xiaozhi
|
||||
# 是否开启CDN
|
||||
VUE_APP_USE_CDN=false
|
||||
Generated
+3249
-11
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,8 @@
|
||||
"sass": "^1.32.7",
|
||||
"sass-loader": "^12.0.0",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"workbox-webpack-plugin": "^7.3.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
@@ -43,4 +44,4 @@
|
||||
"*.scss",
|
||||
"*.vue"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<title>
|
||||
<%= process.env.VUE_APP_TITLE %>
|
||||
</title>
|
||||
<% if (htmlWebpackPlugin.options.cdn) { %>
|
||||
<% if (htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
|
||||
<% for (var i in htmlWebpackPlugin.options.cdn.css) { %>
|
||||
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>">
|
||||
<% } %>
|
||||
@@ -23,7 +23,7 @@
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<% if (htmlWebpackPlugin.options.cdn) { %>
|
||||
<% if (htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
|
||||
<% for (var i in htmlWebpackPlugin.options.cdn.js) { %>
|
||||
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
|
||||
<% } %>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>离线模式 - 小智控制台</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
background-color: #f5f7fa;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
max-width: 80%;
|
||||
}
|
||||
h1 {
|
||||
margin-bottom: 1rem;
|
||||
color: #409EFF;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.btn {
|
||||
background-color: #409EFF;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.8rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
.btn:hover {
|
||||
background-color: #337ecc;
|
||||
}
|
||||
.icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #409EFF;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="icon">📶</div>
|
||||
<h1>您当前处于离线模式</h1>
|
||||
<p>看起来您的网络连接有问题,无法连接到小智控制台服务器。</p>
|
||||
<p>部分已缓存的内容和静态资源可能仍然可用,但功能可能受到限制。</p>
|
||||
<button class="btn" onclick="window.location.reload()">重新尝试连接</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 检测网络状态变化
|
||||
window.addEventListener('online', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
<cache-viewer v-if="isCDNEnabled" :visible.sync="showCacheViewer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -45,4 +46,118 @@ nav {
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import CacheViewer from '@/components/CacheViewer.vue';
|
||||
import { logCacheStatus } from '@/utils/cacheViewer';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CacheViewer
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showCacheViewer: false,
|
||||
isCDNEnabled: process.env.VUE_APP_USE_CDN === 'true'
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// 只有在启用CDN时才添加相关事件和功能
|
||||
if (this.isCDNEnabled) {
|
||||
// 添加全局快捷键Alt+C用于显示缓存查看器
|
||||
document.addEventListener('keydown', this.handleKeyDown);
|
||||
|
||||
// 在全局对象上添加缓存检查方法,便于调试
|
||||
window.checkCDNCacheStatus = () => {
|
||||
this.showCacheViewer = true;
|
||||
};
|
||||
|
||||
// 在控制台输出提示信息
|
||||
console.info(
|
||||
'%c[小智服务] CDN缓存检查工具已加载',
|
||||
'color: #409EFF; font-weight: bold;'
|
||||
);
|
||||
console.info(
|
||||
'按下 Alt+C 组合键或在控制台运行 checkCDNCacheStatus() 可以查看CDN缓存状态'
|
||||
);
|
||||
|
||||
// 检查Service Worker状态
|
||||
this.checkServiceWorkerStatus();
|
||||
} else {
|
||||
console.info(
|
||||
'%c[小智服务] CDN模式已禁用,使用本地打包资源',
|
||||
'color: #67C23A; font-weight: bold;'
|
||||
);
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 只有在启用CDN时才需要移除事件监听
|
||||
if (this.isCDNEnabled) {
|
||||
document.removeEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleKeyDown(e) {
|
||||
// Alt+C 快捷键
|
||||
if (e.altKey && e.key === 'c') {
|
||||
this.showCacheViewer = true;
|
||||
}
|
||||
},
|
||||
async checkServiceWorkerStatus() {
|
||||
// 检查Service Worker是否已注册
|
||||
if ('serviceWorker' in navigator) {
|
||||
try {
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
if (registrations.length > 0) {
|
||||
console.info(
|
||||
'%c[小智服务] Service Worker已注册',
|
||||
'color: #67C23A; font-weight: bold;'
|
||||
);
|
||||
|
||||
// 输出缓存状态到控制台
|
||||
setTimeout(async () => {
|
||||
const hasCaches = await logCacheStatus();
|
||||
if (!hasCaches) {
|
||||
console.info(
|
||||
'%c[小智服务] 还未检测到缓存,请刷新页面或等待缓存建立',
|
||||
'color: #E6A23C; font-weight: bold;'
|
||||
);
|
||||
|
||||
// 开发环境下提供额外提示
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.info(
|
||||
'%c[小智服务] 在开发环境中,Service Worker可能无法正常初始化缓存',
|
||||
'color: #E6A23C; font-weight: bold;'
|
||||
);
|
||||
console.info('请尝试以下方法检查Service Worker是否生效:');
|
||||
console.info('1. 在开发者工具的Application/Application标签页中查看Service Worker状态');
|
||||
console.info('2. 在开发者工具的Application/Cache/Cache Storage中查看缓存内容');
|
||||
console.info('3. 使用生产构建(npm run build)并通过HTTP服务器访问以测试完整功能');
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
} else {
|
||||
console.info(
|
||||
'%c[小智服务] Service Worker未注册,CDN资源可能无法缓存',
|
||||
'color: #F56C6C; font-weight: bold;'
|
||||
);
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.info(
|
||||
'%c[小智服务] 在开发环境中,这是正常现象',
|
||||
'color: #E6A23C; font-weight: bold;'
|
||||
);
|
||||
console.info('Service Worker通常只在生产环境中生效');
|
||||
console.info('要测试Service Worker功能:');
|
||||
console.info('1. 运行npm run build构建生产版本');
|
||||
console.info('2. 通过HTTP服务器访问构建后的页面');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查Service Worker状态失败:', error);
|
||||
}
|
||||
} else {
|
||||
console.warn('当前浏览器不支持Service Worker,CDN资源缓存功能不可用');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
title="CDN资源缓存状态"
|
||||
:visible.sync="visible"
|
||||
width="70%"
|
||||
:before-close="handleClose"
|
||||
>
|
||||
<div v-if="isLoading" class="loading-container">
|
||||
<p>正在加载缓存信息...</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div v-if="!cacheAvailable" class="no-cache-message">
|
||||
<i class="el-icon-warning-outline"></i>
|
||||
<p>您的浏览器不支持Cache API或Service Worker未安装</p>
|
||||
<el-button type="primary" @click="refreshPage">刷新页面</el-button>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<el-alert
|
||||
v-if="cacheData.totalCached === 0"
|
||||
title="未发现缓存的CDN资源"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
<p>Service Worker可能尚未完成初始化或缓存尚未建立。请刷新页面或等待一会后再试。</p>
|
||||
</el-alert>
|
||||
|
||||
<div v-else>
|
||||
<el-alert
|
||||
title="CDN资源缓存状态"
|
||||
type="success"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
共发现 {{ cacheData.totalCached }} 个缓存资源
|
||||
</el-alert>
|
||||
|
||||
<h3>JavaScript 资源 ({{ cacheData.js.length }})</h3>
|
||||
<el-table :data="cacheData.js" stripe style="width: 100%">
|
||||
<el-table-column prop="url" label="URL" width="auto" show-overflow-tooltip />
|
||||
<el-table-column prop="cached" label="状态" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag type="success" v-if="scope.row.cached">已缓存</el-tag>
|
||||
<el-tag type="danger" v-else>未缓存</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<h3>CSS 资源 ({{ cacheData.css.length }})</h3>
|
||||
<el-table :data="cacheData.css" stripe style="width: 100%">
|
||||
<el-table-column prop="url" label="URL" width="auto" show-overflow-tooltip />
|
||||
<el-table-column prop="cached" label="状态" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag type="success" v-if="scope.row.cached">已缓存</el-tag>
|
||||
<el-tag type="danger" v-else>未缓存</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
<el-button type="primary" @click="refreshCache">刷新缓存状态</el-button>
|
||||
<el-button type="danger" @click="clearCache">清除缓存</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
getCacheNames,
|
||||
checkCdnCacheStatus,
|
||||
clearAllCaches,
|
||||
logCacheStatus
|
||||
} from '../utils/cacheViewer';
|
||||
|
||||
export default {
|
||||
name: 'CacheViewer',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoading: true,
|
||||
cacheAvailable: false,
|
||||
cacheData: {
|
||||
css: [],
|
||||
js: [],
|
||||
totalCached: 0,
|
||||
totalNotCached: 0
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
visible(newVal) {
|
||||
if (newVal) {
|
||||
this.loadCacheData();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadCacheData() {
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
// 先检查是否支持缓存API
|
||||
if (!('caches' in window)) {
|
||||
this.cacheAvailable = false;
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有Service Worker缓存
|
||||
const cacheNames = await getCacheNames();
|
||||
this.cacheAvailable = cacheNames.length > 0;
|
||||
|
||||
if (this.cacheAvailable) {
|
||||
// 获取CDN缓存状态
|
||||
this.cacheData = await checkCdnCacheStatus();
|
||||
|
||||
// 在控制台输出完整缓存状态
|
||||
await logCacheStatus();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载缓存数据失败:', error);
|
||||
this.$message.error('加载缓存数据失败');
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async refreshCache() {
|
||||
this.loadCacheData();
|
||||
this.$message.success('正在刷新缓存状态');
|
||||
},
|
||||
|
||||
async clearCache() {
|
||||
this.$confirm('确定要清除所有缓存吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const success = await clearAllCaches();
|
||||
if (success) {
|
||||
this.$message.success('缓存已清除');
|
||||
await this.loadCacheData();
|
||||
} else {
|
||||
this.$message.error('清除缓存失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('清除缓存失败:', error);
|
||||
this.$message.error('清除缓存失败');
|
||||
}
|
||||
}).catch(() => {
|
||||
this.$message.info('已取消清除');
|
||||
});
|
||||
},
|
||||
|
||||
refreshPage() {
|
||||
window.location.reload();
|
||||
},
|
||||
|
||||
handleClose() {
|
||||
this.$emit('update:visible', false);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.no-cache-message {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.no-cache-message i {
|
||||
font-size: 48px;
|
||||
color: #E6A23C;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -6,11 +6,16 @@ import App from './App.vue';
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
import './styles/global.scss';
|
||||
import { register as registerServiceWorker } from './registerServiceWorker';
|
||||
|
||||
Vue.use(ElementUI);
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
// 注册Service Worker
|
||||
registerServiceWorker();
|
||||
|
||||
// 创建Vue实例
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
export const register = () => {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.BASE_URL}service-worker.js`;
|
||||
|
||||
console.info(`[小智服务] 正在尝试注册Service Worker,URL: ${swUrl}`);
|
||||
|
||||
// 先检查Service Worker是否已注册
|
||||
navigator.serviceWorker.getRegistrations().then(registrations => {
|
||||
if (registrations.length > 0) {
|
||||
console.info('[小智服务] 发现已有Service Worker注册,正在检查更新');
|
||||
}
|
||||
|
||||
// 继续注册Service Worker
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
console.info('[小智服务] Service Worker注册成功');
|
||||
|
||||
// 更新处理
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// 内容已缓存更新,通知用户刷新
|
||||
console.log('[小智服务] 新内容可用,请刷新页面');
|
||||
// 可以在这里展示更新提示
|
||||
const updateNotification = document.createElement('div');
|
||||
updateNotification.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: #409EFF;
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
|
||||
z-index: 9999;
|
||||
`;
|
||||
updateNotification.innerHTML = `
|
||||
<div style="display: flex; align-items: center;">
|
||||
<span style="margin-right: 10px;">发现新版本,点击刷新应用</span>
|
||||
<button style="background: white; color: #409EFF; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer;">刷新</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(updateNotification);
|
||||
updateNotification.querySelector('button').addEventListener('click', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
} else {
|
||||
// 一切正常,Service Worker已成功安装
|
||||
console.log('[小智服务] 内容已缓存供离线使用');
|
||||
|
||||
// 可以在这里初始化缓存
|
||||
setTimeout(() => {
|
||||
// 预热CDN缓存
|
||||
const cdnUrls = [
|
||||
'https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css',
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css',
|
||||
'https://unpkg.com/vue@2.6.14/dist/vue.min.js',
|
||||
'https://unpkg.com/vue-router@3.6.5/dist/vue-router.min.js',
|
||||
'https://unpkg.com/vuex@3.6.2/dist/vuex.min.js',
|
||||
'https://unpkg.com/element-ui@2.15.14/lib/index.js',
|
||||
'https://unpkg.com/axios@0.27.2/dist/axios.min.js',
|
||||
'https://unpkg.com/opus-decoder@0.7.7/dist/opus-decoder.min.js'
|
||||
];
|
||||
|
||||
// 预热缓存
|
||||
cdnUrls.forEach(url => {
|
||||
fetch(url, { mode: 'no-cors' }).catch(err => {
|
||||
console.log(`预热缓存 ${url} 失败`, err);
|
||||
});
|
||||
});
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Service Worker 注册失败:', error);
|
||||
|
||||
if (error.name === 'TypeError' && error.message.includes('Failed to register a ServiceWorker')) {
|
||||
console.warn('[小智服务] 注册Service Worker时出现网络错误,CDN资源可能无法缓存');
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.info(
|
||||
'可能原因:1. 服务器未配置正确的MIME类型 2. 服务器SSL证书问题 3. 服务器未返回service-worker.js文件'
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const unregister = () => {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready
|
||||
.then(registration => {
|
||||
registration.unregister();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error.message);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,174 @@
|
||||
/* global self, workbox */
|
||||
|
||||
// 自定义Service Worker安装和激活的处理逻辑
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
});
|
||||
|
||||
// CDN资源列表
|
||||
const CDN_CSS = [
|
||||
'https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css',
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css'
|
||||
];
|
||||
|
||||
const CDN_JS = [
|
||||
'https://unpkg.com/vue@2.6.14/dist/vue.min.js',
|
||||
'https://unpkg.com/vue-router@3.6.5/dist/vue-router.min.js',
|
||||
'https://unpkg.com/vuex@3.6.2/dist/vuex.min.js',
|
||||
'https://unpkg.com/element-ui@2.15.14/lib/index.js',
|
||||
'https://unpkg.com/axios@0.27.2/dist/axios.min.js',
|
||||
'https://unpkg.com/opus-decoder@0.7.7/dist/opus-decoder.min.js'
|
||||
];
|
||||
|
||||
// 当Service Worker被注入manifest后会自动执行
|
||||
const manifest = self.__WB_MANIFEST || [];
|
||||
|
||||
// 检查是否启用CDN模式
|
||||
const isCDNEnabled = manifest.some(entry =>
|
||||
entry.url === 'cdn-mode' && entry.revision === 'enabled'
|
||||
);
|
||||
|
||||
console.log(`Service Worker 已初始化, CDN模式: ${isCDNEnabled ? '启用' : '禁用'}`);
|
||||
|
||||
// 注入workbox相关代码
|
||||
importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.0.0/workbox-sw.js');
|
||||
workbox.setConfig({ debug: false });
|
||||
|
||||
// 开启workbox
|
||||
workbox.core.skipWaiting();
|
||||
workbox.core.clientsClaim();
|
||||
|
||||
// 预缓存离线页面
|
||||
const OFFLINE_URL = '/offline.html';
|
||||
workbox.precaching.precacheAndRoute([
|
||||
{ url: OFFLINE_URL, revision: null }
|
||||
]);
|
||||
|
||||
// 添加安装完成事件处理器,在控制台显示安装消息
|
||||
self.addEventListener('install', event => {
|
||||
if (isCDNEnabled) {
|
||||
console.log('Service Worker 已安装,开始缓存CDN资源');
|
||||
} else {
|
||||
console.log('Service Worker 已安装,CDN模式禁用,仅缓存本地资源');
|
||||
}
|
||||
|
||||
// 确保离线页面被缓存
|
||||
event.waitUntil(
|
||||
caches.open('offline-cache').then((cache) => {
|
||||
return cache.add(OFFLINE_URL);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// 添加激活事件处理器
|
||||
self.addEventListener('activate', event => {
|
||||
console.log('Service Worker 已激活,现在控制着页面');
|
||||
|
||||
// 清理旧版本缓存
|
||||
event.waitUntil(
|
||||
caches.keys().then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames.filter(cacheName => {
|
||||
// 清理除当前版本外的缓存
|
||||
return cacheName.startsWith('workbox-') && !workbox.core.cacheNames.runtime.includes(cacheName);
|
||||
}).map(cacheName => {
|
||||
return caches.delete(cacheName);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// 添加fetch事件拦截器,用于查看CDN资源是否命中缓存
|
||||
self.addEventListener('fetch', event => {
|
||||
// 只有启用CDN模式时才进行CDN资源缓存监控
|
||||
if (isCDNEnabled) {
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// 针对CDN资源,输出是否命中缓存的信息
|
||||
if ([...CDN_CSS, ...CDN_JS].includes(url.href)) {
|
||||
// 不干扰正常的fetch流程,只添加日志
|
||||
console.log(`请求CDN资源: ${url.href}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 仅在CDN模式下缓存CDN资源
|
||||
if (isCDNEnabled) {
|
||||
// 缓存CDN的CSS资源
|
||||
workbox.routing.registerRoute(
|
||||
({ url }) => CDN_CSS.includes(url.href),
|
||||
new workbox.strategies.CacheFirst({
|
||||
cacheName: 'cdn-stylesheets',
|
||||
plugins: [
|
||||
new workbox.expiration.ExpirationPlugin({
|
||||
maxAgeSeconds: 365 * 24 * 60 * 60, // 增加到1年缓存
|
||||
maxEntries: 10, // 最多缓存10个CSS文件
|
||||
}),
|
||||
new workbox.cacheableResponse.CacheableResponsePlugin({
|
||||
statuses: [0, 200], // 缓存成功响应
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// 缓存CDN的JS资源
|
||||
workbox.routing.registerRoute(
|
||||
({ url }) => CDN_JS.includes(url.href),
|
||||
new workbox.strategies.CacheFirst({
|
||||
cacheName: 'cdn-scripts',
|
||||
plugins: [
|
||||
new workbox.expiration.ExpirationPlugin({
|
||||
maxAgeSeconds: 365 * 24 * 60 * 60, // 增加到1年缓存
|
||||
maxEntries: 20, // 最多缓存20个JS文件
|
||||
}),
|
||||
new workbox.cacheableResponse.CacheableResponsePlugin({
|
||||
statuses: [0, 200], // 缓存成功响应
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 无论是否启用CDN模式,都缓存本地静态资源
|
||||
workbox.routing.registerRoute(
|
||||
/\.(?:js|css|png|jpg|jpeg|svg|gif|ico|woff|woff2|eot|ttf|otf)$/,
|
||||
new workbox.strategies.StaleWhileRevalidate({
|
||||
cacheName: 'static-resources',
|
||||
plugins: [
|
||||
new workbox.expiration.ExpirationPlugin({
|
||||
maxAgeSeconds: 7 * 24 * 60 * 60, // 7天缓存
|
||||
maxEntries: 50, // 最多缓存50个文件
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// 缓存HTML页面
|
||||
workbox.routing.registerRoute(
|
||||
/\.html$/,
|
||||
new workbox.strategies.NetworkFirst({
|
||||
cacheName: 'html-cache',
|
||||
plugins: [
|
||||
new workbox.expiration.ExpirationPlugin({
|
||||
maxAgeSeconds: 1 * 24 * 60 * 60, // 1天缓存
|
||||
maxEntries: 10, // 最多缓存10个HTML文件
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// 离线页面 - 使用更可靠的处理方式
|
||||
workbox.routing.setCatchHandler(async ({ event }) => {
|
||||
// 根据请求类型返回适当的默认页面
|
||||
switch (event.request.destination) {
|
||||
case 'document':
|
||||
// 如果是网页请求,返回离线页面
|
||||
return caches.match(OFFLINE_URL);
|
||||
default:
|
||||
// 所有其他请求返回错误
|
||||
return Response.error();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 缓存查看工具 - 用于检查CDN资源是否已被Service Worker缓存
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取所有Service Worker缓存的名称
|
||||
* @returns {Promise<string[]>} 缓存名称列表
|
||||
*/
|
||||
export const getCacheNames = async () => {
|
||||
if (!('caches' in window)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return await caches.keys();
|
||||
} catch (error) {
|
||||
console.error('获取缓存名称失败:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取指定缓存中的所有URL
|
||||
* @param {string} cacheName 缓存名称
|
||||
* @returns {Promise<string[]>} 缓存的URL列表
|
||||
*/
|
||||
export const getCacheUrls = async (cacheName) => {
|
||||
if (!('caches' in window)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const cache = await caches.open(cacheName);
|
||||
const requests = await cache.keys();
|
||||
return requests.map(request => request.url);
|
||||
} catch (error) {
|
||||
console.error(`获取缓存 ${cacheName} 的URL失败:`, error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查特定URL是否已被缓存
|
||||
* @param {string} url 要检查的URL
|
||||
* @returns {Promise<boolean>} 是否已缓存
|
||||
*/
|
||||
export const isUrlCached = async (url) => {
|
||||
if (!('caches' in window)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const cacheNames = await getCacheNames();
|
||||
for (const cacheName of cacheNames) {
|
||||
const cache = await caches.open(cacheName);
|
||||
const match = await cache.match(url);
|
||||
if (match) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error(`检查URL ${url} 是否缓存失败:`, error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前页面所有CDN资源的缓存状态
|
||||
* @returns {Promise<Object>} 缓存状态对象
|
||||
*/
|
||||
export const checkCdnCacheStatus = async () => {
|
||||
// 从CDN缓存中查找资源
|
||||
const cdnCaches = ['cdn-stylesheets', 'cdn-scripts'];
|
||||
const results = {
|
||||
css: [],
|
||||
js: [],
|
||||
totalCached: 0,
|
||||
totalNotCached: 0
|
||||
};
|
||||
|
||||
for (const cacheName of cdnCaches) {
|
||||
try {
|
||||
const urls = await getCacheUrls(cacheName);
|
||||
|
||||
// 区分CSS和JS资源
|
||||
for (const url of urls) {
|
||||
if (url.endsWith('.css')) {
|
||||
results.css.push({ url, cached: true });
|
||||
} else if (url.endsWith('.js')) {
|
||||
results.js.push({ url, cached: true });
|
||||
}
|
||||
results.totalCached++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`获取 ${cacheName} 缓存信息失败:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除所有Service Worker缓存
|
||||
* @returns {Promise<boolean>} 是否成功清除
|
||||
*/
|
||||
export const clearAllCaches = async () => {
|
||||
if (!('caches' in window)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const cacheNames = await getCacheNames();
|
||||
for (const cacheName of cacheNames) {
|
||||
await caches.delete(cacheName);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('清除所有缓存失败:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 将缓存状态输出到控制台
|
||||
*/
|
||||
export const logCacheStatus = async () => {
|
||||
console.group('Service Worker 缓存状态');
|
||||
|
||||
const cacheNames = await getCacheNames();
|
||||
console.log('已发现的缓存:', cacheNames);
|
||||
|
||||
for (const cacheName of cacheNames) {
|
||||
const urls = await getCacheUrls(cacheName);
|
||||
console.group(`缓存: ${cacheName} (${urls.length} 项)`);
|
||||
urls.forEach(url => console.log(url));
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
console.groupEnd();
|
||||
return cacheNames.length > 0;
|
||||
};
|
||||
@@ -6,10 +6,38 @@ const TerserPlugin = require('terser-webpack-plugin');
|
||||
const CompressionPlugin = require('compression-webpack-plugin')
|
||||
// BundleAnalyzerPlugin 用于分析打包后的文件
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
// WorkboxPlugin 用于生成Service Worker
|
||||
const { InjectManifest } = require('workbox-webpack-plugin');
|
||||
// 引入 path 模块
|
||||
const path = require('path');
|
||||
|
||||
const path = require('path')
|
||||
|
||||
function resolve(dir) {
|
||||
return path.join(__dirname, dir)
|
||||
}
|
||||
|
||||
// 确保加载 .env 文件
|
||||
dotenv.config();
|
||||
|
||||
// 定义CDN资源列表,确保Service Worker也能访问
|
||||
const cdnResources = {
|
||||
css: [
|
||||
'https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css',
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css'
|
||||
],
|
||||
js: [
|
||||
'https://unpkg.com/vue@2.6.14/dist/vue.min.js',
|
||||
'https://unpkg.com/vue-router@3.6.5/dist/vue-router.min.js',
|
||||
'https://unpkg.com/vuex@3.6.2/dist/vuex.min.js',
|
||||
'https://unpkg.com/element-ui@2.15.14/lib/index.js',
|
||||
'https://unpkg.com/axios@0.27.2/dist/axios.min.js',
|
||||
'https://unpkg.com/opus-decoder@0.7.7/dist/opus-decoder.min.js'
|
||||
]
|
||||
};
|
||||
|
||||
// 判断是否使用CDN
|
||||
const useCDN = process.env.VUE_APP_USE_CDN === 'true';
|
||||
|
||||
module.exports = defineConfig({
|
||||
productionSourceMap: process.env.NODE_ENV === 'production' ? false : true, // 生产环境不生成 source map
|
||||
devServer: {
|
||||
@@ -29,21 +57,9 @@ module.exports = defineConfig({
|
||||
// 修改 HTML 插件配置,动态插入 CDN 链接
|
||||
config.plugin('html')
|
||||
.tap(args => {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
args[0].cdn = {
|
||||
css: [
|
||||
'https://cdn.jsdelivr.net/npm/element-ui@2.15.14/lib/theme-chalk/index.css',
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css'
|
||||
],
|
||||
js: [
|
||||
'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/vue-router@3.6.5/dist/vue-router.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/vuex@3.6.2/dist/vuex.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/element-ui@2.15.14/lib/index.js',
|
||||
'https://cdn.jsdelivr.net/npm/axios@0.27.2/dist/axios.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/opus-decoder@0.7.7/dist/opus-decoder.min.js'
|
||||
]
|
||||
};
|
||||
// 根据配置决定是否使用CDN
|
||||
if (process.env.NODE_ENV === 'production' && useCDN) {
|
||||
args[0].cdn = cdnResources;
|
||||
}
|
||||
return args;
|
||||
});
|
||||
@@ -101,14 +117,38 @@ module.exports = defineConfig({
|
||||
minRatio: 0.8
|
||||
})
|
||||
);
|
||||
config.externals = {
|
||||
'vue': 'Vue',
|
||||
'vue-router': 'VueRouter',
|
||||
'vuex': 'Vuex',
|
||||
'element-ui': 'ELEMENT',
|
||||
'axios': 'axios',
|
||||
'opus-decoder': 'OpusDecoder'
|
||||
};
|
||||
|
||||
// 根据是否使用CDN来决定是否添加Service Worker
|
||||
config.plugins.push(
|
||||
new InjectManifest({
|
||||
swSrc: path.resolve(__dirname, 'src/service-worker.js'),
|
||||
swDest: 'service-worker.js',
|
||||
exclude: [/\.map$/, /asset-manifest\.json$/],
|
||||
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, // 5MB
|
||||
// 自定义Service Worker注入点
|
||||
injectionPoint: 'self.__WB_MANIFEST',
|
||||
// 添加额外信息传递给Service Worker
|
||||
additionalManifestEntries: useCDN ?
|
||||
[{ url: 'cdn-mode', revision: 'enabled' }] :
|
||||
[{ url: 'cdn-mode', revision: 'disabled' }]
|
||||
})
|
||||
);
|
||||
|
||||
// 如果使用CDN,则配置externals排除依赖包
|
||||
if (useCDN) {
|
||||
config.externals = {
|
||||
'vue': 'Vue',
|
||||
'vue-router': 'VueRouter',
|
||||
'vuex': 'Vuex',
|
||||
'element-ui': 'ELEMENT',
|
||||
'axios': 'axios',
|
||||
'opus-decoder': 'OpusDecoder'
|
||||
};
|
||||
} else {
|
||||
// 确保不使用CDN时不设置externals,让webpack打包所有依赖
|
||||
config.externals = {};
|
||||
}
|
||||
|
||||
if (process.env.ANALYZE === 'true') { // 通过环境变量控制
|
||||
config.plugins.push(
|
||||
new BundleAnalyzerPlugin({
|
||||
@@ -128,13 +168,13 @@ module.exports = defineConfig({
|
||||
config: [__filename] // 每次配置文件修改时缓存失效
|
||||
}
|
||||
};
|
||||
config.resolve.alias = {
|
||||
'@': path.resolve(__dirname, 'src'), // 让 '@' 代表 'src' 目录
|
||||
'@assets': path.resolve(__dirname, 'src/assets'), // 设置 '@assets' 为 'src/assets' 目录
|
||||
'@components': path.resolve(__dirname, 'src/components'), // 设置 '@components' 为 'src/components' 目录
|
||||
'@views': path.resolve(__dirname, 'src/views'), // 设置 '@views' 为 'src/views' 目录
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 将CDN资源信息暴露给service-worker.js
|
||||
pwa: {
|
||||
workboxOptions: {
|
||||
skipWaiting: true,
|
||||
clientsClaim: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user