背景
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/>