前段时间公司有几个前端lead的项目,全由前端自主完成。开发过程中在node端重新搭建了一套通用的基础框架,主要基于koa+koa-joi-router+mysql+ioredis搭建,在此简单记录一下框架的整体架构。
模块
- koa
- koa-body // 帮助解析 http 中 body 部分的中间件
- koa-joi-router // 帮助结合joi验证功能的路由中间件
- nodemon //热更新node
- mysql2 // mysql数据库
- ioredis // redis数据库
业务分层
Router(路由层)
定义应用的路由规则,将不同的URL路由映射到相应的Controller上。
Controller层(控制器层)
负责接收客户端的请求,处理输入参数,并用相应的Service方法来完成业务逻辑。
Service层(服务层)
核心的业务逻辑、对数据的处理、计算、增删改查,与数据库的交互
Middleware层(中间件层)
处理通用的、跨请求的功能,日志、鉴权、错误处理、跨域等。
Config层(配置层)
mysql、redis数据库连接配置、端口配置、集群配置等。
server
-routers
-api1Router.js
-api2Router.js
-index.js
-controllers
-controller1.js
-controller2.js
-service
-service1.js
-service2.js
-index.js
-middleware
-cors.js
-auth.js
-index.js
-config
-dev-config.js
-rpod-config.js
utils
-mysqlUtil.js
-redisUtil.js
-cookies.js
-log.js
app.js // 入口文件
各层业务介绍(伪代码)
入口文件app.js
const Koa = require('koa');
const routers = require('./server/routers');
const middleware = require('./server/middleware');
const app = new Koa();
middleware(app);
routers.apiRouters(app);
app.listen(3000);
Middleware层(中间件层)
const cors = require('./cors');
const auth = require('./auth');
const { koaBody } = require('koa-body');
module.export=function(app){
cors(app) // 处理跨域
app.use(koaBody({ // 格式化响应体
multipart: true,
formLimit: '300mb',
}))
auth(app) // 处理鉴权
。。。
}
// cors.js (也可以使用 koa-cors插件实现)
module.exports = function(app) { // 这块还有待优化,当时post请求被OPTIONS阻塞了
app.use(async (ctx, next)=>{
if(ctx.method==='OPTIONS'){ // 避免post请求被卡住
ctx.status = 204;
ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Headers', 'X-Requested-With,content-type');
ctx.set('Access-Control-Allow-Credentials', true);
ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
ctx.body = '';
await next()
}else{
await next()
}
})
app.use(async (ctx, next)=>{
const whiteOrigin = ['http://aaa.com'];
const origin = ctx.header.origin;
if(whiteOrigin.includes(origin)){ // 白名单内允许跨域
ctx.set('Access-Control-Allow-Origin', origin);
}
ctx.set('Access-Control-Allow-Headers', 'X-Requested-With,content-type');
ctx.set('Access-Control-Allow-Credentials', true);
ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
await next();
})
}
Router(路由层)
const apiRouter = require('koa-router')();
const api1Router = require('./api1Router');
const api2Router = require('./api2Router');
const routers = {
apiRouters: (app) => {
apiRouter.use('/base1', api1Router.middleware()); // 子路由1
apiRouter.use('/base2', api2Router.middleware()); // 子路由2
app.use(apiRouter.routes());
},
};
-----------------------------------------------------
api1Router.js / api2Router.js
-----------------------------------------------------
const controller1 = require('../controller/controller1');
// 主要使用koa-joi-router
const router = require('koa-joi-router');
const Joi = router.Joi;
const router1 = router();
router1.get(
'xxx.json',
validate: {
continueOnError: true,
query: {
name: Joi.required(),
id: Joi.required(),
optional: Joi.optional(), // 可选
},
},(ctx)=>{
if (ctx.invalid) {
logger.error(ctx.invalid.msg || '参数错误'); //日志
throw new Error(APIError.Type.ERR_JOI_VALIDATE(ctx));
}
await controller1.getData(ctx);
},
router1.post(
'xxx.json',
validate: {
type: 'json',
body: Joi.object({
id: Joi.string().required(),
name: Joi.string().required(),
}).options({
allowUnknown: true,
}),
continueOnError: true,
},(ctx)=>{
if (ctx.invalid) {
ctx.type = 'application/json';
ctx.body = {
data: '参数错误',
error_code: 400,
error_description: '',
};
logger.error(ctx.invalid.msg || '参数错误');
return;
// throw new APIError(APIError.Type.ERR_JOI_VALIDATE(ctx)); 之前直接报错,被爬虫把sre报错日志打爆表,不能再这么处理
}
await controller1.setData(ctx);
}
)
module.exports = router;
Controller层(控制器层)
const service1 = require('../server/server1');
class Controller1 {
async getData(ctx) {
const {
id,
optional
} = ctx.request.query;
const result = await service1.getData(id,optional);
ctx.type = 'application/json';
ctx.body = {
data: result,
error_code: 0,
error_description: '',
};
}
}
module.exports = new Controller1();
-------------------------------------
class Controller2 {
async setData(ctx) {
const {
id, name, optional,
} = ctx.request.body;
const result = await service1.setData(id, name, optional);
ctx.type = 'application/json';
ctx.body = {
data: result,
error_code: 0,
error_description: '',
};
}
}
module.exports = new Controller2();
Service层(服务层)
// service1.js
const db = require('../utils/mysqlUtil');
const TABLE_NAME = 'TABLE_NAME'; // 表名
const SQL = {
insert: `INSERT INTO ${TABLE_NAME}(id,name,optional) VALUES(?,?,?)`,
queryByID:(id)=> `SELECT * FROM ${TABLE_NAME} WHERE id=${id}`,
};
class Service1 {
constructor(){
}
async getData(id){
try {
const [info] = await db.query(SQL.queryByID(id));
if (info.length) {
return info[0];
}
throw new Error(APIError.Type.ERR_NOT_FOUND);
} catch (error) {
if (error.code) {
throw new Error(error);
} else {
throw new Error({
status:500,
code: 1000,
message: '未知错误',
});
}
}
}
async setData(id,name, optional){
try {
const [info] = await db.query(SQL.insert, [id,name,optional]);
const [ret] = await db.query(SQL.queryByID(info.id));
return ret[0];
} catch (error) {
switch (error.code) {
case 'ER_DUP_ENTRY':
throw new Error({
status: 400,
code: 3000,
message: '数据查询失败,未找到元素',
});
default:
throw new Error({
status:500,
code: 1000,
message: '未知错误',
});
}
}
}
}
mysql2的链接及使用
const mysql = require('mysql2/promise');
const config = require('../../config');
const poolCluster = mysql.createPoolCluster();
poolCluster.add('MASTER', config.mysql);
poolCluster.add('SLAVE', config.mysqlSlave);
connection.escape = mysql.escape;
module.exports = connection;
redis的链接
const Redis = require('ioredis');
const config = require('../../config');
let redis;
if (process.env.NODE_ENV === 'development') {
redis = new Redis(config.redis);
} else {
redis = new Redis.Cluster(config.redisCluster);
}
module.exports = redis;
Config层(配置层)
// 根据rocess.env.ENV_TAG 读取不同环境的不同配置
switch(rocess.env.ENV_TAG){
case 'dev':
config =dev-config;
case 'prod':
config = prod-config;
}
// - dev-config.js
mysql:{
host:'',
user:'',
password:'',
database:''
},
mysqlSlave:{
},
redis:{
host: '127.0.0.1',
port: 6379,
},
redisCluster:{
}
其他补充
- 因为不需要打包,直接起服务即可,所以用不到webpack\vite打包工具
启动命令:"dev": NODE_ENV=development nodemon app.js, "start": "NODE_ENV=production node app.js",
- 需要一些babel、lint工具来兼容代码及规范格式
- 需要监控、log工具来记录日志及报错信息
- 错误处理等可以根据实际情况来根据不同的错误类型封装成类
以上及一些其他细节自行根据需求配置即可。
ps:对koa2的实现原理感兴趣的请导航到https://www.jianshu.com/p/56d572042839
致敬那些曾经一起奋斗伙伴们