生命不息,重構不止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了
嗯,就醬!
原文地址:網站重構-後臺服務篇