titbit-loader
Version:
loader for titbit framework
1,000 lines (806 loc) • 26.4 kB
JavaScript
;
const fs = require('node:fs');
const process = require('node:process');
/**
* 全局中间件容易因为group选项引发问题,若要把加载的controller目录隔离出来,需要针对每个分组加载中间件。
* 就是在全局中间件不指定group的情况下,默认是所有group而不是全局。
* 这样对前缀支持也可以完全兼容。
*/
let outWarning = (text, errname = 'Warning', color = '\x1b[2;31;47m') => {
setTimeout(() => {
console.error(`${color} ${errname}: ${text} \x1b[0m\n`);
}, 1280);
};
let nameErrorInfo = '名称不能有空格换行特殊字符等,仅支持 字母 数字 减号 下划线,字母开头。';
//获取模型文件列表
function getModelFiles(filepath) {
let mlist = fs.readdirSync(filepath, { withFileTypes: true });
let rlist = [];
for (let i=0; i < mlist.length; i++) {
if (!mlist[i].isFile()) continue;
if (mlist[i].name.substring(mlist[i].name.length-3) !== '.js')
continue;
if (mlist[i].name[0] === '!' || mlist[i].name[0] === '_')
continue;
rlist.push(mlist[i].name);
}
return rlist;
}
class TitbitLoader {
constructor(options = {}) {
//let appDir = __dirname + '.';
let appDir = '.';
this.globalMidTable = [];
this.groupMidTable = {};
this.fileMidTable = {};
if (typeof options !== 'object') {
options = {};
}
if (options.appPath !== undefined && typeof options.appPath === 'string') {
appDir = options.appPath;
}
if (appDir.length > 0 && appDir[0] !== '/') {
appDir = fs.realpathSync(appDir);
}
this.optionsCacheLog = null;
this.methodNumber = {
get: 1,
post: 2,
put: 3,
delete: 4,
_delete: 4,
options: 5,
head: 6,
trace: 7,
patch: 8,
list: 9
}
this.config = {
//当作为模块引入时,根据路径关系,
//可能的位置是node_modules/titbit-loader/loader.js,
//所以默认在父级目录两层和node_modules同级。
appPath : appDir,
controllerPath : appDir+'/controller',
modelPath : appDir+'/model',
midwarePath : appDir+'/middleware',
loadModel : true,
deep : 1,
mname : null,
modelNamePre: '',
//如果作为数组则会去加载指定的子目录
subgroup : null,
prePath: '',
homeFile: '',
initArgs: null,
multi: false,
optionsRoute: true,
fileAsGroup: true,
beforeController: null,
afterController: null
};
//用于在fileAsGroup模式,为options添加的分组避免和文件的分组冲突。
//考虑这样的情况:controller目录中存在 xyz.js文件和xyz目录。
this.groupTag = '@';
//组中间件的缓存,用于fileAsGroup的操作。
this.groupCache = {};
this.globalCache = [];
//在加载Model时可能需要传递参数
this.mdb = null;
this.mdbMap = null;
this.routepreg = /^[a-z\d\-\_]+$/i;
for (let k in options) {
if (k == 'appPath') continue;
if (k === 'subgroup') {
if (typeof options[k] === 'string') options[k] = [ options[k] ];
if (options[k] instanceof Array) {
this.config.subgroup = options[k];
}
this._fmtSubGroup();
continue;
} else if ( (k === 'prePath' || k === 'prepath') && typeof options[k] === 'string') {
this.config.prePath = options[k];
this.fmtPrePath();
continue;
}
switch (k) {
case 'beforeController':
case 'afterController':
if (typeof options[k] === 'function') this.config[k] = options[k];
break;
case 'initArgs':
this.config.initArgs = options[k];
break;
case 'homeFile':
case 'modelNamePre':
;(typeof options[k] === 'string') && (this.config[k] = options[k]);
break;
case 'mname':
;(typeof options[k] === 'string' || typeof options[k] === 'symbol') && (this.config[k] = options[k]);
break;
case 'loadModel':
case 'multi':
case 'optionsRoute':
case 'fileAsGroup':
this.config[k] = !!options[k];
break;
case 'controllerPath':
case 'modelPath':
case 'midwarePath':
if (options[k][0] !== '/') {
this.config[k] = `${this.config.appPath}/${options[k]}`;
}
break;
default:;
}
}
try {
fs.accessSync(this.config.controllerPath, fs.constants.F_OK);
} catch (err) {
if (this.config.controllerPath.length > 0) {
fs.mkdirSync(this.config.controllerPath);
}
}
try {
fs.accessSync(this.config.midwarePath, fs.constants.F_OK);
} catch (err) {
if (this.config.midwarePath.length > 0) {
fs.mkdirSync(this.config.midwarePath);
}
}
try {
fs.accessSync(this.config.modelPath, fs.constants.F_OK);
} catch (err) {
if (this.config.modelPath.length > 0) {
fs.mkdirSync(this.config.modelPath);
}
}
if (options.mdb !== undefined && this.config.loadModel) {
this.mdb = options.mdb;
}
options.mdbMap && (this.mdbMap = options.mdbMap);
}
fmtPrePath() {
let prepath = this.config.prePath.trim().replace(/\/+/g, '/');
if (prepath === '/')
prepath = '';
else if (prepath.length > 0) {
if (prepath[0] !== '/') {
prepath = `/${prepath}`;
}
if (prepath[ prepath.length - 1 ] === '/') {
prepath = prepath.substring(0, prepath.length - 1);
}
}
this.config.prePath = prepath;
}
_fmtSubGroup() {
if (!(this.config.subgroup instanceof Array)) {
return;
}
let a;
for (let i = 0; i < this.config.subgroup.length; i++) {
a = this.config.subgroup[i];
a = a.trim().replace(/\/+/g, '');
this.config.subgroup[i] = a;
}
}
init(app) {
this.config.loadModel && this.loadModel(app);
this.mdbMap && this.loadModelMap(app);
this.defineServiceFunction(app);
this.loadController(app);
this.loadMidware(app);
app.service.__titbit_loader__ = true;
}
loadController(app) {
if (app.service.__titbit_loader__ && !this.config.multi) {
outWarning('您已经使用titbit-loader加载过路由,多次加载容易导致路由冲突,重复操作将会被终止。');
outWarning('若有需要,可设置选项multi为true允许多次加载。', ' 提示');
return false;
}
this.optionsCacheLog = {};
let cfiles = {};
this.readControllers(this.config.controllerPath, cfiles);
let cob = null;
let Ctlr;
for (let k in cfiles) {
try {
Ctlr = require(k);
//不是函数或箭头函数无法进行new操作。
if (typeof Ctlr !== 'function' || !Ctlr.prototype) {
continue;
}
cob = new Ctlr();
if (cob.init && typeof cob.init === 'function') {
cob.init(this.config.initArgs || app.service);
}
if (this.config.beforeController) {
try {
this.config.beforeController(cob, cfiles[k], app);
} catch (err) {
outWarning(`beforeController: ${err.message}`);
}
}
this.setRouter(app, cob, cfiles[k]);
if (this.config.afterController) {
try {
this.config.afterController(cob, cfiles[k], app);
} catch (err) {
outWarning(`afterController: ${err.message}`);
}
}
cob = null;
} catch (err) {
outWarning(`load router file : ${k}\x1b[0m\n \x1b[1;33m${err.message}\n\x1b[1;36m${err.stack || ''}`,
'Error', '\x1b[1;35m');
}
}
this.optionsCacheLog = null;
return cfiles;
}
_autoAddOptions(app, options_path, group) {
app.router.apiTable.OPTIONS[options_path] === undefined
&&
app.router.options(options_path, async c => {}, {group: group});
}
setRouter(app, cob, cf) {
if (cob.mode === undefined) {
cob.mode = 'restful';
}
let group = cf.dirgroup;
if (this.config.fileAsGroup) group = cf.filegroup;
let npre = cf.filegroup;
let prepath = this.config.prePath;
let route_path = `${prepath}${cf.filegroup}`;
npre = `${prepath}${npre}`;
group = `${prepath}${group}`;
//用于在fileAsGroup模式添加options路由。
let dirgroup = `${prepath}${cf.dirgroup}`;
let routeParam = '/:id';
if (cob.param !== undefined && cob.param !== null && typeof cob.param === 'string') {
routeParam = cob.param.trim()
.replace(/\s+/g, '')
.replace(/\/{2,}/g, '/');
if (routeParam.length > 0 && routeParam[0] !== '/') {
routeParam = `/${routeParam}`;
}
}
Object.defineProperty(cob, '__route__', {
configurable: false,
writable: false,
enumerable: false,
value: route_path
});
if (cob.post !== undefined && typeof cob.post === 'function') {
let postParam = (cob.postParam && typeof cob.postParam === 'string') ? cob.postParam : '';
postParam = postParam.replace(/\/+/g, '/');
if (postParam === '/') postParam = '';
if (postParam.length > 0 && postParam[0] !== '/') {
postParam = `/${postParam}`;
}
app.router.post(`${route_path}${postParam}`, cob.post.bind(cob),
{
name: `${npre}/${this.methodNumber.post}`,
group: group
}
);
}
let real_delete_method = '';
if (cob.delete && typeof cob.delete === 'function') {
real_delete_method = 'delete';
} else if (cob._delete && typeof cob._delete === 'function') {
real_delete_method = '_delete';
}
if (real_delete_method) {
app.router.delete(`${route_path}${routeParam}`,
cob[real_delete_method].bind(cob),
{
name: `${npre}/${this.methodNumber.delete}`,
group: group
}
);
}
if (cob.put !== undefined && typeof cob.put === 'function') {
app.router.put(`${route_path}${routeParam}`, cob.put.bind(cob),
{
name: `${npre}/${this.methodNumber.put}`,
group: group
}
);
}
if (cob.get !== undefined && typeof cob.get === 'function') {
app.router.get(`${route_path}${routeParam}`, cob.get.bind(cob),
{
name: `${npre}/${this.methodNumber.get}`,
group: group
}
);
//主页只支持GET请求
if (this.config.homeFile === cf.pathname) {
app.router.get('/', cob.get.bind(cob), {
name: 'home',
group: group
});
}
}
if (cob.list !== undefined && typeof cob.list === 'function') {
let listParam = (cob.listParam && typeof cob.listParam === 'string') ? cob.listParam : '';
listParam = listParam.replace(/\/+/g, '/');
if (listParam === '/') listParam = '';
if (listParam.length > 0 && listParam[0] !== '/') {
listParam = `/${listParam}`;
}
app.router.get(`${route_path}${listParam}`, cob.list.bind(cob),{
name: `${npre}/${this.methodNumber.list}`,
group: group
});
}
if (cob.patch !== undefined && typeof cob.patch === 'function') {
app.router.patch(`${route_path}${routeParam}`, cob.patch.bind(cob),{
name: `${npre}/${this.methodNumber.patch}`,
group: group
});
}
if (cob.options !== undefined && typeof cob.options === 'function') {
app.router.options(`${route_path}${routeParam}`, cob.options.bind(cob),{
name: `${npre}/${this.methodNumber.options}`,
group: group
});
} else if (this.config.optionsRoute) {
let real_group = this.config.fileAsGroup ? dirgroup : group;
let tag = this.config.fileAsGroup ? this.groupTag : '';
if (real_group === `${this.config.prePath}/`) {
this._autoAddOptions(app, `${route_path}/*`, tag + real_group);
} else if (this.optionsCacheLog[real_group] === undefined) {
this._autoAddOptions(app, `${real_group}/*`, tag + real_group);
this.optionsCacheLog[real_group] = true;
}
}
if (cob.head !== undefined && typeof cob.head === 'function') {
app.router.head(`${route_path}${routeParam}`, cob.head.bind(cob),{
name: `${npre}/${this.methodNumber.head}`,
group: group
});
}
this.fileMidTable[cf.filegroup] = {
//group已经是带有前缀的。
group : group,
dirgroup: cf.dirgroup,
mid : []
};
if (cob.__mid && typeof cob.__mid === 'function') {
let mid = cob.__mid();
if (mid && Array.isArray(mid) ) {
this.fileMidTable[cf.filegroup].mid = mid;
}
}
}
_getGroupList() {
this.groupList = [`${this.config.prePath}/`];
this.orgGroupList = ['/'];
try {
let flist = fs.readdirSync(this.config.controllerPath, {withFileTypes: true});
for (let f of flist) {
if (!f.isDirectory()) continue;
if (f.name[0] === '!' || f.name[0] === '.') continue;
this.groupList.push(`${this.config.prePath}/${f.name}`);
this.orgGroupList.push(`/${f.name}`);
}
} catch (err) {
console.error('获取全局所有分组失败,若获取失败会导致程序运行错误,故进程退出,请检查错误。');
console.error(err);
process.exit(1);
}
return this.groupList;
}
/**
* 加载中间件,仅仅是通过一个js文件,
* 中间件不宜过度使用,否则容易混乱。
*/
loadMidware(app) {
if (app.service.__titbit_loader__ && !this.config.multi) {
outWarning('您已经使用titbit-loader加载过中间件,多个实例容易会导致中间件多次加载或冲突,重复操作将会被终止。');
outWarning('若有需要,可设置选项multi为true允许多次加载。', ' 提示');
return false;
}
this._getGroupList();
for (let i = 0; i < this.globalMidTable.length; i++) {
this.loadGlobalMidware(app, this.globalMidTable[i]);
}
//加载组,此时组已经确定
for (let k in this.groupMidTable) {
for (let i=0; i < this.groupMidTable[k].length; i++) {
this.loadGroupMidware(app, this.groupMidTable[k][i], k);
}
}
if (this.config.fileAsGroup && this.config.optionsRoute) {
for (let g of this.orgGroupList) {
this._loadMidForFileAsGroup(app, `${this.groupTag}${this.config.prePath}${g}`, g);
}
}
for (let k in this.fileMidTable) {
this._loadMidForFileAsGroup(app,
this.fileMidTable[k].group,
this.fileMidTable[k].dirgroup);
for (let i = 0; i < this.fileMidTable[k].mid.length; i++) {
this.loadFileMidware(app,
this.fileMidTable[k].mid[i],
k,
this.fileMidTable[k].group,
this.fileMidTable[k].dirgroup
);
}
}
}
_loadMidForFileAsGroup(app, group, dirgroup) {
//此时文件作为分组,从groupCache中取出中间件,并添加到分组。
if (this.config.fileAsGroup) {
let tmp_opts;
for (let g of this.globalCache) {
tmp_opts = {...g[1]};
tmp_opts.group = group;
app.use(g[0], tmp_opts);
}
let ggp = this.groupCache[dirgroup];
if (ggp && Array.isArray(ggp)) {
for (let g of ggp) {
tmp_opts = {...g[1]};
tmp_opts.group = group;
app.use(g[0], tmp_opts);
}
}
}
}
checkMiddleware(m) {
if (m.middleware === undefined) return false;
if (typeof m.middleware === 'function' && m.middleware.constructor.name === 'AsyncFunction') {
return true;
}
if (m.middleware.mid && typeof m.middleware.mid === 'function') {
return true;
}
if (m.middleware.middleware && typeof m.middleware.middleware === 'function') {
return true;
}
return false;
}
getMidwareInstance(m) {
if ( this.checkMiddleware(m) ) {
return m.middleware;
}
if (typeof m.name !== 'string' || m.name.trim() === '') {
console.error(`--Middleware Error--: less name.`, m);
return null;
}
let mt = null;
let tmp = null;
if (m.name[0] == '@') {
tmp = require(this.config.midwarePath+'/'+m.name.substring(1));
if (m.args === undefined) {
mt = new tmp();
} else {
mt = new tmp(m.args);
}
if (mt.middleware && typeof mt.middleware === 'function') {
return mt.middleware.bind(mt);
} else if (mt.mid && typeof mt.mid === 'function') {
return mt.mid()
}
} else {
mt = require(this.config.midwarePath+'/'+m.name);
}
return mt;
}
_checkMidwareMode(app, m) {
if (m.mode !== undefined) {
if (m.mode === 'test' || m.mode === 'dev') {
if (app.service.TEST || app.service.DEV) {
return true;
}
return false;
} else if (m.mode === 'online' || m.mode === 'product') {
//只在正式环境加载
if (app.service.TEST || app.service.DEV) {
//console.log(`测试环境不加载中间件`, m);
return false;
}
return true;
}
}
//console.log('加载···', m);
return true;
}
loadGlobalMidware(app, m) {
//检测是否是开发环境,并确定是否加载中间件。
if (this._checkMidwareMode(app, m) === false) {
return;
}
let opts = null;
let makeOpts = (groupname = null) => {
let op = {};
if (m.method !== undefined) {
op.method = m.method;
}
if (groupname) {
op.group = groupname[0] === '/' ? groupname : `/${groupname}`;
}
if (m.pre) {
op.pre = true;
}
return op;
};
let mobj;
let group = this.groupList;
if (group) {
mobj = this.getMidwareInstance(m);
if (this.config.fileAsGroup) {
mobj && this.globalCache.push([mobj, makeOpts()]);
return;
}
for (let g of group) {
mobj && app.use(mobj, makeOpts(g));
}
return;
}
}
loadGroupMidware(app, m, group) {
if (this._checkMidwareMode(app, m) === false) {
return;
}
if ((!m.name || m.name === '') && !m.middleware) {
return;
}
let opts = {
group: `${this.config.prePath}${group}`,
};
if (m.method !== undefined) {
opts.method = m.method;
}
if (m.pre) {
opts.pre = true;
}
let mobj = this.getMidwareInstance(m);
if (mobj) {
if (!this.config.fileAsGroup) {
app.use(mobj, opts);
} else {
if (!this.groupCache[group]) { this.groupCache[group] = [[mobj, opts]]; }
else { this.groupCache[group].push([mobj, opts]); }
}
}
}
/**
*
* @param {object} app
* @param {object} m
* @param {string} f
* @param {string} group 包括前缀的分组,若开启fileAsGroup则为filegroup
* @param {string} dirgroup 文件名,不包括前缀
* @returns
*/
loadFileMidware(app, m, f, group, dirgroup) {
if (this._checkMidwareMode(app, m) === false) {
return;
}
//group已经带有prepath前缀。
let opts = { group };
f = `${this.config.prePath}${f}`;
if (!this.fileAsGroup && m.path === undefined) {
m.path = [
'get', 'list', 'post', 'put', 'delete',
'options', 'patch', 'head', 'trace'
];
}
if (m.path && typeof m.path === 'string') {
m.path = [ m.path ];
}
if (m.path && Array.isArray(m.path)) {
opts.name = [];
let path_num;
for (let p of m.path) {
path_num = this.methodNumber[p.toLowerCase()];
if (path_num === undefined) continue;
opts.name.push(`${f}/${path_num}`);
}
}
if (m.pre) {
opts.pre = true;
}
let mobj = this.getMidwareInstance(m);
if (mobj) {
app.use(mobj, opts);
}
}
loadModelMap(app) {
if (typeof this.mdbMap !== 'object')
throw new Error('mdbMap必须是object类型。');
let defpath = this.config.modelPath;
let sk;
let curpath;
let obj;
let loadObj = (serv, odb, fpath) => {
let mlist = getModelFiles(fpath);
let mname;
mlist.forEach(m => {
mname = m.substring(0, m.length - 3);
serv[mname] = this.getModelInstance(fpath + '/' + m, odb);
});
};
for (let k in this.mdbMap) {
obj = this.mdbMap[k];
if (!obj || typeof obj !== 'object') continue;
sk = '@' + k;
if (!app.service[sk]) app.service[sk] = {};
curpath = obj.path || defpath;
loadObj(app.service[sk], obj.mdb || null, curpath);
}
}
defineServiceFunction(app) {
if (app.service.getModel) return;
let sfe = function (mk, mpre, name, key = '') {
let m;
if (!key) {
m = mk ? this[mk] : this;
let oo = m[mpre + name];
if (!oo) throw new Error(`无法获取模型实例${name}`);
return oo;
}
let k = '@' + key;
if (!this[k] || !this[k][name]) throw new Error(`无法获取模型实例${name}`);
return this[k][name];
};
app.service.getModel = sfe.bind(app.service, this.config.mname, this.config.modelNamePre);
let fm = function (key) {
let k = '@' + key;
if (!this[k]) throw new Error('无法获取服务容器 ' + key);
return this[k];
};
app.service.modelMap = fm.bind(app.service);
Object.defineProperties(app.service, {
__prepath__: {
value: this.config.prePath,
configurable: false,
writable: false,
enumerable: false
},
__appdir__: {
value: this.config.appPath,
configurable: false,
writable: false,
enumerable: false
}
});
}
bindModelAttr(app) {
if (this.config.mname) {
if (app.service[this.config.mname] === undefined) app.service[this.config.mname] = {};
app.service.__model__ = app.service[this.config.mname];
}
}
/**
* 加载数据库操作接口,一个表要对应一个js文件,
*/
loadModel(app) {
if (this.config.mname && app.service[this.config.mname] === undefined) {
app.service[this.config.mname] = {};
}
this.bindModelAttr(app);
try {
let mlist = getModelFiles(this.config.modelPath);
mlist.forEach( m => {
this.requireModel(app, m);
});
} catch (err) {
console.error(err);
}
}
getModelInstance(mfile, db) {
let m = require(mfile);
// Arrow Function has no prototype
if (typeof m !== 'function' || m.prototype === undefined) {
if (m.init && typeof m.init === 'function') {
m.init(db);
}
return m;
}
return db ? new m(db) : new m();
}
requireModel(app, mfile) {
try {
let mobj = this.getModelInstance(this.config.modelPath + '/' + mfile, this.mdb);
let mname = mfile.substring(0, mfile.length-3);
mname = `${this.config.modelNamePre}${mname}`;
if (!this.config.mname) {
if (app.service[mname] !== undefined) {
outWarning(`model 冲突 ---- ${mfile}{${mname}} 已经设置。`);
return false;
}
app.service[mname] = mobj;
} else {
if (app.service[this.config.mname][mname] !== undefined) {
outWarning(`model 冲突 ---- ${mfile}{${mname}} 已经设置。`);
return false;
}
app.service[this.config.mname][mname] = mobj;
}
} catch (err) {
console.error(err.message, ' -- ', mfile);
return false;
}
return true;
}
stripExtName(filename) {
let sf = filename.split('.js');
return `${sf[0]}`;
}
/**
* 读取控制器目录中的文件
* @param {string} cdir
* @param {object} cfiles
* @param {number} deep
* @param {string} dirgroup
*/
readControllers(cdir, cfiles, deep = 0, dirgroup = '') {
let files = fs.readdirSync(cdir, {withFileTypes:true});
let tmp = '';
for (let i = 0; i < files.length; i++) {
if (files[i].isDirectory() && deep < 1) {
if (files[i].name[0] == '!') {
continue;
}
//检测是否启用了分组控制
//这时候,只有在subgroup之内的才会去加载
if (this.config.subgroup instanceof Array) {
if (this.config.subgroup.indexOf(files[i].name) < 0) {
continue;
}
}
if (this.routepreg.test(files[i].name) === false) {
outWarning(`${files[i].name}/ ${nameErrorInfo}`, 'Error');
continue;
}
this.readControllers(cdir+'/'+files[i].name,
cfiles, deep+1,
`${dirgroup}/${files[i].name}`
);
} else if (files[i].isFile()) {
if (files[i].name[0] === '!') {
continue;
}
if (files[i].name.length < 4) {
continue;
}
if (files[i].name.substring(files[i].name.length-3) !== '.js') {
continue;
}
if (this.config.subgroup instanceof Array && deep < 1) {
if (this.config.subgroup.indexOf('') < 0 && this.config.subgroup.indexOf('/') < 0) {
continue;
}
}
if (files[i].name == '__mid.js') {
//顶层并且忽略全局中间件选项为false则加载全局中间件。
if (deep == 0) {
this.globalMidTable = require(cdir+'/'+files[i].name);
} else {
this.groupMidTable[dirgroup] = require(cdir+'/'+files[i].name);
}
continue;
}
tmp = this.stripExtName(files[i].name);
if (this.routepreg.test(tmp) === false) {
outWarning(`${files[i].name} ${nameErrorInfo}`, 'Error');
continue;
}
cfiles[cdir+'/'+files[i].name] = {
filegroup: dirgroup + '/' + tmp,
dirgroup: dirgroup || '/',
name: files[i].name,
modname: tmp,
pathname : `${dirgroup}${dirgroup ? '/' : ''}${files[i].name}`
};
}
}
}
}
module.exports = TitbitLoader;