React 世界的一等公民 - 組件

Choerodon豬齒魚平臺使用 React 做爲前端應用框架,對前端的展現作了必定的封裝和處理,並配套提供了前端組件庫Choerodon UI。結合實際業務狀況,不斷對組件優化設計,提升代碼質量。css

本文將結合Choerodon豬齒魚平臺使用案例,簡單說明組件的分類、設計原則和設計模式,幫助開發者在不一樣場景下選擇正確的設計和方案編寫組件(示例代碼基於ES6/ES7的語法,適於有必定前端基礎的讀者)。前端

本文做者:Choerodon豬齒魚社區 王柯react

文章的主要內容包括:git

  • React 組件簡介
  • 組件分類
  • 組件設計原則、最佳實踐
  • 組件設計模式簡介

React 組件簡介

React是指用於構建用戶界面的 JavaScript 庫。換言之,React是一個構建視圖層的類庫(或框架)。無論 React 自己如何複雜,無論其生態如何龐大,構建視圖始終是它的核心。github

能夠用個公式說明:web

UI = f(data)

React的基礎原則有三條,分別是:bootstrap

  1. React 界面徹底由數據驅動;
  2. React 中一切都是組件;
  3. props 是 React 組件之間通信的基本方式。

那麼組件又是什麼?設計模式

組件是一個函數或者一個 Class(固然 Class 也是 function),它根據輸入參數,最終返回一個 React Element。簡單地說,React Element 描述了「你想」在屏幕上看到的事物。抽象地說,React Element 元素是一個描述了 Dom Node 的對象。api

因此實際上使用 React Component 來生成 React Element,對於開發體驗有巨大的提高,好比不須要手寫React.createElement等。前端框架

那麼全部 React Component 都須要返回 React Element 嗎?顯然是不須要的。 return null; 或者返回其餘的 React 組件都有存在的意義,它能完成並實現不少巧妙的設計、思想和反作用,在下文會有所擴展。

能夠說,在 React 中一切皆爲組件:

  • 用戶界面就是組件;
  • 組件能夠嵌套包裝組成複雜功能;
  • 組件能夠用來實現反作用。

React 也提供了多種編寫組件的方法適用於各類場景實例。

組件分類

如何在場景下快速正確地選擇組件設計模式和方案,首先得有一個本身接受和經常使用的組件分類,以便從分類中快速肯定編寫方法,再考慮設計模式等後續問題。

Vue的做者尤雨溪在一場Live中也表達過本身對前端組件的見解,「組件能夠是函數,是有分類的。」從功能維度對組件進行了分類,這四種分類方式也適用於Choerodon豬齒魚前端開發中的業務場景:

  • 純展現型組件:數據進,DOM出,直觀明瞭
  • 接入型組件:在React場景下的container
  • component,這種組件會跟數據層的service打交道,會包含一些跟服務器或者說數據源打交道的邏輯,container會把數據向下傳遞給展現型組件、
  • 交互型組件:典型的例子是對於表單組件的封裝和增強,大部分的組件庫都是以交互型組件爲主,好比說Element UI,特色是有比較複雜的交互邏輯,可是是比較通用的邏輯,強調組件的複用
  • 功能型組件:以Vue的應用場景舉例,路由的router-view組件、transition組件,自己並不渲染任何內容,是一個邏輯型的東西,做爲一種擴展或者是抽象機制存在

在此以Choerodon豬齒魚平臺的一個建立界面來分析。

  • 紅色佈局:功能型組件
  • 藍色菜單:交互型組件,菜單項:遍歷菜單數據輸出DOM的純展現型組件
  • 右塊內容:接入型組件(容器組件)
  • Table、btn等:交互型組件

能夠看到,一個複雜界面能夠分割成不少簡單或複雜的組件,複雜組件還包括子組件等。此外,除了從功能維度對組件進行劃分,也能夠從開發者對組件的使用習慣進行分類(如下分類非對立關係):

  • 無狀態組件
  • 有狀態組件
  • 容器組件
  • 高階組件
  • Render Callback組件

