Choerodon豬齒魚平臺使用 React 做爲前端應用框架,對前端的展現作了必定的封裝和處理,並配套提供了前端組件庫Choerodon UI。結合實際業務狀況,不斷對組件優化設計,提升代碼質量。css
本文將結合Choerodon豬齒魚平臺使用案例,簡單說明組件的分類、設計原則和設計模式,幫助開發者在不一樣場景下選擇正確的設計和方案編寫組件(示例代碼基於ES6/ES7的語法,適於有必定前端基礎的讀者)。前端
本文做者:Choerodon豬齒魚社區 王柯react
文章的主要內容包括:git
React是指用於構建用戶界面的 JavaScript 庫。換言之,React是一個構建視圖層的類庫(或框架)。無論 React 自己如何複雜,無論其生態如何龐大,構建視圖始終是它的核心。github
能夠用個公式說明:web
UI = f(data)
React的基礎原則有三條,分別是:bootstrap
那麼組件又是什麼?設計模式
組件是一個函數或者一個 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豬齒魚前端開發中的業務場景:
在此以Choerodon豬齒魚平臺的一個建立界面來分析。
能夠看到,一個複雜界面能夠分割成不少簡單或複雜的組件,複雜組件還包括子組件等。此外,除了從功能維度對組件進行劃分,也能夠從開發者對組件的使用習慣進行分類(如下分類非對立關係):
簡單說明一下幾種組件:
以上這些組件編寫模式基本上能夠覆蓋目前工做中所須要的模式。在寫一些複雜的框架組件的時候,仔細設計和研究組件間的解耦和組合方式,可以使後續的項目可維護性大大加強。
對立的兩大分類:
固然,React v16.7.0-alpha 中第一次引入了 Hooks 的概念,Hooks 的目的是讓開發者不須要再用 class 來實現組件。這是React的將來,基於函數的組件也可處理狀態。
瞭解了這些之後就須要有一個本身開發新組件前的思考,遵循組件設計原則,快速肯定分類開始編寫Code。
React 的組件實際上是軟件設計中的模塊,其設計原則也需聽從通用的組件設計原則,簡單說來,就是要減小組件之間的耦合性(Coupling),讓組件簡單,這樣才能讓總體系統易於理解、易於維護。
即,設計原則:
就像搭積木,複雜的應用和組件都是由簡單的界面和組件組成的。劃分組件也沒有絕對的方法,選擇在當下場景合適的方式劃分,充分利用組合便可。實際編寫代碼也是逐步精進的過程,努力作到:
取Choerodon豬齒魚平臺Devops項目的應用管理模塊實例,導入應用:
這個界面看起來很簡單,功能簡介 + 導入步驟條,實際由於存在步驟條,內容很豐富。
首先組件叫作AppImport,組件內包含簡介和步驟條,須要記錄當前步驟條第幾步狀態’current‘,因此須要維持狀態(state),能夠確定,AppImport 是一個有狀態的組件,不能只是一個純函數,而是一個繼承自 Component 的類。
class AppImport extends React.Component { constructor() { super(...arguments); this.state = { current: 0, }; } render() { //TODO: 返回全部JSX } }
接下來劃分組件,按照數據邊界來分割組件:
在 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));
過程當中會接觸到一些最佳實踐和技巧:
不一樣的業務情境下使用合適的設計模式能大大提升開發效率和可維護性。瞭解以上內容後能更好的理解和選擇設計模式。
經常使用的設計模式有:
網上介紹這些模式的文章有不少,每一個模式均可以長篇詳解。可是,模式就是特定於一種問題場景的解決辦法。
模式(Pattern) = 問題場景(Context) + 解決辦法(Solution)
明確使用場景才能正確發揮模式的功能。因此,簡單介紹一下各模式實際應用於什麼場景較好。
React最簡單也是最經常使用的一種組件模式就是「容器組件和展現組件」。其本質就是把一個功能分配到兩個組件中,造成父子關係,外層的父組件負責管理數據狀態,內層的子組件只負責展現,讓一個模塊都專一於一個功能,這樣更利於代碼的維護。
上文步驟條的實例就是把獲取和管理數據這件事和界面渲染這件事分開。作法就是,把獲取和管理數據的邏輯放在父組件,也就是容器組件;把渲染界面的邏輯放在子組件,也就是展現組件。有關數據處理的變更就只須要對容器組件進行修改,例如修改數據狀態管理方式,徹底不影響展現組件。
高階組件適用場景於「不要重複本身」(DRY,Don't Repeat Yourself)編碼原則,某些功能是多個組件通用的,在每一個組件都重複實現邏輯,浪費、可維護行低。第一想法是共用邏輯提取爲一個 React 組件,可是共用邏輯單獨沒法使用,不足以抽象成組件,僅僅是對其餘組件的功能增強。固然,高階組件並非 React 中惟一的重用組件邏輯的方式,下文的 render props 模式也可處理。
例如,不少網站應用,有些模塊都須要在用戶已經登陸的狀況下才顯示。好比,對於一個電商類網站,「退出登陸」按鈕、「購物車」這些模塊,就只有用戶登陸以後才顯示,對應這些模塊的 React 組件若是連「只有在登陸時才顯示」的功能都重複實現,那就浪費了。
所謂 render props,指的是讓 React 組件的 props 支持函數這種模式。由於做爲 props 傳入的函數每每被用來渲染一部分界面,因此這種模式被稱爲 render 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豬齒魚做爲開源多雲應用平臺,是基於Kubernetes的容器編排和管理能力,整合DevOps工具鏈、微服務和移動應用框架,來幫助企業實現敏捷化的應用交付和自動化的運營管理,同時提供IoT、支付、數據、智能洞察、企業應用市場等業務組件,致力幫助企業聚焦於業務,加速數字化轉型。
你們也能夠經過如下社區途徑瞭解豬齒魚的最新動態、產品特性,以及參與社區貢獻:
歡迎加入Choerodon豬齒魚社區,共同爲企業數字化服務打造一個開放的生態平臺。