基于Koa2打造属于自己的MVC框架,仿egg的简易版本

3,341

背景

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致敬,也欢迎各路大神指正和批评。

更多推荐