生命不息,重构不止vue
这不是一篇纯技术文章,只是一篇对这段是重构后端的总结node
国庆先后差很少一个半月的时间,把本身的网站从数据库,到后端,再到A端和C端整个都重构了一篇,国庆7天妥妥地宅 在家里码代码,好在目前来看完成度仍是达到个人预期的,虽然说没有变的多高大上,可是好歹项目比之前工程化了一些,这个重构过程虽然漫长,可是确实仍是有着本身的一些体会的。接下来会分三篇文章来介绍重构经历——后台服务篇、Nuxt应用篇和Docker集成篇git
博客地址:jooger.megithub
总共差很少200个commits吧,欢迎star,欢迎留言 😄。废话很少说,先来看下后端的重构经历吧mongodb
缘由有如下几点docker
遇到没用过的就想玩一下,后续有可能还会在来个Nest版也说不定(很喜欢注解这种形式)数据库
之前以为日志啥的不重要,没有造成日志备份,因此不少次线上故障缘由都无从查证,只能说之前太年轻express
之前是用pm2-deploy手动部署,每次都是看着console等部署完成,哈哈,“刀耕火种”,如今是用docker+jenkins,配合github webhook和阿里镜像容器实现自动化部署后端
这个没啥好说的,逻辑层和controller层混合在一块儿,复用性差,重构是迟早的事儿
至于为啥没用TypeScript
,我只想说我最开始是用了TS的,也搜了一些文章,可是使用起来莫名其妙的很不爽,而后就放弃了,不过其余俩项目都是用TS重构的
之前用的是“常规操做-Koa
,配合上一些插件,还算不错
重构后用的是阿里开源的Egg,文档是真心好评,虽然文档我没有彻底看完整(进阶那部分略微搂了两眼),特别是《多进程模型和进程间通信》那一节讲的真的很详细,而且图文并茂地介绍了Egg在多进程架构下的实践,对于我这种接触Node直接pm2,没有接触过cluster
的人颇有帮助。
目前社区的优质插件的话我搜了下,也很多了,没有尝试过的能够玩儿一下,另外还推荐一下Nest.js框架,基于express
的,我只大体看了几眼,发现跟Srping很像,之后说不定会用这个再重构下
数据库这边我一直用的mongodb
,driver用的mongoose
,此次重构主要是重构了下setting
表,而且新增了notification
和stat
表
setting
表主要存网站的配置,分四个部分
site
C端的一些配置personal
我的信息keys
一些第三方插件的参数,好比阿里云OSS的,Github,阿里node平台(这个稍后要讲),我的邮箱的一些配置至于keys
,之前的server启动时,一些服务的初始化参数每每都是在集成工具里配置的,我这边将其迁移到数据库中存储了,server启动前先从数据库中加载这些配置参数,而后启动各服务便可,这样若是参数有变更,也就不用从新启动server了,只须要重启相对应的服务便可
notification
表主要存一些C端和内部系统服务的一些操做通知,目前包括了4个大类,18个小类的通知类型,#L188
stat
表则是统计一些C端操做,而后在A端展现出来,像一些关键词搜索,点赞,用户建立等操做都会生成统计记录的,目前只统计了6种操做#L217,与此同时C端也用Google tag作了一些埋点,方便整个网站的统计
能够看看效果
看下重构前的Controller
流程图
图中全部业务逻辑都是在Controller
中完成,并且是直接在逻辑中调用Model
的接口,这样作有三个问题
Controller
代码会不少,可维护性差Model
层都要catch一下,没有作统一处理,修改起来很麻烦Controller
之间的业务逻辑复用问题这仨问题任何一个都是须要重视的
而后再看下重构后的流程图
这样逻辑分离后,很好地解决了上面的三个问题
整个流程配合上Egg的logger,能够快速定位问题
至于Proxy我是这样实现的
// service/proxy.js
const { Service } = require('egg')
// 代理须要继承自EggService,由于其余模块service须要继承Proxy
module.exports = class ProxyService extends Service {
getList (query = {}) {
return this.model.find(query, // ...)
}
// ... 一些Model的统一接口
}
// service/user.js
const ProxyService = require('./proxy')
// 继承Proxy,定义当前模块所属的model
module.exports = class UserService extends ProxyService {
get model () {
return this.app.model.User
}
getListWithComments () {}
// 其余业务逻辑方法
}
// controller/user.js
const { Controller } = require('egg')
module.exports = class UserController extends Controller {
async list () {
const data = await this.service.user.getListWithComments()
data
? ctx.success(data, '获取用户列表成功')
: ctx.fail('获取用户列表失败')
}
}
复制代码
如上所述,重构前是没有所谓的日志记录的,对于一些线上问题的定位和复现很棘手,这也是我看好Egg的一个很重要的缘由。
Egg的日志有如下几个特性
appLogger
, coreLogger
, errorLogger
, agentLogger
),5种日志级别(NONE
, DEBUG
, INFO
, WARN
, ERROR
),并且能够根据环境变量配置打印级别ERROR
级别日志会统一打印到统一的错误日志(common-error.log文件)中,便于追踪example-app-web.log.YYYY-MM-DD
形式的日志文件这个我会在后续文章里,结合其余两个项目讲一下,目前先给个大概的重构后的流程吧
本地开发 -> github webhook -> 阿里云镜像容器 -> docker镜像构建 -> 镜像发版 -> hook通知服务端jenkins -> jenkins拉取docker镜像 -> 启动容器 -> 邮件(QQ)通知 -> 完成部署
每次写reponse的时候都须要
ctx.status = 200
ctx.body = {//...}
复制代码
很烦,因此我这边就实现了一个封装reponse操做的中间件
如今config里定义下code map
// config/config.default.js
module.exports = appInfo => {
const config = exports = {}
config.codeMap = {
'-1': '请求失败',
200: '请求成功',
401: '权限校验失败',
403: 'Forbidden',
404: 'URL资源未找到',
422: '参数校验失败',
500: '服务器错误'
// ...
}
}
复制代码
而后实现如下中间件
// app/middleware/response.js
module.exports = (opt, app) => {
const { codeMap } = app.config
const successMsg = codeMap[200]
const failMsg = codeMap[-1]
return async (ctx, next) => {
ctx.success = (data = null, message = successMsg) => {
if (app.utils.validate.isString(data)) {
message = data
data = null
}
ctx.status = 200
ctx.body = {
code: 200,
success: true,
message,
data
}
}
ctx.fail = (code = -1, message = '', error = null) => {
if (app.utils.validate.isString(code)) {
error = message || null
message = code
code = -1
}
const body = {
code,
success: false,
message: message || codeMap[code] || failMsg
}
if (error) body.error = error
ctx.status = code === -1 ? 200 : code
ctx.body = body
}
await next()
}
}
复制代码
而后就能够在controller里这样用了
// success
ctx.success() // { code: 200, success: true, message: codeMap[200] data: null }
ctx.success(any[], '获取列表成功') // { code: 200, success: true, message: '获取列表成功' data: any[] }
// fail
ctx.fail() // { code: -1, success: false, message: codeMap[-1], data: null }
ctx.fail(-1, '请求失败', '错误信息') // { code: -1, success: false, message: '请求失败', error: '错误信息', data: null }
复制代码
对于Controll和Service抛出来的异常,好比
有时咱们自定义异常的统一拦截处理,在这个拦截内能够根据本身业务定义的response code
来作适配,这时能够利用koa
的middleware
来处理
// app/middleware/error.js
module.exports = (opt, app) => {
return async (ctx, next) => {
try {
await next()
} catch (err) {
// 全部的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志
ctx.app.emit('error', err, ctx)
let code = err.status || 500
// code是200,说明是业务逻辑主动抛出的异常,code = -1是由于我约定的错误请求status是-1
if (code === 200) code = -1
let message = ''
if (app.config.isProd) {
// 若是是production环境,就跟预先约定的请求code集进行匹配
message = app.config.codeMap[code]
} else {
// dev环境下,那么久返回实际的错误信息了
message = err.message
}
// 这里会统一reponse给client
ctx.fail(code, message, err.errors)
}
}
}
复制代码
场景在上面也提到了,个人一些服务的配置参数是存在数据库中的,因此在服务启动前,也就须要先查询数据库中配置参数,而后再启动对应的服务,好在Egg提供了个自启动方法来解决
// app.js
module.exports = app => {
app.beforeStart(async () => {
const ctx = app.createAnonymousContext()
const setting = await ctx.service.setting.getData()
// 而后能够启动一些服务了,好比邮件服务,反垃圾评论服务等
ctx.service.mailer.start()
})
}
复制代码
嗯,一切都进行的很顺利,直到我遇到了egg-alinode
(阿里Node.js 性能平台),它的的启动是在agent里启动的,这个理所固然,由于它只是上报node runtime的一些系统参数给平台,因此这些脏活儿累活儿都交给agent去作了,不须要主进程和各个worker来管理
因此我就须要“异步”启动alinode服务了,而egg-alinode
是在主进程启动后,fork agent进程初始化的时候就启动的,因此它是不支持这种我这种启动方式的,因此我就fork了egg-alinode的仓库稍微改造了一下,能够看看egg-alinode-async,在支持原功能的基础上,利用egg的IPC来通知agent初始化alinode服务
因此app.js
的代码变成以下
module.exports = app => {
app.beforeStart(async () => {
const ctx = app.createAnonymousContext()
const setting = await ctx.service.setting.getData()
// ... 启动一些服务
// production环境下异步启动alinode
if (app.config.isProd) {
// 利用IPC向agent发送启动alinode的event来异步启动服务
app.messenger.sendToAgent('alinode-run', setting.keys.alinode)
}
})
}
复制代码
这样就解决了个人所有的参数初始化的问题了
这个第三篇文章《网站重构-Docker+Jenkins集成》会详细讲述
能够看看VSCode 调试 Egg 完美版 - 进化史这篇文章
写了这么多,回头看一遍,发现其实重构的地方仍是蛮多的,从重构的缘由到最后达到的效果,目前来看都还蛮好的。并且最近公司项目也须要重构,我也看了一些相关的文章,但愿这写经验重构的时候能用到,也但愿上面的那些解决方案对于有相同疑问的其余人会有些微帮助吧。最后话外谈下我这断时间来读的相关文章的一些感悟吧
重构讲究的是先明确why,when,再谈how,what,最后再来review,如今why和when都已经逐渐清晰了,势在必行。而how则是技术上结合业务给出的量化指标,方案设计和规范,以及后续的一些维护规划等,what就涉及到具体的系统技术上的实现了。整体其实规划下来,重构的复杂度并不亚于一个全新的产品,并且必定要重视重构中的非技术问题,若是单纯只是技术上的重构的话,那就须要再慎重审视一下 why和when了
嗯,就酱!
原文地址:网站重构-后台服务篇