时间:2023-04-19 01:52:01 | 来源:网站运营
时间:2023-04-19 01:52:01 来源:网站运营
网站重构-后台服务篇:生命不息,重构不止这不是一篇纯技术文章,只是一篇对这段是重构后端的总结
TypeScript
,我只想说我最开始是用了TS的,也搜了一些文章,但是使用起来莫名其妙的很不爽,然后就放弃了,不过其他俩项目都是用TS重构的Koa
,配合上一些插件,还算不错cluster
的人很有帮助。express
的,我只大致看了几眼,发现跟Srping很像,以后说不定会用这个再重构下mongodb
,driver用的mongoose
,这次重构主要是重构了下setting
表,并且新增了notification
和stat
表setting
表主要存网站的配置,分四个部分site
C端的一些配置personal
个人信息keys
一些第三方插件的参数,比如阿里云OSS的,Github,阿里node平台(这个稍后要讲),个人邮箱的一些配置keys
,以前的server启动时,一些服务的初始化参数往往都是在集成工具里配置的,我这边将其迁移到数据库中存储了,server启动前先从数据库中加载这些配置参数,然后启动各服务即可,这样如果参数有变动,也就不用重新启动server了,只需要重启相对应的服务即可notification
表主要存一些C端和内部系统服务的一些操作通知,目前包括了4个大类,18个小类的通知类型,#L188stat
表则是统计一些C端操作,然后在A端展示出来,像一些关键词搜索,点赞,用户创建等操作都会生成统计记录的,目前只统计了6种操作#L217,与此同时C端也用Google tag做了一些埋点,方便整个网站的统计Controller
流程图Controller
中完成,而且是直接在逻辑中调用Model
的接口,这样做有三个问题Controller
代码会很多,可维护性差Model
层都要catch一下,没有做统一处理,修改起来很麻烦Controller
之间的业务逻辑复用问题// service/proxy.jsconst { Service } = require('egg')// 代理需要继承自EggService,因为其他模块service需要继承Proxymodule.exports = class ProxyService extends Service { getList (query = {}) { return this.model.find(query, // ...) } // ... 一些Model的统一接口}// service/user.jsconst ProxyService = require('./proxy')// 继承Proxy,定义当前模块所属的modelmodule.exports = class UserService extends ProxyService { get model () { return this.app.model.User } getListWithComments () {} // 其他业务逻辑方法}// controller/user.jsconst { Controller } = require('egg')module.exports = class UserController extends Controller { async list () { const data = await this.service.user.getListWithComments() data ? ctx.success(data, '获取用户列表成功') : ctx.fail('获取用户列表失败') }}
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)通知 -> 完成部署
ctx.status = 200ctx.body = {//...}
很烦,所以我这边就实现了一个封装reponse操作的中间件code map
// config/config.default.jsmodule.exports = appInfo => { const config = exports = {} config.codeMap = { '-1': '请求失败', 200: '请求成功', 401: '权限校验失败', 403: 'Forbidden', 404: 'URL资源未找到', 422: '参数校验失败', 500: '服务器错误' // ... }}
然后实现以下中间件// app/middleware/response.jsmodule.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里这样用了// successctx.success() // { code: 200, success: true, message: codeMap[200] data: null }ctx.success(any[], '获取列表成功') // { code: 200, success: true, message: '获取列表成功' data: any[] }// failctx.fail() // { code: -1, success: false, message: codeMap[-1], data: null }ctx.fail(-1, '请求失败', '错误信息') // { code: -1, success: false, message: '请求失败', error: '错误信息', data: null }
response code
来做适配,这时可以利用koa
的middleware
来处理// app/middleware/error.jsmodule.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) } }}
// app.jsmodule.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来管理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) } })}
这样就解决了我的全部的参数初始化的问题了重构讲究的是先明确why,when,再谈how,what,最后再来review,现在why和when都已经逐渐清晰了,势在必行。而how则是技术上结合业务给出的量化指标,方案设计和规范,以及后续的一些维护规划等,what就涉及到具体的系统技术上的实现了。总体其实规划下来,重构的复杂度并不亚于一个全新的产品,而且一定要重视重构中的非技术问题,如果单纯只是技术上的重构的话,那就需要再慎重审视一下 why和when了嗯,就酱!
关键词:服务,后台