視頻教程推薦:nodejs 教程
引言
在 node.js 領域中,express.js 是一個為人所熟知的 rest apis 開發(fā)框架。雖然它非常的出色,但是該如何組織的項目代碼,卻沒人告訴你。
通常這沒什么,不過對于開發(fā)者而言這又是我們必須面對的問題。
一個好的項目結構,不僅能消除重復代碼,提升系統(tǒng)穩(wěn)定性,改善系統(tǒng)的設計,還能在將來更容易的擴展。
多年以來,我一直在處理重構和遷移項目結構糟糕、設計不合理的 node.js 項目。而這篇文章正是對我此前積累經(jīng)驗的總結。
目錄結構
下面是我所推薦的項目代碼組織方式。
它來自于我參與的項目的實踐,每個目錄模塊的功能與作用如下:
src │ app.js # app 統(tǒng)一入口 └───api # express route controllers for all the endpoints of the app └───config # 環(huán)境變量和配置信息 └───jobs # 隊列任務(agenda.js) └───loaders # 將啟動過程模塊化 └───models # 數(shù)據(jù)庫模型 └───services # 存放所有商業(yè)邏輯 └───subscribers # 異步事件處理器 └───types # typescript 的類型聲明文件 (d.ts)而且,這不僅僅只是代碼的組織方式…
3 層結構
這個想法源自 關注點分離原則,把業(yè)務邏輯從 node.js api 路由中分離出去。
因為將來的某天,你可能會在 cli 工具或是其他地方處理你的業(yè)務。當然,也有可能不會,但在項目中使用api調用的方式來處理自身的業(yè)務終究不是一個好主意…
不要在控制器中直接處理業(yè)務邏輯!!
在你的應用中,你可能經(jīng)為了圖便利而直接的在控制器處理業(yè)務。不幸的是,這么做的話很快你將面對相面條一樣復雜的控制器代碼,“惡果”也會隨之而來,比如在處理單元測試的時候不得不使用復雜的 request 或 response 模擬。
同時,在決定何時向客戶端返回響應,或希望在發(fā)送響應之后再進行一些處理的時候,將會變得很復雜。
請不要像下面例子這樣做.
route.post('/', async (req, res, next) => { // 這里推薦使用中間件或joi 驗證器 const userdto = req.body; const isuservalid = validators.user(userdto) if(!isuservalid) { return res.status(400).end(); } // 一堆義務邏輯代碼 const userrecord = await usermodel.create(userdto); delete userrecord.password; delete userrecord.salt; const companyrecord = await companymodel.create(userrecord); const companydashboard = await companydashboard.create(userrecord, companyrecord); ...whatever... // 這里是“優(yōu)化”,但卻搞亂了所有的事情 // 向客戶端發(fā)送響應... res.json({ user: userrecord, company: companyrecord }); // 但這里的代碼仍會執(zhí)行 :( const salaryrecord = await salarymodel.create(userrecord, companyrecord); eventtracker.track('user_signup',userrecord,companyrecord,salaryrecord); intercom.createuser(userrecord); gaanalytics.event('user_signup',userrecord); await emailservice.startsignupsequence(userrecord) });使用服務層(service)來處理業(yè)務
在單獨的服務層處理業(yè)務邏輯是推薦的做法。
這一層是遵循適用于 node.js 的 solid 原則的“類”的集合
在這一層中,不應該有任何形式的數(shù)據(jù)查詢操作。正確的做法是使用數(shù)據(jù)訪問層.
從 express.js 路由中清理業(yè)務代碼。
服務層不應包含 request 和 response。
服務層不應返回任何與傳輸層關聯(lián)的數(shù)據(jù),如狀態(tài)碼和響應頭。
示例
route.post('/', validators.usersignup, // 中間件處理驗證 async (req, res, next) => { // 路由的實際責任 const userdto = req.body; // 調用服務層 // 這里演示如何訪問服務層 const { user, company } = await userservice.signup(userdto); // 返回響應 return res.json({ user, company }); });下面是服務層示例代碼。
import usermodel from '../models/user'; import companymodel from '../models/company'; export default class userservice { async signup(user) { const userrecord = await usermodel.create(user); const companyrecord = await companymodel.create(userrecord); // 依賴用戶的數(shù)據(jù)記錄 const salaryrecord = await salarymodel.create(userrecord, companyrecord); // 依賴用戶與公司數(shù)據(jù) ...whatever await emailservice.startsignupsequence(userrecord) ...do more stuff return { user: userrecord, company: companyrecord }; } }在 github 查看示例代碼
https://github.com/santiq/bulletproof-nodejs
使用發(fā)布/訂閱模式
嚴格來講發(fā)布/訂閱模型并不屬于 3 層結構的范疇,但卻很實用。
這里有一個簡單的 node.js api 用來創(chuàng)建用戶,于此同時你可能還需要調用外部服務、分析數(shù)據(jù)、或發(fā)送一連串的郵件。很快,這個簡單的原本用于創(chuàng)建用戶的函數(shù),由于充斥各種功能,代碼已經(jīng)超過了 1000 行。
現(xiàn)在是時候把這些功能都拆分為獨立功能了,這樣才能讓你的代碼繼續(xù)保持可維護性。
import usermodel from '../models/user'; import companymodel from '../models/company'; import salarymodel from '../models/salary'; export default class userservice() { async signup(user) { const userrecord = await usermodel.create(user); const companyrecord = await companymodel.create(user); const salaryrecord = await salarymodel.create(user, salary); eventtracker.track( 'user_signup', userrecord, companyrecord, salaryrecord ); intercom.createuser( userrecord ); gaanalytics.event( 'u