UNPKG

titbit-loader

Version:
1,000 lines (806 loc) 26.4 kB
'use strict'; 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;