大家好!在本手冊(cè)中,您將了解軟件架構(gòu)這一廣闊而復(fù)雜的領(lǐng)域。
當(dāng)我第一次開始編碼之旅時(shí),我發(fā)現(xiàn)這是一個(gè)既令人困惑又令人生畏的領(lǐng)域。所以我會(huì)盡量避免你的困惑。
在這本手冊(cè)中,我將嘗試為您提供一個(gè)簡(jiǎn)單、淺顯、易于理解的軟件架構(gòu)介紹。
我們將討論軟件世界中的架構(gòu)是什么,您應(yīng)該了解的一些主要概念,以及當(dāng)今使用最廣泛的一些架構(gòu)模式。
對(duì)于每個(gè)主題,我都會(huì)給出一個(gè)簡(jiǎn)短的理論介紹。然后我將分享一些代碼示例,讓您更清楚地了解它們是如何工作的。讓我們開始吧!
目錄
- 什么是軟件架構(gòu)?
- 要了解的重要軟件架構(gòu)概念什么是客戶端-服務(wù)器模型?什么是 API?什么是模塊化?
- 你的基礎(chǔ)設(shè)施是什么樣的?單體架構(gòu)微服務(wù)架構(gòu)什么是前端(BFF)的后端?如何使用負(fù)載均衡器和水平擴(kuò)展
- 您的基礎(chǔ)設(shè)施所在的位置本地托管傳統(tǒng)服務(wù)器提供商托管在云端傳統(tǒng)的松緊帶無服務(wù)器許多其他服務(wù)
- 要了解的不同文件夾結(jié)構(gòu)一站式文件夾結(jié)構(gòu)圖層文件夾結(jié)構(gòu)MVC 文件夾結(jié)構(gòu)
- 結(jié)論
根據(jù)這個(gè)來源:
系統(tǒng)的軟件架構(gòu)代表與整個(gè)系統(tǒng)結(jié)構(gòu)和行為相關(guān)的設(shè)計(jì)決策。
這很籠統(tǒng),對(duì)吧?絕對(duì)地。在研究軟件架構(gòu)時(shí),這正是讓我非常困惑的地方。這是一個(gè)包含很多內(nèi)容的主題,該術(shù)語(yǔ)用于談?wù)撛S多不同的事物。
我可以說的最簡(jiǎn)單的方式是,軟件架構(gòu)是指您在創(chuàng)建軟件的過程中如何組織東西。而這里的“東西”可以指:
- 實(shí)現(xiàn)細(xì)節(jié)(即你的 repo 的文件夾結(jié)構(gòu))
- 實(shí)施設(shè)計(jì)決策(您使用服務(wù)器端還是客戶端渲染?關(guān)系型數(shù)據(jù)庫(kù)還是非關(guān)系型數(shù)據(jù)庫(kù)?)
- 您選擇的技術(shù)(您的 API 使用 REST 還是 GraphQl?Python/ target=_blank class=infotextkey>Python 和 Django 還是 Node 和 Express 作為后端?)
- 系統(tǒng)設(shè)計(jì)決策(比如你的系統(tǒng)是單體還是被劃分為微服務(wù)?)
- 基礎(chǔ)架構(gòu)決策(您是在本地還是在云提供商上托管您的軟件?)
這是很多不同的選擇和可能性。更復(fù)雜一點(diǎn)的是,在這 5 個(gè)部門中,可以組合不同的模式。這意味著,我可以擁有一個(gè)使用 REST 或 GraphQL 的單體 API,一個(gè)托管在本地或云上的基于微服務(wù)的應(yīng)用程序,等等。
為了更好地解釋這個(gè)混亂,首先我們要解釋一些基本的通用概念。然后我們將介紹其中的一些部門,解釋當(dāng)今用于構(gòu)建應(yīng)用程序的最常見的架構(gòu)模式或選擇。
需要了解的重要軟件架構(gòu)概念什么是客戶端-服務(wù)器模型?
客戶端-服務(wù)器是一種在資源或服務(wù)提供者(服務(wù)器)與服務(wù)或資源請(qǐng)求者(客戶端)之間構(gòu)建應(yīng)用程序任務(wù)或工作負(fù)載的模型。
簡(jiǎn)而言之,客戶端是請(qǐng)求某種信息或執(zhí)行動(dòng)作的應(yīng)用程序,而服務(wù)器是根據(jù)客戶端所做的事情發(fā)送信息或執(zhí)行動(dòng)作的程序。
客戶端通常由運(yùn)行在 Web 或移動(dòng)應(yīng)用程序上的前端應(yīng)用程序表示(盡管也存在其他平臺(tái),并且后端應(yīng)用程序也可以充當(dāng)客戶端)。服務(wù)器通常是后端應(yīng)用程序。
為了舉例說明這一點(diǎn),假設(shè)您正在進(jìn)入您最喜歡的社交網(wǎng)絡(luò)。當(dāng)您在瀏覽器上輸入 URL 并按 Enter 鍵時(shí),您的瀏覽器將充當(dāng)客戶端應(yīng)用程序并向社交網(wǎng)絡(luò)服務(wù)器發(fā)送請(qǐng)求,該服務(wù)器通過向您發(fā)送網(wǎng)站內(nèi)容來響應(yīng)。
現(xiàn)在大多數(shù)應(yīng)用程序都使用客戶端-服務(wù)器模型。需要記住的最重要的概念是客戶端請(qǐng)求服務(wù)器執(zhí)行的資源或服務(wù)。
另一個(gè)需要了解的重要概念是客戶端和服務(wù)器是同一系統(tǒng)的一部分,但每個(gè)都是獨(dú)立的應(yīng)用程序/程序。這意味著它們可以單獨(dú)開發(fā)、托管和執(zhí)行。
如果你不熟悉前端和后端的區(qū)別,這里有一篇很酷的文章來解釋它。這是另一篇擴(kuò)展客戶端-服務(wù)器概念的文章。
什么是 API?
我們剛剛提到客戶端和服務(wù)器是相互通信以請(qǐng)求事物和響應(yīng)事物的實(shí)體。這兩個(gè)部分通常通信的方式是通過 API(應(yīng)用程序編程接口)。
API 只不過是一組定義的規(guī)則,用于確定應(yīng)用程序如何與另一個(gè)應(yīng)用程序通信。這就像兩部?分之間的合同,上面寫著“如果您發(fā)送 A,我將始終響應(yīng) B。如果您發(fā)送 C,我將始終響應(yīng) D……”等等。
有了這組規(guī)則,客戶端就可以準(zhǔn)確地知道完成某項(xiàng)任務(wù)需要什么,而服務(wù)器也可以準(zhǔn)確地知道客戶端在必須執(zhí)行某個(gè)操作時(shí)需要什么。
API 有多種實(shí)現(xiàn)方式。最常用的是 REST、SOAP 和 GraphQl。
關(guān)于 API 的通信方式,最常見的是使用 HTTP 協(xié)議,并以 JSON 或 XML 格式交換內(nèi)容。但是其他協(xié)議和內(nèi)容格式是完全可能的。
如果您想擴(kuò)展此主題,這里有一篇不錯(cuò)的文章供您閱讀。
什么是模塊化?
當(dāng)我們談?wù)撥浖軜?gòu)中的“模塊化”時(shí),我們指的是把大的東西分成小塊的做法。這種分解事物的做法是為了簡(jiǎn)化大型應(yīng)用程序或代碼庫(kù)。
模塊化具有以下優(yōu)點(diǎn):
- 它有利于劃分關(guān)注點(diǎn)和特征,這有助于項(xiàng)目的可視化、理解和組織。
- 當(dāng)項(xiàng)目被清晰地組織和細(xì)分時(shí),它往往更容易維護(hù)并且不易出錯(cuò)和錯(cuò)誤。
- 如果您的項(xiàng)目被細(xì)分為許多不同的部分,則每個(gè)部分都可以單獨(dú)和獨(dú)立地進(jìn)行處理和修改,這通常非常有用。
我知道這聽起來有點(diǎn)籠統(tǒng),但是模塊化或細(xì)分事物的實(shí)踐是軟件架構(gòu)的重要組成部分。所以只要把這個(gè)概念放在你的腦海里——當(dāng)我們通過一些例子時(shí),它會(huì)變得更加清晰和明顯。;)
如果您想了解有關(guān)此主題的更多信息,我最近寫了一篇關(guān)于在 JS中使用模塊的文章,您可能會(huì)發(fā)現(xiàn)它很有用。
您的基礎(chǔ)設(shè)施是什么樣的?
好的,現(xiàn)在讓我們來看看好東西。我們將開始討論組織軟件應(yīng)用程序的多種不同方式,首先是如何組織項(xiàng)目背后的基礎(chǔ)設(shè)施。
為了讓這一切不那么抽象,我們將使用一個(gè)名為 Notflix 的假設(shè)應(yīng)用程序。
旁注:請(qǐng)記住,這個(gè)例子可能不是最現(xiàn)實(shí)的例子,我將假設(shè)/強(qiáng)制情況以呈現(xiàn)某些概念。這里的想法是通過示例幫助您理解核心架構(gòu)概念,而不是執(zhí)行現(xiàn)實(shí)世界的分析。
單體架構(gòu)
所以 Notflix 將是一個(gè)典型的視頻流應(yīng)用程序,用戶將能夠在其中觀看電影、連續(xù)劇、紀(jì)錄片等。用戶將能夠在網(wǎng)絡(luò)瀏覽器、移動(dòng)應(yīng)用程序和電視應(yīng)用程序中使用該應(yīng)用程序。
我們應(yīng)用程序中包含的主要服務(wù)將是身份驗(yàn)證(因此人們可以創(chuàng)建帳戶、登錄等)、支付(因此人們可以訂閱和訪問內(nèi)容......因?yàn)槟悴徽J(rèn)為這一切都是免費(fèi)的,對(duì)吧? )當(dāng)然還有流媒體(這樣人們就可以實(shí)際觀看他們所支付的費(fèi)用)。
我們的架構(gòu)的快速草圖可能如下所示:
經(jīng)典的單體架構(gòu)
在左側(cè),我們有三個(gè)不同的前端應(yīng)用程序,它們將充當(dāng)該系統(tǒng)中的客戶端。例如,它們可能是使用 React 和 React-native 開發(fā)的。
我們有一個(gè)服務(wù)器,它將接收來自所有三個(gè)客戶端應(yīng)用程序的請(qǐng)求,在必要時(shí)與數(shù)據(jù)庫(kù)通信,并相應(yīng)地響應(yīng)每個(gè)前端。比方說,后端可以使用 Node 和 Express 開發(fā)。
這種架構(gòu)被稱為單體架構(gòu),因?yàn)橛幸粋€(gè)服務(wù)器應(yīng)用程序負(fù)責(zé)系統(tǒng)的所有功能。在我們的例子中,如果用戶想要驗(yàn)證、支付我們或觀看我們的一部電影,所有的請(qǐng)求都將被發(fā)送到同一個(gè)服務(wù)器應(yīng)用程序。
單片設(shè)計(jì)的主要好處是它的簡(jiǎn)單性。它的功能和所需的設(shè)置簡(jiǎn)單易懂,這就是大多數(shù)應(yīng)用程序以這種方式開始的原因。
微服務(wù)架構(gòu)
事實(shí)證明,Notflix 完全搖擺不定。我們剛剛發(fā)布了最新一季的“Stranger thugs”,這是一部關(guān)于青少年說唱歌手的科幻系列,以及我們的電影“Agent 404”(關(guān)于一個(gè)秘密特工,他潛入一家模擬高級(jí)程序員的公司,但實(shí)際上并沒有了解代碼)正在打破所有記錄......
我們每個(gè)月都會(huì)從世界各地獲得數(shù)以萬計(jì)的新用戶,這對(duì)我們的業(yè)務(wù)來說非常好,但對(duì)于我們的單一應(yīng)用程序來說卻不是那么好。
最近我們一直在經(jīng)歷服務(wù)器響應(yīng)時(shí)間的延遲,即使我們已經(jīng)垂直擴(kuò)展了服務(wù)器(將更多的 RAM 和 GPU 放入其中),可憐的東西似乎無法承受它所承受的負(fù)載。
此外,我們一直在為我們的系統(tǒng)開發(fā)新功能(例如,一個(gè)可以讀取用戶偏好并推薦適合用戶個(gè)人資料的電影的推薦工具),我們的代碼庫(kù)開始看起來龐大且使用起來非常復(fù)雜。
深入分析這個(gè)問題,我們發(fā)現(xiàn)占用資源最多的功能是流媒體,而其他服務(wù)如身份驗(yàn)證和支付并不代表很大的負(fù)載。
為了解決這個(gè)問題,我們將實(shí)現(xiàn)一個(gè)看起來像這樣的微服務(wù)架構(gòu):
我們的第一個(gè)微服務(wù)實(shí)現(xiàn)
因此,如果您不熟悉這一切,您可能會(huì)想“微服務(wù)到底是什么”,對(duì)吧?好吧,我們可以將其定義為將服務(wù)器端功能劃分為許多只負(fù)責(zé)一個(gè)或幾個(gè)特定功能的小型服務(wù)器的概念。
按照我們的示例,之前我們只有一個(gè)服務(wù)器負(fù)責(zé)所有功能(單體架構(gòu))。實(shí)現(xiàn)微服務(wù)后,我們將有一個(gè)服務(wù)器負(fù)責(zé)身份驗(yàn)證,另一個(gè)負(fù)責(zé)支付,另一個(gè)負(fù)責(zé)流式傳輸,最后一個(gè)負(fù)責(zé)推薦。
當(dāng)用戶想要登錄時(shí),客戶端應(yīng)用程序?qū)⑴c身份驗(yàn)證服務(wù)器通信,當(dāng)用戶想要支付時(shí)與支付服務(wù)器通信,當(dāng)用戶想要觀看時(shí)與流媒體服務(wù)器通信。
所有這些通信都是通過 API 進(jìn)行的,就像使用常規(guī)的單片服務(wù)器(或通過其他通信系統(tǒng),如Kafka或RabbitMQ)一樣。唯一的區(qū)別是,現(xiàn)在我們有不同的服務(wù)器負(fù)責(zé)不同的操作,而不是一個(gè)單獨(dú)的服務(wù)器來完成所有操作。
這聽起來有點(diǎn)復(fù)雜,確實(shí)如此,但微服務(wù)為我們提供了以下好處:
- 您可以根據(jù)需要擴(kuò)展特定服務(wù),而不是一次擴(kuò)展整個(gè)后端。按照我們的示例,當(dāng)我們開始遇到性能問題時(shí),我們垂直擴(kuò)展了整個(gè)服務(wù)器——但實(shí)際上請(qǐng)求更多資源的功能只是流式傳輸。現(xiàn)在我們已經(jīng)將流功能分離到一個(gè)服務(wù)器中,我們可以只擴(kuò)展那個(gè),只要它們繼續(xù)正常工作就可以不用管其余的。
- 功能將更加松散耦合,這意味著我們將能夠獨(dú)立開發(fā)和部署它們。
- 每個(gè)服務(wù)器的代碼庫(kù)會(huì)更小更簡(jiǎn)單。這對(duì)于從一開始就與我們合作的開發(fā)人員來說是一件好事,對(duì)于新開發(fā)人員來說也更容易和更快地理解。
微服務(wù)是一種設(shè)置和管理更復(fù)雜的架構(gòu),這就是為什么它只對(duì)非常大的項(xiàng)目有意義。大多數(shù)項(xiàng)目將作為單體開始,僅在出于性能原因需要時(shí)遷移到微服務(wù)。
如果您想了解更多關(guān)于微服務(wù)的信息,這里有一個(gè)很好的解釋。
什么是前端(BFF)的后端?
實(shí)現(xiàn)微服務(wù)時(shí)出現(xiàn)的一個(gè)問題是與前端應(yīng)用程序的通信變得更加復(fù)雜。現(xiàn)在我們有許多服務(wù)器負(fù)責(zé)不同的事情,這意味著前端應(yīng)用程序需要跟蹤這些信息才能知道向誰(shuí)發(fā)出請(qǐng)求。
通常這個(gè)問題可以通過在前端應(yīng)用程序和微服務(wù)之間實(shí)現(xiàn)一個(gè)中間層來解決。這一層會(huì)接收所有的前端請(qǐng)求,重定向到對(duì)應(yīng)的微服務(wù),接收微服務(wù)響應(yīng),然后將響應(yīng)重定向到對(duì)應(yīng)的前端應(yīng)用。
BFF 模式的好處是我們獲得了微服務(wù)架構(gòu)的好處,而不會(huì)使與前端應(yīng)用程序的通信過于復(fù)雜。
我們的 BFF 實(shí)施
如果您想了解更多信息,這里有一個(gè)解釋 BFF 模式的視頻。
如何使用負(fù)載均衡器和水平擴(kuò)展
因此,我們的流媒體應(yīng)用程序以指數(shù)級(jí)的速度不斷增長(zhǎng)。我們?cè)谌蛴袛?shù)百萬用戶 24/7 全天候觀看我們的電影,而且比我們預(yù)期的更快,我們?cè)俅伍_始遇到性能問題。
我們?cè)俅伟l(fā)現(xiàn)流媒體服務(wù)是壓力最大的服務(wù),我們已經(jīng)盡我們所能垂直擴(kuò)展了該服務(wù)器。將該服務(wù)進(jìn)一步細(xì)分為更多的微服務(wù)是沒有意義的,因此我們決定橫向擴(kuò)展該服務(wù)。
之前我們提到垂直擴(kuò)展意味著向單個(gè)服務(wù)器/計(jì)算機(jī)添加更多資源(RAM、磁盤空間、GPU 等)。另一方面,水平擴(kuò)展意味著設(shè)置更多的服務(wù)器來執(zhí)行相同的任務(wù)。
我們現(xiàn)在將擁有三個(gè)服務(wù)器,而不是一個(gè)負(fù)責(zé)流式傳輸?shù)姆?wù)器。然后客戶端執(zhí)行的請(qǐng)求將在這三臺(tái)服務(wù)器之間進(jìn)行平衡,以便所有服務(wù)器都能處理可接受的負(fù)載。
這種請(qǐng)求的分配通常由稱為負(fù)載均衡器的東西執(zhí)行。負(fù)載均衡器充當(dāng)我們服務(wù)器的反向代理,在客戶端請(qǐng)求到達(dá)服務(wù)器之前攔截它們并將該請(qǐng)求重定向到相應(yīng)的服務(wù)器。
雖然典型的客戶端-服務(wù)器連接可能如下所示:
這是我們之前的
使用負(fù)載均衡器,我們可以將客戶端請(qǐng)求分發(fā)到多個(gè)服務(wù)器:
這就是我們現(xiàn)在想要的
您應(yīng)該知道水平擴(kuò)展也可以用于數(shù)據(jù)庫(kù),因?yàn)樗梢杂糜诜?wù)器。實(shí)現(xiàn)這一點(diǎn)的一種方法是使用源-副本模型,其中一個(gè)特定的源 DB 將接收所有寫入查詢并將其數(shù)據(jù)沿一個(gè)或多個(gè)副本 DB 復(fù)制。副本數(shù)據(jù)庫(kù)將接收并響應(yīng)所有讀取查詢。
數(shù)據(jù)庫(kù)復(fù)制的優(yōu)點(diǎn)是:
- 更好的性能:該模型提高了性能,允許并行處理更多查詢。
- 可靠性和可用性:如果您的一個(gè)數(shù)據(jù)庫(kù)服務(wù)器因任何原因被破壞或無法訪問,數(shù)據(jù)仍保留在其他數(shù)據(jù)庫(kù)中。
因此,在實(shí)現(xiàn)負(fù)載均衡器、水平擴(kuò)展和數(shù)據(jù)庫(kù)復(fù)制之后,我們的架構(gòu)可能如下所示:
我們的水平擴(kuò)展架構(gòu)
如果您有興趣了解更多,這里有一個(gè)關(guān)于負(fù)載均衡器的精彩視頻解釋。
旁注:當(dāng)我們談?wù)撐⒎?wù)、負(fù)載均衡器和擴(kuò)展時(shí),我們可能總是在談?wù)摵蠖藨?yīng)用程序。對(duì)于前端應(yīng)用程序,它們大多總是作為單體開發(fā),盡管還有一個(gè)奇怪有趣的東西叫做微前端。
您的基礎(chǔ)架構(gòu)所在的位置
現(xiàn)在我們已經(jīng)對(duì)如何組織應(yīng)用程序基礎(chǔ)架構(gòu)有了基本的了解,接下來要考慮的是我們將把所有這些東西放在哪里。
正如我們將要看到的,在決定在何處以及如何托管應(yīng)用程序時(shí)主要有三個(gè)選項(xiàng):在本地、在傳統(tǒng)服務(wù)器提供商上或在云上。
本地托管
內(nèi)部部署意味著您擁有運(yùn)行應(yīng)用程序的硬件。在過去,這曾經(jīng)是托管應(yīng)用程序的最傳統(tǒng)方式。公司曾經(jīng)有專門的房間供服務(wù)器使用,并且有專門的團(tuán)隊(duì)負(fù)責(zé)硬件的設(shè)置和維護(hù)。
這個(gè)選項(xiàng)的好處是公司可以完全控制硬件。不好的是它需要空間、時(shí)間和金錢。
想象一下,如果您想橫向擴(kuò)展某臺(tái)服務(wù)器,那將意味著購(gòu)買更多設(shè)備,對(duì)其進(jìn)行設(shè)置,不斷對(duì)其進(jìn)行監(jiān)督,修復(fù)任何損壞的東西......如果您以后需要縮減該服務(wù)器,那么,通常你'這些東西買了就不能退了
對(duì)于大多數(shù)公司而言,擁有本地服務(wù)器意味著將大量資源用于與公司目標(biāo)沒有直接關(guān)系的任務(wù)。
我們?nèi)绾蜗胂笪覀冊(cè)?Notflix 的服務(wù)器機(jī)房
結(jié)局如何
本地服務(wù)器仍然有意義的一種情況是在處理非常微妙或私密的信息時(shí)。例如,想想運(yùn)行發(fā)電廠的軟件或私人銀行信息。其中許多組織決定使用本地服務(wù)器作為完全控制其軟件和硬件的一種方式。
傳統(tǒng)服務(wù)器提供商
對(duì)于大多數(shù)公司來說,更舒適的選擇是傳統(tǒng)的服務(wù)器提供商。這些公司擁有自己的服務(wù)器,他們只是租用它們。你決定你的項(xiàng)目需要什么樣的硬件,并按月支付費(fèi)用(或根據(jù)其他條件支付一定的費(fèi)用)。
此選項(xiàng)的優(yōu)點(diǎn)在于您不再需要擔(dān)心任何與硬件相關(guān)的事情。供應(yīng)商會(huì)照顧它,作為一家軟件公司,您只擔(dān)心您的主要目標(biāo),即軟件。
另一個(gè)很酷的事情是擴(kuò)大或縮小規(guī)模很容易且無風(fēng)險(xiǎn)。如果您需要更多硬件,則需要付費(fèi)。如果您不再需要它,您只需停止付款。
眾所周知的服務(wù)器提供商的一個(gè)例子是托管程序。
托管在云端
如果您已經(jīng)接觸過技術(shù)一段時(shí)間,您可能不止一次聽說過“云”這個(gè)詞。起初,這聽起來很抽象,有點(diǎn)神奇,但實(shí)際上它背后的東西只不過是亞馬遜、谷歌和微軟等公司擁有的巨大數(shù)據(jù)中心。
在某些時(shí)候,這些公司發(fā)現(xiàn)他們擁有并非一直在使用的 huuuuuuuuge 計(jì)算能力。由于所有這些硬件無論你是否使用它都代表著成本,明智的做法是將計(jì)算能力商業(yè)化給其他人。
這就是云計(jì)算。使用AWS(亞馬遜網(wǎng)絡(luò)服務(wù))、谷歌云或 MicrosoftAzure等不同的服務(wù),我們能夠在這些公司的數(shù)據(jù)中心托管我們的應(yīng)用程序,并利用所有這些計(jì)算能力。
“云”實(shí)際上可能是什么樣子
在了解云服務(wù)時(shí),重要的是要注意有許多不同的方式可以使用它們:
傳統(tǒng)的
第一種方法是以與使用傳統(tǒng)服務(wù)器提供商類似的方式使用它們。您可以選擇所需的硬件類型并按月準(zhǔn)確支付費(fèi)用。
松緊帶
第二種方法是利用大多數(shù)提供商提供的“彈性”計(jì)算。“彈性”意味著您的應(yīng)用程序的硬件容量將根據(jù)您的應(yīng)用程序的使用情況自動(dòng)增長(zhǎng)或縮小。
例如,您可以從具有 8gb 內(nèi)存和 500gb 磁盤空間的服務(wù)器開始。如果您的服務(wù)器開始收到越來越多的請(qǐng)求并且這些容量不再足以提供良好的性能,系統(tǒng)可以自動(dòng)執(zhí)行垂直或水平擴(kuò)展。
令人敬畏的是,您可以預(yù)先配置所有這些,而不必再擔(dān)心它。隨著服務(wù)器自動(dòng)擴(kuò)展和縮減,您只需為消耗的資源付費(fèi)。
無服務(wù)器
使用云計(jì)算的另一種方式是使用無服務(wù)器架構(gòu)。
按照這種模式,您將不會(huì)擁有一個(gè)接收所有請(qǐng)求并響應(yīng)它們的服務(wù)器。相反,您會(huì)將單個(gè)函數(shù)映射到訪問點(diǎn)(類似于 API 端點(diǎn))。
這些函數(shù)將在每次收到請(qǐng)求時(shí)執(zhí)行,并執(zhí)行您為它們編寫的任何操作(連接到數(shù)據(jù)庫(kù)、執(zhí)行 CRUD 操作或您可以在常規(guī)服務(wù)器中執(zhí)行的任何其他操作)。
無服務(wù)器架構(gòu)的好處在于您忘記了所有關(guān)于服務(wù)器維護(hù)和擴(kuò)展的事情。您只有在需要時(shí)執(zhí)行的功能,并且每個(gè)功能都會(huì)根據(jù)需要自動(dòng)放大和縮小。
作為客戶,您只需為函數(shù)執(zhí)行的次數(shù)和每次執(zhí)行持續(xù)的處理時(shí)間支付費(fèi)用。
如果您想了解更多信息,這里是對(duì)無服務(wù)器模式的解釋。
許多其他服務(wù)
您可能會(huì)看到彈性和無服務(wù)器服務(wù)如何為設(shè)置軟件基礎(chǔ)架構(gòu)提供非常簡(jiǎn)單方便的替代方案。
除了與服務(wù)器相關(guān)的服務(wù)之外,云提供商還提供大量其他解決方案,例如關(guān)系和非關(guān)系數(shù)據(jù)庫(kù)、文件存儲(chǔ)服務(wù)、緩存服務(wù)、身份驗(yàn)證服務(wù)、機(jī)器學(xué)習(xí)和數(shù)據(jù)處理服務(wù)、監(jiān)控和性能分析等等。一切都托管在云中。
通過Terraform或 AWS Cloud Formation 等工具,我們甚至可以將基礎(chǔ)設(shè)施設(shè)置為代碼。這意味著我們可以在幾分鐘內(nèi)編寫一個(gè)腳本,在云上設(shè)置服務(wù)器、數(shù)據(jù)庫(kù)以及我們可能需要的任何其他內(nèi)容。
從工程的角度來看,這是令人興奮的,對(duì)于我們作為開發(fā)人員來說真的很方便。如今的云計(jì)算提供了一套非常完整的解決方案,可以輕松地從微小的小項(xiàng)目適應(yīng)地球上最大的數(shù)字產(chǎn)品。這就是為什么現(xiàn)在越來越多的軟件項(xiàng)目選擇將其基礎(chǔ)架構(gòu)托管在云中的原因。
如前所述,最常用和知名的云提供商是AWS、谷歌云和Azure。盡管還有其他選擇,例如IBM、DigitalOcean和Oracle。
這些提供商中的大多數(shù)都提供相同類型的服務(wù),盡管它們可能有不同的名稱。例如,無服務(wù)器函數(shù)在 AWS 上稱為“lambdas”,在 google 云上稱為“云函數(shù)”。
要了解的不同文件夾結(jié)構(gòu)
好的,到目前為止,我們已經(jīng)了解了架構(gòu)如何指代基礎(chǔ)設(shè)施組織和托管。現(xiàn)在讓我們看看一些代碼以及架構(gòu)如何引用文件夾結(jié)構(gòu)和代碼模塊化。
一站式文件夾結(jié)構(gòu)
為了說明為什么文件夾結(jié)構(gòu)很重要,讓我們構(gòu)建一個(gè)虛擬示例 API。我們將有一個(gè)兔子的模擬數(shù)據(jù)庫(kù),API 將對(duì)其執(zhí)行CRUD操作。我們將使用 Node 和 Express 構(gòu)建它。
這是我們的第一種方法,根本沒有文件夾結(jié)構(gòu)。我們的 repo 將由node modules文件夾、App.js、package-lock.json和package.json文件組成。
在我們的 app.js 文件中,我們將擁有我們的微型服務(wù)器、我們的模擬數(shù)據(jù)庫(kù)和兩個(gè)端點(diǎn):
// App.js const express = require('express'); const app = express() const port = 7070 // Mock DB const db = [ { id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { id: 3, name: 'Joe' }, { id: 4, name: 'Jack' }, { id: 5, name: 'Jill' }, { id: 6, name: 'Jak' }, { id: 7, name: 'Jana' }, { id: 8, name: 'Jan' }, { id: 9, name: 'Jas' }, { id: 10, name: 'Jasmine' }, ] /* Routes */ app.get('/rabbits', (req, res) => { res.json(db) }) app.get('/rabbits/:idx', (req, res) => { res.json(db[req.params.idx]) }) app.listen(port, () => console.log(`??[server]: Server is running at http://localhost:${port}`))
如果我們測(cè)試端點(diǎn),我們會(huì)發(fā)現(xiàn)它們工作得很好:
http://localhost:7070/rabbits # [ # { # "id": 1, # "name": "John" # }, # { # "id": 2, # "name": "Jane" # }, # { # "id": 3, # "name": "Joe" # }, # .... # ] ### http://localhost:7070/rabbits/1 # { # "id": 2, # "name": "Jane" # }
那么這有什么問題呢?沒什么,實(shí)際上,它工作得很好。只有當(dāng)代碼庫(kù)變得更大、更復(fù)雜并且我們開始向 API 添加新功能時(shí),才會(huì)出現(xiàn)問題。
與我們之前在解釋單體架構(gòu)時(shí)所討論的類似,將所有東西放在一個(gè)地方一開始很好也很容易。但是當(dāng)事情開始變得更大更復(fù)雜時(shí),這是一種令人困惑且難以遵循的方法。
遵循模塊化原則,更好的想法是為我們需要執(zhí)行的不同職責(zé)和操作設(shè)置不同的文件夾和文件。
為了更好地說明這一點(diǎn),讓我們向我們的 API 添加新功能,看看我們?nèi)绾卧趯蛹軜?gòu)的幫助下采用模塊化方法。
圖層文件夾結(jié)構(gòu)
分層架構(gòu)是將關(guān)注點(diǎn)和職責(zé)劃分到不同的文件夾和文件中,并且只允許在某些文件夾和文件之間進(jìn)行直接通信。
你的項(xiàng)目應(yīng)該有多少層,每個(gè)層應(yīng)該有什么名稱,以及它應(yīng)該處理什么動(dòng)作都是討論的問題。因此,讓我們看看我認(rèn)為對(duì)于我們的示例來說是一個(gè)好的方法。
我們的應(yīng)用程序?qū)⒂形鍌€(gè)不同的層,它們將以這種方式排序:
應(yīng)用層
- 應(yīng)用層將具有我們服務(wù)器的基本設(shè)置以及與我們的路由(下一層)的連接。
- 路由層將定義我們所有的路由以及與控制器(下一層)的連接。
- 控制器層將具有我們想要在每個(gè)端點(diǎn)中執(zhí)行的實(shí)際邏輯以及與模型層的連接(下一層,你明白了......)
- 模型層將保存與我們的模擬數(shù)據(jù)庫(kù)交互的邏輯。
- 最后,持久層是我們的數(shù)據(jù)庫(kù)所在的位置。
您可以看到這種方法更加結(jié)構(gòu)化,并且具有明確的關(guān)注點(diǎn)劃分。這聽起來像是很多樣板。但是在設(shè)置好之后,這個(gè)架構(gòu)將讓我們清楚地知道每個(gè)東西在哪里,以及哪些文件夾和文件負(fù)責(zé)我們的應(yīng)用程序執(zhí)行的每個(gè)操作。
要記住的重要一點(diǎn)是,在這類架構(gòu)中,必須遵循層之間定義的通信流才能使其有意義。
這意味著請(qǐng)求首先必須通過第一層,然后是第二層,然后是第三層,依此類推。任何請(qǐng)求都不應(yīng)該跳過層,因?yàn)檫@會(huì)破壞架構(gòu)的邏輯以及它給我們帶來的組織和模塊化的好處。
描繪我們的架構(gòu)的另一種方式
現(xiàn)在讓我們看一些代碼。使用層架構(gòu),我們的文件夾結(jié)構(gòu)可能如下所示:
- 我們有一個(gè)名為的新文件夾db,它將保存我們的數(shù)據(jù)庫(kù)文件。
- 另一個(gè)名為的文件夾rabbits將保存與該實(shí)體相關(guān)的路由、控制器和模型。
- app.js設(shè)置我們的服務(wù)器并連接到路由。
// App.js const express = require('express'); const rabbitRoutes = require('./rabbits/routes/rabbits.routes') const app = express() const port = 7070 /* Routes */ app.use('/rabbits', rabbitRoutes) app.listen(port, () => console.log(`??[server]: Server is running at http://localhost:${port}`))
- rabbits.routes.js保存與該實(shí)體相關(guān)的每個(gè)端點(diǎn)并將它們鏈接到相應(yīng)的控制器(當(dāng)請(qǐng)求到達(dá)該端點(diǎn)時(shí)我們想要執(zhí)行的函數(shù))。
// rabbits.routes.js const express = require('express') const bodyParser = require('body-parser') const jsonParser = bodyParser.json() const { listRabbits, getRabbit, editRabbit, addRabbit, deleteRabbit } = require('../controllers/rabbits.controllers') const router = express.Router() router.get('/', listRabbits) router.get('/:id', getRabbit) router.put('/:id', jsonParser, editRabbit) router.post('/', jsonParser, addRabbit) router.delete('/:id', deleteRabbit) module.exports = router
- rabbits.controllers.js持有每個(gè)端點(diǎn)對(duì)應(yīng)的邏輯。在這里,我們編寫函數(shù)應(yīng)該將什么作為輸入,它應(yīng)該執(zhí)行什么過程以及它應(yīng)該返回什么。 此外,每個(gè)控制器都鏈接到相應(yīng)的模型函數(shù)(將執(zhí)行數(shù)據(jù)庫(kù)相關(guān)操作)。
// rabbits.controllers.js const { getAllItems, getItem, editItem, addItem, deleteItem } = require('../models/rabbits.models') const listRabbits = (req, res) => { try { const resp = getAllItems() res.status(200).send(resp) } catch (err) { res.status(500).send(err) } } const getRabbit = (req, res) => { try { const resp = getItem(parseInt(req.params.id)) res.status(200).send(resp) } catch (err) { res.status(500).send(err) } } const editRabbit = (req, res) => { try { const resp = editItem(req.params.id, req.body.item) res.status(200).send(resp) } catch (err) { res.status(500).send(err) } } const addRabbit = (req, res) => { try { console.log( req.body.item ) const resp = addItem(req.body.item) res.status(200).send(resp) } catch (err) { res.status(500).send(err) } } const deleteRabbit = (req, res) => { try { const resp = deleteItem(req.params.idx) res.status(200).send(resp) } catch (err) { res.status(500).send(err) } } module.exports = { listRabbits, getRabbit, editRabbit, addRabbit, deleteRabbit }
- rabbits.models.js是我們定義將在我們的數(shù)據(jù)庫(kù)上執(zhí)行 CRUD 操作的函數(shù)的地方。每個(gè)函數(shù)代表一種不同類型的操作(讀取一個(gè)、讀取所有、編輯、刪除等)。該文件是連接到我們的數(shù)據(jù)庫(kù)的文件。
// rabbits.models.js const db = require('../../db/db') const getAllItems = () => { try { return db } catch (err) { console.error("getAllItems error", err) } } const getItem = id => { try { return db.filter(item => item.id === id)[0] } catch (err) { console.error("getItem error", err) } } const editItem = (id, item) => { try { const index = db.findIndex(item => item.id === id) db[index] = item return db[index] } catch (err) { console.error("editItem error", err) } } const addItem = item => { try { db.push(item) return db } catch (err) { console.error("addItem error", err) } } const deleteItem = id => { try { const index = db.findIndex(item => item.id === id) db.splice(index, 1) return db return db } catch (err) { console.error("deleteItem error", err) } } module.exports = { getAllItems, getItem, editItem, addItem, deleteItem }
- 最后,db.js托管我們的模擬數(shù)據(jù)庫(kù)。在實(shí)際項(xiàng)目中,這可能是您的實(shí)際數(shù)據(jù)庫(kù)連接所在的位置。
// db.js const db = [ { id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { id: 3, name: 'Joe' }, { id: 4, name: 'Jack' }, { id: 5, name: 'Jill' }, { id: 6, name: 'Jak' }, { id: 7, name: 'Jana' }, { id: 8, name: 'Jan' }, { id: 9, name: 'Jas' }, { id: 10, name: 'Jasmine' }, ] module.exports = db
正如我們所看到的,在這種架構(gòu)下還有更多的文件夾和文件。但因此,我們的代碼庫(kù)更加結(jié)構(gòu)化和組織清晰。一切都有自己的位置,不同文件之間的通信被明確定義。
這種組織方式極大地方便了新功能的添加、代碼修改和錯(cuò)誤修復(fù)。
一旦您熟悉了文件夾結(jié)構(gòu)并知道在哪里可以找到每個(gè)東西,您會(huì)發(fā)現(xiàn)使用這些越來越小的文件非常方便,而不必滾動(dòng)瀏覽一個(gè)或兩個(gè)將所有內(nèi)容放在一起的大文件。
我還支持為您的應(yīng)用程序中的每個(gè)主要實(shí)體(在我們的例子中是兔子)創(chuàng)建一個(gè)文件夾。這樣可以更清楚地了解每個(gè)文件的相關(guān)內(nèi)容。
假設(shè)我們現(xiàn)在也想添加新功能來添加/編輯/刪除貓和狗。我們將為它們中的每一個(gè)創(chuàng)建新文件夾,并且每個(gè)文件夾都有自己的路由、控制器和模型文件。我們的想法是分離關(guān)注點(diǎn)并將每件事放在自己的位置。
MVC 文件夾結(jié)構(gòu)
MVC 是一種架構(gòu)模式,代表Model View Controller。我們可以說 MVC 架構(gòu)就像是層架構(gòu)的簡(jiǎn)化,也包含了應(yīng)用程序的前端 (UI)。
在這種架構(gòu)下,我們將只有三個(gè)主要層:
- 視圖層將負(fù)責(zé)渲染 UI。
- 控制器層將負(fù)責(zé)定義路由和每個(gè)路由的邏輯。
- 模型層將負(fù)責(zé)與我們的數(shù)據(jù)庫(kù)進(jìn)行交互。
和以前一樣,每一層只與下一層交互,所以我們有一個(gè)明確定義的通信流。
描繪我們架構(gòu)的另一種方式
有許多框架允許您開箱即用地實(shí)現(xiàn) MVC 架構(gòu)(例如Django或Ruby on Rails)。要使用 Node 和 Express 做到這一點(diǎn),我們需要像EJS這樣的模板引擎。
如果您不熟悉模板引擎,它們只是一種輕松呈現(xiàn) html 的方式,同時(shí)利用變量、循環(huán)、條件等編程特性(非常類似于我們?cè)?React 中使用 JSX 所做的) .
正如我們將在幾秒鐘內(nèi)看到的那樣,我們將為我們想要呈現(xiàn)的每種頁(yè)面創(chuàng)建 EJS 文件,并且從每個(gè)控制器我們將呈現(xiàn)這些文件作為我們的響應(yīng),并將相應(yīng)的響應(yīng)傳遞給它們作為變量。
我們的文件夾結(jié)構(gòu)將如下所示:
- 看到我們刪除了之前的大部分文件夾并保留了db,controllers和models文件夾。
- 我們添加了一個(gè)views文件夾,與我們要呈現(xiàn)的每個(gè)頁(yè)面/響應(yīng)相對(duì)應(yīng)。
- db.js和models.js文件保持完全相同。
- 我們app.js看起來像這樣:
// App.js const express = require("express"); var path = require('path'); const rabbitControllers = require("./rabbits/controllers/rabbits.controllers") const app = express() const port = 7070 // Ejs config app.set("view engine", "ejs") app.set('views', path.join(__dirname, './rabbits/views')) /* Controllers */ app.use("/rabbits", rabbitControllers) app.listen(port, () => console.log(`??[server]: Server is running at http://localhost:${port}`))
- rabbits.controllers.js更改以定義路由,連接到相應(yīng)的模型函數(shù),并為每個(gè)請(qǐng)求呈現(xiàn)相應(yīng)的視圖。看到在渲染方法中我們將請(qǐng)求響應(yīng)作為參數(shù)傳遞給視圖。
// rabbits.controllers.js const express = require('express') const bodyParser = require('body-parser') const jsonParser = bodyParser.json() const { getAllItems, getItem, editItem, addItem, deleteItem } = require('../models/rabbits.models') const router = express.Router() router.get('/', (req, res) => { try { const resp = getAllItems() res.render('rabbits', { rabbits: resp }) } catch (err) { res.status(500).send(err) } }) router.get('/:id', (req, res) => { try { const resp = getItem(parseInt(req.params.id)) res.render('rabbit', { rabbit: resp }) } catch (err) { res.status(500).send(err) } }) router.put('/:id', jsonParser, (req, res) => { try { const resp = editItem(req.params.id, req.body.item) res.render('editRabbit', { rabbit: resp }) } catch (err) { res.status(500).send(err) } }) router.post('/', jsonParser, (req, res) => { try { const resp = addItem(req.body.item) res.render('addRabbit', { rabbits: resp }) } catch (err) { res.status(500).send(err) } }) router.delete('/:id', (req, res) => { try { const resp = deleteItem(req.params.idx) res.render('deleteRabbit', { rabbits: resp }) } catch (err) { res.status(500).send(err) } }) module.exports = router
- 最后,在視圖文件中,我們將接收到的變量作為參數(shù)并將其呈現(xiàn)為 HTML。
All rabbits
<% rabbits.forEach(function(rabbit) { %>
Id: <%= rabbit.id %> Name: <%= rabbit.name %>
Rabbit view
Id: <%= rabbit.id %> Name: <%= rabbit.name %>
現(xiàn)在我們可以進(jìn)入我們的瀏覽器,點(diǎn)擊http://localhost:7070/rabbits并獲取:
或者h(yuǎn)ttp://localhost:7070/rabbits/2得到:
這就是 MVC!
結(jié)論
當(dāng)我們?cè)谲浖澜缰刑岬?ldquo;架構(gòu)”時(shí),我希望所有這些示例都能幫助您理解我們所談?wù)摰膬?nèi)容。
正如我在開始時(shí)所說,這是一個(gè)龐大而復(fù)雜的主題,通常包含許多不同的事物。
在這里,我們介紹了基礎(chǔ)架構(gòu)模式和系統(tǒng)、托管選項(xiàng)和云提供商,最后介紹了一些您可以在項(xiàng)目中使用的常見且有用的文件夾結(jié)構(gòu)。
我們已經(jīng)了解了垂直和水平擴(kuò)展、單體應(yīng)用程序和微服務(wù)、彈性和無服務(wù)器云計(jì)算……很多東西。但這只是冰山一角!因此,請(qǐng)繼續(xù)學(xué)習(xí)和自己進(jìn)行研究。