簡單說明一下幾種組件:

  • 無狀態組件:無狀態組件(Stateless Component)是最基礎的組件形式,因爲沒有狀態的影響因此就是純靜態展現的做用。基本組成結構就是屬性(props)加上一個渲染函數(render)。因爲不涉及到狀態的更新,因此這種組件的複用性也最強。例如在各UI庫中開發的按鈕、輸入框、圖標等等。
  • 有狀態組件:組件內部包含狀態(state)且狀態隨着事件或者外部的消息而發生改變的時候,這就構成了有狀態組件(Stateful Component)。有狀態組件一般會帶有生命週期(lifecycle),用以在不一樣的時刻觸發狀態的更新。在寫業務邏輯時經常使用到,不一樣場景所用的狀態和生命週期也會不一樣。
  • 容器組件:爲使組件的職責更加單一,耦合性進一步地下降,引入了容器組件(Container Component)的概念。重要負責對數據獲取以及處理的邏輯。下文的設計模式也會提到。
  • 高階組件:「高階組件(HoC)」也算是種組件設計模式。作爲一個高階組件,能夠在原有組件的基礎上,對其增長新的功能和行爲。如打印日誌,獲取數據和校驗數據等和展現無關的邏輯的時候,抽象出一個高階組件,用以給基礎的組件增長這些功能,減小公共的代碼。
  • Render Callback組件:組件模式是在組件中使用渲染回調的方式,將組件中的渲染邏輯委託給其子組件。也是種重用組件邏輯的方式,也叫render props 模式。

以上這些組件編寫模式基本上能夠覆蓋目前工做中所須要的模式。在寫一些複雜的框架組件的時候,仔細設計和研究組件間的解耦和組合方式,可以使後續的項目可維護性大大加強。

對立的兩大分類:

  • 基於類的組件:基於類的組件(Class based components)是包含狀態和方法的。
  • 基於函數的組件:基於函數的組件(Functional Components)是沒有狀態和方法的。它們是純粹的、易讀的。儘量的使用它們。

固然,React v16.7.0-alpha 中第一次引入了 Hooks 的概念,Hooks 的目的是讓開發者不須要再用 class 來實現組件。這是React的將來,基於函數的組件也可處理狀態。

瞭解了這些之後就須要有一個本身開發新組件前的思考,遵循組件設計原則,快速肯定分類開始編寫Code。

設計原則/最佳實踐

React 的組件實際上是軟件設計中的模塊,其設計原則也需聽從通用的組件設計原則,簡單說來,就是要減小組件之間的耦合性(Coupling),讓組件簡單,這樣才能讓總體系統易於理解、易於維護。

即,設計原則:

  1. 接口小,props 數量少;
  2. 劃分組件,充分利用組合(composition);
  3. 把 state 往上層組件提取,讓下層組件只須要實現爲純函數。

就像搭積木,複雜的應用和組件都是由簡單的界面和組件組成的。劃分組件也沒有絕對的方法,選擇在當下場景合適的方式劃分,充分利用組合便可。實際編寫代碼也是逐步精進的過程,努力作到:

  1. 功能正常;
  2. 代碼整潔;
  3. 高性能。

取Choerodon豬齒魚平臺Devops項目的應用管理模塊實例,導入應用:

這個界面看起來很簡單,功能簡介 + 導入步驟條,實際由於存在步驟條,內容很豐富。

首先組件叫作AppImport,組件內包含簡介和步驟條,須要記錄當前步驟條第幾步狀態’current‘,因此須要維持狀態(state),能夠確定,AppImport 是一個有狀態的組件,不能只是一個純函數,而是一個繼承自 Component 的類。

