import { resolve } from 'path'; import type { Plugin, UserConfig } from 'vite'; import { defineConfig } from 'vite'; import visualizer from 'rollup-plugin-visualizer'; 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, }, // 添加中间件,在代理之前拦截前端路由 middlewareMode: false, proxy: { // 上传静态资源:/api/upload/xxx -> http://api.nanafa.com/upload/xxx // 必须放在通用 /api 规则前,避免被转发到 /itfx/upload 导致 404 '/api/upload': { target: 'http://api.nanafa.com', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ''), timeout: 30000, proxyTimeout: 30000, }, // 权限(admin)43.139.210.35:5281 -> http://api.nanafa.com/admin(路径保持 /rbac/xxx -> .../admin/rbac/xxx) '/rbac': { target: 'http://api.nanafa.com/admin', changeOrigin: true, secure: false, }, // OMS 订单等 admin 接口:/order/list、/order/detail(与 Swagger OMS-Order 一致) '/order': { target: 'http://api.nanafa.com/admin', changeOrigin: true, secure: false, }, '/customer': { target: 'http://api.nanafa.com/admin', changeOrigin: true, secure: false, }, '/recharge': { target: 'http://api.nanafa.com/admin', changeOrigin: true, secure: false, }, '/channel': { target: 'http://api.nanafa.com/admin', changeOrigin: true, secure: false, }, '/operation': { target: 'http://api.nanafa.com/admin', changeOrigin: true, secure: false, }, '/system': { target: 'http://api.nanafa.com/admin', changeOrigin: true, secure: false, }, // 轨迹(track)43.139.210.35:8889 -> http://api.nanafa.com/track,路径与 rbac、api 一致:/Track/xxx '/Track': { target: 'http://api.nanafa.com', changeOrigin: true, rewrite: (path) => '/track' + path, configure: (proxy) => { proxy.on('error', (err, req, res) => { console.error('proxy error', err); }); }, }, // API(itfx)43.139.210.35:8888 -> http://api.nanafa.com/itfx // 使用正则表达式匹配,排除前端路由路径 '^/api/(?!upstreamInterface|upstreamInterfaceConfig|upstreamInterfaceConfigEdit|downstreamInterface|downstreamInterface/newPort|appList|appList/createApplication|requestLog|callStatistics|apiDebugger|trackApi)': { target: 'http://api.nanafa.com/itfx', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ''), timeout: 30000, proxyTimeout: 30000, }, // 外部域名(用于 ApiTest 调试,避免浏览器 CORS) // 示例:把 https://lsp.globex.cn/rs/xxx 代理到 /external/rs/xxx '/external': { target: 'https://lsp.globex.cn', changeOrigin: true, secure: false, rewrite: (path) => path.replace(/^\/external/, ''), timeout: 30000, proxyTimeout: 30000, }, }, }, plugins: [ { name: 'filter-known-build-warnings', configResolved(config) { const rawWarn = config.logger.warn; config.logger.warn = (msg, options) => { if ( msg.includes('[baseline-browser-mapping]') || msg.includes('~@/assets/logo.png referenced in ~@/assets/logo.png') || msg.includes('new URL("../assets/hi.png", import.meta.url) doesn\'t exist at build time') ) { return; } rawWarn(msg, options); }; }, }, // 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/priceArea', '/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) { // 标记请求,让代理中间件知道不要代理 (req as any).__skipProxy = true; (req as any).__isFrontendRoute = true; // 关键:将 URL 重写为 /index.html,否则以 /api 开头的路径在 Vite 中不会走 SPA 回退,会直接 404 // /track/* 等非 /api 路径会正常走 SPA 回退,所以只有 /api/* 前端路由需要显式重写 req.url = '/index.html'; next(); return; } // 对于 /api/ 路径,检查是否是驼峰命名(前端路由的特征) if (url.startsWith('/api/')) { const pathAfterApi = url.substring(5); // 去掉 '/api/' // 如果路径包含大写字母(驼峰命名),很可能是前端路由 if (/[A-Z]/.test(pathAfterApi)) { (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' ? ((): Plugin[] => { const p = configCompressPlugin('brotli'); return Array.isArray(p) ? p : [p]; })() : []), ...(process.env.REPORT === 'true' ? [ visualizer({ filename: './node_modules/.cache/visualizer/stats.html', open: true, gzipSize: true, brotliSize: true, }), ] : []), ], resolve: { alias: [ // 统一走 lodash-es,避免与 lodash CJS 整包重复打入 { find: 'lodash', replacement: 'lodash-es' }, { 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', ], // 确保 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, // 构建输出配置:使用 Vite 默认的代码分割策略,避免过度自定义导致库初始化顺序问题 rollupOptions: { onwarn(warning, warn) { const message = warning.message || ''; if ( message.includes('~@/assets/logo.png referenced in ~@/assets/logo.png') || message.includes('new URL("../assets/hi.png", import.meta.url) doesn\'t exist at build time') || message.includes('Use of eval in "node_modules/vg-print/dist/vg-print.es.js"') || message.includes('dynamically imported by') && message.includes('but also statically imported by') ) { return; } warn(warning); }, output: { manualChunks(id) { if (!id.includes('node_modules')) return; if (id.includes('handsontable') || id.includes('@handsontable')) { return 'vendor-handsontable'; } if (id.includes('codemirror') || id.includes('@codemirror')) { return 'vendor-codemirror'; } if (id.includes('element-plus')) { return 'vendor-element-plus'; } if (id.includes('@arco-design')) { return 'vendor-arco'; } if (id.includes('vxe-table') || id.includes('xe-utils')) { return 'vendor-vxe'; } if (id.includes('echarts')) { return 'vendor-echarts'; } if (id.includes('lodash')) { return 'vendor-lodash'; } }, // 确保文件名包含 hash,便于浏览器缓存 chunkFileNames: 'assets/js/[name]-[hash].js', entryFileNames: 'assets/js/[name]-[hash].js', assetFileNames: (assetInfo) => { const name = assetInfo.name ?? 'asset'; const info = name.split('.'); const ext = info[info.length - 1] ?? 'bin'; if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(name)) { return `assets/media/[name]-[hash].${ext}`; } if (/\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/.test(name)) { return `assets/img/[name]-[hash].${ext}`; } if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(name)) { return `assets/fonts/[name]-[hash].${ext}`; } return `assets/[name]-[hash].${ext}`; }, }, }, // 设置chunk大小警告限制(提高警告阈值,因为我们已经做了代码分割) chunkSizeWarningLimit: 2000, // 启用 terser 压缩以获得更好的压缩率(可选,esbuild更快但压缩率稍低) // minify: 'terser', // 如果需要更好的压缩率,可以启用,但构建会慢一些 }, css: { preprocessorOptions: { less: { modifyVars: { hack: `true; @import (reference) "${resolve( './src/assets/style/breakpoint.less' )}";`, }, javascriptEnabled: true, }, }, }, } as UserConfig);