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

129 lines
4.0 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
}
})
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))
})
},
}
}