class AppImport extends React.Component {
    constructor() {
    super(...arguments);
    this.state = {
      current: 0,
    };
  }
  render() {
     //TODO: 返回全部JSX
  }
}

接下來劃分組件,按照數據邊界來分割組件:

  • 使用了choerodon-front-boot 中定義好的容器組件,Page、Header、Content;
  • 渲染 Header,返回上級菜單,渲染當前界面title。
  • 渲染 Content,封裝好的組件處理了導入應用和其詳情簡介;
  • 渲染 Steps 卡片,步驟條卡片渲染,state 爲當前步以及後續須要導入提交的數據 data;
  • 最後,Steps 每一步數據需求都不一樣,均拆成單獨子組件。

在 React 中,有一個誤區,就是把 render 中的代碼分拆到多個 renderXXXX 函數中去,好比下面這樣:

class AppImport extends React.Component {
  render() {
    const Header = this.renderHeader();
    const Content = this.renderContent();
    const Steps = this.renderSteps();

    return (
       <Page>
          {Header}
          {Content}
          {Steps}
       </Page>
    );
  }

  renderHeader() {
     //TODO: 返回上級菜單,渲染當前界面title
  }

  renderContent() {
     //TODO: 導入應用和其詳情簡介
  }

  renderSteps() {
     //TODO: 返回步驟條卡片
  }
}

用上面的方法組織代碼,固然比寫一個巨大的 render 函數要強,可是,實現這麼多 renderXXXX 函數並非一個明智之舉,由於這些 renderXXXX 函數訪問的是一樣的 props 和 state,這樣代碼依然耦合在了一塊兒。更好的方法是把這些 renderXXXX 重構成各自獨立的 React 組件,像下面這樣

class AppImport extends React.Component {
  constructor() {
    super(...arguments);
    this.state = {
      data: {},
      current: 0,
    };
  }

  next = () => {}

  cancel = () => {}

  render() {
    return (
      <Page>
        <Header title='xxx' backPath='xxxxxx' />
        <Content code="app.import" values={{ appName }}>
          <div className="c7n-app-import-wrap">
            <Steps current={current} className="steps-line">
              <Step key={item.key} title={item.title} />
            </Steps>
            <div className="steps-content">
              <Step0 onNext={this.next} onCancel={this.cancel} values={data} />
            </div>
          </div>
        </Content>
      </Page>
    );
  }
}

const Step = (props) => {
  //TODO: 返回步驟條Content
};

const Steps = (props) => {
  //TODO: Steps
};

const Page = (props) => {
  //TODO: Page
}

// Header / Content 

// 根據代碼量,儘可能每一個組件都有本身專屬的源代碼文件 導出,再導入
// 示例代碼中 Page、Header、Content 使用了choerodon-front-boot 中定義好的容器組件,
// Steps 使用了choerodon-ui 庫
// 因此在頭部導入便可
// import { Steps } from 'choerodon-ui';
// import { Content, Header, Page } from 'choerodon-front-boot';

實際狀況下,步驟條不止一步,處理函數也不止那麼簡單,可是通過劃分和抽取,做爲展現組件的 AppImport 結構清晰,代碼整潔,接口少(props只涉及公共的 store、history 等 )。再處理下StepN(子組件根據實際內容處理,這裏略過),整個 AppImport 代碼不超過150行,相比不劃分組件,代碼隨便超過1000+行,劃分優化後思路清晰,可維護性高。

最終代碼:

import React, { Component, Fragment } from 'react';
import { observer } from 'mobx-react';
import { withRouter } from 'react-router-dom';
import { injectIntl, FormattedMessage } from 'react-intl';
import { Steps } from 'choerodon-ui';
import { Content, Header, Page, stores } from 'choerodon-front-boot';
import '../../../main.scss';
import './AppImport.scss';
import { Step0, Step1, Step2, Step3 } from './steps/index';

const { AppState } = stores;
const Step = Steps.Step;

