|
|
/**
|
|
|
* 自定义 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
|
|
|
}
|
|
|
})
|
|
|
|
|
|
// 参数化路由匹配:支持 /mock-api/admin/brand/:id 格式
|
|
|
function matchRoute(pattern: string, path: string): { matched: boolean; params: Record<string, string> } {
|
|
|
// 无参数占位符时精确匹配
|
|
|
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<string, string> = {}
|
|
|
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<string, string> = {}
|
|
|
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<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, params: matchedParams }
|
|
|
responseBody = matched.response(mockReq)
|
|
|
} else {
|
|
|
responseBody = matched.response
|
|
|
}
|
|
|
|
|
|
res.setHeader('Content-Type', 'application/json')
|
|
|
res.statusCode = 200
|
|
|
res.end(JSON.stringify(responseBody))
|
|
|
})
|
|
|
},
|
|
|
}
|
|
|
}
|