close

前言

通過例子很完整的介紹軟件架構。今日前端早讀課文章由 @PapayaHUANG 翻譯分享,由公號:freeCodeCamp 授權。

非營利組織 freeCodeCamp.org 成立於 2014 年,以 「幫助人們免費學習編程」 為使命,創建了大量免費的編程教程,包括交互式課程、視頻課程、文章等。freeCodeCamp 正在幫助全球數百萬人學習編程,希望讓世界上每個人都有機會獲得免費的優質的編程教育資源,成為開發者或者運用編程去解決問題。

正文從這開始~~

在本文中,我們將討論一個非常有趣、廣泛且複雜的主題:軟件架構。

我剛開始寫代碼時就被這個主題困擾過,在這篇文章中,我將嘗試為你提供簡單的、表面的、易於理解的介紹。

我們將討論軟件領域中的架構是什麼、一些主要概念以及當今最流行的架構模式。

對於每個主題,我都會給出一個簡單、初級的理論介紹和代碼或者偽代碼示例,你可以從中了解每個概念是如何運行的。讓我們開始吧!

目錄

1、什麼是軟件架構2、重要的軟件架構概念

什麼是客戶端 —— 服務器模型

什麼是 API

什麼是模塊化

3、你的基礎架構是什麼樣的

單體式架構

微服務架構

服務於前端的後端是什麼(BFF)

如何使用負載均衡器和水平擴展

4、你的基礎架構所在的位置

傳統的

彈性的

無服務的

更多其他服務

本地託管

傳統服務器供應商

雲託管

5、不同的文件夾結構

全在一個文件夾中的結構

分層文件夾結構

MVC 文件夾結構

6、總結

什麼是軟件架構

卡耐基・梅隆大學軟件工程學院給的定義:

系統的軟件架構代表與整個系統結構和行為相關的設計決策。

這個說法很籠統,對吧?當然!這正是在研究軟件架構時讓我非常困惑的地方。軟件架構包含很多內容,這個術語可以指代不同的事物。

我用簡單的話來概括就是:軟件架構是指你在創建軟件的過程中如何組織內容。而這裡的 「內容」 可以指:

實現細節(即你倉庫的文件夾結構)

實現設計決策(你是使用服務端還是客戶端渲染?使用關係型還是非關係性數據庫)

你選擇的技術(你是使用 REST 還是 GraphQl API? 後端使用 Python/Django 還是 Nod/Express 技術棧?)

系統設計決策(你的系統是採用單體式架構還是微服務架構?)

基礎設施決策(你是在本地還是在雲提供商上託管軟件?)

以上概括了非常多的選擇和可能性。讓情況變得更複雜的是,在這個五個類別中,不同的模式可以結合。比方說,我可以採用一個單體式的 REST 或者 GraphQL 的 API,或者一個微服務架構的應用託管在雲供應商或者本地。

為了更好地解釋這些混沌的概念,首先我們將討論一些基礎的概念,然後再逐條講解這些分類,並解釋時下搭建應用最常用的架構模式和選擇。

重要的軟件結構概念什麼是客戶端 - 服務器模型

客戶端 - 服務器是一種構建應用程序任務或者工作負載結構的模型,連接資源或服務提供者(服務器)和服務或資源請求者(客戶端)。

簡言之,客戶端就是請求信息或者行為的應用程序;服務器就是根據客戶端的請求,發送信息或者執行行為的程序。

客戶端通常是前端應用,可以在 Web 或者手機應用上運行(雖然也可以通過其他平台使用以及後端應用也可以被當作客戶端);服務器通常是後端應用。

舉個例子,想象你在瀏覽你最喜歡的社交網絡,當你在瀏覽器輸入 URL 並點擊回車之後,你的瀏覽器就像客戶端應用一樣,向社交網絡服務器發送請求,社交網絡服務器響應請求,並向你發送網站內容。

時下大部分應用都採用客戶端 - 服務器模型,最重要的概念是客戶端請求資源和服務,服務器實現。

另一個重要的概念是,雖然客戶端和服務器隸屬於同一個系統,但是兩者各自都擁有自己的應用或者程序。也就是說你可以分別開發、託管和執行兩者。

如果你不熟悉前端和後端的區別,這裡有一篇寫得不錯的文章,供你參考。這裡還有另一篇文章介紹了客戶端 - 服務器的概念。

什麼是 API