@observer
class AppImport extends Component {
  constructor() {
    super(...arguments);
    this.state = {
      data: {},
      current: 0,
    };
  }

  next = (values) => {
    // 點擊下一步處理函數,略
  };

  prev = () => {
    // 點擊上一步處理函數,略
  };

  cancel = () => {
    // 點擊取消處理函數,略
  };

  importApp = () => {
    // 點擊導入,數據處理,略
  };

  render() {
    const { current, data } = this.state;
    // const ...

    const steps = [{
      key: 'step0',
      title: <FormattedMessage id="app.import.step1" />,
      content: <Step0 onNext={this.next} onCancel={this.cancel} store={AppStore} values={data} />,
    }, {
      key: 'step1',
      title: <FormattedMessage id="app.import.step2" />,
      content: <Step1 onNext={this.next} onPrevious={this.prev} onCancel={this.cancel} store={AppStore} values={data} />,
    }, {
      key: 'step2',
      title: <FormattedMessage id="app.import.step3" />,
      content: <Step2 onNext={this.next} onPrevious={this.prev} onCancel={this.cancel} store={AppStore} values={data} />,
    }, {
      key: 'step3',
      title: <FormattedMessage id="app.import.step4" />,
      content: <Step3 onImport={this.importApp} onPrevious={this.prev} onCancel={this.cancel} store={AppStore} values={data} />,
    }];

    return (
      <Page>
        <Header title='xxx' backPath='xxxxxx' />
        <Content code="app.import" values={{ name }}>
          <div className="c7n-app-import-wrap">
            <Steps current={current} className="steps-line">
              {steps.map(item => <Step key={item.key} title={item.title} />)}
            </Steps>
            <div className="steps-content">{steps[current].content}</div>
          </div>
        </Content>
      </Page>
    );
  }
}

export default withRouter(injectIntl(AppImport));

過程當中會接觸到一些最佳實踐和技巧:

  1. 避免 renderXXXX 函數
  2. 給回調函數類型的 props 加統一前綴(onNext、onXXX 或 handleXXX 規範,可讀性好)
  3. 使用 propTypes 來定義組件的 props
  4. 儘可能每一個組件都有本身專屬的源代碼文件(StepN)
  5. 用解構賦值(destructuring assignment)的方法獲取參數 props 的每一個屬性值
  6. 利用屬性初始化(property initializer)來定義 state 和成員函數

組件設計模式

不一樣的業務情境下使用合適的設計模式能大大提升開發效率和可維護性。瞭解以上內容後能更好的理解和選擇設計模式。

經常使用的設計模式有:

  1. 容器組件和展現組件(Container and Presentational Components);
  2. 高階組件;
  3. render props 模式;
  4. 提供者模式(Provider Pattern);
  5. 組合組件。

網上介紹這些模式的文章有不少,每一個模式均可以長篇詳解。可是,模式就是特定於一種問題場景的解決辦法。

模式(Pattern) = 問題場景(Context) + 解決辦法(Solution)

明確使用場景才能正確發揮模式的功能。因此,簡單介紹一下各模式實際應用於什麼場景較好。

容器組件和展現組件

React最簡單也是最經常使用的一種組件模式就是「容器組件和展現組件」。其本質就是把一個功能分配到兩個組件中,造成父子關係,外層的父組件負責管理數據狀態,內層的子組件只負責展現,讓一個模塊都專一於一個功能,這樣更利於代碼的維護。

上文步驟條的實例就是把獲取和管理數據這件事和界面渲染這件事分開。作法就是,把獲取和管理數據的邏輯放在父組件,也就是容器組件;把渲染界面的邏輯放在子組件,也就是展現組件。有關數據處理的變更就只須要對容器組件進行修改,例如修改數據狀態管理方式,徹底不影響展現組件。

高階組件

