國慶前后差不多一個半月的時間,把自己的網(wǎng)站從數(shù)據(jù)庫,到后端,再到A端和C端整個都重構(gòu)了一篇,國慶7天妥妥地宅 在家里" />

国产成人精品无码青草_亚洲国产美女精品久久久久∴_欧美人与鲁交大毛片免费_国产果冻豆传媒麻婆精东

18143453325 在線咨詢 在線咨詢
18143453325 在線咨詢
所在位置: 首頁 > 營銷資訊 > 網(wǎng)站運營 > 網(wǎng)站重構(gòu)-后臺服務(wù)篇

網(wǎng)站重構(gòu)-后臺服務(wù)篇

時間:2023-04-19 01:52:01 | 來源:網(wǎng)站運營

時間:2023-04-19 01:52:01 來源:網(wǎng)站運營

網(wǎng)站重構(gòu)-后臺服務(wù)篇:

寫在前面

生命不息,重構(gòu)不止
這不是一篇純技術(shù)文章,只是一篇對這段是重構(gòu)后端的總結(jié)

國慶前后差不多一個半月的時間,把自己的網(wǎng)站從數(shù)據(jù)庫,到后端,再到A端和C端整個都重構(gòu)了一篇,國慶7天妥妥地宅 在家里碼代碼,好在目前來看完成度還是達到我的預(yù)期的,雖說沒有變的多高大上,但是好歹項目比以前工程化了一些,這個重構(gòu)過程雖然漫長,但是確實還是有著自己的一些體會的。接下來會分三篇文章來介紹重構(gòu)經(jīng)歷——后臺服務(wù)篇、Nuxt應(yīng)用篇和Docker集成篇

博客地址:jooger.me

倉庫地址:后端,C端,A端

總共差不多200個commits吧,歡迎star,歡迎留言 。廢話不多說,先來看下后端的重構(gòu)經(jīng)歷吧

為什么要重構(gòu)后端?

原因有以下幾點

  1. 單純想體驗下傳說中的企業(yè)級框架-Egg
遇到?jīng)]用過的就想玩一下,后續(xù)有可能還會在來個Nest版也說不定(很喜歡注解這種形式)

  1. 日志系統(tǒng)不完善
以前覺得日志啥的不重要,沒有形成日志備份,所以很多次線上故障原因都無從查證,只能說以前太年輕

  1. 部署流程不理想
以前是用pm2-deploy手動部署,每次都是看著console等部署完成,哈哈,“刀耕火種”,現(xiàn)在是用docker+jenkins,配合github webhook和阿里鏡像容器實現(xiàn)自動化部署

  1. 代碼爛(雖然現(xiàn)在依然很爛)
這個沒啥好說的,邏輯層和controller層混合在一起,復(fù)用性差,重構(gòu)是早晚的事兒

至于為啥沒用TypeScript,我只想說我最開始是用了TS的,也搜了一些文章,但是使用起來莫名其妙的很不爽,然后就放棄了,不過其他倆項目都是用TS重構(gòu)的

哪些地方重構(gòu)了?

框架

以前用的是“常規(guī)操作-Koa,配合上一些插件,還算不錯

重構(gòu)后用的是阿里開源的Egg,文檔是真心好評,雖然文檔我沒有完全看完整(進階那部分略微摟了兩眼),特別是《多進程模型和進程間通訊》那一節(jié)講的真的很詳細,并且圖文并茂地介紹了Egg在多進程架構(gòu)下的實踐,對于我這種接觸Node直接pm2,沒有接觸過cluster的人很有幫助。

目前社區(qū)的優(yōu)質(zhì)插件的話我搜了下,也不少了,沒有嘗試過的可以玩兒一下,另外還推薦一下Nest.js框架,基于express的,我只大致看了幾眼,發(fā)現(xiàn)跟Srping很像,以后說不定會用這個再重構(gòu)下

數(shù)據(jù)庫

數(shù)據(jù)庫這邊我一直用的mongodb,driver用的mongoose,這次重構(gòu)主要是重構(gòu)了下setting表,并且新增了notificationstat

setting表主要存網(wǎng)站的配置,分四個部分

至于keys,以前的server啟動時,一些服務(wù)的初始化參數(shù)往往都是在集成工具里配置的,我這邊將其遷移到數(shù)據(jù)庫中存儲了,server啟動前先從數(shù)據(jù)庫中加載這些配置參數(shù),然后啟動各服務(wù)即可,這樣如果參數(shù)有變動,也就不用重新啟動server了,只需要重啟相對應(yīng)的服務(wù)即可

