本文档深入介绍 Dev Server Proxy 的技术实现、架构设计和设计模式。
Dev Server Proxy 采用中间件模式设计,通过文件监听实现热更新。核心是一个 Express/Connect 风格的中间件,拦截请求并将其路由到适当的处理器。
┌─────────────────────────────────────────────────────────┐
│ webpack-dev-server │
└───────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 代理中间件 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 请求路由器 (server.ts) │ │
│ │ ├─ 日志请求 → log 中间件 │ │
│ │ ├─ 路径提取 → CUSTOM_API / AJAX_API │ │
│ │ ├─ 代理目标 → proxy 中间件 │ │
│ │ └─ Mock → mock 中间件 │ │
│ └──────────────────────────────────────────────────┘ │
└───────────────────────┬─────────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 日志处理器 │ │ 代理处理器 │ │ Mock 处理器 │
└──────────────┘ └──────────────┘ └──────────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ 远程服务器 │ │ 本地文件 │
└──────────────┘ └──────────────┘
┌─────────────────────────────────┐
│ 监听系统 │
│ ┌────────────┐ ┌─────────────┐│
│ │配置监听 │ │Mock 文件 ││
│ │(.devserverrc)│ │监听 ││
│ └────────────┘ └─────────────┘│
└─────────────────────────────────┘
职责:
- 解析启动参数,确定代理类型(mock/test/staging/production)
- 启动配置和 mock 文件监听
- 生成 webpack devServer 配置对象
- 处理 webpack 4 和 webpack 5 之间的兼容性
关键逻辑:
// 1. 从 CLI 参数或环境变量获取代理类型
const type = getServerType(config);
// 2. 启动文件监听
startMockWatch(config, type);
// 3. 根据 webpack 版本返回不同配置
if (webpackVersion < 5) {
return {
before(app) {
app.use(server(config));
}
};
} else {
return {
setupMiddlewares: (middlewares) => {
middlewares.unshift({
name: 'proxy mock',
middleware: server(config)
});
return middlewares;
}
};
}职责: 核心路由逻辑,决定请求的处理方式
处理流程:
请求到达
│
├─ 1. 是否为日志请求?(LOG_API)
│ └─ 是 → log 中间件 → 返回
│
├─ 2. 提取 API 路径
│ ├─ 优先尝试 CUSTOM_API 规则
│ └─ 然后尝试 AJAX_API + query.path
│
├─ 3. 未提取到路径?
│ └─ 是 → next() → 传递给下一个中间件
│
├─ 4. 确定代理目标
│ ├─ 优先级 1: PATH_RULES[path].target
│ ├─ 优先级 2: PATH_RULES[path].proxy
│ └─ 优先级 3: 全局 PROXY 配置
│
├─ 5. 有代理目标?
│ └─ 是 → proxy 中间件 → 返回
│
└─ 6. 默认 → mock 中间件 → 返回
关键代码:
function getPath(config, parsedUrl) {
// 1. 尝试自定义规则
if (CUSTOM_API) {
for (let api of CUSTOM_API) {
const customPath = api.rule(parsedUrl);
if (customPath) return customPath;
}
}
// 2. 尝试标准 AJAX_API
if (AJAX_API.test(parsedUrl.pathname)) {
return parsedUrl.query.path;
}
return '';
}职责: 监听 .devserverrc 文件变化,实现配置热更新
实现原理:
// 1. 初始化时写入配置文件
writeServerConfig(configFile, innerConfig);
// 2. 使用 fs.watch 监听文件变化
fs.watch(config.WORK_PATH, (event, filename) => {
if (filename === '.devserverrc') {
// 3. 重新读取并更新内存中的配置
innerConfig = JSON.parse(fs.readFileSync(configFile));
}
});
// 4. 提供 get() 方法供其他模块获取最新配置
export const get = () => innerConfig;设计亮点:
- 单例模式确保全局只有一个监听实例
- 配置存储在内存中,访问速度快
- 支持特定字段的动态修改(PROXY、PATH_RULES 等)
职责: 监听 mock 目录变化,实现 mock 文件热更新
实现原理:
// 1. 初始化时扫描目录,建立路径映射
pathMap = getPathMap(mockPath);
// 示例: { "api/GET/user/info": "/path/to/api_GET_user_info.js" }
// 2. 监听目录变化(递归监听)
fs.watch(mockPath, {recursive: true}, (evt, file) => {
// 3. 使用防抖机制,避免频繁更新
clearTimeout(updateTimer);
updateTimer = setTimeout(() => {
// 4. 重新扫描目录
update(getPathMap(mockPath));
// 5. 清除 require 缓存,强制重新加载
delete require.cache[require.resolve(newFile)];
}, 100);
});设计亮点:
- 递归监听整个目录树
- 防抖处理,避免频繁触发
- 清除 Node.js require 缓存,实现真正的热更新
- 检测路径冲突,当多个文件映射到同一路径时发出警告
职责: 加载并执行本地 mock 文件,返回模拟数据
处理流程:
// 1. 从 watchMockFiles 获取路径映射
const {pathMap, pathErrors} = watchMockFiles.get();
// 2. 查找对应的 mock 文件
const file = pathMap[path];
// 3. 动态加载并执行
const func = require(file);
const {status, response} = func(params);
// 4. 返回结果
res.status(status);
res.send(JSON.stringify(response));特性:
- 支持函数式 mock,可根据请求参数动态生成数据
- 自动解析请求参数(支持 querystring 和 JSON 格式)
- 错误处理:文件不存在、非函数、执行异常等
职责: 将请求转发到远程服务器
实现细节:
// 1. 构造目标 URL
const url = `${target}${req.url}`;
// 2. 处理请求头(关键:修改 origin、referer、host)
const headers = {
...req.headers,
'origin': target,
'referer': target,
'host': target.replace(/https?:\/\//, '')
};
// 3. 使用 request 库转发请求
request({
url,
headers,
method: 'POST',
body: req.body
}, (error, response, body) => {
res.status(200);
res.send(body);
});设计考虑:
- 修改请求头以绕过跨域限制
- 保留原始请求体
- 统一使用 POST 方法(适配特定后端接口规范)
职责: 处理前端日志上报请求
特殊处理:
- 支持 lz-string 压缩格式的日志数据
- 自动解压并格式化输出
- 不转发到远程服务器,直接在本地处理
整个项目基于 Express/Connect 中间件模式:
(request, response, next) => {
if (shouldHandle) {
// 处理并返回
} else {
next(); // 传递给下一个中间件
}
}配置监听和文件监听都使用单例模式:
let started = false;
export const start = () => {
if (started) return;
started = true;
// 初始化逻辑
};根据不同条件选择不同的处理策略:
- 日志请求 → log 中间件
- 有代理目标 → proxy 中间件
- 默认 → mock 中间件
使用 fs.watch 监听文件变化,实现配置和 mock 文件的热更新。
┌─────────────┐
│ 用户请求 │
└──────┬──────┘
│
▼
┌─────────────────────────────────────┐
│ server.ts (主路由) │
│ 1. 解析 URL │
│ 2. 读取最新配置 │
└──────┬──────────────────────────────┘
│
├─ 匹配 LOG_API?
│ └─ 是 → log.ts → 返回
│
├─ 提取路径
│ ├─ CUSTOM_API.rule(url)
│ └─ AJAX_API + query.path
│
├─ 无路径?
│ └─ 是 → next() → 其他中间件
│
├─ 确定目标
│ ├─ PATH_RULES[path].target
│ ├─ PATH_RULES[path].proxy → config[proxy].target
│ └─ config[PROXY].target
│
├─ 有目标?
│ └─ 是 → proxy.ts → 远程服务器 → 返回
│
└─ 默认 → mock.ts
│
├─ watchMockFiles.get()
├─ 查找 pathMap[path]
├─ require(file)
├─ func(params)
└─ 返回 mock 数据
// 监听目录
fs.watch(path, {recursive: true}, callback);
// 清除 require 缓存实现热更新
delete require.cache[require.resolve(file)];同时支持 webpack 4 和 webpack 5:
// webpack 4
return {
before(app) {
app.use(middleware);
}
};
// webpack 5
return {
setupMiddlewares: (middlewares) => {
middlewares.unshift({
name: 'proxy mock',
middleware: middleware
});
return middlewares;
}
};灵活的路径提取,支持多种模式:
- 自定义 API 规则(最高优先级)
- 标准 AJAX_API 模式
- 回退到下一个中间件
清晰的配置优先级系统:
- 路径特定目标(PATH_RULES[path].target)
- 路径特定代理引用(PATH_RULES[path].proxy)
- 全局代理配置(PROXY)
这种分层方法在保持简单性的同时提供了最大的灵活性。