Files
SearcjGal-frontend/vite.config.ts
AdingApkgg 3d414157a0 依赖升级 + 代码质量、打包体积与组件结构优化
依赖:
- pnpm update --latest(vite、vue、eslint、typescript 等 14 个包到最新版)
- 显式加入 vue-eslint-parser ^10.4.0 / workbox-build ^7.4.1,消除 peer 警告

代码质量:
- 修复 ESLint vue parser 配置(用 vue-eslint-parser 作主 parser),lint 错误 17 → 0
- tsconfig 启用 noUncheckedIndexedAccess,修复 16 个潜在 undefined 访问 bug
- 删除未使用的 src/utils/icons.ts(723 行死代码)

打包体积:
- PWA precache 10.5 MB / 424 项 → 740 KB / 24 项(字体改为运行时缓存)
- vendor CSS gzip 201 KB → 141 KB(禁用字体 base64 内联,保留 unicode-range 子集策略)
- Artalk CSS 跟随 CommentsModal 异步加载
- 字体精简:移除未使用的 300 字重,补上用到的 600
- 删除僵尸的 fancybox manualChunks 配置

健壮性:
- SSE 搜索新增 AbortController + 60s 超时,新搜索取消旧请求,组件卸载取消进行中
- settings 持久化加 300ms 防抖 + QuotaExceededError 处理 + beforeunload 强制落盘

组件拆分:
- SearchHeader.vue 1330 → 1061 行,抽出 SearchErrorCard 子组件
- SettingsModal.vue 1289 → 1171 行,抽出 AdvancedApiSettings 子组件

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:23:19 +08:00

226 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import tailwindcss from "@tailwindcss/vite";
import { fileURLToPath, URL } from 'node:url';
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
server: {
host: "localhost",
port: 5500,
// 预热常用文件
warmup: {
clientFiles: [
'./src/App.vue',
'./src/components/*.vue',
'./src/stores/*.ts',
],
},
},
plugins: [
{
name: 'prismjs-esm-fix',
transform(code, id) {
if (id.includes('prismjs/components/') && !id.includes('prism-core')) {
return {
code: `import Prism from 'prismjs/components/prism-core';\n${code}`,
map: null,
}
}
},
},
vue(),
tailwindcss(),
// PWA 配置
VitePWA({
registerType: 'autoUpdate', // 收到新版本立即更新
includeAssets: ['logo.svg', 'robots.txt'],
manifest: {
name: 'SearchGal - Galgame 聚合搜索',
short_name: 'SearchGal',
description: '多平台 Galgame 资源聚合搜索引擎',
start_url: '/',
display: 'standalone',
background_color: '#fff5fa',
theme_color: '#ff1493',
orientation: 'portrait-primary',
scope: '/',
lang: 'zh-CN',
dir: 'ltr',
icons: [
{
src: '/logo.svg',
sizes: 'any',
type: 'image/svg+xml',
purpose: 'any maskable',
},
],
categories: ['entertainment', 'games', 'utilities'],
shortcuts: [
{
name: '游戏模式搜索',
short_name: '游戏搜索',
description: '快速搜索游戏资源',
url: '/?mode=game',
icons: [{ src: '/logo.svg', sizes: 'any' }],
},
{
name: '补丁模式搜索',
short_name: '补丁搜索',
description: '搜索游戏补丁资源',
url: '/?mode=patch',
icons: [{ src: '/logo.svg', sizes: 'any' }],
},
],
},
workbox: {
// 预缓存核心构建产物(首屏必需,不包含字体——字体按 unicode-range 子集太多,改为运行时缓存)
globPatterns: ['**/*.{js,css,html,svg,ico}'],
// 单文件最大预缓存 3 MB避免大依赖膨胀 precache
maximumFileSizeToCacheInBytes: 3 * 1024 * 1024,
// 排除按需加载的资源(评论、编辑器在用户触发时再缓存)
globIgnores: [
'**/artalk-*.{js,css}',
'**/editor-*.{js,css}',
'**/CommentsModal-*.{js,css}',
'**/SettingsModal-*.{js,css}',
],
// 运行时缓存策略
runtimeCaching: [
{
// 本地字体子集 - 缓存优先(用户用到哪个子集才缓存哪个)
urlPattern: /\/fonts\/.*\.(woff2?|ttf|otf)$/i,
handler: 'CacheFirst',
options: {
cacheName: 'local-fonts',
expiration: {
maxEntries: 60,
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 年
},
},
},
{
// 字体文件 - 缓存优先
urlPattern: /^https:\/\/fonts\.(googleapis|gstatic)\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts',
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 年
},
},
},
{
// 图片 API - 网络优先
urlPattern: /^https:\/\/api\.illlights\.com\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'background-images',
expiration: {
maxEntries: 20,
maxAgeSeconds: 60 * 60 * 24, // 1 天
},
},
},
{
// VNDB API - 网络优先
urlPattern: /^https:\/\/api\.vndb\.org\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'vndb-api',
expiration: {
maxEntries: 50,
maxAgeSeconds: 60 * 30, // 30 分钟
},
},
},
],
// 离线时的导航回退
navigateFallback: null, // 不使用默认回退,由 offlineFallback 处理
},
// 开发环境启用 SW便于测试
devOptions: {
enabled: false, // 开发时禁用,避免干扰
},
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
// 构建优化
build: {
target: 'esnext',
cssCodeSplit: true,
sourcemap: false,
chunkSizeWarningLimit: 600,
// 禁止字体 base64 内联:保留 fontsource 的 unicode-range 子集策略,
// 浏览器按需下载用到的子集而不是把全部字体 base64 进 CSS
assetsInlineLimit: (filePath) => {
if (/\.(woff2?|eot|ttf|otf)$/i.test(filePath)) {
return false
}
return undefined
},
rolldownOptions: {
output: {
assetFileNames: (assetInfo) => {
const name = assetInfo.name || '';
if (/\.(woff2?|eot|ttf|otf)$/i.test(name)) {
return 'fonts/[name]-[hash][extname]';
}
if (/\.(png|jpe?g|gif|svg|webp|ico)$/i.test(name)) {
return 'images/[name]-[hash][extname]';
}
if (/\.css$/i.test(name)) {
return 'css/[name]-[hash][extname]';
}
return 'assets/[name]-[hash][extname]';
},
entryFileNames: 'js/[name]-[hash].js',
chunkFileNames: 'js/[name]-[hash].js',
manualChunks: (id) => {
if (id.includes('node_modules')) {
if (id.includes('/vue/') || id.includes('/@vue/')) {
return 'vue-core';
}
if (id.includes('/pinia/')) {
return 'pinia';
}
if (id.includes('/lucide-vue-next/')) {
return 'ui-libs';
}
if (id.includes('/artalk/')) {
return 'artalk';
}
if (id.includes('/prismjs/') || id.includes('/vue-prism-editor/')) {
return 'editor';
}
return 'vendor';
}
},
},
},
},
// 依赖优化
optimizeDeps: {
include: [
'vue',
'pinia',
'lucide-vue-next',
],
exclude: ['artalk'],
},
// CSS 配置
css: {
devSourcemap: true,
},
});