動態

詳情 返回 返回

基於Koa2打造屬於自己的MVC框架,仿egg的簡易版本 - 動態 詳情

背景

Express和Koa作為輕量級的web框架,沒有任何約束的框架在一開始的時候會非常的爽快,開發幾個demo,手到擒來,但是一旦代碼真正上去的時候(而且一定會),你就會發現,大量重複的操作,重複的邏輯。導致項目的複雜度越來越高,代碼越來越醜,非常的難以維護。我的quark-h5也是開始隨意的寫,寫到最後只能重構一波了。正好期間做了個在線文檔管理的項目用了egg.js,讓我這種 node 小白有眼前一亮的感覺,重構quark-h5 server端就參考egg.js實現基於koa2的MVC結構

Github: 傳送門<br/>

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

更多推薦

  • Vue + Koa從零打造一個H5頁面可視化編輯器——Quark-h5<br/>
  • egg+vue+mongodb實踐開發在線文檔管理平台——水墨文檔<br/>

Add a new 評論

Some HTML is okay.