用上帝視角來看待元件的設計模式

網易雲社群發表於2018-10-31

此文已由作者黃鍇授權網易雲社群釋出。

歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。


元件設計,從簡單來看,就是如何提高編碼效率,提高程式碼的複用率的方法,從高階來看,這是一門程式設計的藝術

最近看了redux作者——Dan Abramov寫的《Presentational and Container Components》 感覺受益匪淺,發現有很多人都在討論元件模式,作為一個每次寫需求就抓耳撓腮的思考如何組織程式碼結構的人來說,如何更好的在工作中使用設計和使用元件,這確實是一個值得討論的問題。

注:本文圖片部分參考於Michael Chan 做的有關React component patterns的演講: React component patterns, 有興趣的童鞋可以去看看。

前言

組合模式

首先,我們先從巨集觀的角度來聊聊元件。

現在很多人現在都在談設計模式,設計模式。什麼是設計模式?設計模式主要是指GoF(四人組)《設計模式》一書中提出了23種設計模式,這23種設計模式被廣泛應用在軟體工程中,代表著最佳的實踐方案。而在這些模式中,專門有一個設計模式叫:組合模式(Composite pattern),根據wikipedia的解釋:

組合模式的目的是將物件組合成樹型結構來表示部分-整體(part-whole)的層次結構。

組合模式使得使用者對單個物件和組合物件的使用具有一致性。

img

它有以下優點

  • 可擴充套件性強:由於元件間是充分解耦的,你可以輕鬆的更新,替換元件

  • 編碼高效:由於把功能拆解開,你可以專注於每個子模組功能的實現。

javascript中的組合模式——元件

如果你在現在的工作中使用了框架,你會發現,這些框架基本都使用了組合模式這種設計模式。例如Vue和React的Components,它允許你編寫小的元件,然後通過他們的組合構建出你的實際應用。

æ

在學校,總被老師說,要有大局觀,大局觀。使用組合模式,就可以很好的鍛鍊你的大局觀。 這裡我覺得雲謙前輩 說的很貼切:

要記得,接到需求的第一步永遠不是寫程式碼,而是想清楚你要做什麼,以上帝模式做整體設計。

開啟上帝視角

接下來,就讓我們在我們的程式設計世界扮演一個上帝(準確來說是女挖),看看我們這個充滿元件世界到底發生了什麼。

元件的誕生

在框架類的語言中,元件就是我們操作的基本單位,React對元件的操作提供了很多實用的API,一個元件就是通過使用這些API,產生了強大的生命力,你可以把它看作一個功能完善的細胞或者個體,我們所有的APP都是由這一個個細胞(個體)構成的。

img

然後,你在日常使用中會發現,有些元件,它總是使用其中的一部分API,有些元件經常使用另一部分API。說明這些元件開始有了自己的思考,產生了差異化(你可以把它看作基因突變)。當這些差異化越來越多,我們發現,這些元件因為各自的能力偏好,逐減組成了不同的陣營:

image-20181019135735406

對於元件的陣營分類,有很多的說法:

  • 胖和瘦( Fat and Skinny,)元件

  • 狀態和無狀態/純(Stateful and Stateless/pure)元件

  • 聰明和愚笨(Smart and Dumb)元件

  • 容器和展示( Container and Presentational )元件……

他們本質上都差不多,這裡我們選用Redux作者Dan Abramov對元件的稱法,叫他們容器展示元件,這裡使用Container和Components

元件分好了陣營,就要開始制定標準,正所謂無規矩不成方圓,一個良好的組織對確定他們統一的旗號,核心價值觀,才能方便後續找到更多的同類人,發展壯大。

元件的陣營

容器元件(Container)陣營

他們稱自己是統治者,這裡匯聚的都是精英份子,它們習慣管理和組織。作為高高在上的管理者,能讓下屬去做的事情自己一定不會動手。

因此他們制定了一套符合他們價值觀的標準:

  • 代表結構:我只關心

    事情如何運作的
    ,而不關心它是
    如何表現的

  • 無樣式:我這裡除了一些包裝div,基本沒有其他標籤,並且從不具有任何樣式

  • 有狀態:我掌握著核心資料,剩下的事情你們去做

  • 通常由其他元件生成:我們通常代表更高等的智慧(一般使用更高階的元件生成)

  • 代表人物:各種Page頁面,路由頁面

展示元件(Components)陣營

他們通常由藍白領組成,代表這著廣大的工人階層,什麼髒活累活都是他們做,他們通常沒有太多想法,只是想簡單的做好自己的工作,少背鍋就好。

在這裡,每個元件關心的事情很少,他們的志向只是做好本質工作,因此他們制定了一套符合他們工作習慣的標準:

  • 代表渲染:關注

    事物的外觀

  • 有樣式:基本上dom的渲染和樣式這些髒活累活都在這裡幹。

  • 強調獨立(很重要): 我們彼此分工明確,不依賴於應用程式的其餘部分(例如Flux操作或Store)。

  • 不關心資料:我們不關心資料怎麼產生的,不要在我們這裡指定資料的載入方式或變更方式。

  • 接受傳回指令:僅通過props接收資料和回撥。

  • 弱狀態:我們基本不需要有自己的狀態(當我們這樣做時,它是UI狀態而不是資料)。

  • 代表人物:Search,SiderBar,UserList,Pagintation

