引言視頻教程推薦:nodejs 教程
成都創(chuàng)新互聯(lián)服務(wù)項(xiàng)目包括瓊結(jié)網(wǎng)站建設(shè)、瓊結(jié)網(wǎng)站制作、瓊結(jié)網(wǎng)頁(yè)制作以及瓊結(jié)網(wǎng)絡(luò)營(yíng)銷(xiāo)策劃等。多年來(lái),我們專(zhuān)注于互聯(lián)網(wǎng)行業(yè),利用自身積累的技術(shù)優(yōu)勢(shì)、行業(yè)經(jīng)驗(yàn)、深度合作伙伴關(guān)系等,向廣大中小型企業(yè)、政府機(jī)構(gòu)等提供互聯(lián)網(wǎng)行業(yè)的解決方案,瓊結(jié)網(wǎng)站推廣取得了明顯的社會(huì)效益與經(jīng)濟(jì)效益。目前,我們服務(wù)的客戶(hù)以成都為中心已經(jīng)輻射到瓊結(jié)省份的部分城市,未來(lái)相信會(huì)繼續(xù)擴(kuò)大服務(wù)區(qū)域并繼續(xù)獲得客戶(hù)的支持與信任!
在 node.js 領(lǐng)域中,Express.js 是一個(gè)為人所熟知的 REST APIs 開(kāi)發(fā)框架。雖然它非常的出色,但是該如何組織的項(xiàng)目代碼,卻沒(méi)人告訴你。
通常這沒(méi)什么,不過(guò)對(duì)于開(kāi)發(fā)者而言這又是我們必須面對(duì)的問(wèn)題。
一個(gè)好的項(xiàng)目結(jié)構(gòu),不僅能消除重復(fù)代碼,提升系統(tǒng)穩(wěn)定性,改善系統(tǒng)的設(shè)計(jì),還能在將來(lái)更容易的擴(kuò)展。
多年以來(lái),我一直在處理重構(gòu)和遷移項(xiàng)目結(jié)構(gòu)糟糕、設(shè)計(jì)不合理的 node.js 項(xiàng)目。而這篇文章正是對(duì)我此前積累經(jīng)驗(yàn)的總結(jié)。
目錄結(jié)構(gòu)下面是我所推薦的項(xiàng)目代碼組織方式。
它來(lái)自于我參與的項(xiàng)目的實(shí)踐,每個(gè)目錄模塊的功能與作用如下:
src │ app.js # App 統(tǒng)一入口 └───api # Express route controllers for all the endpoints of the app └───config # 環(huán)境變量和配置信息 └───jobs # 隊(duì)列任務(wù)(agenda.js) └───loaders # 將啟動(dòng)過(guò)程模塊化 └───models # 數(shù)據(jù)庫(kù)模型 └───services # 存放所有商業(yè)邏輯 └───subscribers # 異步事件處理器 └───types # Typescript 的類(lèi)型聲明文件 (d.ts)
而且,這不僅僅只是代碼的組織方式...
3 層結(jié)構(gòu)這個(gè)想法源自 關(guān)注點(diǎn)分離原則,把業(yè)務(wù)邏輯從 node.js API 路由中分離出去。
因?yàn)閷?lái)的某天,你可能會(huì)在 CLI 工具或是其他地方處理你的業(yè)務(wù)。當(dāng)然,也有可能不會(huì),但在項(xiàng)目中使用API調(diào)用的方式來(lái)處理自身的業(yè)務(wù)終究不是一個(gè)好主意...
不要在控制器中直接處理業(yè)務(wù)邏輯!!
在你的應(yīng)用中,你可能經(jīng)為了圖便利而直接的在控制器處理業(yè)務(wù)。不幸的是,這么做的話很快你將面對(duì)相面條一樣復(fù)雜的控制器代碼,“惡果”也會(huì)隨之而來(lái),比如在處理單元測(cè)試的時(shí)候不得不使用復(fù)雜的 request或 response模擬。
同時(shí),在決定何時(shí)向客戶(hù)端返回響應(yīng),或希望在發(fā)送響應(yīng)之后再進(jìn)行一些處理的時(shí)候,將會(huì)變得很復(fù)雜。
請(qǐng)不要像下面例子這樣做.
route.post('/', async (req, res, next) => { // 這里推薦使用中間件或Joi 驗(yàn)證器 const userDTO = req.body; const isUserValid = validators.user(userDTO) if(!isUserValid) { return res.status(400).end(); } // 一堆義務(wù)邏輯代碼 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)化”,但卻搞亂了所有的事情 // 向客戶(hù)端發(fā)送響應(yīng)... res.json({ user: userRecord, company: companyRecord }); // 但這里的代碼仍會(huì)執(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) });使用服務(wù)層(service)來(lái)處理業(yè)務(wù)
在單獨(dú)的服務(wù)層處理業(yè)務(wù)邏輯是推薦的做法。
這一層是遵循適用于 node.js 的 SOLID 原則的“類(lèi)”的集合
在這一層中,不應(yīng)該有任何形式的數(shù)據(jù)查詢(xún)操作。正確的做法是使用數(shù)據(jù)訪問(wèn)層.
從 express.js 路由中清理業(yè)務(wù)代碼。
服務(wù)層不應(yīng)包含 request 和 response。
服務(wù)層不應(yīng)返回任何與傳輸層關(guān)聯(lián)的數(shù)據(jù),如狀態(tài)碼和響應(yīng)頭。
示例
route.post('/', validators.userSignup, // 中間件處理驗(yàn)證 async (req, res, next) => { // 路由的實(shí)際責(zé)任 const userDTO = req.body; // 調(diào)用服務(wù)層 // 這里演示如何訪問(wèn)服務(wù)層 const { user, company } = await UserService.Signup(userDTO); // 返回響應(yīng) return res.json({ user, company }); });
下面是服務(wù)層示例代碼。
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); // 依賴(lài)用戶(hù)的數(shù)據(jù)記錄 const salaryRecord = await SalaryModel.create(userRecord, companyRecord); // 依賴(lài)用戶(hù)與公司數(shù)據(jù) ...whatever await EmailService.startSignupSequence(userRecord) ...do more stuff return { user: userRecord, company: companyRecord }; } }
使用發(fā)布/訂閱模式在 Github 查看示例代碼
https://github.com/santiq/bulletproof-nodejs
嚴(yán)格來(lái)講發(fā)布/訂閱模型并不屬于 3 層結(jié)構(gòu)的范疇,但卻很實(shí)用。
這里有一個(gè)簡(jiǎn)單的 node.js API 用來(lái)創(chuàng)建用戶(hù),于此同時(shí)你可能還需要調(diào)用外部服務(wù)、分析數(shù)據(jù)、或發(fā)送一連串的郵件。很快,這個(gè)簡(jiǎn)單的原本用于創(chuàng)建用戶(hù)的函數(shù),由于充斥各種功能,代碼已經(jīng)超過(guò)了 1000 行。
現(xiàn)在是時(shí)候把這些功能都拆分為獨(dú)立功能了,這樣才能讓你的代碼繼續(xù)保持可維護(hù)性。
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( 'user_signup', userRecord ); await EmailService.startSignupSequence(userRecord) ...more stuff return { user: userRecord, company: companyRecord }; } }
同步的調(diào)用依賴(lài)服務(wù)仍不是的解決辦法.
更好的做法是觸發(fā)事件,如:一個(gè)新用戶(hù)使用郵箱方式注冊(cè)了。
就這樣,創(chuàng)建用戶(hù)的Api 完成了它的功能,剩下的就交給訂閱事件的處理器來(lái)負(fù)責(zé)。
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 this.userModel.create(user); const companyRecord = await this.companyModel.create(user); this.eventEmitter.emit('user_signup', { user: userRecord, company: companyRecord }) return userRecord } }
并且你可以把事件處理器分割為多個(gè)獨(dú)立的文件。
eventEmitter.on('user_signup', ({ user, company }) => { eventTracker.track( 'user_signup', user, company, ); intercom.createUser( user ); gaAnalytics.event( 'user_signup', user ); })
eventEmitter.on('user_signup', async ({ user, company }) => { const salaryRecord = await SalaryModel.create(user, company); })
eventEmitter.on('user_signup', async ({ user, company }) => { await EmailService.startSignupSequence(user) })
你可以使用 try-catch 來(lái)包裹 await 語(yǔ)句,或 通過(guò) process.on('unhandledRejection',cb)的形式注冊(cè) 'unhandledPromise' 的事件處理器
依賴(lài)注入依賴(lài)注入或者說(shuō)控制反轉(zhuǎn)(IoC) 是一個(gè)通用的模式,通過(guò) ‘注入’ 或者傳遞類(lèi)或函數(shù)中涉及的依賴(lài)項(xiàng)的構(gòu)造器,幫助你組織代碼。
例如,當(dāng)你為服務(wù)編寫(xiě)單元測(cè)試,或者在另外的上下文中使用服務(wù)時(shí),通過(guò)這種方式,注入依賴(lài)項(xiàng)就會(huì)變得很靈活。
不使用依賴(lài)注入的代碼
import UserModel from '../models/user'; import CompanyModel from '../models/company'; import SalaryModel from '../models/salary'; class UserService { constructor(){} Sigup(){ //調(diào)用 UserMode, CompanyModel,等 ... } }
手動(dòng)依賴(lài)注入的代碼
export default class UserService { constructor(userModel, companyModel, salaryModel){ this.userModel = userModel; this.companyModel = companyModel; this.salaryModel = salaryModel; } getMyUser(userId){ // 通過(guò)this獲取模型 const user = this.userModel.findById(userId); return user; } }
現(xiàn)在你可以注入自定義的依賴(lài)。
import UserService from '../services/user'; import UserModel from '../models/user'; import CompanyModel from '../models/company'; const salaryModelMock = { calculateNetSalary(){ return 42; } } const userServiceInstance = new UserService(userModel, companyModel, salaryModelMock); const user = await userServiceInstance.getMyUser('12346');
一個(gè)服務(wù)可以擁有的依賴(lài)項(xiàng)數(shù)量是無(wú)限的,當(dāng)您添加一個(gè)新的服務(wù)時(shí)重構(gòu)它的每個(gè)實(shí)例是一個(gè)無(wú)聊且容易出錯(cuò)的任務(wù)。
這就是創(chuàng)建依賴(lài)注入框架的原因。
其思想是在類(lèi)中聲明依賴(lài)項(xiàng),當(dāng)需要該類(lèi)的實(shí)例時(shí),只需調(diào)用「服務(wù)定位器」。
我們可以參考一個(gè)在 nodejs 引入 D.I npm庫(kù)typedi的例子。
你可以在官方文檔上查看更多關(guān)于使用 typedi的方法
https://www.github.com/typestack/typedi
警告 typescript 例子
import { Service } from 'typedi'; @Service() export default class UserService { constructor( private userModel, private companyModel, private salaryModel ){} getMyUser(userId){ const user = this.userModel.findById(userId); return user; } }
services/user.ts
現(xiàn)在,typedi將負(fù)責(zé)解析 UserService 所需的任何依賴(lài)項(xiàng)。
import { Container } from 'typedi'; import UserService from '../services/user'; const userServiceInstance = Container.get(UserService); const user = await userServiceInstance.getMyUser('12346');
濫用服務(wù)定位器是一種反面模式
在 Express.js 中使用依賴(lài)注入
在express.js中使用 D.I. 是 node.js 項(xiàng)目架構(gòu)的最后一個(gè)難題。
路由層
route.post('/', async (req, res, next) => { const userDTO = req.body; const userServiceInstance = Container.get(UserService) // Service locator const { user, company } = userServiceInstance.Signup(userDTO); return res.json({ user, company }); });
太棒了,項(xiàng)目看起來(lái)很棒!
它是如此有組織,以至于我現(xiàn)在就想寫(xiě)代碼了。
一個(gè)單元測(cè)試的例子在github上查看源碼
https://github.com/santiq/bulletproof-nodejs
通過(guò)使用依賴(lài)注入和這些組織模式,單元測(cè)試變得非常簡(jiǎn)單。
您不必模擬 req/res 對(duì)象或要求(…)調(diào)用。
例子:注冊(cè)用戶(hù)方法的單元測(cè)試
tests/unit/services/user.js
import UserService from '../../../src/services/user'; describe('User service unit tests', () => { describe('Signup', () => { test('Should create user record and emit user_signup event', async () => { const eventEmitterService = { emit: jest.fn(), }; const userModel = { create: (user) => { return { ...user, _id: 'mock-user-id' } }, }; const companyModel = { create: (user) => { return { owner: user._id, companyTaxId: '12345', } }, }; const userInput= { fullname: 'User Unit Test', email: 'test@example.com', }; const userService = new UserService(userModel, companyModel, eventEmitterService); const userRecord = await userService.SignUp(teamId.toHexString(), userInput); expect(userRecord).toBeDefined(); expect(userRecord._id).toBeDefined(); expect(eventEmitterService.emit).toBeCalled(); }); }) })定時(shí)任務(wù)
因此,既然業(yè)務(wù)邏輯封裝到了服務(wù)層中,那么在定時(shí)任務(wù)中使用它就更容易了。
您永遠(yuǎn)不應(yīng)該依賴(lài) node.js的 setTimeout
或其他延遲執(zhí)行代碼的原生方法,而應(yīng)該依賴(lài)一個(gè)框架把你的定時(shí)任務(wù)和執(zhí)行持久化到數(shù)據(jù)庫(kù)。
這樣你就可以控制失敗的任務(wù)和成功的反饋信息。
配置項(xiàng)和私密信息我已經(jīng)寫(xiě)了一個(gè)關(guān)于這個(gè)的好的練習(xí),
查看我關(guān)于使用 node.js 最好的任務(wù)管理器 agenda.js 的指南.
https://softwareontheroad.com/nodejs-scalability-issues
根據(jù) 應(yīng)用程序的12個(gè)因素 的概念,我們存儲(chǔ) API 密鑰和數(shù)據(jù)庫(kù)連接配置的方式是使用 .env文件。
創(chuàng)建一個(gè) .env
文件,一定不要提交 (在你的倉(cāng)庫(kù)里要有一個(gè)包含默認(rèn)值的.env 文件),dotenv
這個(gè)npm 包會(huì)加載 .env 文件,并把變量添加到 node.js 的 process.env
對(duì)象中。
這些本來(lái)已經(jīng)足夠了,但是我喜歡添加一個(gè)額外的步驟。
擁有一個(gè) config/index.ts
文件,在這個(gè)文件中,dotenv
這個(gè)npm 包會(huì)加載 .env 文件,然后我使用一個(gè)對(duì)象存儲(chǔ)這些變量,至此我們有了一個(gè)結(jié)構(gòu)和代碼自動(dòng)加載。
config/index.js
const dotenv = require('dotenv'); //config()方法會(huì)讀取你的 .env 文件,解析內(nèi)容,添加到 process.env。 dotenv.config(); export default { port: process.env.PORT, databaseURL: process.env.DATABASE_URI, paypal: { publicKey: process.env.PAYPAL_PUBLIC_KEY, secretKey: process.env.PAYPAL_SECRET_KEY, }, paypal: { publicKey: process.env.PAYPAL_PUBLIC_KEY, secretKey: process.env.PAYPAL_SECRET_KEY, }, mailchimp: { apiKey: process.env.MAILCHIMP_API_KEY, sender: process.env.MAILCHIMP_SENDER, } }
這樣可以避免代碼中充斥著 process.env.MY_RANDOM_VAR
指令,并且通過(guò)自動(dòng)完成,您不必知道 .env 文件中是如何命名的。
加載器在github 上查看源碼
https://github.com/santiq/bulletproof-nodejs
加載器源于 W3Tech microframework 但不依賴(lài)于他們擴(kuò)展。
這個(gè)想法是指,你可以拆分啟動(dòng)加載過(guò)程到可測(cè)試的獨(dú)立模塊中。
先來(lái)看一個(gè)傳統(tǒng)的 express.js 應(yīng)用的初始化示例:
const mongoose = require('mongoose'); const express = require('express'); const bodyParser = require('body-parser'); const session = require('express-session'); const cors = require('cors'); const errorhandler = require('errorhandler'); const app = express(); app.get('/status', (req, res) => { res.status(200).end(); }); app.head('/status', (req, res) => { res.status(200).end(); }); app.use(cors()); app.use(require('morgan')('dev')); app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.json(setupForStripeWebhooks)); app.use(require('method-override')()); app.use(express.static(__dirname + '/public')); app.use(session({ secret: process.env.SECRET, cookie: { maxAge: 60000 }, resave: false, saveUninitialized: false })); mongoose.connect(process.env.DATABASE_URL, { useNewUrlParser: true }); require('./config/passport'); require('./models/user'); require('./models/company'); app.use(require('./routes')); app.use((req, res, next) => { var err = new Error('Not Found'); err.status = 404; next(err); }); app.use((err, req, res) => { res.status(err.status || 500); res.json({'errors': { message: err.message, error: {} }}); }); ... more stuff ... maybe start up Redis ... maybe add more middlewares async function startServer() { app.listen(process.env.PORT, err => { if (err) { console.log(err); return; } console.log(`Your server is ready !`); }); } // 啟動(dòng)服務(wù)器 startServer();
天吶,上面的面條代碼,不應(yīng)該出現(xiàn)在你的項(xiàng)目中對(duì)吧。
再來(lái)看看,下面是一種有效處理初始化過(guò)程的示例:
const loaders = require('./loaders'); const express = require('express'); async function startServer() { const app = express(); await loaders.init({ expressApp: app }); app.listen(process.env.PORT, err => { if (err) { console.log(err); return; } console.log(`Your server is ready !`); }); } startServer();
現(xiàn)在,各加載過(guò)程都是功能專(zhuān)一的小文件了。
loaders/index.js
import expressLoader from './express'; import mongooseLoader from './mongoose'; export default async ({ expressApp }) => { const mongoConnection = await mongooseLoader(); console.log('MongoDB Intialized'); await expressLoader({ app: expressApp }); console.log('Express Intialized'); // ... 更多加載器 // ... 初始化 agenda // ... or Redis, or whatever you want }
express 加載器。
loaders/express.js
import * as express from 'express'; import * as bodyParser from 'body-parser'; import * as cors from 'cors'; export default async ({ app }: { app: express.Application }) => { app.get('/status', (req, res) => { res.status(200).end(); }); app.head('/status', (req, res) => { res.status(200).end(); }); app.enable('trust proxy'); app.use(cors()); app.use(require('morgan')('dev')); app.use(bodyParser.urlencoded({ extended: false })); // ...More middlewares // Return the express app return app; })
mongo 加載器
loaders/mongoose.js
import * as mongoose from 'mongoose' export default async (): Promise=> { const connection = await mongoose.connect(process.env.DATABASE_URL, { useNewUrlParser: true }); return connection.connection.db; }
結(jié)語(yǔ)在 Github 查看更多加載器的代碼示例
https://github.com/santiq/bulletproof-nodejs
我們深入的分析了 node.js 項(xiàng)目的結(jié)構(gòu),下面是一些可以分享給你的總結(jié):
使用3層結(jié)構(gòu)。
控制器中不要有任何業(yè)務(wù)邏輯代碼。
采用發(fā)布/訂閱模型來(lái)處理后臺(tái)異步任務(wù)。
使用依賴(lài)注入。
使用配置管理,避免泄漏密碼、密鑰等機(jī)密信息。
將 node.js 服務(wù)器的配置拆分為可獨(dú)立加載的小文件。
前往 Github 查看完整示例
https://github.com/santiq/bulletproof-nodejs
原文地址:https://dev.to/santypk4/bulletproof-node-js-project-architecture-4epf
譯文地址:https://learnku.com/nodejs/t/38129
更多編程相關(guān)知識(shí),請(qǐng)?jiān)L問(wèn):編程入門(mén)??!
網(wǎng)站名稱(chēng):詳解NodejsExpress.js項(xiàng)目架構(gòu)
文章起源:http://weahome.cn/article/cjdihh.html