高階組件適用場景於「不要重複本身」(DRY,Don't Repeat Yourself)編碼原則,某些功能是多個組件通用的,在每一個組件都重複實現邏輯,浪費、可維護行低。第一想法是共用邏輯提取爲一個 React 組件,可是共用邏輯單獨沒法使用,不足以抽象成組件,僅僅是對其餘組件的功能增強。固然,高階組件並非 React 中惟一的重用組件邏輯的方式,下文的 render props 模式也可處理。

例如,不少網站應用,有些模塊都須要在用戶已經登陸的狀況下才顯示。好比,對於一個電商類網站,「退出登陸」按鈕、「購物車」這些模塊,就只有用戶登陸以後才顯示,對應這些模塊的 React 組件若是連「只有在登陸時才顯示」的功能都重複實現,那就浪費了。

render props 模式

所謂 render props,指的是讓 React 組件的 props 支持函數這種模式。由於做爲 props 傳入的函數每每被用來渲染一部分界面,因此這種模式被稱爲 render props。適用場景和高階組件差很少,可是與其仍是有一些差異:

  1. render props 模式的應用,是一個 React 組件,而高階組件,雖然名爲「組件」,其實只是一個產生 React 組件的函數
  2. 高階組件可鏈式調用,由於實質是函數
  3. render props 相對於高階組件還有一個顯著優點,就是對於新增的 props 更加靈活

因此以上對比,當須要重用 React 組件的邏輯時,建議首先看這個功能是否能夠抽象爲一個簡單的組件;若是行不通的話,考慮是否能夠應用 render props 模式;再不行的話,才考慮應用高階組件模式。固然,沒有絕對的使用順序,實際場景爲準。

提供者模式

在 React 中,props 是組件之間通信的主要手段,可是,有一種場景單純靠 props 來通信是不恰當的,那就是兩個組件之間間隔着多層其餘組件。避免 props 逐級傳遞,便是提供者模式的適用場景。實現方式也分老Context API和新Context API。新版本的 Context API 纔是將來,在 React v17 中,可能就會刪除對老版 Context API 的支持,因此,如今你們都應該使用第二種實現方式。新版API詳解

典型用例就是實現「樣式主題」(Theme),多語言支持等。

組合組件

組合組件模式要解決的是這樣一類問題:父組件想要傳遞一些信息給子組件,可是,若是用 props 傳遞又顯得十分麻煩。利用 Context?固然還有其餘解決方案,就是組合組件模式。

應用組合組件場景的每每是共享組件庫,把一些經常使用的功能封裝在組件裏,讓應用層直接用就行。在 antd 和 bootstrap 這樣的共享庫中,都使用了組合組件這種模式。將複雜度都封裝起來了,從使用者角度,連 props 都看不見。實例擴展

總 結

對前端來講,前端不是不用設計模式,而是已經把設計模式融入到了開發的基礎當中。Choerodon豬齒魚平臺前端真實的業務場景每每須要應用多個設計模式,界面也會包含多個大小不一的組件。開發設計時,符合程序設計的原則:「高內聚,低耦合」便可。本文只是簡單總結,提供一些思路和簡單的應用場景給開發者,真正的熟練把握和應用還得多實踐開發使用,多對本身欠缺的知識點去深挖學習和思考,不斷進步。

參考/引用資料:

關於Choerodon豬齒魚

Choerodon豬齒魚做爲開源多雲應用平臺,是基於Kubernetes的容器編排和管理能力,整合DevOps工具鏈、微服務和移動應用框架,來幫助企業實現敏捷化的應用交付和自動化的運營管理,同時提供IoT、支付、數據、智能洞察、企業應用市場等業務組件,致力幫助企業聚焦於業務,加速數字化轉型。

你們也能夠經過如下社區途徑瞭解豬齒魚的最新動態、產品特性,以及參與社區貢獻:

歡迎加入Choerodon豬齒魚社區,共同爲企業數字化服務打造一個開放的生態平臺。

相關文章
相關標籤/搜索