<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Vue3+TS+Node打造個(gè)人博客(后端架構(gòu))

          共 10606字,需瀏覽 22分鐘

           ·

          2022-03-17 07:23

          點(diǎn)擊上方卡片“前端司南”關(guān)注我您的關(guān)注意義重大
          285b4f9a42df797c8b5e332aad4d5edb.webp原創(chuàng)@前端司南

          本項(xiàng)目代碼已開(kāi)源,具體見(jiàn):

          前端工程:vue3-ts-blog-frontend[1]

          后端工程:express-blog-backend[2]

          數(shù)據(jù)庫(kù)初始化腳本:關(guān)注公眾號(hào)前端司南,回復(fù)關(guān)鍵字“博客數(shù)據(jù)庫(kù)腳本”,即可獲取。

          Express[3] 是基于 Node.js 平臺(tái),快速、開(kāi)放、極簡(jiǎn)的 Web 開(kāi)發(fā)框架。目前已經(jīng)更新到 5.x 版本。

          我的博客后端其實(shí)開(kāi)發(fā)得比較早,19年年底基本上已經(jīng)完成了主體功能的開(kāi)發(fā),當(dāng)時(shí)用的是 Express 4.x 版本。

          6bba21c76312c2ca07efb551f00d27fc.webp

          在使用 Express 搭建后端服務(wù)時(shí),主要關(guān)注的幾個(gè)點(diǎn)是:

          • 路由中間件和控制器
          • SQL處理
          • 響應(yīng)返回體數(shù)據(jù)結(jié)構(gòu)
          • 錯(cuò)誤碼
          • Web安全
          • 環(huán)境變量/配置
          路由和控制器

          路由基本上是按模塊或功能去劃分的。

          首先是按模塊去劃分一級(jí)路由,各個(gè)模塊的子功能相當(dāng)于是用二級(jí)路由處理。

          簡(jiǎn)單舉個(gè)例子,/article路由開(kāi)頭的是文章模塊,/article/add用于新增文章功能。

          控制器的概念其實(shí)是從其他語(yǔ)言中借鑒而來(lái)的,Express 并沒(méi)有明確說(shuō)什么是控制器,但在我看來(lái),路由中間件的處理模塊/函數(shù)就是控制器的概念。

          下面是本項(xiàng)目使用到的一些控制器。

          ab14e244de2739f28a3d2762626fcf67.webp
          const?BaseController?=?require('../controllers/base');
          const?ValidatorController?=?require('../controllers/validator');
          const?UserController?=?require('../controllers/user');
          const?BannerController?=?require('../controllers/banner');
          const?ArticleController?=?require('../controllers/article');
          const?TagController?=?require('../controllers/tag');
          const?CategoryController?=?require('../controllers/category');
          const?CommentController?=?require('../controllers/comment');
          const?ReplyController?=?require('../controllers/reply');

          module.exports?=?function(app)?{
          ?app.use(BaseController);
          ?app.use('/validator',?ValidatorController);
          ?app.use('/user',?UserController);
          ?app.use('/banner',?BannerController);
          ?app.use('/article',?ArticleController);
          ?app.use('/tag',?TagController);
          ?app.use('/category',?CategoryController);
          ?app.use('/comment',?CommentController);
          ?app.use('/reply',?ReplyController);
          };

          BaseController

          其中,BaseController是用作第一道關(guān)卡,對(duì)所有的請(qǐng)求做一個(gè)基本的校驗(yàn)和攔截。

          其實(shí)主要是對(duì)一些敏感接口(比如后臺(tái)維護(hù)類(lèi)的)做一個(gè)權(quán)限校驗(yàn)。

          權(quán)限控制這塊,我設(shè)計(jì)得還是比較簡(jiǎn)單粗暴的,因?yàn)槲以跀?shù)據(jù)庫(kù)表中目前只預(yù)留了一個(gè)用戶Tusi,關(guān)聯(lián)的角色也是唯一用到的admin。畢竟目前還沒(méi)考慮開(kāi)放用戶注冊(cè)這類(lèi)的能力,有一個(gè)管理用戶基本上也夠用了。

          所以我的設(shè)計(jì)是:只要在我登錄成功后的有效期內(nèi),就有權(quán)限操作敏感接口,否則就無(wú)權(quán)操作!

          BaseController大體工作流程如下:

          5cdcdda45d510ea69e5b0eb87d97a464.webp

          BaseController的主體代碼結(jié)構(gòu)大概如下:

          router.use(function(req,?res,?next)?{
          ????//?authMap?維護(hù)了敏感接口列表
          ????const?authority?=?authMap.get(req.path);
          ????//?首先檢查是不是敏感接口
          ????if?(authority)?{
          ????????//?需要檢驗(yàn)身份的接口
          ????????if?(req.cookies.token)?{
          ????????????//?取到?token?去做校驗(yàn)
          ????????????dbUtils.getConnection(function?(connection)?{
          ????????????????req.connection?=?connection;
          ?????????????????//?這里會(huì)直接查庫(kù)驗(yàn)明身份
          ????????????????connection.query(indexSQL.GetCurrentUser,?[req.cookies.token],?function?(error,?results,?fileds)?{
          ??????????????????//?身份校驗(yàn)通過(guò),才繼續(xù),否則返回錯(cuò)誤碼
          ????????????????})
          ????????????})
          ????????}?else?{
          ????????????return?res.send({
          ????????????????...errcode.AUTH.UNAUTHORIZED
          ????????????});
          ????????}
          ????}?else?{
          ????????//?不是敏感接口,不校驗(yàn)身份
          ????????if?(req.method?==?'OPTIONS')?{
          ????????????//?OPTIONS?類(lèi)型請(qǐng)求不能去連數(shù)據(jù)庫(kù),否則會(huì)導(dǎo)致數(shù)據(jù)庫(kù)連接過(guò)多崩了
          ????????????next();
          ????????}?else?{
          ????????????//?從mysql連接池取得connection
          ????????????dbUtils.getConnection(function?(connection)?{
          ????????????????req.connection?=?connection;
          ????????????????next();
          ????????????},?function?(err)?{
          ????????????????return?res.send({
          ????????????????????...errcode.DB.CONNECT_EXCEPTION
          ????????????????});
          ????????????})
          ????????}
          ????}
          }

          如注釋所述,BaseController主要是針對(duì)敏感接口做一個(gè)身份檢查,防止系統(tǒng)數(shù)據(jù)被一些不懷好意的 HTTP 請(qǐng)求給黑了。

          20220218更新

          按照上面的邏輯實(shí)現(xiàn)功能并上線后,服務(wù)運(yùn)行一段時(shí)間(可能是3~5天)后,能觀察到服務(wù)請(qǐng)求會(huì)變成無(wú)法正常響應(yīng)的狀態(tài)。

          d16626d43317079eb3c43a64cf3375ed.webp

          其實(shí)我能感覺(jué)到可能是mysql連接池未合理釋放導(dǎo)致的。

          但是由于我一開(kāi)始采取的方案是:在BaseControllerreq掛載connection,并在具體的業(yè)務(wù)控制器執(zhí)行完sql查詢(xún)語(yǔ)句后再自行釋放connection,這個(gè)基本使用過(guò)程我在后面一節(jié)也說(shuō)到了。

          如果要完全改掉這種調(diào)用方式,代碼改動(dòng)還是挺大的,所以我一直拖著沒(méi)改,發(fā)現(xiàn)問(wèn)題了就通過(guò) PM2 重啟服務(wù)也能接著用。最近還是咬咬牙全部重構(gòu)了,具體見(jiàn)refactor: 重構(gòu)sql調(diào)用部分[4]。

          d668a1036acfa9320caee7a772343622.webp

          業(yè)務(wù)Controller

          前端會(huì)分模塊,后端自然也會(huì)。業(yè)務(wù)模塊會(huì)有很多,比如文章,分類(lèi),標(biāo)簽,等等。這些都可以分成不同的Controller處理。

          業(yè)務(wù)Controller的大體結(jié)構(gòu)如下,一個(gè)子路由就對(duì)應(yīng)一個(gè)功能:

          /**
          ?*?@param?{Number}?count?查詢(xún)數(shù)量
          ?*?@description?根據(jù)傳入的count獲取閱讀排行top?N的文章
          ?*/

          router.get('/top_read',?function?(req,?res,?next)?{
          ??//?業(yè)務(wù)代碼
          }

          /**
          ?*?@param?{Number}?pageNo?頁(yè)碼數(shù)
          ?*?@param?{Number}?pageSize?一頁(yè)數(shù)量
          ?*?@description?分頁(yè)查詢(xún)文章
          ?*/

          router.get('/page',?function?(req,?res,?next)?{
          ??//?業(yè)務(wù)代碼
          }

          /**
          ?*?@param?{Number}?id?當(dāng)前文章的id
          ?*?@description?查詢(xún)上一篇和下一篇文章的id
          ?*/

          router.get('/neighbors',?function?(req,?res,?next)?{
          ??//?業(yè)務(wù)代碼
          }
          SQL處理

          SQL 這塊,我沒(méi)有直接用 ORM 工具。因?yàn)槲矣X(jué)得自己的 SQL 基礎(chǔ)并不是很好,還需要自己多寫(xiě) SQL 語(yǔ)句練習(xí)一下,所以我只用了一個(gè)mysql的庫(kù)。

          523dca06a71d5581d4836955af4c69fa.webp

          安裝mysql依賴(lài):

          npm?install?--save?mysql

          簡(jiǎn)單使用時(shí),可以直接創(chuàng)建連接,然后執(zhí)行 SQL 語(yǔ)句:

          var?mysql??????=?require('mysql');
          var?connection?=?mysql.createConnection({
          ??host?????:?'localhost',
          ??user?????:?'me',
          ??password?:?'secret',
          ??database?:?'my_db'
          });
          ?
          connection.connect();
          ?
          connection.query('SELECT?1?+?1?AS?solution',?function?(error,?results,?fields)?{
          ??if?(error)?throw?error;
          ??console.log('The?solution?is:?',?results[0].solution);
          });
          ?
          connection.end();

          實(shí)際上,更推薦使用連接池,可以避免重復(fù)向 MySQL 申請(qǐng)連接,實(shí)現(xiàn)了連接的重用,在響應(yīng)速度上也會(huì)更快!

          var?mysql?=?require('mysql');
          var?pool??=?mysql.createPool(...);
          ?
          pool.getConnection(function(err,?connection)?{
          ??if?(err)?throw?err;?//?not?connected!
          ?
          ??//?Use?the?connection
          ??connection.query('SELECT?something?FROM?sometable',?function?(error,?results,?fields)?{
          ????//?When?done?with?the?connection,?release?it.
          ????connection.release();
          ?
          ????//?Handle?error?after?the?release.
          ????if?(error)?throw?error;
          ?
          ????//?Don't?use?the?connection?here,?it?has?been?returned?to?the?pool.
          ??});
          });

          實(shí)際操作時(shí),我是在BaseController中執(zhí)行了pool.getConnection,然后把connection對(duì)象掛載到req對(duì)象上,后續(xù)的路由中間件就可以直接從req對(duì)象中取得connection,可以少嵌套一層回調(diào),也避免了每處業(yè)務(wù)代碼都寫(xiě)這部分重復(fù)的getConnection代碼。

          BaseController的關(guān)鍵代碼:

          //?從mysql連接池取得connection
          dbService.getConnection(function?(connection)?{
          ??req.connection?=?connection;
          ??next();
          },?function?(err)?{
          ??return?res.send({
          ????...errcode.DB.CONNECT_EXCEPTION
          ??});
          })

          業(yè)務(wù)處直接從req獲取到connection對(duì)象:

          router.get('/page',?function?(req,?res,?next)?{
          ??const?connection?=?req.connection;
          ??const?pageNo?=?Number(req.query.pageNo?||?1);
          ??const?pageSize?=?Number(req.query.pageSize?||?10);
          ??connection.query(indexSQL.GetPagedArticle,?[(pageNo?-?1)?*?pageSize,?pageSize],?function?(error,?results,?fileds)?{
          ????connection.release();
          ????//?其他業(yè)務(wù)代碼
          ??})

          SQL 語(yǔ)句主要是以字符串的形式編寫(xiě),通過(guò)?作為一個(gè)參數(shù)槽位,接收一些動(dòng)態(tài)的值。

          比如一個(gè)邏輯刪除的語(yǔ)句,我們會(huì)這樣寫(xiě):

          //?邏輯刪除/恢復(fù)
          UpdateArticleDeleted:?'UPDATE?article?SET?deleted?=???WHERE?id?=??',

          第一個(gè)?是留給字段deleted的值,第二個(gè)?便是傳具體的id值。

          而參數(shù)傳值是通過(guò)connection.query的第二個(gè)參數(shù)攜帶的。

          注意,這個(gè)參數(shù)是一個(gè)數(shù)組,數(shù)組中的值會(huì)按照從左到右的順序依次替換掉 SQL 字符串中的?,變成一個(gè)真實(shí)的可執(zhí)行的 SQL 語(yǔ)句。

          connection.query(indexSQL.UpdateArticleDeleted,?[params.deleted,?params.id],?function?(error,?results,?fileds)?{})

          connection.query執(zhí)行回調(diào)后切記調(diào)用connection.release釋放連接。

          另外要注意的一個(gè)就是 MySQL 的事務(wù)處理。對(duì)事務(wù)而言,初步要關(guān)注的是這三個(gè) API!具體的使用場(chǎng)景我在后面的具體應(yīng)用會(huì)再提到,這里就不展開(kāi)了!

          //?開(kāi)始事務(wù),對(duì)應(yīng)?MySQL?begin?語(yǔ)句
          connection.beginTransaction();

          //?事務(wù)提交,對(duì)應(yīng)?MySQL?commit?語(yǔ)句
          connection.commit();

          //?事務(wù)回滾,對(duì)應(yīng)?MySQL?rollback?語(yǔ)句
          connection.rollback();

          20220218更新

          為了保留在這個(gè)項(xiàng)目中我使用mysql思路的一個(gè)轉(zhuǎn)變過(guò)程,前面的 mysql 調(diào)用過(guò)程,我還是按照最初的想法展開(kāi)介紹的,關(guān)鍵的也就是這么幾點(diǎn)。

          1. BaseController 統(tǒng)一獲取 mysql pool 的 connection 對(duì)象,并掛載到 req 對(duì)象上,供后面的業(yè)務(wù)使用。
          2. 業(yè)務(wù) Controller 與 mysql 交互時(shí),只需要從 req 對(duì)象中取得 connection,通過(guò) connection.query 去執(zhí)行 sql 語(yǔ)句。
          3. 業(yè)務(wù) Controller 執(zhí)行完 sql 語(yǔ)句后,主動(dòng) release 釋放掉 connection。
          4. 事務(wù)場(chǎng)景中,事務(wù)處理完畢后,統(tǒng)一 release 釋放掉 connection,而不是每個(gè) query 都自行釋放 connection。

          這樣的設(shè)計(jì),雖然省去了在具體業(yè)務(wù) Controller 執(zhí)行getConnection(少一層回調(diào)寫(xiě)法),但是在connection.release()的把控上還存在漏洞,一旦業(yè)務(wù)調(diào)用方忘記調(diào)用release(),就有可能造成服務(wù)不可用。而且有的業(yè)務(wù)不需要與 mysql 交互,也必須要記得 release(),雖然可以用一些配置字段去規(guī)避,也并不能從根本上解決問(wèn)題!

          所以我的修改方案是:

          1. 總體的原則是高內(nèi)聚,低耦合
          2. 封裝 mysql 的查詢(xún)過(guò)程,把 getConnection, query, release 等幾個(gè)關(guān)鍵行為都放在封裝的代碼中控制,對(duì)外只暴露一些封裝好的方法,這樣就不用擔(dān)心調(diào)用方忘記某些關(guān)鍵操作(比如release())。
          3. 關(guān)鍵 API Promise 化,這樣在一些復(fù)雜的異步過(guò)程中可以做到事半功倍,特別是涉及事務(wù)處理的時(shí)候!

          核心代碼見(jiàn)db.js[5]

          響應(yīng)返回體

          響應(yīng)返回體的數(shù)據(jù)結(jié)構(gòu)是需要前后端進(jìn)行約定的,只有約定好規(guī)范,雙方才能緊密有序地配合起來(lái)。通常來(lái)說(shuō),會(huì)涉及到錯(cuò)誤碼,信息,數(shù)據(jù)等字段。

          其中錯(cuò)誤碼code,信息message兩個(gè)字段應(yīng)該是通用的。數(shù)據(jù)部分data則隨業(yè)務(wù)的需要,可能會(huì)有多種情況,比如數(shù)組結(jié)構(gòu),對(duì)象結(jié)構(gòu),或者是普通數(shù)據(jù)類(lèi)型。

          {
          ??code:?"0",
          ??message:?"查詢(xún)成功",
          ??data:?{
          ????id:?1,
          ????name:?'xxx'
          ??}
          }
          錯(cuò)誤碼

          錯(cuò)誤碼是后端規(guī)范中必不可少的部分。錯(cuò)誤碼的設(shè)計(jì)是為了快速定位問(wèn)題,也為一些業(yè)務(wù)監(jiān)控系統(tǒng)提供了分析和統(tǒng)計(jì)依據(jù)。

          每個(gè)程序員會(huì)有自己的一些編碼風(fēng)格,在錯(cuò)誤碼這塊,我是通過(guò)語(yǔ)義化的屬性名去定位到錯(cuò)誤碼的。通常,一個(gè)錯(cuò)誤碼會(huì)配對(duì)一條錯(cuò)誤信息,也就是下面的msg字段。

          module.exports?=?{
          ??DB:?{
          ????CONNECT_EXCEPTION:?{
          ??????code:?"-1",
          ??????msg:?"數(shù)據(jù)庫(kù)連接異常"
          ????}
          ??},
          ??AUTH:?{
          ????UNAUTHORIZED:?{
          ??????code:?"000001",
          ??????msg:?"對(duì)不起,您還未獲得授權(quán)"
          ????},
          ????AUTHORIZE_EXPIRED:?{
          ??????code:?"000002",
          ??????msg:?"授權(quán)已過(guò)期"
          ????},
          ????FORBIDDEN:?{
          ??????code:?"000003",
          ??????msg:?"抱歉,您沒(méi)有權(quán)限訪問(wèn)該內(nèi)容"
          ????}
          ??},
          }

          錯(cuò)誤碼的設(shè)計(jì)還有一個(gè)好處,就是方便做映射

          什么意思呢?后端返回錯(cuò)誤碼-1,并且通過(guò)msg字段告訴前端錯(cuò)誤信息是數(shù)據(jù)庫(kù)連接異常。但是,前端到底要不要反饋用戶這么直接粗暴的信息呢?我想,有時(shí)候是不需要的,而是通過(guò)一條委婉的提示來(lái)安撫一下用戶情緒

          比如,

          62e7da22b652aec1ddd7d68fe92208c1.webp

          所以,有了錯(cuò)誤碼,前端就可以收放自如,在錯(cuò)誤提示上有更多發(fā)揮的余地,而不是直白地把后端反饋的錯(cuò)誤信息直接暴露給用戶。

          簡(jiǎn)單的一個(gè)映射可以是:

          //?ERR_MSG
          {
          ??"-1":?"系統(tǒng)開(kāi)了個(gè)小差,請(qǐng)稍后重試!",
          }

          那么message的展示邏輯就可以是:

          message.error(ERR_MSG[res.code])
          Web安全

          主要是考慮幾個(gè)方面,XSS,CSRF,響應(yīng)頭。

          XSS,指的是 Cross-Site-Scripting 跨站腳本攻擊。出現(xiàn) XSS 漏洞的主要場(chǎng)景是用戶輸入,比如評(píng)論,富文本等信息,如果不加以校驗(yàn),就可能會(huì)被植入惡意代碼,造成數(shù)據(jù)和財(cái)產(chǎn)損失!

          針對(duì) XSS 的校驗(yàn)不能光靠客戶端,服務(wù)端也必須進(jìn)行校驗(yàn)。我這里用的是[email protected]

          npm?install?--save?xss

          xss默認(rèn)會(huì)處理掉常見(jiàn)的 XSS 風(fēng)險(xiǎn),使用起來(lái)也非常簡(jiǎn)單。比如,在新增評(píng)論的接口處,我們可以對(duì)參數(shù)這樣處理:

          const?xss?=?require("xss");
          router.post('/add',?function?(req,?res,?next)?{
          ??const?params?=?Object.assign(req.body,?{
          ????create_time:?new?Date(),
          ??});
          ??//?XSS防護(hù)
          ??if?(params.content)?{
          ????params.content?=?xss(params.content)
          ??}
          }

          雖然我目前還沒(méi)有用富文本承載評(píng)論內(nèi)容,但是還是先預(yù)備一下,萬(wàn)一哪天想用富文本了呢!

          至于 CSRF(跨站請(qǐng)求偽造)攻擊,常見(jiàn)的漏洞來(lái)源就是基于 Cookie 的身份驗(yàn)證,因?yàn)?Cookie 會(huì)在發(fā) HTTP 請(qǐng)求的時(shí)候自動(dòng)帶上,這樣一來(lái)攻擊者就有了可乘之機(jī),通過(guò)腳本注入,或者一些引誘點(diǎn)擊,讓你不知不覺(jué)就上了套,發(fā)出了意料之外的請(qǐng)求。

          不過(guò),瀏覽器也是在不斷完善 Cookie 安全這塊,比如 Chrome 80 版本默認(rèn)啟用的 SameSite=Lax,也防范了很多 CSRF 的攻擊場(chǎng)景。

          為了安全起見(jiàn),在 Set-Cookie 時(shí),最好帶上這些屬性。

          Set-Cookie:?token=74afes7a8;?HttpOnly;?Secure;?SameSite=Lax;

          為了防止 CSRF 攻擊,還可以采用 csrf-token 方式,或者采用 JWT 認(rèn)證,共同點(diǎn)都是避開(kāi)基于 Cookie 的身份/口令認(rèn)證方式。

          另外,設(shè)置一些必要的響應(yīng)頭對(duì)于 Web 安全也至關(guān)重要!

          Express 推薦我們直接用上helmet

          Helmet 通過(guò)設(shè)置各種 HTTP 請(qǐng)求頭,提升 Express 應(yīng)用的安全性。它不是 Web 安全的銀彈,但的確有所幫助!

          安裝helmet

          npm?install?--save?helmet

          使用起來(lái)也很簡(jiǎn)單,因?yàn)樗褪且粋€(gè)中間件。

          app.use(helmet());
          292179f4f54ed6ef00d59ad29c6a9ea4.webp環(huán)境變量/配置

          由于后端配置文件中一般會(huì)出現(xiàn)一些私密性的配置,比如數(shù)據(jù)庫(kù)配置,服務(wù)器配置,這些都不適合在開(kāi)源項(xiàng)目中直接出現(xiàn)。所以,在本項(xiàng)目[6]中,我只給出了example示例,大家按照說(shuō)明給出自己的配置文件即可。

          • 通用配置:config/env.example.js
          • 開(kāi)發(fā)環(huán)境配置:config/dev.env.example.js
          • 生產(chǎn)環(huán)境配置:config/prod.env.example.js
          • PM2 deploy 配置:deploy.config.example.js

          數(shù)據(jù)庫(kù)、郵箱配置,以及其他的參數(shù)配置,建議是給開(kāi)發(fā)環(huán)境和生產(chǎn)環(huán)境單獨(dú)配置,避免本地開(kāi)發(fā)時(shí)直接影響到生產(chǎn)環(huán)境。

          所以,我們需要設(shè)置環(huán)境標(biāo)識(shí),并且根據(jù)環(huán)境標(biāo)識(shí)來(lái)引用對(duì)應(yīng)的參數(shù)配置。

          環(huán)境標(biāo)識(shí)我們都不陌生了,它就是process.env.NODE_ENV。由于項(xiàng)目中用到了pm2,所以我是通過(guò)pm2來(lái)配置NODE_ENV的。

          env:?{
          ??NODE_ENV:?"development",
          ??PORT:?8002,
          },
          env_production:?{
          ??NODE_ENV:?'production',
          ??PORT:?8002,
          },

          所以,我們只要根據(jù)NODE_ENV來(lái)判斷開(kāi)發(fā)環(huán)境或生產(chǎn)環(huán)境,然后加載對(duì)應(yīng)的參數(shù)配置即可。邏輯非常簡(jiǎn)單!

          //?配置入口文件,根據(jù)環(huán)境標(biāo)識(shí)導(dǎo)出配置
          const?baseEnv?=?require("./env")
          const?devEnv?=?require("./dev.env")
          const?prodEnv?=?require("./prod.env")

          module.exports?=?process.env.NODE_ENV?===?'production'???{
          ??...baseEnv,
          ??...prodEnv
          }?:?{
          ??...baseEnv,
          ??...devEnv
          }
          小結(jié)

          本文是Vue3+TS+Node打造個(gè)人博客(后端架構(gòu)篇),從一個(gè)不太專(zhuān)業(yè)的視角來(lái)切入后端,主要介紹了我在為博客系統(tǒng)設(shè)計(jì)后端時(shí)的一些主要思路,諸多細(xì)節(jié)不便展開(kāi),可以打開(kāi)源碼[7]了解。

          有了這次全棧開(kāi)發(fā)的經(jīng)驗(yàn),大大提高了我對(duì)前后端全鏈路的理解程度,這之后和后端開(kāi)發(fā)們聊天也更有話題可聊了,有時(shí)候還能幫后端捋捋思路、一起排查下問(wèn)題。總之非常奈斯!

          但是,要把后端做完善還有很多的路要走,看看 Java 那么多中間件就知道了,道阻且長(zhǎng),行則將至,加油吧!

          b8176890d1e0cf44d8e8870db77f661b.webp系列文章

          Vue3+TS+Node打造個(gè)人博客系列文章入口可點(diǎn)擊下方鏈接,持續(xù)更新,歡迎閱讀!點(diǎn)贊關(guān)注不迷路!??

          • Vue3+TS+Node打造個(gè)人博客(總覽篇)[8]

          參考

          [1]

          vue3-ts-blog-frontend: https://github.com/cumt-robin/vue3-ts-blog-frontend

          [2]

          express-blog-backend: https://github.com/cumt-robin/express-blog-backend

          [3]

          Express: https://www.expressjs.com.cn/

          [4]

          refactor: 重構(gòu)sql調(diào)用部分: https://github.com/cumt-robin/express-blog-backend/commit/41628e98b2e1f2fee14289fdb8d13fe1bc0501e3

          [5]

          db.js: https://github.com/cumt-robin/express-blog-backend/blob/main/utils/db.js

          [6]

          本項(xiàng)目: https://github.com/cumt-robin/express-blog-backend

          [7]

          源碼: https://github.com/cumt-robin/express-blog-backend

          [8]

          Vue3+TS+Node打造個(gè)人博客(總覽篇): https://juejin.cn/post/7066966456638013477


          END


          e46f6f7706c40cdfc50dd7a82319dfe0.webp


          如果覺(jué)得這篇文章還不錯(cuò)點(diǎn)擊下面卡片關(guān)注我來(lái)個(gè)【分享、點(diǎn)贊、在看】三連支持一下吧845731df59d92a47fc8223ea2aa84d81.webp



          ???“分享、點(diǎn)贊在看” 支持一波?1d426d33ed44f2b099c9806c90fc233c.webp?

          瀏覽 63
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产特级毛片A片WWW | 靠逼视频免费看 | 欧美性交成人网站 | 成人视频亚洲 | 日韩一区二区免费看 |