api-proxy-mock/index.api.ts

325 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as http from 'http';
import * as https from 'https';
import * as fs from 'fs';
import * as path from 'path';
// 配置文件路径
const CONFIG_FILE = path.join(__dirname, 'config.json');
// 定义类型
interface RouteConfig {
[route: string]: string;
}
interface AppConfig {
/** 为 false 时所有请求走代理,不命中 mock 路由;缺省为 true */
mockEnabled?: boolean;
cacheConfig: boolean;
reloadOnChange: boolean;
defaultContentType: string;
proxyPort: number;
targetHost: string;
}
interface ConfigFile {
routes: RouteConfig;
config: AppConfig;
}
// 存储当前的路由配置
let MOCK_ROUTES: RouteConfig = {};
let CONFIG: AppConfig = {
cacheConfig: true,
reloadOnChange: true,
defaultContentType: "application/json",
proxyPort: 9443,
targetHost: "devrmtapp.resmart.cn"
};
// 过滤掉以 # 开头的路由(视为注释,不参与 mock
function filterActiveRoutes(routes: RouteConfig): RouteConfig {
const filtered: RouteConfig = {};
for (const [route, filePath] of Object.entries(routes)) {
if (route.startsWith('#')) continue;
filtered[route] = filePath;
}
return filtered;
}
// 加载配置文件
function loadConfig() {
try {
if (!fs.existsSync(CONFIG_FILE)) {
console.warn(`[CONFIG] 配置文件不存在: ${CONFIG_FILE}`);
console.warn(`[CONFIG] 正在创建默认配置文件...`);
// 创建默认配置
const defaultConfig: ConfigFile = {
routes: {
"/api2/test": "mock/test.txt"
},
config: {
cacheConfig: true,
reloadOnChange: true,
defaultContentType: "application/json",
proxyPort: 9443,
targetHost: "devrmtapp.resmart.cn"
}
};
// 确保mock目录存在
const mockDir = path.join(__dirname, 'mock');
if (!fs.existsSync(mockDir)) {
fs.mkdirSync(mockDir, { recursive: true });
}
// 保存配置文件
fs.writeFileSync(CONFIG_FILE, JSON.stringify(defaultConfig, null, 2), 'utf-8');
console.log(`[CONFIG] 已创建默认配置文件: ${CONFIG_FILE}`);
// 加载配置(# 开头的路由视为注释,不参与 mock
const configData: ConfigFile = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
MOCK_ROUTES = filterActiveRoutes(configData.routes || {});
CONFIG = configData.config || CONFIG;
} else {
const configData: ConfigFile = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
MOCK_ROUTES = filterActiveRoutes(configData.routes || {});
CONFIG = configData.config || CONFIG;
console.log(`[CONFIG] 配置文件已加载,共 ${Object.keys(MOCK_ROUTES).length} 个路由${CONFIG.mockEnabled !== false ? '' : 'mock 已关闭,全部走代理)'}`);
}
// 验证mock文件是否存在
validateMockFiles();
} catch (error) {
console.error(`[CONFIG] 加载配置文件失败:`, error);
// 使用默认配置
MOCK_ROUTES = {
"/api2/user/list": "mock/user_list.txt"
};
CONFIG = {
cacheConfig: true,
reloadOnChange: true,
defaultContentType: "application/json",
proxyPort: 9443,
targetHost: "devrmtapp.resmart.cn"
};
}
}
// 验证mock文件是否存在
function validateMockFiles() {
console.log(`[CONFIG] 验证mock文件...`);
let missingFiles = [];
for (const [route, filePath] of Object.entries(MOCK_ROUTES)) {
const fullPath = path.join(__dirname, filePath);
if (!fs.existsSync(fullPath)) {
missingFiles.push({ route, filePath, fullPath });
console.warn(`[CONFIG] 警告: mock文件不存在 - ${filePath} (用于路由: ${route})`);
}
}
if (missingFiles.length > 0) {
console.log(`[CONFIG] 缺少 ${missingFiles.length} 个mock文件请创建这些文件`);
} else {
console.log(`[CONFIG] 所有mock文件验证通过`);
}
}
// 检查是否为mock路由的函数
function isMockRoute(requestPath: string): boolean {
if (CONFIG.mockEnabled === false) return false;
return MOCK_ROUTES.hasOwnProperty(requestPath);
}
// 获取mock文件路径
function getMockFilePath(requestPath: string): string {
return MOCK_ROUTES[requestPath];
}
// 代理服务器
const proxyServer = http.createServer((clientReq, clientRes) => {
// 解析客户端请求的 URL
const parsedUrl = new URL(`http://localhost${clientReq.url!}`);
const requestPath = parsedUrl.pathname;
// 检查是否为需要mock的路由
if (isMockRoute(requestPath)) {
const mockFile = getMockFilePath(requestPath);
console.log(`[MOCK] 拦截路由: ${requestPath} -> 使用文件: ${mockFile}`);
try {
// 构建完整的文件路径
const mockFilePath = path.join(__dirname, mockFile);
// 检查文件是否存在
if (!fs.existsSync(mockFilePath)) {
console.error(`[MOCK] Mock文件不存在: ${mockFilePath}`);
clientRes.writeHead(404, { 'Content-Type': 'application/json' });
clientRes.end(JSON.stringify({
error: 'Mock file not found',
route: requestPath,
file: mockFile,
timestamp: new Date().toISOString()
}));
return;
}
// 读取本地mock文件
const mockData = fs.readFileSync(mockFilePath, 'utf-8');
// 设置响应头
const contentType = CONFIG.defaultContentType || 'application/json';
clientRes.writeHead(200, {
'Content-Type': contentType,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'X-Mock-Source': mockFile,
'X-Mock-Timestamp': new Date().toISOString()
});
// 返回mock数据
clientRes.end(mockData);
console.log(`[MOCK] 成功返回mock数据: ${mockFile}`);
} catch (error) {
console.error(`[MOCK] 读取mock文件失败: ${mockFile}`, error);
clientRes.writeHead(500, { 'Content-Type': 'application/json' });
clientRes.end(JSON.stringify({
error: 'Failed to read mock data',
route: requestPath,
file: mockFile,
message: (error as Error).message,
timestamp: new Date().toISOString()
}));
}
return; // 直接返回,不转发到目标服务器
}
// 如果不是mock路由正常代理转发
console.log(`[PROXY] 转发请求: ${requestPath}`);
// 目标服务器的选项
const options: http.RequestOptions = {
hostname: CONFIG.targetHost,
port: 443, // HTTPS 默认端口
method: clientReq.method,
path: parsedUrl.pathname + parsedUrl.search,
headers: {
...clientReq.headers,
host: CONFIG.targetHost
}
};
// 创建到目标服务器的 HTTPS 请求
const proxyReq = https.request(options, (proxyRes) => {
// 将目标服务器的响应头复制到客户端响应
clientRes.writeHead(proxyRes.statusCode!, proxyRes.headers);
// 将目标服务器的响应数据管道传输到客户端
proxyRes.pipe(clientRes);
});
// 错误处理
proxyReq.on('error', (err) => {
console.error('Proxy request error:', err);
clientRes.writeHead(500, { 'Content-Type': 'application/json' });
clientRes.end(JSON.stringify({
error: 'Proxy error',
message: err.message,
timestamp: new Date().toISOString()
}));
});
// 将客户端请求体管道传输到代理请求
clientReq.pipe(proxyReq);
});
// 添加管理接口用于重新加载配置
proxyServer.on('request', (req, res) => {
const parsedUrl = new URL(`http://localhost${req.url!}`);
const pathname = parsedUrl.pathname;
// 管理接口:重新加载配置
if (pathname === '/__reload-config' && req.method === 'POST') {
try {
const oldCount = Object.keys(MOCK_ROUTES).length;
loadConfig();
const newCount = Object.keys(MOCK_ROUTES).length;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
message: 'Configuration reloaded successfully',
routesCount: newCount,
routesChanged: newCount - oldCount
}));
console.log(`[ADMIN] 通过API重新加载配置 (路由数: ${oldCount} -> ${newCount})`);
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: false,
error: (error as Error).message
}));
}
}
// 管理接口:查看当前配置
if (pathname === '/__config' && req.method === 'GET') {
try {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
routes: MOCK_ROUTES,
config: CONFIG,
timestamp: new Date().toISOString(),
totalRoutes: Object.keys(MOCK_ROUTES).length
}, null, 2));
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: false,
error: (error as Error).message
}));
}
}
});
// 初始化:第一次加载配置
loadConfig();
// 如果配置了文件监视,则监听配置文件变化
if (CONFIG.reloadOnChange) {
fs.watchFile(CONFIG_FILE, (curr, prev) => {
console.log(`[CONFIG] 配置文件已修改,重新加载...`);
try {
const oldRoutesCount = Object.keys(MOCK_ROUTES).length;
loadConfig();
const newRoutesCount = Object.keys(MOCK_ROUTES).length;
console.log(`[CONFIG] 配置重载完成 (路由数: ${oldRoutesCount} -> ${newRoutesCount})`);
} catch (error) {
console.error(`[CONFIG] 重新加载配置文件失败:`, error);
}
});
console.log(`[CONFIG] 已启用配置文件监视: ${CONFIG_FILE}`);
}
proxyServer.listen(CONFIG.proxyPort, "0.0.0.0", () => {
console.log(`========================================`);
console.log(`代理服务器运行在: http://localhost:${CONFIG.proxyPort}`);
console.log(`目标服务器: https://${CONFIG.targetHost}`);
console.log(`配置文件: ${CONFIG_FILE}`);
console.log(`已配置Mock路由: ${Object.keys(MOCK_ROUTES).length}${CONFIG.mockEnabled !== false ? '' : '(当前 mockEnabled=false未生效'}`);
console.log(`========================================`);
console.log(`管理接口:`);
console.log(` GET http://localhost:${CONFIG.proxyPort}/__config 查看当前配置`);
console.log(` POST http://localhost:${CONFIG.proxyPort}/__reload-config 重新加载配置`);
console.log(`========================================`);
console.log(`Mock路由列表:`);
for (const [route, file] of Object.entries(MOCK_ROUTES)) {
const filePath = path.join(__dirname, file);
const exists = fs.existsSync(filePath) ? '✓' : '✗';
console.log(` ${exists} ${route} -> ${file}`);
}
console.log(`========================================`);
});