You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
haoliang-net/frontend/mock/plugin.ts

155 lines
5.2 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* 自定义 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))
})
},
}
}