Files
chunyu_prject_react/vite.config.ts
T
chunyu 24f200bfa6 feat(C-01):路由/代理前缀冲突修复
SPA冲突路由改名+旧路径Navigate重定向(/api-docs→/open-api-docs、/api-detail→/open-api-detail、/article→/post、/user→/user-home);vite代理宽前缀改精确正则白名单;nginx.conf同步同口径;新增86路由回归脚本+Playwright 3项(86/86全绿)。
2026-09-15 15:25:59 +08:00

174 lines
6.9 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 react from '@vitejs/plugin-react'
import path from 'path';
// C-01: vite 代理精确收窄 —— 只代理后端真实存在的 API 路径,SPA 路由一律放行。
// 后端真实挂载(chunyu_project/chunyu_project/urls.py + 各 app urls.py):
// /api/*(api.urls:getIpData/baiduFanyi/tool/weather/air-quality/currency/captcha/shorturl/...)
// /api/apidirectory/*、/user/*、/bug/reports/*、/article/articles|my-articles|comments/*
// /learn/*、/history/records/*、/message/*、/chat/*、/tool/*、/search/*、/app/*、
// /logs/api/*、/shorturl/*、/s/<code>/(短链跳转)、/admin/、/swagger*、/redoc/、/i18n/、/media/
// SPA 路由(/open-api*、/post/*、/user-home*、/api-docs 旧重定向等)不在此列,
// 由 bypass 精确放行,避免前缀误伤(如 /api 劫持 /open-api?不——新 SPA 已迁走,
// 但 /api-docs 旧重定向、/article 旧重定向等仍需放行到前端)。
const BACKEND = 'http://127.0.0.1:8002'
// 纯 SPA 前缀:命中即放行(返回 undefined → 走 vite dev 中间件/前端路由)
const SPA_PREFIXES = [
'/open-api',
'/post/',
'/user-home',
'/articles',
'/utility',
'/learn-manage',
'/learn-editor',
'/article-manage',
'/article-editor',
'/bug-detail',
'/tool-detail',
'/course-learn',
'/shorturl-detail',
'/currency-detail',
'/ip-location',
'/baidu-translate',
'/qrcode-generator',
'/color-picker',
'/changelog',
'/favorites',
'/console',
'/profile',
'/settings',
'/history',
'/favorites',
'/use',
'/document',
'/manual',
'/privacy',
'/agreement',
'/messages',
'/chat',
'/search',
'/bug',
'/learn',
'/wallet',
'/task-center',
'/invite',
'/forgot-password',
'/register',
]
// 精确 SPA 路由(无尾部、与后端前缀撞名的几个)
const SPA_EXACT = new Set([
'/api-docs', '/api-directory', '/api',
'/user', '/article-manage', '/article-editor', '/bug-detail',
])
// C-01 旧分享链接对应的 SPA 页面(已迁到 /open-api*,但旧 URL 仍由前端 <Navigate> 承接)
const LEGACY_API_PAGES = new Set([
'/api/air-quality-details', '/api/weather-details', '/api/qrcode-generator',
'/api/ip-location', '/api/password-generator',
])
function isSpaPath(url: string | undefined): boolean {
if (!url) return false
const p = url.split('?')[0]
if (SPA_EXACT.has(p)) return true
if (LEGACY_API_PAGES.has(p)) return true
// 旧重定向路由也属 SPA
if (p.startsWith('/api-detail') || p.startsWith('/api-docs') || p.startsWith('/api-directory')) return true
if (p.startsWith('/article/') || p === '/article') return true
if (p.startsWith('/user/') || p === '/user') return true
return SPA_PREFIXES.some((pre) => p === pre || p.startsWith(pre + '/') || (pre.endsWith('/') ? p.startsWith(pre) : p.startsWith(pre + '/') || p === pre))
}
function spaBypass(req: { url?: string }) {
if (isSpaPath(req.url)) return req.url // 放行到前端
}
// 后端真实 API 上下文 → 全部走精确前缀(每个都以 / 结尾或带参数段,
// 且各自的 bypass 先放行 SPA,避免 /message 误伤 /messages、/s 误伤 /settings)
const proxy: Record<string, object> = {
'/ws': {
target: 'ws://127.0.0.1:8002',
ws: true,
rewrite: (path: string) => path.replace(/^\/ws/, ''),
},
'/media/': { target: BACKEND, changeOrigin: true },
// /api/*:后端真实 API(含 /api/apidirectory/*、/api/shorturl/* 等)
'^/api/': { target: BACKEND, changeOrigin: true, bypass: spaBypass },
// /user/*:后端 user.urls 全是 /user/<action>/;SPA 已迁到 /user-home*,
// 仅放行旧分享链接 /user/<数字id>(LegacyUserHomeRedirect),其余一律代理
'^/user/': {
target: BACKEND,
changeOrigin: true,
bypass: (req: { url?: string }) => {
const p = (req.url || '').split('?')[0]
if (/^\/user\/\d+\/?$/.test(p)) return req.url // 旧用户主页分享链接 → SPA 重定向
},
},
// /bug/reports/*:后端仅 reports/ 三条;SPA 的 /bug 与 /bug-detail 放行
'^/bug/reports/': { target: BACKEND, changeOrigin: true },
// /article/articles|my-articles|comments/*:后端仅这几段;SPA /post/*、/articles、/article-manage 等放行
'^/article/(articles|my-articles|comments)/': { target: BACKEND, changeOrigin: true },
// /learn/*:后端 learn.urls;SPA 无 /learn/<id> 冲突(/learn 是列表页、/learn-manage 等已迁出判断)
// 但 /learn 本体是 SPA 列表页 → 仅代理带子段的后端路径,根 /learn 放行
'^/learn/(courses|chapters|my-courses|my-progress|materials|favorites|cdn)/': { target: BACKEND, changeOrigin: true },
// /history/records/*:后端仅 records/;SPA /history 是列表页
'^/history/records/': { target: BACKEND, changeOrigin: true },
// /message/*:后端 message.urls;SPA /messages(多 s)天然不命中
'^/message/': { target: BACKEND, changeOrigin: true },
// /chat/*:后端 chat.urls;SPA /chat 本体是页面 → 仅代理后端子段
'^/chat/(friend-requests|friends|users|conversations|messages|upload|favorite-stickers)/': { target: BACKEND, changeOrigin: true },
// /tool/*:后端 tool.urls;SPA /tool-detail 放行(不以 /tool/ 开头,天然安全)
'^/tool/': { target: BACKEND, changeOrigin: true },
// /search/*:后端 search.urls(GET /search/ 本体 + suggestions/hot-keywords)
// SPA /search 本体是搜索页 → 无查询串的裸 /search 放行到前端
'^/search/': {
target: BACKEND,
changeOrigin: true,
bypass: (req: { url?: string }) => {
const raw = req.url || ''
const p = raw.split('?')[0]
if (p === '/search/' && !raw.includes('?')) return req.url
},
},
// /app/*:后端 changelog;SPA 无 /app 冲突
'^/app/': { target: BACKEND, changeOrigin: true },
// /logs/api/*:后端日志;SPA 无冲突
'^/logs/api/': { target: BACKEND, changeOrigin: true },
// /shorturl/*:后端 shorturl.urls;SPA /shorturl-detail 放行
'^/shorturl/': { target: BACKEND, changeOrigin: true },
// /s/<code>/:短链跳转(后端 ShortUrlRedirectView);SPA /settings、/shorturl-detail 必须放行
'/s': {
target: BACKEND,
changeOrigin: true,
bypass: (req: { url?: string }) => {
const p = (req.url || '').split('?')[0]
if (p === '/s' || p.startsWith('/settings') || p.startsWith('/shorturl-detail') || p.startsWith('/src') || p.includes('.')) return req.url
if (!/^\/s\/[^/]+\/?$/.test(p)) return req.url // 非短码形态一律放行
},
},
'/i18n': { target: BACKEND, changeOrigin: true, bypass: spaBypass },
}
// https://vite.dev/config/
export default defineConfig({
server: {
hmr: true,
host: '::',
allowedHosts: [
'.mymoyu.top',
'localhost',
'127.0.0.1',
'192.168.1.4'
],
proxy,
},
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})