如果你使用過 Redux 開發 React,你一定聽過 容器元件(Smart/Container Components) 或 展示元件(Dumb/Presentational Components),這樣劃分有什麼樣的好處,我們能否能借鑑這種劃分方式來編寫 Vue 程式碼呢?這篇文章會演示為什麼我們應該採取這種模式,以及如何在 Vue 中編寫這兩種元件。
為什麼要使用容器元件?
假如我們要寫一個元件來展示評論,在沒聽過容器元件之前,我們的程式碼一般都是這樣寫的:
components/CommentList.vue
store/index.js
這樣寫看起來理所當然,有沒有什麼問題,或者可以優化的地方呢?
有一個很顯而易見的問題,由於 CommentList.vue 與 專案的 Vuex store 產生了耦合,導致脫離當前的專案很難複用。
有沒有更好的元件的組織方式,可以解決這個問題呢?是時候瞭解下 React 社群的容器元件的概念了。
什麼是容器元件
在 React.js Conf 2015 ,有一個 Making your app fast with high-performance components 的主題介紹了容器元件。
容器元件專門負責和 store 通訊,把資料通過 props 傳遞給普通的展示元件,展示元件如果想發起資料的更新,也是通過容器元件通過 props 傳遞的回撥函式來告訴 store。
由於展示元件不再直接和 store 耦合,而是通過 props 介面來定義自己所需的資料和方法,使得展示元件的可複用性會更高。
容器元件 和 展示元件 的區別
展示元件 | 容器元件 | |
---|---|---|
作用 | 描述如何展現(骨架、樣式) | 描述如何執行(資料獲取、狀態更新) |
直接使用 store | 否 | 是 |
資料來源 | props | 監聽 store state |
資料修改 | 從 props 呼叫回撥函式 | 向 store 派發 actions |
來自 Redux 文件 https://user-gold-cdn.xitu.io/2018/5/2/1631f590aa5512b7
用 容器元件/展示元件 模式改造上面的例子
針對最初的例子,如何快速按照這種模式來劃分元件呢?我們主要針對 CommentList.vue 進行拆分,首先是基本的概要設計:
概要設計
展示元件
components/CommentListNew.vue
這是一個新的評論展示元件,用於展示評論comments: Array
prop 接收以{ id, author, body }
形式顯示的 comment 項陣列。fetch()
接收更新評論資料的方法
展示元件只定義外觀並不關心資料來源和如何改變。傳入什麼就渲染什麼。
comments、fetch 等這些 props 並不關心背後是否是由 Vuex 提供的,你可以使用 Vuex,或者其他狀態管理庫,甚至是一個 EventBus,都可以複用這些展示元件。
同時,可以利用 props 的型別和驗證來約束傳入的內容,比如驗證傳入的 comments 是否是一個含有指定欄位的物件,這在之前混合元件的情況是下是沒有的,提高了程式碼的健壯性。
容器元件
containers/CommentListContainer.vue
將 CommentListNew 元件連線到 store
容器元件可以將 store 對應的 state 或者 action 等封裝傳入展示元件。
編碼實現
Talk is cheap, show me the code!
components/CommentListNew.vue
這個檔案不再依賴 store,改為從 props 傳遞。
值得注意的是 comments 和 fetch 分別定義了 type 、default 和 validator,用以定義和驗證 props。
containers/CommentListContainer.vue
容器元件的職責
- 通過 computed 來獲取到狀態更新,傳遞給展示元件
- 通過 methods 定義回撥函式,回撥函式內部呼叫 store 的 dispatch 方法,傳遞給展示元件
使用 @xunlei/vuex-connector 實現容器元件
上面演示的容器元件的程式碼非常簡單,實際上如果直接投入生產環境,會產生一些問題。
手動實現容器元件存在的不足
程式碼比較繁瑣
在上面的例子中,每次傳遞一個 state 都要定義一個 computed,每傳遞一個 mutation 或者 action 都需要定義一個方法,而且還要注意這個方法的引數要透傳過去,同時還要處理返回值,比如非同步的 action 需要返回 promise 的時候,定義的這個 method 也得把 action 的返回值返回出去。
無法透傳其他 props 給展示元件
比如展示元件新增了一個 prop 叫做 type,可以傳遞一個評論的型別,用來區分是熱門還是最新,如果用上面的容器實現方式,首先需要在容器元件這層新增一個 prop 叫做 type 接受外部傳來的引數,然後在展示元件內部同樣定義一個 叫做 type 的 prop,然後才能傳遞下去。
需要透傳的 props 必須定義兩遍,增加了維護的成本。
容器元件無法統一進行優化
每一個手動實現的容器元件實質上程式碼邏輯非常近似,但是沒有經過同一層封裝,如果目前實現的容器元件存在一些效能優化的地方,需要每個容器元件都進行統一的修改。
無法控制展示元件不去獲取 store
因為容器元件是通過 this.$store 獲取 store 的,展示元件內部實質上也可以直接跟 store 通訊,如果沒有約束,很難統一要求展示元件不得直接和 store 通訊。
使用 @xunlei/vuex-connector
@xunlei/vuex-connector 借鑑了 react redux 的 connect 方法,在 vuex 基礎上進行的開發。
有以下幾個特點:
程式碼非常簡潔
下面是上面例子中手動實現的容器元件的改造版本:
comonents/ConnectCommentListContainer.vue
通過 connector 的 connnect 方法,傳入要對映的配置,支援 mapStateToProps, mapGettersToProps, mapDispatchToProps, mapCommitToProps 這四種,每一種都是隻要配置一個簡單的 map 函式,或者字串即可。
然後在返回的函式中傳入要連線的展示元件即可,是不是非常的簡潔。
容器元件本身也可以複用
由於借鑑了 redux 優雅的函式式風格, connector 的 connnect 方法 返回的函式實際上是一個高階元件,也就是一個可以建立元件的函式。這樣帶來了額外的好處,不同的展示元件也可以複用同一個容器元件。
舉個例子:
如果你寫了多個版本的評論展示元件,接受的資料和更新資料的方式都是一樣的,那麼你就沒有必要為每個版本的評論元件都搞一個容器元件了,只要複用同一個高階元件函式即可。
問題來了,connector 是什麼?
connector 實際上是一個能獲取到 store 例項的聯結器,可以在初始化 vuex store 的時候進行初始化。
一個 Vue 程式實際上只需要初始化一次即可。
支援透傳其他 props 給展示元件
VuexConnector 實現的時候採用了函式式元件( functional: true
)
函式式元件是無狀態 (沒有響應式資料),無例項 (沒有 this 上下文)。
在作為包裝元件時函式式元件非常有用,比如,當你需要做這些時:
- 程式化地在多個元件中選擇一個
- 在將 children, props, data 傳遞給子元件之前操作它們。
另外,函式式元件只是一個函式,所以渲染開銷也低很多。然而,對持久化例項的缺乏也意味著函式式元件不會出現在 Vue devtools 的元件樹裡。
因此需要透傳的 props 可以直接透傳,需要通過 map 方式從 store 裡進行獲取的 props 直接會根據配置生成。
統一封裝方便後續統一優化
VuexConnector.connect 方法將本來需要重複做的事情進行了抽象,也帶來了後期進行統一優化和升級的便利。
可以控制展示元件無法直接與 store 通訊
VuexConnector 不依賴 this.$store,而是依賴初始化傳入的 store 例項,容器元件可以用 connect 將展示元件與 store 進行連線。
由於不依賴 this.$store,我們在程式入口 new Vue 的時候,就不需要傳入 store 例項了。
比如,之前我們是通過下面的方式進行初始化:
使用了 VuexConnector 之後,在最初 new Vue 的時候就不需要也最好不要傳遞 store 了,這樣就避免了 this.$store 氾濫導致程式碼耦合的問題。
引入容器元件/展示元件模式帶來的好處
可複用性
容器元件/展示元件的劃分,採用了單一職責原則的設計模式,容器元件專門負責和 store 通訊,展示元件只負責展示,解除了元件的耦合,可以帶來更好的可複用性。
健壯性
由於展示元件和容器元件是通過 props 這種介面來連線,可以利用 props 的校驗來增強程式碼的可靠性,混合的元件就沒有這種好處。
另外對 props 的校驗可以採取一下幾種方式:
Vue 元件 props 驗證
可以驗證 props 的型別,預設可以校驗是否是以下型別:
- String
- Number
- Boolean
- Function
- Object
- Array
- Symbol
如果你的 props 是類的一個例項,type 也可以是一個自定義構造器函式,使用 instanceof 檢測。
如果還是不滿足需求,可以自定義驗證函式:
TypeScript 型別系統
Vue 元件 props 驗證對於物件或者其他複雜的型別校驗還是不太友好,所以很多人也推薦大家的 props 儘量採取簡單型別,不過如果你有在用 TypeScript 開發 Vue 應用,可以利用 TypeScript 靜態型別檢查來宣告你的 props 。
可測試性
由於元件做的事情更少了,使得測試也會變得容易。
容器元件不用關心 UI 的展示,只關心資料和更新。
展示元件只是呈現傳入的 props ,寫單元測試的時候也非常容易 mock 資料層。
引入容器元件/展示元件模式帶來的限制
學習和開發成本
因為容器元件/展示元件的拆分,初期會增加一些學習成本,不過當你看完這篇文章,基本上也就入門了。
在開發的時候,由於需要封裝一個容器,包裝一些資料和介面給展示元件,會增加一些工作量, @xunlei/vuex-connector 通過配置的方式可以減輕不少你的工作量。
另外,在展示元件內對 props 的宣告也會帶來少量的工作。
總體來說,引入容器元件/展示元件模式投入產出比還是比較值得的。
延伸閱讀
- Redux 文件 user-gold-cdn.xitu.io/2018/5/2/16…
- Making your app fast with high-performance components www.youtube.com/watch?v=KYz…
程式碼示例
Vue Vuex 容器-展示元件模式 Demo
Demo 線上地址:
xunleif2e.github.io/vue-vuex-co…
Demo 原始碼:
@xunlei/vuex-connector
基於 Vue 生態實現的Vuex store Connector
@xunlei/vuex-connector 原始碼:
歡迎 Star
掃一掃關注迅雷前端公眾號
作者:binggg
校對:珈藍