notification表主要存一些C端和內(nèi)部系統(tǒng)服務(wù)的一些操作通知,目前包括了4個大類,18個小類的通知類型,#L188

stat表則是統(tǒng)計一些C端操作,然后在A端展示出來,像一些關(guān)鍵詞搜索,點贊,用戶創(chuàng)建等操作都會生成統(tǒng)計記錄的,目前只統(tǒng)計了6種操作#L217,與此同時C端也用Google tag做了一些埋點,方便整個網(wǎng)站的統(tǒng)計

可以看看效果







業(yè)務(wù)邏輯層和Controller層分離

看下重構(gòu)前的Controller流程圖







圖中所有業(yè)務(wù)邏輯都是在Controller中完成,而且是直接在邏輯中調(diào)用Model的接口,這樣做有三個問題

  1. 邏輯臃腫,如果邏輯復(fù)雜的話,一個Controller代碼會很多,可維護性差
  2. 每次調(diào)用Model層都要catch一下,沒有做統(tǒng)一處理,修改起來很麻煩
  3. Controller之間的業(yè)務(wù)邏輯復(fù)用問題
這仨問題任何一個都是需要重視的

然后再看下重構(gòu)后的流程圖







這樣邏輯分離后,很好地解決了上面的三個問題

  1. Controller很清爽,邏輯已經(jīng)被拆分出來,流程一步一步來,很清晰
  2. 可以看到在Model層之上加了個Proxy層,用以統(tǒng)一輸出接口供業(yè)務(wù)邏輯層調(diào)用,而且還可以在這里做catch統(tǒng)一處理
  3. 將業(yè)務(wù)邏輯層抽離出來后,各Controller都可以調(diào)用,復(fù)用問題解決
整個流程配合上Egg的logger,可以快速定位問題

至于Proxy我是這樣實現(xiàn)的

// service/proxy.jsconst { Service } = require('egg')// 代理需要繼承自EggService,因為其他模塊service需要繼承Proxymodule.exports = class ProxyService extends Service { getList (query = {}) { return this.model.find(query, // ...) } // ... 一些Model的統(tǒng)一接口}// service/user.jsconst ProxyService = require('./proxy')// 繼承Proxy,定義當前模塊所屬的modelmodule.exports = class UserService extends ProxyService { get model () { return this.app.model.User } getListWithComments () {} // 其他業(yè)務(wù)邏輯方法}// 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('獲取用戶列表失敗') }}

日志系統(tǒng)

如上所述,重構(gòu)前是沒有所謂的日志記錄的,對于一些線上問題的定位和復(fù)現(xiàn)很棘手,這也是我看好Egg的一個很重要的原因。

Egg的日志有以下幾個特性

部署流程

這個我會在后續(xù)文章里,結(jié)合其他兩個項目講一下,目前先給個大概的重構(gòu)后的流程吧

本地開發(fā) -> github webhook -> 阿里云鏡像容器 -> docker鏡像構(gòu)建 -> 鏡像發(fā)版 -> hook通知服務(wù)端jenkins -> jenkins拉取docker鏡像 -> 啟動容器 -> 郵件(QQ)通知 -> 完成部署

一些解決方案

ctx.body封裝

每次寫reponse的時候都需要

