import { resolve } from 'path'; import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import vueJsx from '@vitejs/plugin-vue-jsx'; import svgLoader from 'vite-svg-loader'; import AutoImport from 'unplugin-auto-import/vite'; import Components from 'unplugin-vue-components/vite'; import UnoCSS from 'unocss/vite'; import configArcoStyleImportPlugin from './config/plugin/arcoStyleImport'; import configCompressPlugin from './config/plugin/compress'; export default defineConfig({ server: { open: true, fs: { strict: true, }, historyApiFallback: true, // 添加中间件,在代理之前拦截前端路由 middlewareMode: false, proxy: { // RBAC 权限管理服务代理 - http://43.139.210.35:5281 // 注意:后端 API 路径包含 /rbac 前缀,所以不需要 rewrite '/rbac': { target: 'http://43.139.210.35:5281', changeOrigin: true, secure: false, configure: (proxy, options) => { proxy.on('proxyReq', (proxyReq, req, res) => { console.log('[RBAC-5281] Sending Request:', req.method, req.url); console.log('[RBAC-5281] Target Path:', proxyReq.path); }); proxy.on('proxyRes', (proxyRes, req, res) => { console.log( '[RBAC-5281] Received Response:', proxyRes.statusCode, req.url ); }); }, }, // 对应 nginx: location /api/trackApi -> http://43.139.210.35:8889 '/api/trackApi': { target: 'http://43.139.210.35:8889', changeOrigin: true, rewrite: (path) => path.replace(/^\/api\/trackApi/, ''), configure: (proxy, options) => { proxy.on('error', (err, req, res) => { console.log('proxy error', err); }); proxy.on('proxyReq', (proxyReq, req, res) => { console.log('[trackApi-8889] Sending Request to the Target:', req.method, req.url); }); proxy.on('proxyRes', (proxyRes, req, res) => { console.log( '[trackApi-8889] Received Response from the Target:', proxyRes.statusCode, req.url ); }); }, }, // 对应 nginx: location /api -> http://43.139.210.35:8888 // 使用正则表达式匹配,排除前端路由路径 // 只代理真正的 API 请求,不代理前端路由 // 排除所有驼峰命名的前端路由(包含大写字母的路径) '^/api/(?!upstreamInterface|upstreamInterfaceConfig|upstreamInterfaceConfigEdit|downstreamInterface|downstreamInterface/newPort|appList|appList/createApplication|requestLog|callStatistics|apiDebugger|trackApi)': { target: 'http://43.139.210.35:8888', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ''), timeout: 30000, proxyTimeout: 30000, configure: (proxy, options) => { proxy.on('proxyReq', (proxyReq, req, res) => { console.log('[8888] Sending Request:', req.method, req.url); console.log('[8888] Proxy Target:', proxyReq.path); }); proxy.on('proxyRes', (proxyRes, req, res) => { console.log( '[8888] Received Response:', proxyRes.statusCode, req.url ); }); }, }, }, }, plugins: [ // eslint({ // cache: false, // include: ['src/**/*.ts', 'src/**/*.tsx', 'src/**/*.vue'], // exclude: ['node_modules'], // }), vue(), vueJsx(), svgLoader({ svgoConfig: {} }), configArcoStyleImportPlugin(), UnoCSS(), AutoImport({ imports: ['vue', 'vue-router', 'vue-i18n', 'pinia', '@vueuse/core'], dts: 'src/auto-imports.d.ts', dirs: ['src/composables', 'src/store'], vueTemplate: true, eslintrc: { enabled: true, }, }), Components({ extensions: ['vue'], include: [/\.vue$/, /\.vue\?vue/], dts: 'src/components.d.ts', dirs: ['src/components'], resolvers: [], // directoryAsNamespace: true, }), // 自定义插件:在代理之前拦截前端路由 { name: 'exclude-frontend-routes-from-proxy', configureServer(server) { // 前端路由路径列表(这些路径不应该被代理,应该走前端路由) // 所有前端路由都使用驼峰命名,以这些前缀开头的路径都是前端路由 const frontendRoutes = [ // API 相关前端路由 '/api/upstreamInterface', '/api/upstreamInterfaceConfig', '/api/upstreamInterfaceConfigEdit', '/api/downstreamInterface', '/api/downstreamInterface/newPort', '/api/appList', '/api/appList/createApplication', '/api/requestLog', '/api/callStatistics', '/api/apiDebugger', // Channel 相关前端路由 '/channel/salesProducts', '/channel/serviceChannels', '/channel/blacklist', '/channel/remoteZones', '/channel/weightRules', '/channel/weightRounding', '/channel/autoTrigger', // Order 相关前端路由 '/order/priceCheck', '/order/placeOrder', '/order/myOrders', // Track 相关前端路由 '/track/order', '/track/warningOrder', '/track/state', '/track/flag', '/track/content', '/track/pull', '/track/push', '/track/kpiRegion', '/track/kpiEarlyWarningConfig', // Operation 相关前端路由 '/operation/inboundScan', '/operation/outboundScan', // System 相关前端路由 '/system/users', '/system/roles', '/system/menus', '/system/organizations', // User 相关前端路由 '/user/recharge', '/user/rechargeRecord', ]; // 直接在 configureServer 中设置中间件,确保在代理之前执行 const middleware = (req: any, res: any, next: any) => { const url = req.url?.split('?')[0] || ''; // 检查是否是前端路由 const isFrontendRoute = frontendRoutes.some(route => { // 精确匹配或路径以路由开头 return url === route || url.startsWith(route + '/'); }); if (isFrontendRoute) { console.log('[Vite Middleware] Intercepting frontend route:', url); // 标记请求,让代理中间件知道不要代理 (req as any).__skipProxy = true; // 标记为前端路由 (req as any).__isFrontendRoute = true; // 继续处理,让 Vite 的 SPA 路由处理(通过 historyApiFallback) next(); return; } // 对于 /api/ 路径,检查是否是驼峰命名(前端路由的特征) if (url.startsWith('/api/')) { const pathAfterApi = url.substring(5); // 去掉 '/api/' // 如果路径包含大写字母(驼峰命名),很可能是前端路由 if (/[A-Z]/.test(pathAfterApi)) { console.log('[Vite Middleware] Detected camelCase route (likely frontend):', url); (req as any).__skipProxy = true; (req as any).__isFrontendRoute = true; next(); return; } } next(); }; // 使用 unshift 将中间件插入到最前面,确保优先执行 const stack = (server.middlewares as any).stack; if (Array.isArray(stack)) { stack.unshift({ route: '', handle: middleware }); } else { server.middlewares.use(middleware); } }, }, // 生产环境启用压缩 // 注意:如果服务器不支持压缩,可以禁用此插件 // 只保留 brotli 压缩(压缩率更高,文件数量减少一半) // 如果需要同时支持 gzip 和 brotli,取消下面的注释 ...(process.env.NODE_ENV === 'production' ? [ // 只启用 brotli 压缩(减少文件数量) ...configCompressPlugin('brotli'), // 如果需要同时支持 gzip,取消下面的注释 // ...configCompressPlugin('gzip'), ] : []), ], resolve: { alias: [ { find: '@', replacement: resolve(__dirname, './src'), }, { find: 'assets', replacement: resolve(__dirname, './src/assets'), }, { find: 'vue-i18n', replacement: 'vue-i18n/dist/vue-i18n.cjs.js', // Resolve the i18n warning issue }, { find: 'vue', replacement: 'vue/dist/vue.esm-bundler.js', // compile template }, { find: 'xe-utils', replacement: resolve( __dirname, './node_modules/.pnpm/xe-utils@3.7.5/node_modules/xe-utils' ), // Add xe-utils alias }, ], extensions: ['.ts', '.js'], }, define: { 'process.env': {}, __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false', __VUE_OPTIONS_API__: 'true', __VUE_PROD_DEVTOOLS__: 'false', }, optimizeDeps: { // 预构建常用依赖,加速开发体验 include: [ 'vue', 'vue-router', 'pinia', '@arco-design/web-vue', 'axios', 'dayjs', 'lodash-es', ], // 排除 monaco-editor,因为它很大且只在特定页面使用,应该按需加载 exclude: ['monaco-editor'], // 确保 Vue 相关依赖的预构建顺序 esbuildOptions: { target: 'es2017', // 启用 tree-shaking treeShaking: true, }, }, build: { // 启用压缩(使用esbuild,Vite默认,性能更好) minify: 'esbuild', // 生产环境移除console和debugger esbuild: { drop: process.env.NODE_ENV === 'production' ? ['console', 'debugger'] : [], }, // 启用CSS代码分割 cssCodeSplit: true, // 生成source map(可选,生产环境可以关闭以减小体积) sourcemap: false, // 构建输出配置 rollupOptions: { output: { // 手动代码分割,优化缓存 manualChunks: (id) => { // node_modules中的包单独打包 if (id.includes('node_modules')) { // vue核心库和vue相关库打包在一起,确保加载顺序 if (id.includes('vue') || id.includes('vue-router') || id.includes('pinia') || id.includes('@vue')) { return 'vue-vendor'; } // monaco-editor单独打包(按需加载,不阻塞初始加载) if (id.includes('monaco-editor')) { return 'monaco-editor'; } // echarts 相关库单独打包 if (id.includes('echarts') || id.includes('zrender')) { return 'echarts-vendor'; } // arco-design相关 if (id.includes('@arco-design')) { return 'arco-vendor'; } // vxe-table相关 if (id.includes('vxe-table') || id.includes('xe-utils')) { return 'vxe-vendor'; } // axios 和请求相关库 if (id.includes('axios') || id.includes('query-string')) { return 'http-vendor'; } // lodash 单独打包(如果使用较多) if (id.includes('lodash')) { return 'lodash-vendor'; } // dayjs 单独打包 if (id.includes('dayjs')) { return 'dayjs-vendor'; } // handsontable 单独打包(如果使用) if (id.includes('handsontable')) { return 'handsontable-vendor'; } // 其他第三方库进一步拆分 // 按大小拆分,避免单个vendor包过大 return 'vendor'; } }, // 确保文件名包含hash,便于浏览器缓存 chunkFileNames: 'assets/js/[name]-[hash].js', entryFileNames: 'assets/js/[name]-[hash].js', assetFileNames: (assetInfo) => { const info = assetInfo.name.split('.'); const ext = info[info.length - 1]; if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(assetInfo.name)) { return `assets/media/[name]-[hash].${ext}`; } if (/\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/.test(assetInfo.name)) { return `assets/img/[name]-[hash].${ext}`; } if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(assetInfo.name)) { return `assets/fonts/[name]-[hash].${ext}`; } return `assets/[name]-[hash].${ext}`; }, }, }, // 设置chunk大小警告限制(提高警告阈值,因为我们已经做了代码分割) chunkSizeWarningLimit: 2000, // 启用 terser 压缩以获得更好的压缩率(可选,esbuild更快但压缩率稍低) // minify: 'terser', // 如果需要更好的压缩率,可以启用,但构建会慢一些 // 启用 rollup 的 tree-shaking treeshake: { moduleSideEffects: 'no-external', }, }, css: { preprocessorOptions: { less: { modifyVars: { hack: `true; @import (reference) "${resolve( './src/assets/style/breakpoint.less' )}";`, }, javascriptEnabled: true, }, }, }, });