whistle.mock-plugins
Version:
Whistle 插件,用于快速创建 API 模拟数据
543 lines (449 loc) • 18 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const url = require('url');
const storage = require('./storage');
// 数据存储目录
const DATA_DIR = path.join(process.env.WHISTLE_PLUGIN_DATA_DIR || storage.DATA_DIR);
// 接口配置文件
const INTERFACES_FILE = path.join(DATA_DIR, 'interfaces.json');
// 添加功能模块配置文件
const FEATURES_FILE = path.join(DATA_DIR, 'features.json');
// 配置项
const CONFIG = {
// 日志级别:0=NONE, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG
LOG_LEVEL: process.env.MOCK_PLUGIN_LOG_LEVEL || 4,
// 缓存有效期(毫秒),默认 60 秒
CACHE_INTERVAL: process.env.MOCK_PLUGIN_CACHE_INTERVAL || 60000,
// 是否在控制台输出详细日志
VERBOSE_CONSOLE: process.env.MOCK_PLUGIN_VERBOSE === 'true'
};
// 缓存控制变量
let enabledInterfacesCache = null; // 仅缓存启用状态的接口
let cacheTime = 0;
let cacheVersion = 1;
// 正则表达式缓存
const regexCache = new Map();
// 记录日志
const log = (message, level = 3) => {
// 只记录小于等于当前日志级别的日志
if (level <= CONFIG.LOG_LEVEL) {
const prefix = level === 1 ? '[ERROR] ' :
level === 2 ? '[WARN] ' :
level === 4 ? '[DEBUG] ' : '';
console.log(`[mock-plugin:rulesServer] ${prefix}${message}`);
}
};
// 错误日志
const logError = (message) => log(message, 1);
// 警告日志
const logWarn = (message) => log(message, 2);
// 信息日志
const logInfo = (message) => log(message, 3);
// 调试日志
const logDebug = (message) => log(message, 4);
// 获取文件最后修改时间
const getFileModTime = (filePath) => {
try {
const stats = fs.statSync(filePath);
return stats.mtimeMs;
} catch (err) {
return 0;
}
};
// 加载所有启用的接口(整合了功能模块和接口的加载逻辑)
const loadEnabledInterfaces = () => {
try {
const currentTime = Date.now();
const interfacesFileModTime = getFileModTime(INTERFACES_FILE);
const featuresFileModTime = getFileModTime(FEATURES_FILE);
// 取两个文件的最后修改时间的最大值
const lastModTime = Math.max(interfacesFileModTime, featuresFileModTime);
// 如果缓存有效且文件未被修改,直接返回缓存
if (enabledInterfacesCache &&
currentTime - cacheTime < CONFIG.CACHE_INTERVAL &&
lastModTime <= cacheTime) {
logDebug('使用已启用接口的缓存');
return enabledInterfacesCache;
}
logDebug('缓存无效或文件已修改,重新加载接口');
// 1. 读取并解析所有功能模块
let enabledFeatureIds = [];
if (fs.existsSync(FEATURES_FILE)) {
try {
const featuresData = fs.readJsonSync(FEATURES_FILE);
let features = [];
if (featuresData && Array.isArray(featuresData)) {
features = featuresData;
} else if (featuresData && featuresData.features && Array.isArray(featuresData.features)) {
features = featuresData.features;
}
// 过滤出启用状态的功能ID
enabledFeatureIds = features
.filter(feature => feature.active === true)
.map(feature => feature.id);
logDebug(`找到 ${enabledFeatureIds.length} 个启用的功能模块`);
} catch (err) {
logError(`读取功能模块失败: ${err.message}`);
}
}
// 2. 读取并解析所有接口
let enabledInterfaces = [];
if (fs.existsSync(INTERFACES_FILE)) {
try {
const interfacesData = fs.readJsonSync(INTERFACES_FILE);
let interfaces = [];
if (interfacesData && Array.isArray(interfacesData)) {
interfaces = interfacesData;
} else if (interfacesData && interfacesData.interfaces && Array.isArray(interfacesData.interfaces)) {
interfaces = interfacesData.interfaces;
}
// 过滤出启用状态的接口
enabledInterfaces = interfaces.filter(intf => {
// 首先检查接口本身是否启用
const isInterfaceEnabled = intf.active !== false; // 默认为true
if (!isInterfaceEnabled) {
return false;
}
// 如果接口有关联功能,检查功能是否启用
if (intf.featureId) {
return enabledFeatureIds.includes(intf.featureId);
}
// 如果接口没有关联功能,只要接口本身启用就可以
return true;
});
logInfo(`找到 ${enabledInterfaces.length} 个启用的接口`);
} catch (err) {
logError(`读取接口配置失败: ${err.message}`);
}
}
// 更新缓存
enabledInterfacesCache = enabledInterfaces;
cacheTime = currentTime;
return enabledInterfaces;
} catch (err) {
logError(`加载启用接口失败: ${err.message}`);
return enabledInterfacesCache || [];
}
};
// 匹配URL是否符合模式
const isUrlMatch = (url, pattern, proxyType, fullUrl) => {
if (!pattern) return false;
try {
// 针对不同的代理类型采用不同的匹配策略
// 对于url_redirect类型,需要完全匹配fullPath才命中
if (proxyType === 'url_redirect') {
// 精确匹配完整路径,这是最快的比较方式
return pattern === fullUrl;
}
// 对于redirect类型,只要url以pattern开头即可命中(前缀匹配)
if (proxyType === 'redirect') {
// 使用字符串原生的startsWith方法更高效
return fullUrl.startsWith(pattern);
}
// 以下是默认的匹配逻辑(用于response类型等)
// 精确匹配 - 最高效的匹配方式
if (pattern === url) {
return true;
}
// 通配符匹配
if (pattern.includes('*')) {
// 使用缓存的正则表达式
const cacheKey = `wildcard:${pattern}`;
let regex = regexCache.get(cacheKey);
if (!regex) {
// 需要转换为正则表达式,处理特殊字符
const regexPattern = pattern
.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&') // 转义特殊字符
.replace(/\*/g, '.*'); // 将 * 替换为 .*
regex = new RegExp('^' + regexPattern + '$');
regexCache.set(cacheKey, regex);
}
return regex.test(url);
}
// 正则表达式匹配
if (pattern.startsWith('/') && pattern.length > 2 && pattern.endsWith('/')) {
// 使用缓存的正则表达式
const cacheKey = `regex:${pattern}`;
let regex = regexCache.get(cacheKey);
if (!regex) {
const regexStr = pattern.slice(1, -1);
regex = new RegExp(regexStr);
regexCache.set(cacheKey, regex);
}
return regex.test(url);
}
} catch (e) {
log(`正则表达式匹配错误: ${e.message}`);
}
return false;
};
// 验证目标URL是否有效
const isValidTargetUrl = (url) => {
if (!url) return false;
try {
// 检查是否是合法的URL格式
new URL(url);
return true;
} catch (e) {
log(`URL验证错误: ${e.message}`);
return false;
}
};
// 验证重定向规则的有效性
const validateRedirectRule = (interface) => {
if (!interface) return false;
const { proxyType, targetUrl } = interface;
// 对于redirect和url_redirect类型,必须有有效的目标URL
if ((proxyType === 'redirect' || proxyType === 'url_redirect') && !isValidTargetUrl(targetUrl)) {
log(`接口 ${interface.id || interface.name || '未知'} 的目标URL无效: ${targetUrl}`);
return false;
}
return true;
};
// 生成随机数工具函数
const generateRandomValue = (pattern) => {
// 如果不是以@开头的模式,直接返回原值
if (!pattern || !pattern.startsWith('@')) {
return pattern;
}
const formatPattern = pattern.substring(1); // 去掉@前缀
// 使用单次正则替换代替每个字符的遍历
return formatPattern.replace(/x/g, () => {
// 使用一个固定的字符集,避免生成不兼容的字符
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
return chars.charAt(Math.floor(Math.random() * chars.length));
});
};
// 构建重定向规则
const buildRedirectRule = (fullUrl, targetUrl, customHeaders, pattern, isUrlRedirect = 0) => {
// 构建目标URL
let finalTargetUrl = fullUrl.replace(pattern, targetUrl);
// 定义结果数组,避免多次字符串拼接
const rules = [`${fullUrl} ${finalTargetUrl}`];
// 如果有自定义请求头,添加到规则中
if (customHeaders && Object.keys(customHeaders).length > 0) {
// 为每个请求头创建一个规则行
Object.entries(customHeaders).forEach(([key, value]) => {
// 处理随机值
const processedValue = generateRandomValue(value);
// 添加到规则数组
rules.push(`${fullUrl} headerReplace://req.${key}:/(.*)/=${processedValue}`);
});
}
// 最后才将所有规则合并为一个字符串,减少字符串操作
return rules.join('\n');
};
// 缓存管理接口 - 简化版本
const cacheManager = {
// 清除缓存
clearCache: () => {
logInfo('主动清除接口缓存');
enabledInterfacesCache = null;
cacheTime = 0;
cacheVersion++;
},
// 获取当前缓存状态
getStatus: () => {
return {
enabled: {
cached: !!enabledInterfacesCache,
cacheTime: cacheTime,
itemCount: enabledInterfacesCache ? enabledInterfacesCache.length : 0
},
cacheVersion,
configInterval: CONFIG.CACHE_INTERVAL
};
}
};
// Whistle 规则服务器实现
module.exports = (server, options) => {
// 优化:缓存常用路径匹配
const debugConfigPath = '/_debug/config';
const debugEnabledInterfacesPath = '/_debug/enabled-interfaces';
const clearCachePath = '/_flush_cache';
server.on('request', (req, res) => {
const oReq = req.originalReq;
// 提取请求相关信息
const fullUrl = oReq.url;
logDebug(`原始URL: ${fullUrl}`);
// 处理缓存刷新请求
if (fullUrl.startsWith(clearCachePath)) {
cacheManager.clearCache();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
message: '缓存已刷新',
cacheStatus: cacheManager.getStatus()
}));
return;
}
// 特殊调试路径,查看缓存状态
if (fullUrl.endsWith('/_cache_status')) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(cacheManager.getStatus()));
return;
}
// 特殊调试路径,使用更高效的匹配
if (fullUrl.endsWith(debugConfigPath)) {
try {
const configs = {
interfaces: fs.existsSync(INTERFACES_FILE) ? fs.readJsonSync(INTERFACES_FILE) : null,
features: fs.existsSync(FEATURES_FILE) ? fs.readJsonSync(FEATURES_FILE) : null
};
res.end(JSON.stringify(configs, null, 2));
return;
} catch (err) {
logError(`读取配置文件失败: ${err.message}`);
res.end(`读取配置文件失败: ${err.message}`);
return;
}
}
// 调试路径,查看启用的接口
if (fullUrl.endsWith(debugEnabledInterfacesPath)) {
try {
const enabledInterfaces = loadEnabledInterfaces();
res.end(JSON.stringify({
count: enabledInterfaces.length,
interfaces: enabledInterfaces
}, null, 2));
return;
} catch (err) {
logError(`获取启用接口失败: ${err.message}`);
res.end(`获取启用接口失败: ${err.message}`);
return;
}
}
// 解析URL
let parsedUrl;
try {
parsedUrl = url.parse(fullUrl);
} catch (err) {
logError(`解析URL失败: ${err.message}`);
// 解析失败时,不处理请求
res.end('');
return;
}
// 尝试获取 pathname 或 path,用于匹配
const method = oReq.method;
const path = parsedUrl.pathname || '';
logDebug(`请求方法: ${method}, 请求路径: ${path}`);
// 加载启用的接口配置(使用缓存优化)
const enabledInterfaces = loadEnabledInterfaces();
if (enabledInterfaces.length === 0) {
logDebug('没有找到启用的接口配置,跳过处理');
res.end('');
return;
}
// 优化:使用 find 查找匹配的接口,只遍历一次数组
const matchedInterface = enabledInterfaces.find(intf => {
// 获取URL模式
const pattern = intf.urlPattern || '';
const proxyType = intf.proxyType || 'response';
if (!pattern) {
logDebug(`接口 ${intf.id || intf.name || '未知'} 缺少URL模式`);
return false;
}
// 检查URL模式匹配,根据proxyType选择适当的匹配策略
const urlMatches = isUrlMatch(path, pattern, proxyType, fullUrl);
if (!urlMatches) {
// 不记录每个失败的匹配,以减少日志量
return false;
}
logDebug(`URL模式匹配成功: ${fullUrl} 匹配 ${pattern} (${proxyType}模式)`);
// 检查HTTP方法匹配
const methodField = intf.httpMethod || intf.method || '';
const methodValue = methodField.toUpperCase();
const methodMatches = !methodValue ||
methodValue === 'ALL' ||
methodValue === 'ANY' ||
methodValue === method;
if (!methodMatches) {
logDebug(`方法不匹配: 请求方法 ${method}, 接口方法 ${methodValue}`);
return false;
}
logDebug(`方法匹配成功: ${method} 匹配 ${methodValue || 'ALL'}`);
return true;
});
// 如果找到匹配的接口,处理对应的规则
if (matchedInterface) {
logInfo(`找到匹配接口 "${matchedInterface.name || matchedInterface.id || '未知'}" 用于 ${method} ${path}`);
// 处理不同的代理类型
const proxyType = matchedInterface.proxyType || 'response';
// 优先级判断:首先处理 response 类型
if (proxyType === 'response') {
// 返回指向插件服务的规则
logDebug(`接口 "${matchedInterface.name}" 使用模拟响应模式`);
res.end(`${fullUrl} mock-plugins://`);
return;
}
// 对于redirect和url_redirect类型,验证规则有效性
if ((proxyType === 'redirect' || proxyType === 'url_redirect') && !validateRedirectRule(matchedInterface)) {
logWarn(`接口 "${matchedInterface.name}" 的重定向规则无效,跳过处理`);
res.end('');
return;
}
// 处理 redirect 类型:将请求的 url 中的源 url 部分替换为目标 url
if (proxyType === 'redirect') {
const targetUrl = matchedInterface.targetUrl.trim();
const pattern = matchedInterface.urlPattern.trim();
logDebug(`接口 "${matchedInterface.name}" 使用重定向模式,模式: ${pattern}, 目标URL: ${targetUrl}`);
// 确保目标URL合法
if (!isValidTargetUrl(targetUrl)) {
logWarn(`目标URL不合法: ${targetUrl},跳过处理`);
res.end('');
return;
}
// 执行重定向:将fullUrl中匹配的pattern替换为targetUrl
// 使用startsWith优化匹配效率
if (fullUrl.startsWith(pattern)) {
// 获取自定义请求头
const customHeaders = matchedInterface.customHeaders || {};
// 构建重定向规则,包括自定义请求头
const redirectRule = buildRedirectRule(fullUrl, targetUrl, customHeaders, pattern, 0);
// 返回规则
res.end(redirectRule);
logDebug(`重定向规则: ${redirectRule}`);
return;
} else {
logDebug(`URL ${fullUrl} 不以 ${pattern} 开头,跳过处理`);
res.end('');
return;
}
}
// 处理 url_redirect 类型:完全匹配 fullPath 并直接返回目标URL
if (proxyType === 'url_redirect') {
const targetUrl = matchedInterface.targetUrl.trim();
const pattern = matchedInterface.urlPattern.trim();
logDebug(`接口 "${matchedInterface.name}" 使用URL重定向模式,模式: ${pattern}, 目标URL: ${targetUrl}`);
// 确保目标URL合法
if (!isValidTargetUrl(targetUrl)) {
logWarn(`目标URL不合法: ${targetUrl},跳过处理`);
res.end('');
return;
}
// 对于url_redirect,我们需要完全匹配
if (fullUrl === matchedInterface.urlPattern) {
// 获取自定义请求头
const customHeaders = matchedInterface.customHeaders || {};
// 构建重定向规则,包括自定义请求头
const redirectRule = buildRedirectRule(fullUrl, targetUrl, customHeaders, pattern, 1);
// 返回规则
res.end(redirectRule);
logDebug(`URL重定向规则: ${redirectRule}`);
return;
} else {
logDebug(`URL ${fullUrl} 不完全匹配 ${matchedInterface.urlPattern},跳过处理`);
res.end('');
return;
}
}
// 默认情况下使用mock-plugins处理
logDebug(`接口 "${matchedInterface.name}" 使用默认处理模式`);
res.end(`${fullUrl} mock-plugins://`);
} else {
// 如果没有匹配的接口,返回空字符串,Whistle 会继续处理下一个规则
logDebug(`未找到匹配接口,跳过处理 ${method} ${path}`);
res.end('');
}
});
};