ctx.status = 200ctx.body = {//...}很煩,所以我這邊就實現(xiàn)了一個封裝reponse操作的中間件

現(xiàn)在config里定義下code map

// config/config.default.jsmodule.exports = appInfo => { const config = exports = {} config.codeMap = { '-1': '請求失敗', 200: '請求成功', 401: '權(quán)限校驗失敗', 403: 'Forbidden', 404: 'URL資源未找到', 422: '參數(shù)校驗失敗', 500: '服務(wù)器錯誤' // ... }}然后實現(xiàn)以下中間件

// 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 }

自定義統(tǒng)一錯誤處理

對于Controll和Service拋出來的異常,比如

有時我們自定義異常的統(tǒng)一攔截處理,在這個攔截內(nèi)可以根據(jù)自己業(yè)務(wù)定義的response code來做適配,這時可以利用koamiddleware來處理

// app/middleware/error.jsmodule.exports = (opt, app) => { return async (ctx, next) => { try { await next() } catch (err) { // 所有的異常都在 app 上觸發(fā)一個 error 事件,框架會記錄一條錯誤日志 ctx.app.emit('error', err, ctx) let code = err.status || 500 // code是200,說明是業(yè)務(wù)邏輯主動拋出的異常,code = -1是因為我約定的錯誤請求status是-1 if (code === 200) code = -1 let message = '' if (app.config.isProd) { // 如果是production環(huán)境,就跟預(yù)先約定的請求code集進行匹配 message = app.config.codeMap[code] } else { // dev環(huán)境下,那么久返回實際的錯誤信息了 message = err.message } // 這里會統(tǒng)一reponse給client ctx.fail(code, message, err.errors) } }}

server啟動前的參數(shù)初始化

場景在上面也提到了,我的一些服務(wù)的配置參數(shù)是存在數(shù)據(jù)庫中的,所以在服務(wù)啟動前,也就需要先查詢數(shù)據(jù)庫中配置參數(shù),然后再啟動對應(yīng)的服務(wù),好在Egg提供了個自啟動方法來解決

// app.jsmodule.exports = app => { app.beforeStart(async () => { const ctx = app.createAnonymousContext() const setting = await ctx.service.setting.getData() // 然后可以啟動一些服務(wù)了,比如郵件服務(wù),反垃圾評論服務(wù)等 ctx.service.mailer.start() })}嗯,一切都進行的很順利,直到我遇到了egg-alinode(阿里Node.js 性能平臺),它的的啟動是在agent里啟動的,這個理所當然,因為它只是上報node runtime的一些系統(tǒng)參數(shù)給平臺,所以這些臟活兒累活兒都交給agent去做了,不需要主進程和各個worker來管理

所以我就需要“異步”啟動alinode服務(wù)了,而egg-alinode是在主進程啟動后,fork agent進程初始化的時候就啟動的,所以它是不支持這種我這種啟動方式的,所以我就fork了egg-alinode的倉庫稍微改造了一下,可以看看egg-alinode-async,在支持原功能的基礎(chǔ)上,利用egg的IPC來通知agent初始化alinode服務(wù)

所以app.js的代碼變成如下

module.exports = app => { app.beforeStart(async () => { const ctx = app.createAnonymousContext() const setting = await ctx.service.setting.getData() // ... 啟動一些服務(wù) // production環(huán)境下異步啟動alinode if (app.config.isProd) { // 利用IPC向agent發(fā)送啟動alinode的event來異步啟動服務(wù) app.messenger.sendToAgent('alinode-run', setting.keys.alinode) } })}這樣就解決了我的全部的參數(shù)初始化的問題了

docker和docker-compose加持

這個第三篇文章《網(wǎng)站重構(gòu)-Docker+Jenkins集成》會詳細講述

vscode調(diào)試egg

可以看看VSCode 調(diào)試 Egg 完美版 - 進化史這篇文章

不足之處

總結(jié)

寫了這么多,回頭看一遍,發(fā)現(xiàn)其實重構(gòu)的地方還是蠻多的,從重構(gòu)的原因到最后達到的效果,目前來看都還蠻好的。而且最近公司項目也需要重構(gòu),我也看了一些相關(guān)的文章,希望這寫經(jīng)驗重構(gòu)的時候能用到,也希望上面的那些解決方案對于有相同疑問的其他人會有些微幫助吧。最后話外談下我這斷時間來讀的相關(guān)文章的一些感悟吧

重構(gòu)講究的是先明確why,when,再談how,what,最后再來review,現(xiàn)在why和when都已經(jīng)逐漸清晰了,勢在必行。而how則是技術(shù)上結(jié)合業(yè)務(wù)給出的量化指標,方案設(shè)計和規(guī)范,以及后續(xù)的一些維護規(guī)劃等,what就涉及到具體的系統(tǒng)技術(shù)上的實現(xiàn)了??傮w其實規(guī)劃下來,重構(gòu)的復(fù)雜度并不亞于一個全新的產(chǎn)品,而且一定要重視重構(gòu)中的非技術(shù)問題,如果單純只是技術(shù)上的重構(gòu)的話,那就需要再慎重審視一下 why和when了
嗯,就醬!

參考文章

關(guān)鍵詞:服務(wù),后臺

74
73
25
news

版權(quán)所有? 億企邦 1997-2025 保留一切法律許可權(quán)利。

為了最佳展示效果,本站不支持IE9及以下版本的瀏覽器,建議您使用谷歌Chrome瀏覽器。 點擊下載Chrome瀏覽器
關(guān)閉