众所周知,Egg.js是由阿里开源的一款基于Koa的Node框架,上手也很简单,我们今天就来实现一个“企业级”的Node框架
如果你没有用过Egg,那么推荐你先去看一下官网文档,或者看我之前的教程,右上角搜索egg关键字应该可以找到。尝试一下编写代码,掌握了使用方法对实现有很大的帮助
以下内容默认你已经掌握了egg的使用方法
路由
作为服务端框架,路由是最基本的功能,egg框架的理念是约定大于配置,即所有的控制器都放在controller目录下、服务层都放在service目录下……
虽然在egg中,路由是一个单独的文件,但是这里我们也一起放在一个文件夹中,也方便以后分模块维护
我们先来构思一下整体框架
src
├── routes # 路由
│ └── index.js
├── controller # 控制层
│ └── index.js
├── service # 服务层
│ └── index.js
├── model # 模型
│ └── user.js
├── config # 配置
│ └── index.js
├── fgg-loader.js # 加载器
├── fgg.js # fgg构造器
└── app.js # 程序入口
在布局好整体框架之后我们开始编码,首先我们来实现loader,loader里面有一个load函数,用来加载不同目录下的文件,接收两个参数,一个文件夹名,一个回调函数,通过调函数返回文件名以及文件内容
const fs = require('fs')
const path = require('path')
// 读取目录下的文件
function load(dir, cb) {
// 绝对路径
const url = path.resolve(__dirname, dir)
// 读取文件
const files = fs.readdirSync(url)
// 遍历文件,解析到路由器
files.forEach(fileName => {
fileName = fileName.replace('.js', '')
// 导入文件,加载路由文件中导出的routes
const file = require(url + '/' + fileName)
cb(fileName, file)
})
}
有了load函数我们就用它来加载路由文件
const Router = require('koa-router')
function initRouter() {
const router = new Router()
load('routes', (fileName, routes) => {
// 前缀。index下没有前缀,否则文件名为前缀
const prefix = fileName === 'index' ? '' : '/' + fileName
Object.keys(routes).forEach(key => {
const [method, path] = key.split(' ')
// 映射路由
router[method](prefix + path, routes[key])
})
})
return router
}
我们这里需要规定路由文件的编写方式如下
module.exports = {
'get /': async ctx => {
ctx.body = '首页'
},
'get /detail': async ctx => {
ctx.body = '详情'
}
}
然后我们来看过程
通过load函数加载routes
目录下的文件,然后遍历读取出来的文件,通过调用回调函数将文件名和文件内容进行处理,如果文件名是index则默认/
,否则就是/filename
,然后通过解析路由对象属性名来获得HTTPMethod和path,然后将处理路由的中间件绑定在koa-router的实例上,最后返回这个路由实例
路由加载器我们已经完成了,然后我们来编写fgg的构造函数,因为是基于Koa的框架所以在创建构造函数时需要借助Koa
const Koa = require('koa')
const { initRouter } = require('./fgg-loader')
class Fgg {
constructor(conf) {
this.$app = new Koa(conf)
this.$router = initRouter()
this.$app.use(this.$router.routes())
}
start(port) {
this.$app.listen(port, () => {
console.log(`启动成功,端口${port}`)
})
}
}
module.exports = Fgg
最后我们来编写一个入口程序测试一下
const Fgg = require('./fgg')
const app = new Fgg()
app.start(3000)
看下效果,完成
控制器
在没有引入控制器之前我们的逻辑都放在了routes里面,显然这是不方便的,路由的功能应该只是引导至某一控制器,接下来我们来将控制器逻辑添加到fgg中
还是我们先定好使用规则,然后倒推实现。控制器依旧是约定在controller中,我们要将所有的controller目录中的控制器绑定到app实例的$controller
上
首先编写路由文件
module.exports = {
index: async app => {
app.ctx.body = '首页Ctrl'
},
detail: async app => {
app.ctx.body = '详情Ctrl'
}
}
这时我们上面的路由需要做一些改动,因为需要在路由中使用app实例寻找controller,所以我们需要将路由文件改成一个函数,返回值一个对象
module.exports = app => ({
'get /': app.$controller.index.index,
'get /detail': app.$controller.index.detail
})
然后修改initRouter函数,使它能够解析现在的路由文件,当然我们也可以兼容之前的
function initRouter(app) {
const router = new Router()
load('routes', (fileName, routes) => {
// 前缀。index下没有前缀,否则文件名为前缀
const prefix = fileName === 'index' ? '' : '/' + fileName
// 判断路由文件类型,如果是一个函数就执行,将app传入
routes = typeof routes === 'function' ? routes(app) : routes
Object.keys(routes).forEach(key => {
const [method, path] = key.split(' ')
// 映射路由
router[method](prefix + path, async ctx => {
app.ctx = ctx
await routes[key](app) // controller需要接收app
})
})
})
return router
}
然后我们来编写initController
函数,负责解析控制器,同样使用load函数,将解析出来的控制器对象返回
function initController() {
const controllers = {}
// 加载controller
load('controller', (fileName, controller) => {
controllers[fileName] = controller
})
return controllers
}
最后修改一下fgg的构造函数
const Koa = require('koa')
const { initRouter, initController } = require('./fgg-loader')
class Fgg {
constructor(conf) {
this.$app = new Koa(conf)
this.$controller = initController()
this.$router = initRouter(this) // 将app实例作为参数
this.$app.use(this.$router.routes())
}
start(port) {
this.$app.listen(port, () => {
console.log(`启动成功,端口${port}`)
})
}
}
module.exports = Fgg
完事,来看效果
服务层
虽然有了控制层,我们可以将路由的压力分担到控制层,但是所有的逻辑都混在控制层后期维护起来也是一个很头疼的事情,所以我们再抽离出服务层用于查询数据,服务层的使用方法同路由基本一致,跟前面基本一致这里也不再废话了
一般查询数据库都是异步的,所以我们使用setTimeout+Promise先来模拟一下
// /service/index.js
const delay = (data, time) => new Promise(resolve => {
setTimeout(() => {
resolve(data)
}, time)
})
module.exports = app => ({
getDetail() {
return delay('fgg: 企业级Node框架', 500)
}
})
// controller/index.js
module.exports = {
detail: async app => {
app.ctx.body = await app.$service.index.getDetail()
}
}
将service绑定在app.$servcie上,在controller调用service方法,然后又是熟悉的一步——解析service文件
function initService(app) {
const services = {}
load('service', (fileName, service) => {
// 规定service必须是一个函数,接收app参数
// (后面需要模型层调用model所以直接就设置成函数形式吧)
services[fileName] = service(app)
})
return services
}
然后在fgg.js构造函数中添加service
const Koa = require('koa')
const { initRouter, initController, initService, loadConfig} = require('./fgg-loader')
class Fgg {
constructor(conf) {
this.$app = new Koa(conf)
this.$service = initService(this)
this.$controller = initController()
this.$router = initRouter(this)
this.$app.use(this.$router.routes())
}
start(port) {
this.$app.listen(port, () => {
console.log(`启动成功,端口${port}`)
})
}
}
module.exports = Fgg
再来测试一下
模型层
最后就是与数据库的交互了,这里使用Sequelize做底层来实现ORM,需要安装sequelize依赖
首先在config配置文件中配置好数据库的信息
module.exports = {
database: {
dialect: 'mysql',
host: 'localhost',
database: 'example',
username: 'root',
password: '123456'
},
}
在fgg-loader中加入创建sequelize实例的代码,仍然是约定好的,放在config目录下,如果存在数据库配置就初始化数据库
function loadConfig(app) {
app.$model = {}
load('config', (file, config) => {
// 加载model
if (config.database) {
// 初始化数据库
app.$database = new Sequelize(config.database)
// 加载模型
load('model', (fileName, { schema, options }) => {
app.$model[fileName] = app.$database.define(fileName, schema, options)
})
app.$database.sync()
}
})
}
同时加载model目录下的模型,将定义好的模型挂载到app.$model上
在构造函数中添加配置解析
const Koa = require('koa')
const { initRouter, initController, initService, loadConfig} = require('./fgg-loader')
class Fgg {
constructor(conf) {
this.$app = new Koa(conf)
loadConfig(this)
this.$service = initService(this)
this.$controller = initController()
this.$router = initRouter(this)
this.$app.use(this.$router.routes())
}
start(port) {
this.$app.listen(port, () => {
console.log(`启动成功,端口${port}`)
})
}
}
module.exports = Fgg
模型定义如下(Sequelize的模型定义方式)
const { STRING } = require('sequelize')
module.exports = {
schema: {
name: STRING
},
options: {
timestamps: false
}
}
然后就可以直接使用定义好的模型进行数据库查询
// controller
module.exports = {
index: async app => {
app.ctx.body = await app.$service.index.getUsers()
},
}
// service
module.exports = app => ({
async getUsers() {
// app.$model.user.create({name: 'feng'})
const res = await app.$model.user.findOne({
where:
{ name: 'feng' }
})
return res
}
})
齐活
中间件
中间件的使用我们借助Koa的中间件机制,直接通过use使用中间件,就直接在config中一起加载,将中间件放在middleware属性下
module.exports = {
db: {
// ……
},
middleware: ['logger']
}
然后添加解析中间件的函数
function loadConfig(app) {
app.$model = {}
load('config', (file, config) => {
// 加载model
if (config.db) {
// 初始化数据库
app.$db = new Sequelize(config.db)
// 加载模型
load('model', (fileName, { schema, options }) => {
app.$model[fileName] = app.$db.define(fileName, schema, options)
})
app.$db.sync()
}
// 加载中间件
if (config.middleware) {
config.middleware.forEach(mid => {
const midPath = path.resolve(__dirname, "middleware", mid)
app.$app.use(require(midPath))
})
}
})
}
然后中间件的编写就跟我们以前写koa的中间件一样的
module.exports = async (ctx, next) => {
const start = new Date()
await next()
const time = new Date() - start
console.log(
`[logger]${start.getHours()}:${start.getMinutes()}:${start.getSeconds()}: ${ctx.method} ${ctx.path} ${ctx.status} ${time}ms`
);
}
一个简易的框架就搭好了