|
|
/**
|
|
|
* 自定义 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<MockMethod[]> {
|
|
|
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<string, string> = {}
|
|
|
parsedUrl.searchParams.forEach((v, k) => { query[k] = v })
|
|
|
|
|
|
// 读取请求体(POST/PUT/PATCH)
|
|
|
const body = await new Promise<any>((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))
|
|
|
})
|
|
|
},
|
|
|
}
|
|
|
}
|