背景
- 以系列一为背景
- 当前每一个entry都有自己的代理路径,如果直接写到vite的 proxy配置里,可太难维护了,而且还容易出现冲突
实现思路
vite plugin
中有个configureServer
的钩子可以拿到我们的dev server
实例,可以通过自定义的中间件形式去实现动态设置代理。通过遍历views
, 去尝试查找entry所在的目录下是否存在代理设置文件
async function importFile(modName: string) {
const mod = await import('file://' + modName + '?t=' + Date.now())
return mod.default || mod
}
function getEntryConfig(proxyFile: string): EntryRuleConfig {
return {
file: proxyFile,
ruleMap: {}
}
}
/**
* 异步函数:更新入口代理规则映射
* 该函数用于更新特定入口点的代理规则映射,根据给定的代理文件路径导入规则,并映射到该入口点
*
* @param proxyMap 代理文件映射,是一个映射表,用于存储不同入口点的代理配置
* @param entry 入口点标识符,用于在代理映射中唯一标识一个入口点
* @param proxyFilePath 代理文件路径,指向包含代理规则配置的文件
*/
async function updateEntryProxyRuleMap(
proxyMap: ProxyFileMap,
entry: string,
proxyFilePath: string
) {
// 获取入口点的配置,如果不存在,则从代理文件路径中获取
const entryMap = proxyMap.get(entry) || getEntryConfig(proxyFilePath)
// 从代理文件路径中导入所有的规则
const rules = await importFile(proxyFilePath)
// 清空入口点配置中的原有规则映射,准备更新
entryMap.ruleMap = {}
// 遍历导入的规则,将每个规则添加到入口点的规则映射中
rules.forEach((rule: Obj) => {
// 解构规则对象,分离出上下文数组和其他规则属性
const { contexts, ...ruleObj } = rule
// 如果规则是启用状态且包含上下文数组,则遍历每个上下文,创建规则配置并添加到映射中
if (rule.enable && rule.contexts?.length) {
rule.contexts.forEach((context: string) => {
// 将规则配置与上下文一起存储在入口点的规则映射中
entryMap.ruleMap[context] = {
...ruleObj,
context
} as RuleConfig
})
}
})
// 更新代理映射,将新的入口点配置存储回去
proxyMap.set(entry, entryMap)
}
/**
* 异步函数,通过视图数组生成带有代理映射的条目
* 该函数旨在为每个视图创建一个代理文件映射,便于后续处理网络请求的代理规则
*
* @param views 视图数组,包含需要生成代理映射的文件信息
* @returns 返回一个Promise,解析为ProxyFileMap实例,映射了每个视图的代理文件路径
*/
async function getEntryWithProxyMap(views: View[]) {
// 初始化一个新的Map对象,用于存储视图路径与其对应的代理文件路径的映射
const newMap: ProxyFileMap = new Map()
// 遍历视图数组,为每个视图创建代理映射
for (let i = 0; i < views.length; i++) {
// 获取当前视图项
const item = views[i]
// 构造视图路径的键值,用于Map中映射
const key = `/${item.filename}`
// 拼接当前视图的代理文件完整路径
const proxyFile = path.join(process.cwd(), item.entryDir, 'proxyRules.js')
// 检查代理文件是否存在,如果存在则更新到Map中
if (fs.existsSync(proxyFile)) {
// 调用异步函数updateEntryProxyRuleMap,更新Map中的代理规则
await updateEntryProxyRuleMap(newMap, key, proxyFile)
}
}
// 返回构造完成的代理文件映射Map
return newMap
}
如果存在,这读取该文件内容,维护到proxyMap
中,方便后续读取规则,避免反复读取文件。
然后我们插件的入口可以这么引入中间件
function devServerPlugin(pages: View[]): Plugin {
return {
name: 'dev-server',
apply: 'serve',
enforce: 'post',
async configureServer(server: ViteDevServer) {
server.middlewares.use(await serviceProxyMiddleware({ pages, server }))
}
}
}
中间件
async function serviceProxyMiddleware(options: ProxyOptions): Promise<Connect.NextHandleFunction> {
proxyFnMap.clear()
// 获取entry与代理配置文件路径的映射
const proxyFileMap: ProxyFileMap = await getEntryWithProxyMap(options.pages)
watchProxyRuleFile(options, proxyFileMap)
return function proxy(req, res, next) {
if (
!req.headers.referer ||
/@vite|@react|node_modules|src|@|css|js|ts|vue|\.html/g.test(req.url || '')
) {
return next()
}
// 请求来源页面
const refererUrl = new URL(req.headers.referer)
if (!proxyFileMap.has(refererUrl.pathname)) {
return next()
}
const proxyRuleConfig = proxyFileMap.get(refererUrl.pathname) as EntryRuleConfig
for (const context in proxyRuleConfig.ruleMap) {
if (!contextMatch(context, req.originalUrl || req.url || '')) {
continue
}
// 运行跨域代理
return getProxyFn(proxyRuleConfig.ruleMap[context], proxyRuleConfig.file)(req, res, next)
}
next()
}
}
通过拿到req.url命中proxyFileMap中配置后,核心方法getProxyFn
,使用http-proxy-middleware
这个包实现
import { createProxyMiddleware } from 'http-proxy-middleware'
/**
* 运行proxy配置规则
* @param proxyRule 配置规则
* @param filename 配置文件
* @param opts 更多配置
*/
function getProxyFn(
ruleConfig: RuleConfig,
filename: string
): RequestHandler<
http.IncomingMessage,
http.ServerResponse<http.IncomingMessage>,
(err?: any) => void
> {
const { context, target, ...opts } = ruleConfig
const cacheKey = `${context}||${target}`
const cacheFn = proxyFnMap.get(cacheKey)
if (cacheFn) {
return cacheFn
}
const proxyOptions: ProxyOptionsOptions = {
pathFilter: context,
target,
changeOrigin: true,
ws: true,
on: {
proxyRes(proxyRes, req) {
const reqURl = req.url || ''
const url = new URL(target + reqURl)
proxyRes.headers['service-proxy-middleware-filename'] = filename
proxyRes.headers['service-proxy-middleware-context'] = context
proxyRes.headers['service-proxy-middleware-pathname'] = url.pathname
proxyRes.headers['service-proxy-middleware-target'] = target
proxyRes.headers['service-proxy-middleware-match'] = target + reqURl
}
},
...opts
}
const newProxyFn = createProxyMiddleware<http.IncomingMessage, http.ServerResponse>(proxyOptions)
proxyFnMap.set(cacheKey, newProxyFn)
return newProxyFn
}
这样我们就完成了接口代理,但是代理文件内容变更时候怎么自动重启,watchProxyRuleFile
/**
* 监听代理配置文件
* 该函数使用chokidar库监视代理规则文件的更改,以便在文件变化时动态更新代理配置
*
* @param options - 包含代理选项的对象,包括是否实时日志记录的配置
* @param proxyFileMap - 一个映射,将代理规则与其对应的文件路径关联起来
*/
function watchProxyRuleFile(options: ProxyOptions, proxyFileMap: ProxyFileMap) {
// 创建一个唯一的代理规则文件路径数组
const arrProxyRules = [
...new Set(
[...proxyFileMap].reduce((prev, curr) => {
return prev.concat(curr[1].file)
}, [] as string[])
)
] as string[]
// 初始化文件监视器
const watcher = chokidar.watch(arrProxyRules, {
persistent: true
})
// 监听文件变化事件
watcher.on('all', async (event, path) => {
// 忽略某些事件类型
if (['unlinkDir', 'addDir', 'ready', 'error', 'raw', 'add'].includes(event)) {
return
}
// 遍历代理文件映射,查找并更新受影响的代理规则
for (const proxyItem of proxyFileMap) {
if (proxyItem[1].file === path) {
await updateEntryProxyRuleMap(proxyFileMap, proxyItem[0], path)
break
}
}
})
}
使用chokidar轻松实现文件监听
结语
这样我们还可以使用cli-table3
终端内表格打印启动的entry下的代理信息。最后核心就是在pugin中通过configureServer钩子拿到server实例,自定义中间件拿到req后,res.send随你怎么操作