我們剛剛講解了客戶端和服務器是兩個相互通信的實體,前端發送請求,後端響應請求。兩者相互通信通常是通過 API(應用程序接口)。

API 只不過是一系列確定應用間如何通信的規則,就像兩方之間的協議:「如果你發送 A,我就響應 B;如果你發送 C,我就響應 D……」。

有了這一系列規則,客戶端就知道完成特定任務需要發送什麼請求;而服務器也知道客戶端特定行為意味着什麼需求。

API 的實現方式多種多樣,時下最常用的是 REST、SOAP 和 GraphQL。

在 API 通信中,HTTP 協議是最常使用的,內容通常採用 JSON 或者 XML 格式。不過也存在其他的協議和內容格式。

如果你想要進一步了解這個話題, 推薦你閱讀這篇文章 。

什麼是模塊化

當我們在軟件工程中討論 「模塊化」,我們指的是將大事化小的行為。拆解的目的是為了簡化龐大的應用或者代碼庫。

模塊化具備以下優勢:

這有利於將關注點和功能分離,有助於項目的可視化、理解和組織。

當項目被清晰地構建和細分之後,就更容易維護也更不容易出錯。

如果項目被細分為許多不同的部分,每個部分可以單獨進行處理和修改,這樣更利於軟件開發。

這聽上去有些籠統,但是模塊化或者說將項目細分是軟件架構中非常重要的一部分。所以只要記住這個概念,通過一些例子,你對它的理解會更加清晰。;)

如果你想要閱讀更多該話題相關內容,我最近寫了一篇關於在JS 中使用模塊的文章,希望對你有幫助。

你的基礎架構是什麼樣的

好的,我們進入文章的精華部分了。我們將討論構建軟件應用程序的不同方式,從項目的基礎架構開始。

為了讓概念不那麼抽象,我將創建一個虛構的應用,叫作 Notflix。🤔🤫

注意:請記住這個例子可能不太現實,我僅以此作為講解概念的例子。這裡只是為了幫助你通過例子來了解架構的核心概念,而不是分析現實例子。

單體式架構

Notflix 將是一視頻流媒體應用,用戶可以使用它觀看電影、劇集、紀錄片等。用戶可以在 Web 瀏覽器、手機和 TV 應用上使用它。

這個應用的主要服務包括:驗證(用戶可以創建賬戶、登陸等)、支付(用戶可以訂閱並獲取內容,你不希望服務完全免費,對吧?😑)和流媒體(用戶可以觀看付費內容)。

基礎的架構如圖:

經典的單體式架構

左手邊是三種不同的前端應用,將作為系統中的客戶端。它們可以通過 React 和 React-native 開發。

一個服務器接受三個客戶端應用的請求,並在必要的時候和數據庫通信,並返回給對應的前端。後端可以由 Node 和 Express 開發。

這種形式的架構就被稱為單體式,因為僅有一個服務器應用來負責系統的所有功能。在我們的例子中,如果用戶需要註冊、支付或者觀看任意一部影片,所有的請求都發送到同一個服務器應用。

單體式的優勢在於設計簡單。這種架構的功能和設置簡單易操作,這也是為什麼大多數應用採用這種架構的原因。

微服務架構

結果 Noflix 表現相當不錯。我們剛剛發布了最新一季的《怪奇物語》,這是一部關於青少年說唱歌手的科幻片,以及一部關於一個人潛入公司假扮資深程序員的電影,創造了新的收視紀錄。

每個月來自世界各地成千上萬的新用戶註冊 Noflix,這對於我們的經營狀況來說是好事,但對於單體式的應用來說可不妙。

最近我們一直在經歷服務器響應時間延遲,儘管我們已經垂直擴展了服務器(增加了 RAM 和 GPU),但是服務器還是超負載了。

此外,我們也在系統中開發新的功能(如根據用戶喜好推薦電影的推薦工具),代碼庫變得臃腫且複雜。

深入分析問題之後,我們發現是流媒體占用了大量的資源,其他服務如認證和支付資源占比不大。

為了解決這個問題,我們決定實現微服務架構,如圖所示:

我們的首個微服務架構

如果你剛接觸這個概念,你可能會問 「微服務到底是個什麼玩意兒?」,其實就是把服務器細分成不同的小服務器,負責一個或者幾個功能。

在我們例子中,起初我們僅有一個服務器來響應所有功能(單體式架構),實現微服務架構後,我們就有一個服務器負責認證,另一個負責支付,還有一個負責流媒體,最後一個負責推薦。