這兩種陣營基本是完全獨立的,由於這種良好的劃分,他們在社會中能很好的相處(藍色是展示元件,灰色是容器元件。

img

實際執行

好了,看他們風風火火的把陣營分了,得給他們找點事幹,看這個社會分工的實際執行效果怎麼樣。

這裡順便提一句React的元件定義,通常有兩種方式類定義無狀態函式定義

無狀態函式定義:

這樣的好處是它本身是獨立的,沒有狀態的,它只會根據傳入的props(可以考慮成指令)來修改自己的顯示,這樣的元件就具備很好的通用性,而且修改起來也方便,不用擔心會影響到別的元件。

var User = ({name}) => (  <span>
    {name}  </span>);複製程式碼

類定義:

如果有些內建的狀態需要維護,或者需要使用生命週期,可以使用類定義的方式。

class User extends Component {
  constructor(props) {
    super(props);    this.state = {
      name: props.name,
      editable: false,
    };
  } 
  render(){     return ...
  }
}複製程式碼

容器元件——定義結構,下發指令

容器元件可以通過類定義,因為可能會使用到生命週期鉤子(使用dva也可能無需生命週期鉤子)。

如果使用了redux類的狀態管理機制,一般就會由connect生成(Relay的

createContainer()
,或Flux Utils的
Container.create()
)。

我們現在要做一個使用者頁面,我們把這個任務佈置下去,有一個容器陣營拿到了這個頁面製作指標,然後這些精英開始做組織架構工作,他們覺得完成這個工作需要造三個元件:UserSearchUserListUserModal。然後每個元件需要的處理的指令(資料)也寫好了,放在userXXXProps裡,好了,他們的任務完成,開始去找展示容器陣營的人去實現這些功能。

function Users({ users }) {  // 從Redux裡獲取資料
  const {
    loading,
    list,
    total,
    current,
  } = users;  // 下發任務
  const userSearchProps = {};  const userListProps = {};  const userModalProps = {};  // 整體結構設計
  return (    <div>
      <UserSearch {...userSearchProps} />
      <UserList {...userListProps} />
      <UserModal {...userModalProps} />
    </div>
  );
}

// 指定訂閱資料,這裡關聯了 users
function mapStateToProps({ users }) {
  return { users };
}

// 建立資料關聯關係
export default connect(mapStateToProps)(Users);複製程式碼

展示容器——接收指令,開工

一般來說,展示容器推薦使用 無狀態函式 的方式生成

一個良好的展示容器應該是彼此獨立的,這也是一個良好的分工社會應該具備的,大家各司其職,因此這裡通常使用無狀態函式生成一個元件。在這裡,通常是根據props傳入的值去處理相應的事情(老闆要你幹嘛就幹嘛)。

// 從props裡獲取指令(資料)const UserList = ({
  total,
  current,
  loading,
  dataSource,
}) => {const columns = [……];// 開始工作,構建元件的渲染return (  <div>
    <Table
      columns={columns}
      dataSource={dataSource}
      loading={loading}
    />
  </div>);
}複製程式碼

這樣看下來,這種社會分工好像執行的挺順暢的。那這就是這個社會的全貌嗎?當然不是,實際的生活中,元件的區分可以更加精確更加精細。

例如有一些元件設計模式裡把元件模式細分為:

  • Proxy component

  • Presentational component

  • Layout component

  • Container component

  • Higher-order component (HOC's)

  • Render callback

但是,我個人認為以上兩種劃分已經足夠應付生活中的絕大部分情形,剩下的,可以通過兩者的組合實現mixed Component

總結

這樣看下來,這種社會分工好像執行的挺順暢的,實際上,這種分類方式也是 很多開發者經驗的總結。

根據“約定優於配置(convention over configuration)”的思想,提前做一定的規範肯定是沒錯的,但很多人將這種分工看做了他們設計元件的教條,這樣就不好了,因為凡事總有例外,也許在某些複雜的業務場景中,Container 和 Components需要混用。你需要靈活的去使用,這也是為什麼Dan修改了自己文章:

I amended the article because people were taking the separation as a dogma. 90% of React users don’t ever plan to have something like a living styleguide. There is no need to make life harder for them. Even those that do, don’t need to make

every
component available in such tool.

適合自己的才是最好的。

何苦讓生活更艱難?

最後,再說說比較好的應用開發步驟是什麼?我覺得可能是:

  1. 徹底弄清楚你要做什麼

  2. 功能的劃分

  3. 根據功能組織目錄結構

  4. 設計你的資料模型(modal)

  5. 設計你的容器,確定整體框架結構

  6. 開始依次填充展示容器

  7. 連線元件和資料模型

  8. 測試

參考

Composite pattern

React component patterns

React Patterns

Presentational and Container Components

Container Components

React.js Conf 2015 - Making your app fast with high-performance components

Components and Props

Guide to Using the Composite Pattern with JavaScript

dva快速上手


免費體驗雲安全(易盾)內容安全、驗證碼等服務

更多網易技術、產品、運營經驗分享請點選



相關文章:
【推薦】 [翻譯]pytest測試框架(一)


相關文章