若是你使用過 Redux 開發 React,你必定聽過 容器組件(Smart/Container Components) 或 展現組件(Dumb/Presentational Components),這樣劃分有什麼樣的好處,咱們可否能借鑑這種劃分方式來編寫 Vue 代碼呢?這篇文章會演示爲何咱們應該採起這種模式,以及如何在 Vue 中編寫這兩種組件。前端
假如咱們要寫一個組件來展現評論,在沒聽過容器組件以前,咱們的代碼通常都是這樣寫的:vue
這樣寫看起來理所固然,有沒有什麼問題,或者能夠優化的地方呢?react
有一個很顯而易見的問題,因爲 CommentList.vue 與 項目的 Vuex store 產生了耦合,致使脫離當前的項目很難複用。git
有沒有更好的組件的組織方式,能夠解決這個問題呢?是時候瞭解下 React 社區的容器組件的概念了。github
在 React.js Conf 2015 ,有一個 Making your app fast with high-performance components 的主題介紹了容器組件。vuex
容器組件專門負責和 store 通訊,把數據經過 props 傳遞給普通的展現組件,展現組件若是想發起數據的更新,也是經過容器組件經過 props 傳遞的回調函數來告訴 store。redux
因爲展現組件再也不直接和 store 耦合,而是經過 props 接口來定義本身所需的數據和方法,使得展現組件的可複用性會更高。設計模式
展現組件 | 容器組件 | |
---|---|---|
做用 | 描述如何展示(骨架、樣式) | 描述如何運行(數據獲取、狀態更新) |
直接使用 store | 否 | 是 |
數據來源 | props | 監聽 store state |
數據修改 | 從 props 調用回調函數 | 向 store 派發 actions |
來自 Redux 文檔 https://user-gold-cdn.xitu.io/2018/5/2/1631f590aa5512b7數組
針對最初的例子,如何快速按照這種模式來劃分組件呢?咱們主要針對 CommentList.vue 進行拆分,首先是基本的概要設計:promise
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!
這個文件再也不依賴 store,改成從 props 傳遞。
值得注意的是 comments 和 fetch 分別定義了 type 、default 和 validator,用以定義和驗證 props。
容器組件的職責
上面演示的容器組件的代碼很是簡單,實際上若是直接投入生產環境,會產生一些問題。
在上面的例子中,每次傳遞一個 state 都要定義一個 computed,每傳遞一個 mutation 或者 action 都須要定義一個方法,並且還要注意這個方法的參數要透傳過去,同時還要處理返回值,好比異步的 action 須要返回 promise 的時候,定義的這個 method 也得把 action 的返回值返回出去。
好比展現組件新增了一個 prop 叫作 type,能夠傳遞一個評論的類型,用來區分是熱門仍是最新,若是用上面的容器實現方式,首先須要在容器組件這層新增一個 prop 叫作 type 接受外部傳來的參數,而後在展現組件內部一樣定義一個 叫作 type 的 prop,而後才能傳遞下去。
須要透傳的 props 必須定義兩遍,增長了維護的成本。
每個手動實現的容器組件實質上代碼邏輯很是近似,可是沒有通過同一層封裝,若是目前實現的容器組件存在一些性能優化的地方,須要每一個容器組件都進行統一的修改。
由於容器組件是經過 this.$store 獲取 store 的,展現組件內部實質上也能夠直接跟 store 通訊,若是沒有約束,很難統一要求展現組件不得直接和 store 通訊。
@xunlei/vuex-connector 借鑑了 react redux 的 connect 方法,在 vuex 基礎上進行的開發。
有如下幾個特色:
下面是上面例子中手動實現的容器組件的改造版本:
經過 connector 的 connnect 方法,傳入要映射的配置,支持 mapStateToProps, mapGettersToProps, mapDispatchToProps, mapCommitToProps 這四種,每一種都是隻要配置一個簡單的 map 函數,或者字符串便可。
而後在返回的函數中傳入要鏈接的展現組件便可,是否是很是的簡潔。
因爲借鑑了 redux 優雅的函數式風格, connector 的 connnect 方法 返回的函數其實是一個高階組件,也就是一個能夠建立組件的函數。這樣帶來了額外的好處,不一樣的展現組件也能夠複用同一個容器組件。
舉個例子:
若是你寫了多個版本的評論展現組件,接受的數據和更新數據的方式都是同樣的,那麼你就沒有必要爲每一個版本的評論組件都搞一個容器組件了,只要複用同一個高階組件函數便可。
connector 其實是一個能獲取到 store 實例的鏈接器,能夠在初始化 vuex store 的時候進行初始化。
一個 Vue 程序實際上只須要初始化一次便可。
VuexConnector 實現的時候採用了函數式組件( functional: true
)
函數式組件是無狀態 (沒有響應式數據),無實例 (沒有 this 上下文)。
在做爲包裝組件時函數式組件很是有用,好比,當你須要作這些時:
另外,函數式組件只是一個函數,因此渲染開銷也低不少。然而,對持久化實例的缺少也意味着函數式組件不會出如今 Vue devtools 的組件樹裏。
所以須要透傳的 props 能夠直接透傳,須要經過 map 方式從 store 裏進行獲取的 props 直接會根據配置生成。
VuexConnector.connect 方法將原本須要重複作的事情進行了抽象,也帶來了後期進行統一優化和升級的便利。
VuexConnector 不依賴 this.$store,而是依賴初始化傳入的 store 實例,容器組件能夠用 connect 將展現組件與 store 進行鏈接。
因爲不依賴 this.$store,咱們在程序入口 new Vue 的時候,就不須要傳入 store 實例了。
好比,以前咱們是經過下面的方式進行初始化:
使用了 VuexConnector 以後,在最初 new Vue 的時候就不須要也最好不要傳遞 store 了,這樣就避免了 this.$store 氾濫致使代碼耦合的問題。
容器組件/展現組件的劃分,採用了單一職責原則的設計模式,容器組件專門負責和 store 通訊,展現組件只負責展現,解除了組件的耦合,能夠帶來更好的可複用性。
因爲展現組件和容器組件是經過 props 這種接口來鏈接,能夠利用 props 的校驗來加強代碼的可靠性,混合的組件就沒有這種好處。
另外對 props 的校驗能夠採起一下幾種方式:
能夠驗證 props 的類型,默承認以校驗是不是如下類型:
若是你的 props 是類的一個實例,type 也能夠是一個自定義構造器函數,使用 instanceof 檢測。
若是仍是不知足需求,能夠自定義驗證函數:
Vue 組件 props 驗證對於對象或者其餘複雜的類型校驗仍是不太友好,因此不少人也推薦你們的 props 儘可能採起簡單類型,不過若是你有在用 TypeScript 開發 Vue 應用,能夠利用 TypeScript 靜態類型檢查來聲明你的 props 。
因爲組件作的事情更少了,使得測試也會變得容易。
容器組件不用關心 UI 的展現,只關心數據和更新。
展現組件只是呈現傳入的 props ,寫單元測試的時候也很是容易 mock 數據層。
由於容器組件/展現組件的拆分,初期會增長一些學習成本,不過當你看完這篇文章,基本上也就入門了。
在開發的時候,因爲須要封裝一個容器,包裝一些數據和接口給展現組件,會增長一些工做量, @xunlei/vuex-connector 經過配置的方式能夠減輕很多你的工做量。
另外,在展現組件內對 props 的聲明也會帶來少許的工做。
整體來講,引入容器組件/展現組件模式投入產出比仍是比較值得的。
xunleif2e.github.io/vue-vuex-co…
基於 Vue 生態實現的Vuex store Connector
歡迎 Star
做者:binggg
校對:珈藍