當需要登陸的時候,客戶端應用與認證服務通信,用戶需要支付時,向支付服務器通信,需要觀看視頻時向流媒體服務器通信。

所有通信都通過 API 實現,這和單體式架構一樣(或者通過如 Kafka 或 RabbitMQ 等通信系統)。唯一的區別是,現在我們使用不同的服務器負責不同的行為,而不是採用一個服務器解決所有問題。

聽上去有一點點複雜,確實如此,微服務的優勢在於:

你可以根據需要擴展特定服務,而不是擴展整個後端。在我們的示例中,當碰到體驗問題時,我們垂直擴展了整個服務器,但實際上需要更多資源的僅為流媒體部分。把流媒體功能分離到單個服務器,我們就可以擴展這一個服務器,繼續其他部分的正常工作。

功能將鬆散耦合,意味着我們可以獨立開發和部署這些功能。

每一個服務器的代碼庫更加短小精悍,這對於一開始就一起工作的開發者來說是一件好事,對新加入的開發者快速融入也是好事。

微服務是一個設置和管理更為複雜的架構,這也是為什麼僅有一些非常大的項目才使用這種架構。大部分項目一開始使用的是單體式架構,僅在性能需要時遷移到微服務架構。

如果你想了解更多微服務相關的知識,這裡有一個很好的解釋視頻。

服務於前端的後端是什麼(BFF)

實現微服務的一個問題是與前端的通信變得複雜。在我們示例中,多個服務器負責不同的行為也就意味着前端應用需要記錄是誰發起的請求。

通常解決這個問題的方式是在前端應用和微服務之間增加一個中間層。這個中間層將接受所有前端的請求,重定向到對應的微服務,接受微服務的回應,然後重定向到對應的前端應用。

BFF 模式的好處在於我們在使用了微服務架構的同時,沒有複雜化前端應用的通信。

BFF 實現

如果你想了解更多相關內容,這裡有一期解釋 BFF 模式的視頻。

如何使用負載均衡器和水平擴展

我們的流媒體應用正在呈指數型增長,來自世界各地百萬量級的用戶全天候使用 Noflix 觀看電影,馬上我們又要面臨新的性能問題。

我們再一次發現是流媒體服務承受最大的壓力,我們已經盡我們所能垂直擴展了這個服務器,進一步細分這個服務成更多微服務沒有意義。所以我們決定水平擴展服務器。

在前文中我們提到垂直擴展就是給單個服務器或者計算機增加更多資源(RAM、磁盤空間、GPU 等);水平擴展就是設置更多的服務器來處理同一個任務。

我們不再只使用一個服務器來負責所有流媒體工作,而是使用三個。這樣來自客戶端的請求將被平均分配到這三個服務器處理,每一個服務器的負載就被控制在可承受範圍內。

請求的分配通常由負載均衡器來實現。負載均衡器如同服務器的反向代理,攔截請求並重定向到對應的服務器。

一個典型的客戶端 - 服務器連接如圖:

這是我們之前的形式

使用負載均衡器在多個服務器間分發客戶端請求如圖:

這是我們想要的形式

水平擴展可以在服務器實現就可以在代碼庫實現。其中一個實現辦法是通過源 - 副本模型(source-replica model),一個特定的源 DB 將接受所有寫入的請求然後複製這些數據到更多的副本 DB,副本 DB 將接受和響應所有讀取的請求。

DB 副本的優勢在於:

更優的性能:這一模型使得更多個請求可以並行。

可靠性和可用性:如果一個數據庫服務器因為任何原因被破壞或者無法訪問,其他 DB 仍保有數據。

實現了負載均衡器、水平擴展和 DB 副本之後,我們的架構如圖:

水平擴展架構

如果你想要了解更多內容,這裡有一個介紹負載均衡器的視頻。

注意:當我們在討論微服務、負載均衡器和水平擴展的時候,我們討論的是後端應用。對於前端應用來說,我們通常是以單體式架構開發的,當然也有一個有趣的概念叫作微前端 。🧐

你的基礎架構所在的位置

現在我們對應用的基礎架構是如何組織的有了一定了解,現在讓我們來看看我們把基礎架構放在哪裡。

主要有三種託管應用程序的方式:本地、傳統服務器供應商和雲。

本地託管

