/** * 自定义 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 } }) 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 // 匹配路由:URL 精确匹配 + method 匹配 const matched = routes.find(r => { if (r.url !== reqPath) return false if (!r.method) return true return r.method.toLowerCase() === reqMethod }) 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 } responseBody = matched.response(mockReq) } else { responseBody = matched.response } res.setHeader('Content-Type', 'application/json') res.statusCode = 200 res.end(JSON.stringify(responseBody)) }) }, } }