致敬 React: 為 Vue 引入容器元件和展示元件

迅雷前端發表於2018-05-03

如果你使用過 Redux 開發 React,你一定聽過 容器元件(Smart/Container Components) 或 展示元件(Dumb/Presentational Components),這樣劃分有什麼樣的好處,我們能否能借鑑這種劃分方式來編寫 Vue 程式碼呢?這篇文章會演示為什麼我們應該採取這種模式,以及如何在 Vue 中編寫這兩種元件。

為什麼要使用容器元件?

假如我們要寫一個元件來展示評論,在沒聽過容器元件之前,我們的程式碼一般都是這樣寫的:

components/CommentList.vue

為 Vue 引入容器元件和展示元件

store/index.js

為 Vue 引入容器元件和展示元件

這樣寫看起來理所當然,有沒有什麼問題,或者可以優化的地方呢?

有一個很顯而易見的問題,由於 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。

為 Vue 引入容器元件和展示元件

containers/CommentListContainer.vue

容器元件的職責

  • 通過 computed 來獲取到狀態更新,傳遞給展示元件
  • 通過 methods 定義回撥函式,回撥函式內部呼叫 store 的 dispatch 方法,傳遞給展示元件

為 Vue 引入容器元件和展示元件

使用 @xunlei/vuex-connector 實現容器元件

上面演示的容器元件的程式碼非常簡單,實際上如果直接投入生產環境,會產生一些問題。

手動實現容器元件存在的不足

程式碼比較繁瑣

在上面的例子中,每次傳遞一個 state 都要定義一個 computed,每傳遞一個 mutation 或者 action 都需要定義一個方法,而且還要注意這個方法的引數要透傳過去,同時還要處理返回值,比如非同步的 action 需要返回 promise 的時候,定義的這個 method 也得把 action 的返回值返回出去。

無法透傳其他 props 給展示元件

比如展示元件新增了一個 prop 叫做 type,可以傳遞一個評論的型別,用來區分是熱門還是最新,如果用上面的容器實現方式,首先需要在容器元件這層新增一個 prop 叫做 type 接受外部傳來的引數,然後在展示元件內部同樣定義一個 叫做 type 的 prop,然後才能傳遞下去。

需要透傳的 props 必須定義兩遍,增加了維護的成本。

為 Vue 引入容器元件和展示元件

為 Vue 引入容器元件和展示元件

容器元件無法統一進行優化

每一個手動實現的容器元件實質上程式碼邏輯非常近似,但是沒有經過同一層封裝,如果目前實現的容器元件存在一些效能優化的地方,需要每個容器元件都進行統一的修改。

無法控制展示元件不去獲取 store

因為容器元件是通過 this.$store 獲取 store 的,展示元件內部實質上也可以直接跟 store 通訊,如果沒有約束,很難統一要求展示元件不得直接和 store 通訊。

使用 @xunlei/vuex-connector

@xunlei/vuex-connector 借鑑了 react redux 的 connect 方法,在 vuex 基礎上進行的開發。

有以下幾個特點:

程式碼非常簡潔

下面是上面例子中手動實現的容器元件的改造版本:

comonents/ConnectCommentListContainer.vue

為 Vue 引入容器元件和展示元件

通過 connector 的 connnect 方法,傳入要對映的配置,支援 mapStateToProps, mapGettersToProps, mapDispatchToProps, mapCommitToProps 這四種,每一種都是隻要配置一個簡單的 map 函式,或者字串即可。

然後在返回的函式中傳入要連線的展示元件即可,是不是非常的簡潔。

容器元件本身也可以複用

由於借鑑了 redux 優雅的函式式風格, connector 的 connnect 方法 返回的函式實際上是一個高階元件,也就是一個可以建立元件的函式。這樣帶來了額外的好處,不同的展示元件也可以複用同一個容器元件。

舉個例子:

如果你寫了多個版本的評論展示元件,接受的資料和更新資料的方式都是一樣的,那麼你就沒有必要為每個版本的評論元件都搞一個容器元件了,只要複用同一個高階元件函式即可。

問題來了,connector 是什麼?

connector 實際上是一個能獲取到 store 例項的聯結器,可以在初始化 vuex store 的時候進行初始化。

為 Vue 引入容器元件和展示元件

一個 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 例項了。

比如,之前我們是通過下面的方式進行初始化:

為 Vue 引入容器元件和展示元件

使用了 VuexConnector 之後,在最初 new Vue 的時候就不需要也最好不要傳遞 store 了,這樣就避免了 this.$store 氾濫導致程式碼耦合的問題。

引入容器元件/展示元件模式帶來的好處

可複用性

容器元件/展示元件的劃分,採用了單一職責原則的設計模式,容器元件專門負責和 store 通訊,展示元件只負責展示,解除了元件的耦合,可以帶來更好的可複用性。

健壯性

由於展示元件和容器元件是通過 props 這種介面來連線,可以利用 props 的校驗來增強程式碼的可靠性,混合的元件就沒有這種好處。

另外對 props 的校驗可以採取一下幾種方式:

Vue 元件 props 驗證

可以驗證 props 的型別,預設可以校驗是否是以下型別:

  • String
  • Number
  • Boolean
  • Function
  • Object
  • Array
  • Symbol

如果你的 props 是類的一個例項,type 也可以是一個自定義構造器函式,使用 instanceof 檢測。

如果還是不滿足需求,可以自定義驗證函式:

為 Vue 引入容器元件和展示元件

TypeScript 型別系統

Vue 元件 props 驗證對於物件或者其他複雜的型別校驗還是不太友好,所以很多人也推薦大家的 props 儘量採取簡單型別,不過如果你有在用 TypeScript 開發 Vue 應用,可以利用 TypeScript 靜態型別檢查來宣告你的 props 。

為 Vue 引入容器元件和展示元件

可測試性

由於元件做的事情更少了,使得測試也會變得容易。

容器元件不用關心 UI 的展示,只關心資料和更新。

展示元件只是呈現傳入的 props ,寫單元測試的時候也非常容易 mock 資料層。

引入容器元件/展示元件模式帶來的限制

學習和開發成本

因為容器元件/展示元件的拆分,初期會增加一些學習成本,不過當你看完這篇文章,基本上也就入門了。

在開發的時候,由於需要封裝一個容器,包裝一些資料和介面給展示元件,會增加一些工作量, @xunlei/vuex-connector 通過配置的方式可以減輕不少你的工作量。

另外,在展示元件內對 props 的宣告也會帶來少量的工作。

總體來說,引入容器元件/展示元件模式投入產出比還是比較值得的。

延伸閱讀

程式碼示例

Vue Vuex 容器-展示元件模式 Demo

Demo 線上地址:

xunleif2e.github.io/vue-vuex-co…

Demo 原始碼:

github.com/xunleif2e/v…

@xunlei/vuex-connector

基於 Vue 生態實現的Vuex store Connector

@xunlei/vuex-connector 原始碼:

github.com/xunleif2e/v…

歡迎 Star

掃一掃關注迅雷前端公眾號

致敬 React: 為 Vue 引入容器元件和展示元件

作者:binggg

校對:珈藍

相關文章