本地託管意味着你擁有運行應用軟件的硬件。這曾是最傳統的託管方式。軟件公司為服務器專門提供房間,並且有專業的團隊致力於設置和維護硬件。

這樣做的好處是公司全權掌握硬件,壞處是這樣耗費空間、時間和金錢。

假設你需要水平擴展一個服務器,你需要購買更多的設備,設置好,並且持續監控,一旦出現問題就要維修…… 如果之後你需要縮小服務器,你通常也沒辦法退換你購買的設備。

對於公司來說,採用本地託管意味着將資源和精力分配到非公司目標上。

我們想象中的 Notflix 服務器機房

實際的畫面

當需要處理精密或者私人信息的時候,本地託管還是能派上用場的。假設這個軟件需要處理一個髮電廠或者私人的銀行信息,軟件公司會決定使用本地託管服務器來全權控制軟件和硬件。

傳統服務器供應商

對於大多數公司來說一個更舒適的選擇是傳統服務器供應商。供應商有自己的服務器,並且提供租賃。你決定為你的項目使用什麼樣的硬件,並且提交月費(或者根據其他條件確定的費用)。

使用服務器供應商的好處是你不需要擔心硬件相關的問題,供應商會處理好。軟件公司只需要關注自己的主要目標,軟件本身。

另一個好處是,擴展或者縮小變得更加方便自由。如果需要更多硬件,你就購買;如果不需要了,就停止付費。

一個知名供應商的例子是 hostinger。

雲託管

如果你在科技圈待過一陣子,你可能已經聽說過不止一次 「雲」。乍一聽,這好像是某種抽象的魔術,實際上雲只不過是由 Amazon、Google 和 Microsoft 這樣的大公司擁有的超大數據中心。

這些大公司擁有巨大的算力,這些算力並不是時時被利用。與其讓這些硬件白白浪費錢,更聰明的做法是將這些算力商業化。

這就是雲計算。數據中心可以利用這些算力,使用 AWS(Amazon 的 Web 服務)、Google Cloud 或 Microsoft 的 Azure。

「雲」 實際的樣子

提到雲服務,一個很重要的知識點是存在不同的使用方式:

傳統的

第一種方法與使用傳統服務器提供商類似。你可以選擇所需的硬件類型並按月支付費用。

彈性的

第二種方法利用了大多數供應商提供的 「彈性」 算力。「彈性」 意味着你的應用使用的硬件大小會根據使用情況,自動放大或縮小。

例如,你開始使用的是 8gb 內存和 500gb 磁盤空間的服務器。如果服務器收到越來越多的請求並且這些容量不再足以提供良好的性能,系統可以自動執行垂直或水平擴展。

這樣做的好處是,你預先配置服務器後,就沒有必再擔心它的變化。服務器自動擴展和縮減,你只需為使用的資源付費。

無服務的

使用雲計算的另一種方式是使用無服務架構。

在這個模式中,沒有接受所有請求並響應的服務器,而是獨立的函數映射到訪問點(類似於 API 端點)。

每當接受到一個請求,這些函數就會執行你編寫的程序(鏈接數據庫、執行 CRUD 等普通服務器會做的事情)。

無服務架構的好處是可以免去服務器維護和擴展。如果需要使用,你只需要編寫執行的函數,函數會自動根據需要擴展或者縮小。

作為消費者,你只需要支付函數執行的次數以及函數執行持續時長的費用。

如果你想了解更多這方面的內容,這裡有一個介紹無服務架構的視頻。

更多其他服務

你很容易發現無服務和彈性雲計算提供的簡單便捷的設置軟件架構的方式。

除了提供服務器相關服務,雲供應商還提供許多其他的解決方案,如:關係型和非關係型數據庫、文件存儲服務、緩存服務、認證服務、機器學習和數據處理服務、監控和性能分析等。這些服務都託管在雲。

通過如 Terraform 或 AWS 的 Cloud formation 這樣的工具,我們甚至可以通過編寫代碼來設置基礎架構,也就是說我們可以花幾分鐘編寫腳本來設置服務器、數據庫等在雲上的內容。

對於軟件工程來說這是顛覆想象的舉措,這也給開發者提供了巨大的便利。雲計算提供了豐富的解決方法應對小微項目,也可以處理好非常大的數字產品。這也是為什麼越來越多的軟件工程項目選擇在雲上搭建基礎架構。

