背景
Express和Koa作为轻量级的web框架,没有任何约束的框架在一开始的时候会非常的爽快,开发几个demo,手到擒来,但是一旦代码真正上去的时候(而且一定会),你就会发现,大量重复的操作,重复的逻辑。导致项目的复杂度越来越高,代码越来越丑,非常的难以维护。我的quark-h5也是开始随意的写,写到最后只能重构一波了。正好期间做了个在线文档管理的项目用了egg.js,让我这种 node 小白有眼前一亮的感觉,重构quark-h5 server端就参考egg.js实现基于koa2的MVC结构
Github: 传送门
koa mvc工程目录结构规划
这里是参考 eggjs 的目录结构,这样的目录结构就非常清爽,构建一个应用也因为我们封装得体,只需要几行代码就可以实现
mvc的基本加载流程
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);
这样就启动了一个后端服务,接下来实现个简单的查询接口
接口示例
1、创建用户model, /model文件夹下新建user.js
/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);
};
2、创建user查询service, /service目录下新建user.js
// /service/user.js
module.exports = app => ({
// 获取个人信息
async getUser() {
return await app.$model.user.find();
}
});
3、创建user控制器, /controller文件夹下创建user.js
// /controller/user.js
module.exports = app => ({
// 获取用户信息
async getUser() {
let {ctx, $service} = app;
let userData = await $service.user.getUser();
ctx.body = userData;
}
})
4、添加router配置
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致敬,也欢迎各路大神指正和批评。