/** * 自定义 Vite Mock 插件 * 使用 configureServer 中间件拦截请求,不依赖任何第三方库 * 与所有 Vite 版本兼容 */ import type { Plugin, ViteDevServer } from 'vite' import type { MockMethod, MockRequest } from './types' import fs from 'fs' import path from 'path' interface MockPluginOptions { mockPath: string enable?: boolean } export function viteMockPlugin(options: MockPluginOptions): Plugin { const { mockPath, enable = true } = options return { name: 'vite-mock-plugin', configureServer(server: ViteDevServer) { if (!enable) return // 缓存已加载的 mock 路由 let mockRoutes: MockMethod[] = [] let loaded = false async function loadMockRoutes(): Promise { if (loaded) return mockRoutes const mockDir = path.resolve(process.cwd(), mockPath) if (!fs.existsSync(mockDir)) { console.warn(`[mock] 目录不存在: ${mockDir}`) return [] } const files = fs.readdirSync(mockDir).filter( f => (f.endsWith('.ts') || f.endsWith('.js')) && !f.startsWith('_') ) const routes: MockMethod[] = [] for (const file of files) { try { const filePath = path.join(mockDir, file) // 使用 Vite 的 ssrLoadModule 支持 TypeScript 热加载 const mod = await server.ssrLoadModule(filePath) if (mod.default && Array.isArray(mod.default)) { routes.push(...mod.default) } } catch (e) { console.error(`[mock] 加载失败: ${file}`, e) } } console.log(`[mock] 已加载 ${routes.length} 条 mock 路由`) mockRoutes = routes loaded = true return routes } // 监听 mock 文件变更,清除缓存 server.watcher.add(path.resolve(process.cwd(), mockPath)) server.watcher.on('change', (file) => { if (file.startsWith(path.resolve(process.cwd(), mockPath))) { loaded = false console.log(`[mock] 文件变更,重新加载: ${path.basename(file)}`) } }) server.watcher.on('add', (file) => { if (file.startsWith(path.resolve(process.cwd(), mockPath))) { loaded = false } }) // 参数化路由匹配:支持 /mock-api/admin/brand/:id 格式 function matchRoute(pattern: string, path: string): { matched: boolean; params: Record } { // 无参数占位符时精确匹配 if (!pattern.includes(':')) { return { matched: pattern === path, params: {} } } // 将 :param 转为正则捕获组 const paramNames: string[] = [] const regexStr = pattern.replace(/:([^/]+)/g, (_, name) => { paramNames.push(name) return '([^/]+)' }) const regex = new RegExp('^' + regexStr + '$') const match = path.match(regex) if (!match) return { matched: false, params: {} } const params: Record = {} paramNames.forEach((name, i) => { params[name] = match[i + 1] }) return { matched: true, params } } server.middlewares.use(async (req, res, next) => { const routes = await loadMockRoutes() // 解析请求信息 const reqMethod = req.method?.toLowerCase() || 'get' const parsedUrl = new URL(req.url || '/', 'http://localhost') const reqPath = parsedUrl.pathname // 匹配路由:支持精确匹配 + 参数化路由 + method 匹配 // 参数化路由优先级低于精确匹配,遍历时先匹配到的优先 let matched: MockMethod | null = null let matchedParams: Record = {} for (const r of routes) { const { matched: isMatched, params } = matchRoute(r.url, reqPath) if (!isMatched) continue if (r.method && r.method.toLowerCase() !== reqMethod) continue matched = r matchedParams = params break } if (!matched) return next() // 构建 MockRequest const query: Record = {} parsedUrl.searchParams.forEach((v, k) => { query[k] = v }) // 读取请求体(POST/PUT/PATCH) const body = await new Promise((resolve) => { if (['post', 'put', 'patch'].includes(reqMethod)) { const chunks: Buffer[] = [] req.on('data', (chunk: Buffer) => chunks.push(chunk)) req.on('end', () => { const raw = Buffer.concat(chunks).toString('utf-8') try { resolve(JSON.parse(raw)) } catch { resolve(raw) } }) } else { resolve(undefined) } }) // 执行 response let responseBody: any if (typeof matched.response === 'function') { const mockReq: MockRequest = { query, body, url: req.url || '', method: reqMethod, params: matchedParams } responseBody = matched.response(mockReq) } else { responseBody = matched.response } res.setHeader('Content-Type', 'application/json') res.statusCode = 200 res.end(JSON.stringify(responseBody)) }) }, } }