Open Foveluy opened 6 years ago
诚然,不幸的事情很快就发生了。我们的网站,不可能只有一个路由,而是一大堆路由,那么代码就会变成
//app.js
Router.get('/', (ctx, next) => {
ctx.body = 'hello';
})
Router.post('/create', (ctx, next) => {
ctx.body = 'hello';
})
Router.post('/post', (ctx, next) => {
ctx.body = 'hello';
})
Router.patch('/patch', (ctx, next) => {
ctx.body = 'hello';
})
...
一大堆的router 列表,一个成熟的大网站,势必有几千几万个路由组成,都写在一个文件中是不可能的,这样会导致程序无法维护。我们第一个要做就是路由拆分
最简单的路由拆分,也是github里面大量demo代码的做法,就是定义一个Router文件夹,把所有router代码拆分到各个router文件中去,导出,然后再app.js
进行引入。
第一步 在router文件中只做路由path和HTTP方法的导出:
//user.js
const mapRouter = require('../routerLoader').mapRouter;
const getUser = async (ctx, next) => {
ctx.body = 'getUser';
}
const getUserInfo = async (ctx, next) => {
ctx.body = 'getUserInfo';
}
/**
* 注意,我们规定HTTP方法放在前面,path放在后面,中间用空格隔开
*/
module.exports = {
'get /': getUser,
'get /getUserInfo': getUserInfo
}
第二步
我们先在app.js同级目录下添加一个routerLoader.js,然后添加以下的方法
//routerLoader.js
const router = require('koa-router');
const Router = new router();
const User = require('./router/user');//倒入模块
/**
* 添加router
*/
const addRouters = (router) => {
Object.keys(router).forEach((key) => {
const route = key.split(' ');
console.log(`正在映射地址:${route[1]}--->HTTP Method:${route[0].toLocaleUpperCase()}--->路由方法:${router[key].name}`)
Router[route[0]](route[1], router[key])
})
}
/**
* 返回router中间件
*/
const setRouters = () => {
addRouters(User);
return Router.routes()
}
module.exports = setRouters;
第三步
修改app.js
//app.js
const koa = require('koa');
const setRouters = require('./routerLoader');//引入router中间件
const app = new koa();
app.use(setRouters());//引入router中间件
app.listen(3000, '127.0.0.1', () => {
console.log('服务器启动');
})
到这里,我们完成了简单的路由拆分,由此我们引入了第一个规范:
上述的方法很好,将一大堆的路由,都拆成了小路由,并且没一个文件控制一个路由模块,每一个模块又有了自己的功能,非常的爽!我们维护项目的力度再次变得可控起来。
好景不长,当我们增加到100个路由模块的时候,我们就想哭了,routerLoader.js文件就会变成..
const User = require('./router/user');//倒入模块
const model2 = require('./router/model2');//倒入模块
.....//省略一大堆模块
const model100 = require('./router/model100');//倒入模块
/**
* 返回router中间件
*/
const setRouters = () => {
addRouters(User);
addRouters(model2);
...//省略一大堆模块
addRouters(model100);
return Router.routes()
}
module.exports = setRouters;
这个routerLoader.js又会变成非常长的一个文件,这显然不符合我们的要求,而且,我们每添加一个路由模块,就要跑到routerLoader.js中去添加两行代码,不仅容易犯错,还很烦人。
我们是否可以自动扫描router目录下的文件,让其自动帮我们加载呢?答案是:肯定的。
.........
/**
* 扫描目录
*/
const Scan = () => {
const url = './router';
const dir = fs.readdirSync(url)//同步方法无所谓的,因为是在服务器跑起来之前就完成映射,不会有任何性能影响
dir.forEach((filename) => {
const routerModel = require(url + '/' + filename);
addRouters(routerModel);
})
}
/**
* 返回router中间件
*/
const setRouters = () => {
Scan();
return Router.routes()
}
我们添加一个``Scan()函数,帮助我们去扫描硬编码目录
router```下的所有文件,然后循环自动倒入router中,最后返回这个router。那么我们现在无论是增加,删除路由模块,都不需要再动routerLoader.js模块了。
我们只需要疯狂的飙一下业务代码在router之下就可以了,专注于router中的东西
很高兴,我们的router模块变成了
//user.js
const mapRouter = require('../mapRouter').mapRouter;
const getUser = async (ctx, next) => {
ctx.body = 'getUser';
}
const getUserInfo = async (ctx, next) => {
ctx.body = 'getUserInfo';
}
/**
* 注意,我们规定HTTP方法放在前面,path放在后面,中间用空格隔开
*/
module.exports = {
'get /': getUser,
'get /getUserInfo': getUserInfo
}
这样的一种形式,当我们想要增加一个模块的时候只需要添加一个文件,并做好映射导出就可以了,极大的增加了我们的开发效率,更加的规范化,bug就意味着更少。
但是,这样的一种形式仍然有问题没解决,从宏观上来看我们处理业务的流程是:
用户请求->到达router->处理业务->返回请求给用户
业务处理阶段
在业务处理阶段,我们最好,也是最推荐的,把业务逻辑与控制流程分开,这是什么意思呢?比如我们早起刷牙吃早餐去上班
这件事用为代码表示:
const gotoWork = () => {
起床();//隐藏了如何起床的细节,比如被闹钟吵醒,自然醒
刷牙();//隐藏了如何刷牙的细节,风骚或者不风骚的刷牙手法
完成早餐();//隐藏了如何做早餐的各种细节
去工作();//隐藏了如何去工作的细节,比如坐什么车
}
然后我们分别把起床()
、刷牙()
,完成早餐()
,去工作
,这几个函数的内部细节完善,这样我们就拥有了一个gotoWork controller
。这么做的好处很明显:
这两点在开发中至关重要,如何控制项目的复杂度,以及不要重复写代码,就靠的把业务逻辑与控制流程分开的约定。
我们已经有了两组概念,控制流程放在控制器(controller),那业务逻辑我们也给他安排一个service。service的作用其实就是为了封装一下业务逻辑,以便哪里再次使用,以及方便做单元测试。
很多人不明白,为什么要把事情搞得那么复杂,又分为控制器,又分为业务逻辑。对于还没有太多业务经验的同学来说,肯定要骂街,但是思考一下以下的场景:
这就很明显了,当我们把业务逻辑和控制流程分开以后,我们的代码可以做到最大程度的复用。
创建一个controller目录,我们规定所有的xxxcontroller.js一定要写在controller目录下,这是我们引入的第二条规范。
在controller目录下创建user.js
:
// controller/user.js
module.exports = {
async getUser(ctx) {
ctx.body = 'getUser';
},
async getUserInfo() {
ctx.body = 'getUserInfo';
}
};
对我们的方法进行导出,这里很简单就不多做解释。
新增controllerLoader.js在根目录下,其实很简单就是为了扫描controller目录中的文件,并以一个数组返回
const fs = require('fs');
function loadController() {
const url = './controller';
const dir = fs.readdirSync(url)//同步方法无所谓的,因为是在服务器跑起来之前就完成映射,不会有任何性能影响
return dir.map((filename) => {
const controller = require(url + '/' + filename);
return { name: filename.split('.')[0], controller };
})
}
module.exports = loadController;
这里其实没做什么复杂性操作,就是把目录扫描以后,导出一个数组对象,这个对象里存的就是controller对应的文件名字,以及controller方法.
我们将获得的controller,绑定在koa上,新增下面的代码:
......
//app.js
const koa = require('koa');
const setRouters = require('./routerLoader');//引入router中间件
//新增的代码
const controllerLoader = require('./controllerLoader');
const controllers = controllerLoader();
koa.prototype['controller'] = {};
controllers.forEach((crl) => {
koa.prototype.controller[crl.name] = crl.controller;
})
const app = new koa();
app.use(setRouters(app));//引入router中间件,注意把app的实例传进去
app.listen(3000, '127.0.0.1', () => {
console.log('服务器启动');
})
我们新增了一坨代码,其实可以封装到controllerLoader中去,不过无所谓啦,先这么搞着。新增的这一坨代码目的就是把我们刚刚导出的数组,全部都映射到koa这个类的原型下,当new一个koa对象的时候,就会拥有这些方法了。
注意app.use(setRouters(app));
这里我们将app传入到setRouters
我们自己写的中间件中去,具体要干嘛,往下看。
//routerLoader.js
const router = require('koa-router');
const Router = new router();
const fs = require('fs');
/**
* 返回router中间件
*/
const setRouters = (app) => {
const routers = require('./routers')(app);//在这里使用app
Object.keys(routers).forEach((key) => {
const [method, path] = key.split(' ');
Router[method](path, routers[key])
})
return Router.routes()
}
module.exports = setRouters;
没错,我们app实际上就是用来传递参数的....具体传递去哪里,你可以在routers.js
中看到(等一下创建)
和之前一样,我们规定导出路由的方式是http method + 空格 + path
,这样比较傻瓜,也方便我们写方法。
module.exports = (app) => {
return {
'get /': app.controller.user.getUser
}
}
可以看到,我们的app,用在这里,用于获取controller中的各种方法.
删除之前的router文件夹。到此,我们的controller实现了,并且把他挂载到了koa这个类的原型上,我们将router和控制器分离,把路径都集中在一个文件里管理,controller只负责控制流程。
引入Service的概念就是为了让控制器和业务逻辑完全分离,方便做测试和逻辑的复用,极大的提高我们的维护效率。
同样的,我们引入一个规范,我们的service必须写在service文件夹中,里面的每一个文件,就是一个或者一组相关的service.
在根目录下,新建一个service文件夹:
新增一个service文件就叫它userService
吧!
// service/userService.js
module.exports = {
async storeInfo() {
//doing something
}
};
好了,我们可以在任意时候使用这个函数了,非常简单。
我们在写controller业务控制的时候,我们不得不在使用的时候,就去引入一下这个文件
const serviceA = require('./service/serviceA')
,这种代码是重复的,不必要的,非常影响我们的开发速度
我们来回顾一下controller中的一些逻辑
// controller/user.js
module.exports = {
async getUser(ctx) {
ctx.body = 'getUser';
},
async getUserInfo(ctx) {
ctx.body = 'getUserInfo';
}
};
我们可以看到,在每一个函数中,我们基本上都会使用到ctx
这个变量,那为什么我不能学koa一样,把这个service也像参数一样传递进来呢?
const fs = require('fs');
function loader(path) {
const url = __dirname + '/controller';
const dir = fs.readdirSync(url)//同步方法无所谓的,因为是在服务器跑起来之前就完成映射,不会有任何性能影响
return dir.map((filename) => {
const module = require(url + '/' + filename);
return { name: filename.split('.')[0], module };
})
}
function loadController() {
const url = __dirname + '/controller';
return loader(url);
}
function loadService() {
const url = __dirname + '/service';
return loader(url);
}
module.exports = {
loadController,
loadService
};
代码也非常傻瓜,其实就是去扫描一下service下面的文件夹,并且返回一下,然后把controllerLoader.js改名叫Loader.js,表示这个文件里都是loader.
然后修改一下我们routerLoader.js
//routerLoader.js
const router = require('koa-router');
const Router = new router();
const fs = require('fs');
const services = require('./controllerLoader').loadService();//这里引入service
/**
* 返回router中间件
*/
const setRouters = (app) => {
const routers = require('./routers')(app);
const svs = {};
services.forEach((service) => {
svs[service.name] = service.module;
})
Object.keys(routers).forEach((key) => {
const [method, path] = key.split(' ');
Router[method](path, (ctx) => {
const handler = routers[key];//注意这里的变化
handler(ctx, svs);//注意这里的变化
})
})
return Router.routes()
}
module.exports = setRouters;
这一段的代码变化其实就是把作用域拉长了一点,使得在调用路由方法的时候,给所有的方法添加一个svs参数,也就是我们的service.
// controller/user.js
module.exports = {
async getUser(ctx, service) {
await service.userService.storeInfo();//开心的使用service
ctx.body = 'getUser';
},
async getUserInfo(ctx) {
ctx.body = 'getUserInfo';
}
};
我们的工作目录还比较乱,接下来我们对目录进行一些简单的调整:
我们给我们的框架叫做kluy
,新建一个kluy目录,新建一个core.js
const koa = require('koa');
const fs = require('fs');
const koaRoute = require('koa-router');
class KluyLoader {
removeString(source) {
const string = 'kluy';
const index = source.indexOf(string);
const len = string.length;
return source.substring(0, index);
}
loader(path) {
const dir = fs.readdirSync(path)//同步方法无所谓的,因为是在服务器跑起来之前就完成映射,不会有任何性能影响
return dir.map((filename) => {
const module = require(path + '/' + filename);
return { name: filename.split('.')[0], module };
})
}
loadController() {
const url = this.removeString(__dirname) + '/controller';
return this.loader(url);
}
loadService() {
const url = this.removeString(__dirname) + '/service';
return this.loader(url);
}
}
class Kluy extends koa {
constructor(props) {
super(props);
this.router = new koaRoute();
this.loader = new KluyLoader();
const controllers = this.loader.loadController();
this.controller = {};
controllers.forEach((crl) => {
this.controller[crl.name] = crl.module;
})
}
setRouters() {
const _setRouters = (app) => {
const routers = require('../routers')(app);
const svs = {};
app.loader.loadService().forEach((service) => {
svs[service.name] = service.module;
})
Object.keys(routers).forEach((key) => {
const [method, path] = key.split(' ');
app.router[method](path, (ctx) => {
const handler = routers[key];
handler(ctx, svs);
})
})
return app.router.routes()
}
this.use(_setRouters(this));
}
}
module.exports = Kluy;
上述的代码其实做了一件非常简单的事情。就是把之前的所有启动前初始化的代码,全部封装到了我们的框架类kluy中,然后导出。这么做的好处就是:
我们在一开始的app.js中就可以这么写了
//app.js
const kluy = require('./core');
const app = new kluy();
app.setRouters();
app.listen(3000, '127.0.0.1', () => {
console.log('服务器启动');
})
.
├── package-lock.json
├── package.json
└── src ──>项目代码
├── controller ──>控制器代码目录
│ └── user.js
├── kluy ──>框架代码目录
│ ├── app.js
│ └── core.js
├── routers.js ──>路由器的导出
└── service ──>业务逻辑代码目录
└── userService.js
由此,我们的目录就变成了这么一个清爽的结构,构建一个应用也因为我们封装得体,只需要几行代码就可以实现
到目前为止,我们对我们的项目引入了三个规范
controller
,专门处理业务的控制流程,尽量不出现任何的业务逻辑,而且controller必须放在controller文件夹中,否则无法读取到
router
,路由的设置,我们全部放在了routers.js
中,集中化管理,使得我们的路由、Http方法不会因为散落各地而难以查找
service
,业务逻辑与控制器完全分离,不依赖于控制器,能够方便你的逻辑复用和单元测试
全自动按目录加载:所有的代码,类,都按照规范写好后,就能够全自动的导入到项目中,无需人力再进行对这种无用但是又容易出错的操作进行乱七八糟的维护,极大的提升了我们开发业务代码的效率。
或许聪明的你已经发现了这么做的好处:超出控制范围的代码框架连启动都无法启动,比如有人不爽,想到处写业务逻辑,boom,爆炸。
又比如,有人想到处乱写router,boom爆炸。
由此,我们得出了一个深刻的道理:
一定限制和约束,是企业级(包括个人)大项目所必须的
在我们的项目中,有很多东西是需要我们使用硬编码去书写的,例如,启动ip地址,端口,数据链接端口,数据库名字,密码,跨域的一些http请求等等。
我曾经看过一些非常不规范的开发,把各种硬编码写入逻辑中,有时候,线上和线下的配置是完全不一样的,维护起来那叫一个要命。
按照之前我们的思路,我们可以将配置,写入一个config文件夹中,用名字的限制来区分我们线下,线上的配置
同样我们可以使用前面类似的方法进行对config文件夹中的东西进行扫描,然后自动加载到项目中去。
在考虑如何实现config自动挂载之前,我们得思考一下一个问题:挂去哪里?
我们决定,将config绑定在kluy的实例上:
在kluyLoader类中添加一个方法:
class KluyLoader {
....
loadConfig() {
const url = this.removeString(__dirname) + '/config';
return this.loader(url);
}
.....
}
class Kluy extends koa {
constructor(props) {
super(props);
this.router = new koaRoute();
this.loader = new KluyLoader();
const controllers = this.loader.loadController();
this.controller = {};
controllers.forEach((crl) => {
this.controller[crl.name] = crl.module;
})
this.config = {};//加载config
this.loader.loadConfig().forEach((config) => {
this.config = { ...this.config, ...config.module }
})
}
setRouters() {
const _setRouters = (app) => {
const routers = require('../routers')(app);
const svs = {};
app.loader.loadService().forEach((service) => {
svs[service.name] = service.module;
})
Object.keys(routers).forEach((key) => {
const [method, path] = key.split(' ');
app.router[method](path, (ctx) => {
const handler = routers[key];
handler(ctx, svs, app);//将app,传递给controller
})
})
return app.router.routes()
}
this.use(_setRouters(this));
}
}
上述代码在loader类中加上一个loadconfig方法。然后便可以在我们的kluy类中加载config,最后,传递给controller。
// controller/user.js
module.exports = {
async getUser(ctx, service, app) {
app.service.name...//这里使用
await service.userService.storeInfo();//开心的使用service
ctx.body = 'getUser';
},
async getUserInfo(ctx) {
ctx.body = 'getUserInfo';
}
};
通过上述几个步骤和规范,我们就定制了一套初级低能儿版的企业级框架。虽然是低能儿版本,但是相比于基础koa框架来说,已经强大得很多了。
.
├── package-lock.json
├── package.json
└── src
├── config ──────>项目配置
│ └── dev.js
├── controller ──────>控制器逻辑
│ └── user.js
├── kluy ──────>框架底层
│ ├── app.js
│ └── core.js
├── routers.js ──────>router模块
└── service ──────>业务逻辑层
└── userService.js
然而,企业级框架还是比较复杂的,我们仍然需要考虑更多的情况;
orm的引入能够极大的提升我们的开发效率,在项目越早的时期引入,优势越明显。然而引入orm也是需要一套规范的。
koa这种基础框架,是没有安全防范的,需要开发者自己去做。
分别为开发自动重启以及线上部署自动重启
web应用中需要有一套完整的机制来完成日志记录
对于nodejs开发,我正在使用的一套框架是eggjs,基于koa开发,有完整的生态系统,插件机制,规范公约,阿里内部也在使用。
全文完
Nodejs&koa:摆脱黑工坊发展出一款企业级框架
说着也是奇怪,nodejs发展那么多年了,基础框架依旧横行霸道,当你提到nodejs的时候肯定会有人说koa/express 云云,然后随便搜索一下教程,就是教你如何制作一款博客。
诚然,nodejs强大的能力可不是给大家单单用来制作一款博客的...
无论是express还是koa,都是属于基础框架。我认为基础框架和企业级框架有两点是不同的:
没有任何约束的框架在一开始的时候会非常的爽快,开发几个demo,手到擒来,但是一旦代码真正上去的时候(而且一定会),你就会发现,大量重复的炒作,重复的逻辑,以及无法做单元测试。导致项目的复杂度越来越高,代码越来越丑,非常的难以维护。
为框架添加一些约束,就会增加其难用程度,学习成本变高,很多新手就会觉得:哎哟,我这样写逻辑也是可以的嘛,为什么要搞那么复杂?
编程就是这样,如果你真正接触过一个从零到有的项目,你就会知道,很多东西你刚开始逃避的,到最后你就得全部加回来,一个不少!话虽如此,跑题有甚,今天我们就来看看,如何将基础框架koa变成一款企业级框架。
koa基础
如下几个步骤,就能让你在你的目录下搭建一个koa2的项目,极其简单
访问
http://127.0.0.1:3000/
就能看到我们的hello.是的,简简单单的几步,我们就能够搭建起一个非常简单的koa服务器了。