Express和Koa作为轻量级的web框架,没有任何约束的框架在一开始的时候会非常的爽快,开发几个demo,手到擒来,但是一旦代码真正上去的时候(而且一定会),你就会发现,大量重复的操作,重复的逻辑。导致项目的复杂度越来越高,代码越来越丑,非常的难以维护。我的quark-h5也是开始随意的写,写到最后只能重构一波了。正好期间做了个在线文档管理的项目用了egg.js,让我这种 node 小白有眼前一亮的感觉,重构quark-h5 server端就参考egg.js实现基于koa2的MVC结构
Github: 传送门<br/>
这里是参考 eggjs 的目录结构,这样的目录结构就非常清爽,构建一个应用也因为我们封装得体,只需要几行代码就可以实现
koa2 --> app --> 引入config ---> 引入controller ---> 引入server ---> 引入extend --->引入router --->引入model --->引入定时任务 --->初始化默认中间件 ---> 实列化 ---> 挂载到ctx ---> ctx全局使用
通过nodejs fs文件模块对每个模块文件夹进行扫描,获取js文件,并将js导出的内容赋值给全局app对象上,模块间通过app全局对象进行访问
下面来看核心core加载代码实现:
/core/index.js
/** * 封装koa mvc基础架构初始化工作 */ const path = require('path') const Koa = require('koa'); const { initConfig, initController, initService, initModel, initRouter, initMiddleware, initExtend, initSchedule } = require('./loader'); class Application{ constructor(){ this.$app = new Koa(); // 注册默认中间件 this.initDefaultMiddleware(); // 初始化config this.$config = initConfig(this); // 初始化controller this.$controller = initController(this); // 初始化service this.$service = initService(this); // 初始化middleware this.$middleware = initMiddleware(this); // 初始化model this.$model = initModel(this) // 初始化router this.$router = initRouter(this); // 初始化扩展 initExtend(this); // 初始化定时任务schedule initSchedule(this) // 将ctx注入到app上 this.$app.use(async (ctx, next) => { this.ctx = ctx; await next() }) this.$app.use(this.$router.routes()); } // 设置内置中间件 initDefaultMiddleware(){ const koaStatic = require('koa-static'); const koaBody = require('koa-body'); const cors = require('koa2-cors'); const views = require('koa-views'); // 配置静态web this.$app.use(koaStatic(path.resolve(__dirname, '../public')), { gzip: true, setHeaders: function(res){ res.header( 'Access-Control-Allow-Origin', '*') }}); //跨域处理 this.$app.use(cors()); // body接口数据处理 this.$app.use(koaBody({ multipart: true, formidable: { maxFileSize: 3000*1024*1024 // 设置上传文件大小最大限制,默认30M } })); //配置需要渲染的文件路径及文件后缀 this.$app.use(views(path.join(__dirname,'../views'), { extension:'ejs' })) } // 启动服务 start(port){ this.$app.listen(port, ()=>{ console.log('server is starting........!'); }); } } module.exports = Application;
loader加载器负责将各个文件夹里的内容解析,并挂载到全局app实例上。
/core/loader.js实现逻辑
const path = require('path') const fs = require('fs') const Router = require('koa-router'); const schedule = require("node-schedule"); const mongoose = require('mongoose') //自动扫指定目录下面的文件并且加载 function scanFilesByFolder(dir, cb) { let _folder = path.resolve(__dirname, dir); if(!getFileStat(_folder)){ return; } try { const files = fs.readdirSync(_folder); files.forEach((file) => { let filename = file.replace('.js', ''); let oFileCnt = require(_folder + '/' + filename); cb && cb(filename, oFileCnt); }) } catch (error) { console.log('文件自动加载失败...', error); } } // 检测文件夹是否存在 /** * @param {string} path 路径 */ function getFileStat(path) { try { fs.statSync(path); return true; } catch (err) { return false; } } // 配置信息 const initConfig = function(app){ let config = {}; scanFilesByFolder('../config',(filename, content)=>{ config = {...config, ...content}; }); return config; }; // 初始化路由 const initRouter = function(app){ const router = new Router(); require('../router.js')({...app, router}); return router; } // 初始化控制器 const initController = function(app){ let controllers = {}; scanFilesByFolder('../controller',(filename, controller)=>{ controllers[filename] = controller(app); }) return controllers; } //初始化service function initService(app){ let services = {}; scanFilesByFolder('../service',(filename, service)=>{ services[filename] = service(app); }) return services; } //初始化model function initModel(app){ // 链接数据库, 配置数据库链接 if(app.$config.mongodb){ mongoose.set('useNewUrlParser', true) mongoose.set('useFindAndModify', false); mongoose.set('useUnifiedTopology', true); mongoose.connect(app.$config.mongodb.url, app.$config.mongodb.options); // app上扩展两个属性 app.$mongoose = mongoose; app.$db = mongoose.connection } // 初始化model文件夹 let model = {}; scanFilesByFolder('../model',(filename, modelConfig)=>{ model[filename] = modelConfig({...app, mongoose}); }); return model; } // 初始化中间件middleware function initMiddleware(app){ let middleware = {} scanFilesByFolder('../middleware',(filename, middlewareConf)=>{ middleware[filename] = middlewareConf(app); }) //初始化配置中间件 if(app.$config.middleware && Array.isArray(app.$config.middleware)){ app.$config.middleware.forEach(mid=>{ if(middleware[mid]){ app.$app.use(middleware[mid]); } }) } return middleware; } // 初始化扩展 function initExtend(app) { scanFilesByFolder('../extend',(filename, extendFn)=>{ app[filename] = Object.assign(app[filename] || {}, extendFn(app)) }) } //加载定时任务 function initSchedule(){ scanFilesByFolder('../schedule',(filename, scheduleConf)=>{ schedule.scheduleJob(scheduleConf.interval, scheduleConf.handler) }) } module.exports = { initConfig, initController, initService, initRouter, initModel, initMiddleware, initExtend, initSchedule }
至此我们完成了该封装的核心加载部分,在app.js中引入/core/index.js
工程入口app.js中引用core创建实例 const Application = require('./core'); const app = new Application(); app.start(app.$config.port || 3000);
这样就启动了一个后端服务,接下来实现个简单的查询接口
/model/user.js module.exports = app => { const { mongoose } = app; const Schema = mongoose.Schema // Schema const usersSchema = new Schema({ username: { type: String, required: [true,'username不能为空'] }, password: { type: String, required: [true,'password不能为空'] }, name: { type: String, default: '' }, email: { type: String, default: '' }, avatar: { type: String, default: '' } }, {timestamps: {createdAt: 'created', updatedAt: 'updated'}}) return mongoose.model('user', usersSchema); };
// /service/user.js module.exports = app => ({ // 获取个人信息 async getUser() { return await app.$model.user.find(); } });
// /controller/user.js module.exports = app => ({ // 获取用户信息 async getUser() { let {ctx, $service} = app; let userData = await $service.user.getUser(); ctx.body = userData; } })
module.exports = app => { const { router, $controller } = app; // 示例接口 router.get('/userlist', $controller.user.getUser); return router };
这样就完成了简单接口示例。npm run dev 可以访问http://localhost:3000/userlist
访问该接口
以上就是我自己对koa2实现mvc自己的思路和理解,同时向egg致敬,也欢迎各路大神指正和批评。