如前文所述,時下最知名且最常用的雲有 AWS、Google Cloud 和 Azure。當然還有其他的選擇如 IBM、DigitalOcean 和 Oracle。

大部分雲供應商都提供同樣的服務,雖然服務的命名不相同。同樣是無服務功能,在 AWS 被叫作 「lambdas」,在 Google Cloud 被叫作 「cloud functions」。

不同的文件夾結構

目前我們討論的架構指的是基礎架構的組織和託管,現在讓我們看看一些代碼,以及架構在文件結構和代碼模塊化方面的作用。

全在一個文件夾中的結構

為了演示為什麼文件夾結構很重要,我們一起來搭建一個簡單的示例 API。我們將使用一個模擬的數據庫,名為兔子🐰🐰,這個 API 會執行 CRUD 操作,我們將使用 Node 和 Express 來搭建。

下圖是我們的第一步,沒有任何文件夾結構,我們的倉庫包含 node modules 文件夾,app.js、 package-lock.json 和 package.json 文件。

在 app.js 文件包含一個小服務器,虛擬 DB(數據庫)和兩個端點:

// App.js const express = require('express'); const app = express() const port = 7070 // 虛擬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' }, ] /* 路由 */ 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}`))

測試兩個端點,發現它們運行正常:

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" # }

這有什麼問題嗎?其實沒有,一切運行良好。但當代碼庫變得更大更複雜,我們在 API 中添加新的功能後,問題就會浮現。

這和我們討論單體式架構的問題一樣,一開始把所有內容放在一個地方很方便,但是隨着內容變得更大更複雜,這個方式就會讓人困惑。

根據模塊化原則,更好的處理方法是使用不同的文件夾和文件來執行不同的責任和行為。

為了更好地演示,讓我們給 API 添加新的功能,看看我們怎麼使用模塊的方法來給文件夾結構添加不同層級。

分層文件夾結構

分層文件夾結構是將關注點和責任分配到不同的文件夾和文件中,僅允許在特定的文件夾和文件中進行直接通信。

一個項目應該有幾個層級,每個層級如何命名,應該處理什麼行為都是需要討論的問題。讓我們一起來看看我的例子:

我們的應用程序將有五個層級,並以下面的順序排列:

應用程序分層

應用層(application layer)將處理服務器的基本設置,並且連接到路由(下一層)。

路由層(routes layer)將定義所有路由以及連接到控制器層(下一層)。

控制器層(controllers layer)是每個端點的實現具體邏輯,並且連接到模型層(下一層,你已經知道這是怎麼一回事了……)。

模型層(model layer)是與虛擬數據庫的交互邏輯。

最終持久層(persistence layer)存儲了所有數據。

採用這樣的方法就更有結構感,關注點也實現了分離。這個方法看上去比較像樣板,但設置以後,這樣的結構能夠幫助我們清晰地了解文件夾和文件具體負責應用程序的哪個行為。

需要注意的是,在這樣的結構中層級間的通信流是確定的,這樣這個結構才成立。

也就是說一個請求必須先通過第一層,然後是第二層,然後第三層,以此類推。請求不能夠跳過層級,因為這樣會使得結構的邏輯混亂,就藉助不了組織和模塊化帶來的好處。

結構的另一種表現形式

讓我們看一些代碼,以上面的分層結構為基礎,我們的文件夾結構如下:

一個名為 db 的新文件夾保存所有數據文件

另一個名為 rabbits 的文件夾包含所有路由、控制器和模型

app.js 設置服務器,並與路由連接

// App.js const express = require('express'); const rabbitRoutes = require('./rabbits/routes/rabbits.routes') const app = express() const port = 7070 /* 路由 */ app.use('/rabbits', rabbitRoutes) app.listen(port, () => console.log(`⚡️[server]: Server is running at http://localhost:${port}`))

rabbits.routes.js 連接實體的端點和對應控制器的路由(執行請求到達端點的函數)。

// 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 處理每個端點的邏輯。在這裡函數接受輸入,然後處理輸出和返回。😉 另外,每一個控制器都連接到對應的模型函數(處理數據相關的操作)。

// 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 定義了使用 CRUD 處理數據庫的函數。每一個函數都代表了一種行為(讀取一個數據、讀取所有數據、編輯數據、刪除數據等),這個文件與 DB 連接。

// 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 託管了我們的模擬數據庫。在真實的項目中,這裡是連接真實數據庫的地方。

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

