Skip to content

Instantly share code, notes, and snippets.

@alanwei43
Created September 21, 2019 04:41
Show Gist options
  • Save alanwei43/21b947d453e857cbd8359c262ee85481 to your computer and use it in GitHub Desktop.
Save alanwei43/21b947d453e857cbd8359c262ee85481 to your computer and use it in GitHub Desktop.
ChainResponse.js
const fs = require("fs"), path = require("path");
let debugMode = false;
//#region 日志函数
function log(txt) {
if (debugMode) {
console.log(`[${new Date().toLocaleString()}] ${txt}`);
}
}
function warn(txt) {
if (debugMode) {
console.warn(`[${new Date().toLocaleString()}] ${txt}`);
}
}
function error(txt, e) {
console.error(`[${new Date().toLocaleString()}] ${txt}`, e);
}
//#endregion
//#region 实用函数
/**
* 是否是Promise对象
* @param {Object} obj
* @returns {boolean}
*/
function isPromise(obj) {
return obj && typeof obj.then === "function" && typeof obj.catch === "function";
};
/**
* 递归获取指定目录下的所有文件
* @param {String} dir 目录
* @param {[{filePath: String, fileName: String, extName: String, fileNameWithoutExt: String, relativePath: String}]} files
* @returns {[{fullPath: string, filePath: string, fileName: string, extName: string, fileNameWithoutExt: string, relativePath: string}]}
*/
function recuriseFiles(dir, files) {
if (!files) {
files = [];
}
const children = fs.readdirSync(dir)
.map(item => path.join(dir, item))
.map(item => ({
path: item,
stat: fs.statSync(item)
}));
children.filter(item => item.stat.isDirectory()).map(d => recuriseFiles(d.path, files));
children.filter(item => item.stat.isFile()).map(f => {
const fileInfo = {
fullPath: f.path,
filePath: path.dirname(f.path),
fileName: path.basename(f.path),
extName: path.extname(f.path),
};
if (fileInfo.extName) {
fileInfo.fileNameWithoutExt = fileInfo.fileName.split('.').slice(0, -1).join('.')
fileInfo.relativePath = path.relative(__dirname, fileInfo.filePath);
}
files.push(fileInfo);
});
return files;
}
/**
* 所有Promise都被 settled
* @param {[{promise: function():Promise, data: {}}]} promises
* @retu {Promise<[{isResolved: boolean, isRejected: boolean, result: {}, data: {}, error: {}}]>}
*/
function whenAllSettled(promises) {
const mapedPromises = promises
.map(item => {
if (isPromise(item)) {
return { promise: item, data: undefined };
}
if (item && isPromise(item.promise)) {
return item;
}
return {
promise: Promise.reject(new Error("invalid_promise")),
data: item
};
})
.map(item => item.promise.then(result => {
return {
isResolved: true,
result: result,
data: item.data
}
}, error => {
return {
isRejected: true,
error: error,
data: item.data
};
}));
return Promise.all(mapedPromises);
}
//#endregion
/**
* 校验模块有效性
* @param {{path: string, module: function | {}}} item 模块
* @returns {boolean}
*/
function checkChainModule(item) {
const logPrefix = `[${item.path}]`;
log(`${logPrefix}开始模块校验`);
if (typeof item.module === "function") {
item.module = item.module();
}
if (!item.module || typeof item.module !== "object") {
warn(`${logPrefix}模块必须是一个对象或者是一个能返回有效对象的函数`)
return false;
}
if (typeof item.module.isOpen !== "boolean") {
item.module.isOpen = true;
}
if (typeof item.module.name !== "string") {
item.module.name = item.path;
}
if (typeof item.module.priority !== "number") {
item.module.priority = 100;
}
if (typeof item.module.isMatch !== "function") {
warn(`${logPrefix}模块${item.module.name}的 isMatch 必须是个函数`);
const originMatchFunction = item.module.isMatch;
item.module.isMatch = () => {
const result = originMatchFunction();
if (isPromise(result)) {
return result;
} else if (typeof result === "boolean") {
return result ? Promise.resolve() : Promise.reject();
} else {
error(`${logPrefix}模块${item.module.name}的 isMatch 方法必须返回值必须Promise对象或者boolean类型`);
return Promise.reject();
}
}
return false;
}
if (typeof item.module.getResponse !== "function") {
warn(`${logPrefix}模块${item.module.name}的 getResponse 必须是个函数`)
return false;
}
return true;
}
/**
* 获取所有模块
* @param {String} dir 模块所在目录
* @param {RegExp | function({fullPath: string, fileName: string}): boolean} filter 文件过滤
* @returns {[{isOpen: boolean, isMatch: function(): Promise, getResponse: function(): Promise}]}
*/
function getAllModules(dir, filter) {
log(`开始从目录${dir}加载模块`)
let filterFn = ({ fullPath }) => /\.js$/g.test(fullPath);
if (filter instanceof RegExp) {
filterFn = meta => filter.test(meta);
} else if (typeof filter === "function") {
filterFn = filter;
} else {
log(`加载所有以.js结尾的文件`);
}
return recuriseFiles(dir)
.filter(filterFn)
.map(f => {
const modulePath = `.${path.sep}${f.relativePath}${path.sep}${f.fileName}`;
log(`读取到模块文件: ${modulePath}`);
return modulePath;
})
.map(p => ({ module: require(p), path: p }))
.filter(checkChainModule)
.map(item => item.module);
}
/**
*
* @param {{}} reqInfo
* @param {[{matchResult: Object, module: {name: string, getResponse: function():Promise}}]} modules
* @returns {Promise<{prevResponse: {content: string, headers: Object.<string, string>}, handledModules: []}>}
*/
function chainModules(reqInfo, modules) {
log(`开始串联${modules.length}个模块`);
return modules.reduce((prev, next) => {
return prev.then(({ prevResponse, handledModules }) => {
log(`[模块${next.module.name}]调用 getResponse 方法`);
return next.module.getResponse(reqInfo, next.matchResult, prevResponse, handledModules).then(result => {
log(`[模块${next.module.name}]getResponse 方法执行完成`);
return {
prevResponse: result,
handledModules: [...handledModules, next.module]
};
}).catch(e => {
error(`[模块${next.module.name}]getResponse 方法发生异常: ${e.message}`, e);
return {
prevResponse: prevResponse,
handledModules: [...handledModules, next.module]
}
});
}).catch(e => {
error(`在执行模块${next.module.name}之前的 prev 发生异常: ${e.message}`, e);
return {
prevResponse: undefined,
handledModules: []
};
});
}, Promise.resolve({ prevResponse: undefined, handledModules: [] }));
}
/**
* 获取 express 中间件
* @param {{dir: string, filter: RegExp | function} | Array.<{isOpen: boolean, isMatch: function(): Promise<boolean>, getResponse: function(): Promise}>} modulesOrOptions 模块(可以是已经加载好的模块数组, 也可以指定模块路径)
* @param {{debug: boolean, modules: RegExp}} param1 选项
* @returns {function}
}}
*/
function chainResponse(modulesOrOptions, { debug } = {}) {
debugMode = !!debug;
const modules = Array.isArray(modulesOrOptions) ? modulesOrOptions : getAllModules(modulesOrOptions.dir, modulesOrOptions.filter);
log(`加载了${modules.length}个模块`);
return function (req, res, next) {
let reqInfo = {
method: req.method + "",
originalUrl: req.originalUrl,
path: req.path,
query: req.query,
xhr: req.xhr,
request: req
};
const logPrefix = `[${reqInfo.method.toUpperCase()} ${reqInfo.path}]`;
const matchPromises = modules.map(m => ({ data: m, promise: m.isMatch(reqInfo) }));
whenAllSettled(matchPromises).then(promises => {
const resolvedModules = promises
.filter(p => p.isResolved)
.map(p => ({
matchResult: p.result,
module: p.data,
priority: typeof p.data.priority === "number" ? p.data.priority : 0
}))
.sort((prev, next) => next.priority - prev.priority);
log(`${logPrefix}匹配到${resolvedModules.length}个模块`);
return chainModules(reqInfo, resolvedModules);
}).then(({ prevResponse, handledModules }) => {
if (!prevResponse) {
log(`${logPrefix}模块响应内容为空, 执行 next()`);
next();
return;
}
if (typeof prevResponse.content !== "string") {
log(`${logPrefix}模块响应内容不是字符串, 执行 next()`);
next();
return;
}
Object.keys(prevResponse.headers || {}).forEach(key => {
const value = prevResponse.headers[key];
res.append(key, value);
log(`${logPrefix}设置响应头 ${key}: value`);
});
res.send(prevResponse.content);
log(`${logPrefix}模块发送响应内容, 长度${prevResponse.content.length}`);
});
}
}
exports.recuriseFiles = recuriseFiles;
exports.whenAllSettled = whenAllSettled;
exports.chainResponse = chainResponse;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment