譯者:這篇文章是在 medium 講解前端架構分層系列的第一篇文章,分層和以前翻譯的文章相似,相對普通項目多出來兩層,領域層(從業務抽離出來領域實體)和用例層(實現應用業務邏輯)。另外在編程範式上,相對面對對象,做者更傾向於採用函數式,讀者可根據項目特色選擇適合本身的方式。html
文章首發於個人博客 github.com/mcuking/blo…node
這篇博客是《可擴展的前端》系列的一部分,你能夠看到其餘部分: #2 — Common Patterns 和 #3 — The State Layer。git
關於軟件開發的可擴展性這一律念有兩個最多見的的意義:代碼的性能和可維護性。你能夠同時兼顧這兩點,可是專一於良好的可維護性會讓一件事情變得容易,那就是提高性能且不影響應用的其他部分。更重要的是,前端與後端有一個重要的區別:本地狀態。github
在這個系列博客中,咱們將會討論如何經過實際的通過驗證的方法,來開發和維護可擴展的前端應用。咱們大部分的例子將會使用 React 和 Redux,可是咱們會常常與其餘的技術棧對比較,來展現你如何達到一樣的結果。讓咱們開始這個關於架構方面的系列討論吧,這是你的軟件中最重要的部分。數據庫
那麼架構究竟是什麼?說架構是軟件中的最重要的部分彷佛很自覺得是,但請耐新看下去。npm
架構是使軟件的各個部分相互交互以突出必需要作出的最重要的決策,而且推遲次要的決策和實現細節的方式。設計一個軟件的架構意味着將實際的應用從支持它的技術中分離開來。你的實際應用不知道數據庫、AJAX 請求、或者 GUI;而是由用例和領域模型組成。這些用例和領域模型表明了你的軟件所涵蓋的概念,請忽略執行用例的角色或數據在哪裏存儲等。編程
關於架構還有一個重要的事情要說:那就是架構不意味着文件的組織,也不是如何命名你的文件和文件夾。 |
區分重要與次要的一種方式就是使用層,每一個層都有一些不一樣且特定的職責。基於分層的架構中一種常見的方式是將它分紅四個層:application 層、domain 層、infrastructure 層、input 層。這四個層在另外一個博客中有很好的解釋,NodeJS and Good Practices。我推薦在繼續閱讀下面的文章以前,先看下這篇文章的第一部分。你不須要閱讀第二部分,由於那已經具體到 NodeJS 了。redux
其中 domain 層和 application 層在前端和後端之間是沒有什麼不一樣的,由於它們是與技術無關的,可是對於 input 層和 infrastructure 層咱們不能這麼說。在 Web 瀏覽器中 input 層一般只有一個角色--view。因此咱們甚至能夠稱之爲 view 層。一樣在前端是沒法操做數據庫或隊列引擎的,因此咱們沒法在前端的 infrastructure 層中找到它們。相反咱們可以找的是封裝 AJAX 請求、瀏覽器 cookie、LocalStorage,甚至是與 WebSocket 服務器交互的模塊的抽象。後端
主要的區別是被抽象的內容,因此前端和後端的 Repository 甚至能夠有徹底一致的接口而底層是不一樣的技術。你能看到一個好的抽象有多棒了嗎?
你使用 React,Vue,Angular 或其餘任何工具來操做視圖都沒有關係,重要的是遵照沒有任何邏輯的 view 層規則,將輸入參數委託給下一層。關於基於前端分層的架構,還有另外一個重要規則:使 view 層始終與本地狀態保持同步,你應該遵循單向數據流原則。這個概念是否聽着很熟悉?咱們能夠經過添加第五個層來達到這個目的:state ,或者稱爲 store。
當遵循單向數據流原則時,咱們永遠不會在 view 內部直接更改 view 接收的數據。相反,咱們從 view 中 dispatch 咱們所謂的 「action」。它是這樣的:一個 action 將消息發送到數據源,該數據源將更新自身,而後使用新數據從新渲染 view。須要注意的是,從 view 到 store 沒有直接通道,所以若是兩個子 view 使用了相同的數據,則能夠從任何一個 view 中 dispatch 一個 action,這會致使兩個子 view 都會用新據渲染。彷佛我是在專門談論 React 和 Redux,但事實並不是如此;幾乎能夠經過全部現代的前端框架或庫得到相同的結果,例如 React + context API, Vue + Vuex, Angular + NGXS, 甚至使用 data-down action-up 方式的 Ember。你甚至可使用 jQuery 的事件系統來實現發送 action up!
該層負責管理前端的本地和不斷變化的狀態,例如從後端獲取的數據,在前端操做但還沒有持久化的臨時數據,或者是瞬時信息,例如請求狀態。
即便在 actions 內部,也會常常看到帶有業務規則和用例定義的代碼,若是你仔細閱讀其它層的描述,你會看到咱們已經有放置咱們的用例和業務邏輯的地方了,並且不是 state 層。這是否意味着咱們的 actions 如今是用例?沒有!那麼咱們應該如何對待它們呢?
讓咱們考慮一下……咱們說 action 不是用例,而且咱們已經有了放置用例的層。view 應該 dispatch 一個 action,該 action 從視圖中獲取信息,將其交給用例,根據響應 dispatch 新 action,最後更新 state -- 更新 view 並關閉單向數據流。這些 action 如今看起來不像 controller 嗎?他們不是一個從 view 中獲取參數,委派給用例並根據用例結果進行響應的地方嗎?那就是你應該看待它們的方式。不該有複雜的邏輯或直接的 AJAX 調用,由於這是另外一層的職責。state 層應該只知道如何管理本地存儲,僅此而已。
其中還有另外一個重要因素,因爲 state 層管理着 view 層依賴的本地存儲,所以你會注意到這二者是以某種方式耦合在一塊兒的。state 層中只會有一些數據供 view 使用,例如一個布爾類型的標誌,指示請求是否仍在等待處理,以便視圖能夠顯示 spinner,這徹底能夠。不要爲此而煩惱,你不須要過分歸納 state 層。
好的,分層很酷,可是它們如何互相通訊?咱們如何使一個層依賴另外一個層而不耦合它們?是否能夠在不執委派給用例的狀況下測試 action 的全部可能輸出?是否能夠在不觸發 AJAX 調用的狀況下測試用例?能夠確定的是,咱們能夠經過依賴注入來作到這一點。
依賴注入是一種技術,該技術包括在建立一個模塊的過程當中接收另外一個模塊的耦合依賴關係做爲參數。例如,在其構造函數中接收類的依賴項,或使用 React / Redux 將組件鏈接到 store 並注入必要的數據和 action 做爲參數。這個理論並不複雜,對吧?相關的實踐也不該該複雜,因此讓咱們以 React / Redux 應用程序爲例。
咱們剛剛說過,使用 React / Redux 的 connect 是一種在 view 和 state 層之間實現依賴注入的方法,並且它變得很是簡單。可是咱們以前也說過,action 將業務邏輯委託給用例,那麼咱們如何將用例(application 層)注入到 actions(state 層)中呢?
讓咱們想象一下,你有一個對象,其中包含針對你的應用程序的每一個用例的方法。該對象一般稱爲 dependency container
。是的,看起來很奇怪,並且擴展性很差,但這並不意味着用例的實現就在該對象內。這些只是委託給用例的方法,這些用例在其餘地方定義。應用程序的全部用例一塊兒使用一個對象比將它們分佈在整個代碼庫中要好得多,後者會使它們很難找到。有了這個對象,咱們要作的就是將其注入到 actions 中,讓每一個 action 決定將觸發什麼用例,對嗎?
若是你使用的是 redux-thunk,則使用 withExtraArgument 方法能夠很容易地實現它,該方法容許你將容器中的每一個 thunk 動做做爲 getState 以後的第三個參數注入。若是你使用的是 redux-saga,則該方法應該很簡單,在該方法中,咱們將容器做爲 run 方法的第二個參數進行傳遞。若是你使用的是 Ember 或 Angular,則內置的依賴項注入機制就足夠了。
這樣作會使 action 與用例解耦,由於你無需在定義 action 的每一個文件中手動導入用例。並且將 actions 與用例分開進行測試如今變得很是簡單:只需注入一個僞造的用例實現便可,該實現的行爲徹底符合你想要的方式。你是否想測試若是用例失敗,將 dispatch 什麼 action?注入一個老是失敗的模擬用例,而後測試 action 如何對此作出響應。無需考慮實際用例如何工做。
太好了,咱們將 state 層注入了 view 層,並將 application 層注入了 state 層。其他的呢?咱們如何將依賴項注入用例來構建 dependency container
?這是一個重要的問題,有不少方法能夠解決。首先,不要忘記檢查你使用的框架是否內置了依賴項注入,例如 Angular 或 Ember。若是確實如此,則你不該該本身構造。若是沒有,你能夠經過兩種方式來作到這一點:手動或在軟件包的幫助下。
手動進行操做應該很簡單:
將你的模塊定義爲類或閉包,
首先實例化沒有依賴性的模塊,
而後再實例化有依賴的的模塊,將它們做爲參數傳遞,
重複上述步驟,直到實例化全部用例爲止,
導出它們。
太抽象了?看一些代碼示例:
container.js
import api from './infra/api'; // has no dependencies
import { validateUser } from './domain/user'; // has no dependencies
import makeUserRepository from './infra/user/userRepository';
import makeArticleRepository from './infra/article/articleRepository';
import makeCreateUser from './app/user/createUser';
import makeGetArticle from './app/article/getArticle';
const userRepository = makeUserRepository({
api
});
const articleRepository = makeArticleRepository({
api
});
const createUser = makeCreateUser({
userRepository,
validateUser
});
const getArticle = makeGetArticle({
userRepository,
articleRepository
});
export { createUser, getArticle };
複製代碼
createUser.js
export default ({ validateUser, userRepository }) => async userData => {
if (!validateUser(userData)) {
throw new Error('Invalid user');
}
try {
const user = await userRepository.add(userData);
return user;
} catch (error) {
throw error;
}
};
複製代碼
userRepository.js
export default ({ api }) => ({
async add(userData) {
const user = await api.post('/users', userData);
return user;
}
});
複製代碼
你會注意到,重要部分(用例)已在文件末尾實例化,而且是惟一導出的對象,由於它們將被注入到 actions 中。你的其他代碼無需瞭解 repository 的操做方式和工做方式。這並不重要,而只是技術細節。對於用例,repository 是發送 AJAX 請求仍是在 LocalStorage 中保留某些內容都沒有關係;用例沒有職責須要知道。若是你想在 API 仍在開發中時使用 LocalStorage,而後切換爲使用經過網絡 API 的調用,只要與 API 交互的代碼遵循與 LocalStorage 交互的接口,而無需更改用例。
即便你有數十個 use cases(用例), repositories, services 等,也能夠如上所述手動完成注入。若是太麻煩而沒法構建全部依賴關係,則能夠始終使用依賴注入的庫,只要它不會增長耦合。
檢驗你的 DI(Dependency injection) 庫是否足夠好的一條經驗法則是,檢查從手動方法轉移到使用庫是否只須要操做 container 代碼便可。若是不是這樣,則說明庫太過侵入,你應該選擇其餘庫。若是你確實要使用庫,咱們建議你使用 Awilix。它很是簡單易用,無需手動操做,只需操做 container 文件便可。這個庫的做者撰寫了一系列有關如何使用以及爲何使用它的很好的文章,點擊查看。
好的,咱們已經討論了架構以及如何以一種很好的方式鏈接各層!在下一篇文章中,咱們將爲剛纔討論的層展現一些實際的代碼和通用模式,但 state 層除外,它會在單獨的文章中介紹。花一些時間來吸取這些概念。當咱們詳細介紹這些模式時,它們將很是有用,一切都會變得更加有意義。到時候那裏見!
Bob Martin — Architecture the Lost Years
Rebecca Wirfs-Brock — Why We Need Architects (and Architecture) on Agile Projects