如你所見,現在就有更多的文件夾和文件。但是作為回報,我們的代碼庫變得結構感更加明顯,並且組織更加清晰。每一個代碼都待在應該在的地方,文件之間的通信也被清晰地定義了。

這樣的組織形式能夠極大地方便添加新的功能、修改代碼和改 bug。

一旦你熟悉了這樣的文件夾結構,知道去哪兒找你想要的內容。你就會發現在更短更小的文件中工作,比在一到兩個巨大的文件中滑動尋找想要的內容要方便得多。

我也支持為應用的每一個實體(在我的例子中是兔子)創建一個文件夾。這樣我們就能夠更清晰地知道每一個文件和什麼內容相關。

假設我們需要添加新的功能去添加、修改、刪除貓咪或者小狗,我們就為這些新的動物創建文件夾,每一個文件夾里包含各自的路由、控制器和模型文件。這一方法就是將關注點分離。👌👌

MVC 文件夾結構

MVC 的全稱是 Model View Controller(模型視圖控制器)。我們可以說 MVC 結構就像是分層結構的簡化版,並包含了應用程序的前端(UI)。

在這個結構中只有三層:

視圖層負責渲染 UI

控制層負責定義路由和路由背後的邏輯

模型層負責和數據庫的交互

和之前的一樣,每一個層級只和下一個層級交互,所以必須是清晰定義的通信流。

另一種展現層級的方式

有許多實現 MVC 結構的框架(如 Django 或 Ruby on Rails )。如果要在 Node 和 Express 中使用這個結構,我們需要藉助模版引擎,如 EJS。

如果你對模版引擎這個概念不是太熟悉的話,可以把它理解成更容易渲染的 HTML,它利用了如變量、循環和條件句這些編程特性使得渲染更加容易(和 React 中的 JSX 很像)。

在下面的例子中,我們會使用 EJS 文件來創建每一個頁面,並且由控制器來處理響應,傳入到對應的響應變量。

文件夾結構如下:

我們刪掉了大部分文件夾,但保留了 db、controllers 和 models。

我們添加了 views 文件夾保存我們需要渲染的頁面或響應。

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 設置 app.set("view engine", "ejs") app.set('views', path.join(__dirname, './rabbits/views')) /* 控制器 */ app.use("/rabbits", rabbitControllers) app.listen(port, () => console.log(`⚡️[server]: Server is running at http://localhost:${port}`))

rabbits.controllers.js 用來定義路由、連接對應的模型函數以及渲染每一個請求對應的視圖。可以看到在每一個渲染方法中我們傳入了請求響應作為參數。😉

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

最後,在視圖文件中,我們將變量作為參數並且渲染為 HTML。

<!-- Rabbits view --> <!DOCTYPE html> <html lang="en"> <body> <header>All rabbits</header> <main> <ul> <% rabbits.forEach(function(rabbit) { %> <li> Id: <%= rabbit.id %> Name: <%= rabbit.name %> </li> <% }) %> </ul> </main> </body> </html> <!-- Rabbit view --> <!DOCTYPE html> <html lang="en"> <body> <header>Rabbit view</header> <main> <p> Id: <%= rabbit.id %> Name: <%= rabbit.name %> </p> </main> </body> </html>

打開瀏覽器,登陸http://localhost:7070/rabbits,會得到:

或者http://localhost:7070/rabbits/2 會得到:

這就是 MVC!

總結

希望這些示例能夠幫助你理解軟件工程世界裡的 「架構」。

如我在文章開頭中所說,架構是一個非常巨大且複雜的概念,包含了非常多的內容。

在這篇文章中,我們介紹了架構模式和系統、託管的選擇以及雲供應商,以及一些通用的文件夾結構。

我們還學習了垂直和水平擴展、單體式應用和微服務、彈性和無服務雲計算…… 非常多的內容。但這些只是冰山一角!請再接再厲,探索更多內容。💪💪

希望你喜歡這篇文章,並且有所收穫。

關於本文譯者:@PapayaHUANG譯文:https://mp.weixin.qq.com/s/sgGWXu_nmUmBD9CDWPYIlg作者:@Germán Cocca原文:https://www.freecodecamp.org/news/an-introduction-to-software-architecture-patterns/

關於【架構】相關閱讀。歡迎讀者自薦投稿,前端早讀課等你

為了方便大家閱讀或打印,有需要領取文章 pdf 版的可添加 vx:zhgb_f2er 領取

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()