koa2开发后台服务 - (9) 服务端设计架构

背景说明

前端这几年一直被主流的三大框架所引导,MVC和MVVM等名词相信大家也并不陌生,我最早接触到框架是2016年,当时用的是AngualarJs1.5.8的版本,这个版本也是组件化的开始。我们会把页面的一部分试图和它对应的逻辑抽离成一个组件,一方面是隔离上下文,另一方面是为了多处复用。

当时写一个组件,大概包括一下几个部分:

1.一个view的template模板,基本上由原生的html以及自定义的组件构成,主要负责页面视图和事件绑定。
2.view对应的controller控制器,来处理页面中的主要逻辑。
3.service/factory用来处理controller中从后台获取的数据,以保证controller只处理业务逻辑,不包含其他逻辑。
4.constant用来集中统一的管理service中需要使用的常量。

当然在全局,也有一个util和config文件夹对整个工程以及通用配置和工具做了封装,以便业务中多出复用。

说了这么多,其实也是最近在学习koa2来构建后台服务有一些感悟,软件开发设计,前后端都有类似的经验,人们在使用中可能需要各种调用,数据传来传去,看起来很复杂,但是当你到达一定的阶段,就会感悟到这些分层的设计,其实是为了更方便项目的管理和维护,也更科学的进行项目开发和使用。

  • 废话不多说了,我们来看看一个koa2的后台服务需要怎么进行架构设计。

koa2项目结构简述

koa2项目结构简述

  • node_modules
  • bin
    • www
  • src
    • routes
      • api
      • view
    • controller
    • service
    • middlewares
    • views
    • config
    • util
    • db
    • public
    • validate
    • cache
  • app.js
  • test
  • package.json
  • eslintrc.json
  • .eslintignore
  • .gitignore
  • README.md

在这个koa2的项目中,在koa-generator的基础上,持续优化了项目的结构,首先

  • node_modules和package.json我就不过多介绍了,其实就是管理整个项目依赖的nodejs中使用的模块包。
  • bin目录其实是整个koa2服务的入口,它的作用就是纯粹的搭建一个http-server
  • app.js中,就注册了koa2的http-server所需要的一些插件,例如处理postData,处理cookie-session,处理日志,处理异常捕获等。也就是因为这些插件,我们才不需要对http-server做这些基础的设置。app.js中,我们引入了koa-router,也就是路由。我们通过http请求后台资源,也就是通过路由来映射资源关系。
  • src目录中,包括了:

app.js中对路由的分发routes(包括json的数据返回api目录和html页面的返回view目录);

1
2
3
4
5
6
// 引入定义的两类路由
const userApi = require('./routes/api/users')
const userView = require('./routes/view/users')
// 注册路由
app.use(userApi.routes(), userApi.allowedMethods())
app.use(userView.routes(), userView.allowedMethods())

middleware对应了app.js中在分发路由的时候,需要使用的中间件函数,处理中间异常场景;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 用户信息校验
const userValidator = require('../../validator/user')

// 根据传入的validator生成不同的中间件
const genMiddleWare = require('../../middlewares/validator')

router.prefix('/api/user')

// 注册
router.post('/register', genMiddleWare(userValidator), async (ctx, next) => {
const { username, password, gender } = ctx.request.body
const createResult = await registerUser({ username, password, gender })
console.log('createResult', createResult)
ctx.body = createResult
})

validate则是使用json-schema对用户的输入数据进行格式校验。

  • 封装ajv

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    /**
    * @description ajv对定义的schema校验
    * @author unclePis
    */

    const ajv = require('ajv')

    const ajvInstance = new ajv({
    // allErrors:true // 输出所有的错误(比较慢)
    })

    /**
    * json-schema校验
    * @param {object} schema 校验规则
    * @param {object} data 带校验的数据
    */
    let validator = (schema, data = {}) => {
    const valid = ajvInstance.validate(schema, data)
    if(!valid) {
    return ajvInstance.errors[0]
    }
    }

    module.exports = validator
  • 根据json-schema语法定义schema

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const scheme = {
type: 'object',
properties: {
username: {
type: 'string',
pattern: '^[a-zA-Z][a-zA-Z0-9_]+$',
minLength: 2,
maxLength: 255
},
password: {
type: 'string',
minLength: 3,
maxLength: 255
}
}
}
  • 使用ajv对定义的schema进行校验,并抽离成中间件

    controller对应了api中需要对路由的处理;

service对应了controller中需要对底层数据库获取数据的处理以及统一数据格式化输出;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* @description 对返回的数据做统一处理
* @author unclePis
*/
/**
* 基础模型
*/
class BaseModel {
constructor({ errorno, data, msg }) {
this.errorno = errorno

if(msg) {
this.msg = msg
}

if(data) {
this.data = data
}
}
}

/**
* 成功的模型
*/
class SuccessModel extends BaseModel {
constructor(data = {}) {
super(
{
errorno: 0,
data
})
}

}

/**
* 失败的模型
*/
class ErrorModel extends BaseModel {
constructor({ errorno, msg }) {
super({
errorno,
msg
})
}

}

module.exports = {
SuccessModel,
ErrorModel
}

views中是ssr后端模板渲染使用的一些ejs的模板,在一般的基于restful的前后分离的项目中,都是通过json进行通信的,可能用不到这部分。

config则是对mysql/redis数据库进行连接的基础配置,以及对于项目中使用的const常量和一些密钥的统一管理配置。

db则是对mysql数据库,使用sequelize进行ORM的数据建模。

public是整个项目中使用的静态资源的托管

cache是对redis来缓存用户数据的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
* @description 连接redis的方法
* @author unclepis
*/

const redis = require('redis')

const { redisConfig } = require('../conf/db')


// 创建redis客户端

const redisClient = redis.createClient(redisConfig.port, redisConfig.host)
redisClient.on('error', err => {
console.log('redis error', err)
})

// redis get

/**
*
* @param {string} key key
*/
let get = (key) => {
return new Promise((resolve, reject) => {
redisClient.get(key, (err, val) => {
if(err) {
reject(err)
return
}

if(!val) {
resolve(null)
return
}

try {
resolve(JSON.parse(val)) // 尝试进行json转换
} catch(error) {
resolve(val)
}
})
})


}


//redis set
/**
*
* @param {string} key key
* @param {*} value value
* @param {*} timeout 过期时间 单位是s
*/
let set = (key, value, timeout = 60 * 60) => {
if(typeof value === 'object') {
value = JSON.stringify(value)
}
redisClient.set(key, value)
redisClient.expire(key, timeout)
}

module.exports = {
get,
set
}

util 则是全局通用的工具方法,例如使用nodejs的crypto对密码进行Hmac加密/和获取环境变量等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
   /**
* @description Hmac 算法 - 也叫加盐算法,是将`哈希算法`与`一个密钥`结合在一起,用来阻止对签名完整性的破坏
* @author unclePis
*/

const crypto = require('crypto')
const { SECRET } = require('../conf/secret')

/**
* md5加密
* @param {string} sourceContent 密码
*/
function _Hmac(sourceContent) {
return crypto
.createHmac('sha1', SECRET)
.update(sourceContent)
.digest('hex')
}

module.exports = {
_Hmac
}
  • 剩下的文件包括jest进行单元测试的test目录,以及eslint对代码风格统一的配置,以及git进行版本管理的配置。

总结

到这里,一个项目的结构也就完整了,其他的也就是在处理不同的业务,跟底层数据库的一些交互。

初到贵宝地,有钱的给个钱场,没钱的挤一挤给个钱场