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

背景

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

更多推荐

你可能感兴趣的