<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          淺談 React 組件設(shè)計(jì)

          共 22669字,需瀏覽 46分鐘

           ·

          2022-11-22 14:12

          大廠技術(shù)  高級(jí)前端  Node進(jìn)階

          點(diǎn)擊上方 程序員成長(zhǎng)指北,關(guān)注公眾號(hào)

          回復(fù)1,加入高級(jí)Node交流群

          前言

          前端組件化一直是老生常談的話題,在前面介紹 React 的時(shí)候我們已經(jīng)提到過(guò) React 的一些優(yōu)勢(shì),今天則是帶大家了解一下組件設(shè)計(jì)原則。

          jQuery 插件

          在開始講 React 組件之前,我們還是要先來(lái)聊聊 jQuery。在我看來(lái),jQuery 插件就已經(jīng)具備了組件化的雛形。

          在 jQuery 還大行其道的時(shí)代,我們?cè)诰W(wǎng)上可以看到一些 jQuery 插件網(wǎng)站,里面有各種豐富的插件,比如輪播圖、表單、選項(xiàng)卡等等。

          組件?插件?

          組件和插件的區(qū)別是什么呢?插件是集成到某個(gè)平臺(tái)上的,比如 Jenkins 插件、Chrome 插件等等,jQuery 插件也類似。平臺(tái)只提供基礎(chǔ)能力,插件則提供一些定制化的能力。而組件則是偏向于 ui 層面的,將 ui 和業(yè)務(wù)邏輯封裝起來(lái),供其他人使用。

          封裝 DOM 結(jié)構(gòu)

          在一些最簡(jiǎn)單無(wú)腦的 jQuery 插件中,它們一般會(huì)將 DOM 結(jié)構(gòu)直接寫死到插件中,這樣的插件拿來(lái)即用,但限制也比較大,我們無(wú)法修改插件的 DOM 結(jié)構(gòu)。

          // 輪播圖插件
          $("#slider").slider({
              config: {
                  showDottrue// 是否展示小圓點(diǎn)
                  showArrowtrue // 是否展示左右小箭頭
              }, // 一些配置
              data: [] // 數(shù)據(jù)
          })

          還有另一種極端的插件,它們完全不把 DOM 放到插件中,但會(huì)要求使用者按照某種固定格式的結(jié)構(gòu)來(lái)組織代碼。一旦結(jié)構(gòu)不準(zhǔn)確,就可能會(huì)造成插件內(nèi)部獲取 DOM 出錯(cuò)。但這種插件的好處在于可以由使用者自定義具體的 DOM 結(jié)構(gòu)和樣式。

          <div id="slider">
              <ul class="list">
                  <li data-index="0"><img src="" /></li>
                  <li data-index="1"><img src="" /></li>
                  <li data-index="2"><img src="" /></li>
              </ul>

              <a href="javascript:;" class="left-arrow"><</a>
              <a href="javascript:;" class="left-arrow">></a>
              <div class="dot">
                  <span data-index="0"></span>
                  <span data-index="1"></span>
                  <span data-index="2"></span>
              </div>

          </div>

          $("#slider").slider({
              config: {} /
          / 配置
          })

          當(dāng)然,你也可以選擇將 DOM 通過(guò)配置傳給插件,插件內(nèi)部去做這些渲染的工作,這樣的插件比較靈活。有沒(méi)有發(fā)現(xiàn)?這和 render props 模式非常相似。

          $("#slider").slider({
              config: {}, // 配置
              components: {
                  dot(item, index) => `<span data-index=${index}></span>`,
                  item(item, index) => `<li data-index=${index}><img src=${item.src} /></li>`
              }
          })

          React 組件設(shè)計(jì)

          前面講了幾種 jQuery 插件的設(shè)計(jì)模式,其實(shí)萬(wàn)變不離其宗,不管是 jQuery 還是 React,組件設(shè)計(jì)思想都是一樣的。

          image_1e5jp360218jbi4amj918oin89m.png-39.4kB

          個(gè)人覺得,組件設(shè)計(jì)應(yīng)該遵循以下幾個(gè)原則:

          1. 適當(dāng)?shù)慕M件粒度:一個(gè)組件盡量只做一件事。
          2. 復(fù)用相同部分:盡量復(fù)用不同組件相同的部分。
          3. 松耦合:組件不應(yīng)當(dāng)依賴另一個(gè)組件。
          4. 數(shù)據(jù)解耦:組件不應(yīng)該依賴特定結(jié)構(gòu)的數(shù)據(jù)。
          5. 結(jié)構(gòu)自由:組件不應(yīng)該封閉固定的結(jié)構(gòu)。

          容器組件與展示組件

          顧名思義,容器組件就是類似于“容器”的組件,它可以擁有狀態(tài),會(huì)做一些網(wǎng)絡(luò)請(qǐng)求之類的副作用處理,一般是一個(gè)業(yè)務(wù)模塊的入口,比如某個(gè)路由指向的組件。我們最常見的就是 Redux 中被 connect 包裹的組件。容器組件有這么幾個(gè)特點(diǎn):

          1. 容器組件常常是和業(yè)務(wù)相關(guān)的。
          2. 統(tǒng)一的數(shù)據(jù)管理,可以作為數(shù)據(jù)源給子組件提供數(shù)據(jù)。
          3. 統(tǒng)一的通信管理,實(shí)現(xiàn)子組件之間的通信。

          展示組件就比較簡(jiǎn)單的多,在 React 中組件的設(shè)計(jì)理念是 view = f(data),展示組件只接收外部傳來(lái)的 props,一般內(nèi)部沒(méi)有狀態(tài),只有一個(gè)渲染的作用。

          image_1e5813mbgbmvc623215qo6pf9.png-29.5kB

          適當(dāng)?shù)慕M件粒度

          在項(xiàng)目開發(fā)中,可能你會(huì)看到懶同事一個(gè)幾千行的文件,卻只有一個(gè)組件,render 函數(shù)里面又臭又長(zhǎng),讓人實(shí)在沒(méi)有讀下去的欲望。在寫 React 組件中,我見過(guò)最恐怖的代碼是這樣的:

          function App() {
              let renderHeader,
                  renderBody,
                  renderHTML
              if (xxxxx) {
                  renderHeader = <h1>xxxxx</h1>
              } else {
                  renderHeader = <header>xxxxx</header>
              }
              if (yyyyy) {
                  renderBody = (
                      <div className="main">
                          yyyyy
                      </div>
                  )
              } else {
                  ...
              }
              if (...) {
                  renderHTML = ...
              } else {
                  ...
              }
              return renderHTML
          }

          當(dāng)我看到這個(gè)組件的時(shí)候,我想要搞清楚他最終都渲染了什么。看到 return 的時(shí)候發(fā)現(xiàn)只返回了 renderHTML,而這個(gè) renderHTML 卻是經(jīng)過(guò)一系列的判斷得來(lái)的,相信沒(méi)人愿意去讀這樣的代碼。

          拆分 render

          我們可以將 render 方法進(jìn)行一系列的拆分,創(chuàng)建一系列的子 render 方法,將原來(lái)大的 render 進(jìn)行分割。

          class App extends Component {
           renderHeader() {}
           renderBody() {}
           render() {
            return (
             <>
              {this.renderHeader()}
              {this.renderBody()}
             </>
            )
           }
          }

          當(dāng)然最好的方式還是拆分為更細(xì)粒度的組件,這樣不僅方便測(cè)試,也可以配合 memo/PureComponent/shouldComponentUpdate 做進(jìn)一步性能優(yōu)化。

          const Header = () => {}
          const Body = () => {}
          const App = () => (
           <>
            <Header />
            <Body />
           </>
          )

          復(fù)用相同部分

          對(duì)于可復(fù)用的組件部分,我們要盡量做到復(fù)用。這部分可以是狀態(tài)邏輯,也可以是 HTML 結(jié)構(gòu)。以下面這個(gè)組件為例,這樣寫看上去的確沒(méi)有大問(wèn)題。

          class App extends Component {
              state = {
                  on: props.initial
              }
              toggle = () => {
                  this.setState({
                      on: !this.state.on
                  })
              }
              render() {
                  <>
                      <Button type="primary" onClick={this.toggle}> {this.on ? "Close" : "Open"} Modal </Button>
                      <Modal visible={this.state.on} onOk={this.toggle} onCancel={this.toggle}/>
                  </>
              }
          }

          但如果我們有個(gè) checkbox 的按鈕,它也會(huì)有開關(guān)兩種狀態(tài),完全可以復(fù)用上面的 this.state.onthis.toggle,那該怎么辦呢?

          timg.gif-85.7kB

          就像上一節(jié)講的一樣,我們可以利用 render props 來(lái)實(shí)現(xiàn)狀態(tài)邏輯復(fù)用。

          // 狀態(tài)提取到 Toggle 組件里面
          class Toggle extends Component {
              constructor(props) {
                  this.state = {
                      on: props.initial
                  }
              }
              toggle = () => {
                  this.setState({
                      on: !this.state.on
                  })
              }
              render() {
                  return this.props.children({
                      onthis.state.on,
                      togglethis.toggle
                  })
              }
          }
          // Toggle 結(jié)合 Modal
          function App({
              return (
                  <Toggle initial={false}>
                      {({ on, toggle }) => (
                          <>
                              <Button type="primary" onClick={toggle}> Open Modal </Button>
                              <Modal visible={on} onOk={toggle} onCancel={toggle}/>
                          </>

                      )}
                  </Toggle>
              )
          }
          /
          / Toggle 結(jié)合 CheckBox
          function App() {
              return (
                  <Toggle initial={false}>
                      {({ on, toggle }) => (
                          <CheckBox visible={on} toggle={toggle} /
          >
                      )}
                  </Toggle>
              )
          }

          或者我們可以用上節(jié)講過(guò)的 React Hooks 來(lái)抽離這個(gè)通用狀態(tài)和方法。

          const useToggle = (initialState) => {
              const [state, setState] = useState(initialState);
              const toggle = () => setState(!state);
              return [state, toggle]
          }

          除了這種狀態(tài)邏輯復(fù)用外,還有一種 HTML 結(jié)構(gòu)復(fù)用。比如有兩個(gè)頁(yè)面,他們都有頭部、輪播圖、底部按鈕,大體上的樣式和布局也一致。如果我們對(duì)每個(gè)頁(yè)面都寫一遍,難免會(huì)有一些重復(fù),像這種情況我們就可以利用高階組件來(lái)復(fù)用相同部分的 HTML 結(jié)構(gòu)。

          const PageLayoutHoC = (WrappedComponent) => {
              return class extends Component {
                  render() {
                      const {
                          title,
                          sliderData,
                          onSubmit,
                          submitText
                          ...props
                      } = this.props
                      return (
                          <div className="main">
                              <Header title={title} />
                              <Slider dataList={sliderData} />
                              <WrappedComponent {...props} />
                              <Button onClick={onSubmit}>{submitText}</Button>
                          </div>

                      )
                  }
              }
          }

          組件松耦合

          松耦合一般是和緊耦合相對(duì)立的,兩者的區(qū)別在于:

          1. 組件之間彼此依賴方法和數(shù)據(jù),這種叫做緊耦合。

          2. 組件之間沒(méi)有彼此依賴,一個(gè)組件的改動(dòng)不會(huì)影響到其他組件,這種叫做松耦合。

            很明顯,我們?cè)陂_發(fā)中應(yīng)當(dāng)使用松耦合的方式來(lái)設(shè)計(jì)組件,這樣不僅提供了復(fù)用性,還方便了測(cè)試。

            我們來(lái)看一下簡(jiǎn)單的緊耦合反面例子:

            class App extends Component {  
              state = { count: 0 }
             increment = () => {
              this.setState({
               count: this.state.count + 1
              })
             }
             decrement = () => {
              this.setState({
               count: this.state.count - 1
              })
             }
              render() {
                return <Counter count={this.state.count} parent={this} />
              }
            }

            class Counter extends Component {
              render() {
                return (
                  <div className="counter">
                    <button onClick={this.props.parent.increment}>
                      Increase
                    </button> 
                    <div className="count">{this.props.count}</div>
                    <button onClick={this.props.parent.decrement}>
                      Decrease
                    </button>
                  </div>
                )
              }
            }

            可以看到上面的 Counter 依賴了父組件的兩個(gè)方法,一旦父組件的 incrementdecrement 改了名字呢?那 Counter 組件只能跟著來(lái)修改,破壞了 Counter 的獨(dú)立性,也不好拿去復(fù)用。

            所以正確的方式就是,組件之間的耦合數(shù)據(jù)我們應(yīng)該通過(guò) props 來(lái)傳遞,而非傳遞一個(gè)父組件的引用過(guò)來(lái)。

            class App extends Component {  
              state = { count: 0 }
             increment = () => {
              this.setState({
               count: this.state.count + 1
              })
             }
             decrement = () => {
              this.setState({
               count: this.state.count - 1
              })
             }
              render() {
                return <Counter count={this.state.count} increment={this.increment} decrement={this.decrement}/>
              }
            }

            class Counter extends Component {
              render() {
                return (
                  <div className="counter">
                    <button onClick={this.props.increment}>
                      Increase
                    </button> 
                    <div className="count">{this.props.count}</div>
                    <button onClick={this.props.decrement}>
                      Decrease
                    </button>
                  </div>
                )
              }
            }

          避免通過(guò) ref 來(lái) setState

          對(duì)于需要在組件外面通知組件更新的操作,盡量不要在外面通過(guò) ref 來(lái)調(diào)用組件的 setState,比如下面這種:

          class Counter extends React.Component {
              state = {
                  count0
              }
              render() {
                  return (
                      <div>{this.state.count}</div>
                  );
              }
          }
          class App {
              ref = React.createRef();
              mount() {
                  ReactDOM.render(<Counter ref={this.ref} />document.querySelector('#app'));
              }
              increment() {
                  this.ref.current.setState({
                      countthis.ref.current.state + 1
                  });
              }
          }

          對(duì)于組件 Counter 來(lái)說(shuō),并不知道外面會(huì)直接通過(guò) ref 來(lái)調(diào)用 setState。如果以后發(fā)現(xiàn) count 突然就變化了,也不知道是哪里出了問(wèn)題。

          對(duì)于這種情況我們可以在組件里面注冊(cè)事件,在外面發(fā)送事件來(lái)通知。這樣我們可以明確知道組件監(jiān)聽了外部的事件。

            class Counter extends React.Component {
                 state = {
                     count0
                 }
                 componentDidMount() {
                     event.on('increment'this.increment);
                 }
                 componentWillUnmount() {
                     event.off('increment'this.increment);
                 }
                 increment = () => {
                     this.setState({
                             countthis.state.count + 1
                     });
                 }
                 render() {
                     return (
                         <div>{this.state.count}</div>
                     );
                 }
             }
             class App {
                 ref = React.createRef();
                 mount() {
                     ReactDOM.render(<Counter ref={this.ref} />document.querySelector('#app'));
                 }
                 increment() {
                     event.trigger('increment');
                 }
             }

          如果在函數(shù)組件里面,React 提供了 useImperativeHandle 這個(gè) Hook,配合 forwardRef 可以支持傳遞函數(shù)組件內(nèi)部的方法給外部使用。

          import React, { useState, useImperativeHandle, forwardRef } from 'react';

          const Counter = forwardRef((props, ref) => {
              const [count, setCount] = useState(0);
              useImperativeHandle(
                  ref,
                  () => ({
                      increment() => {
                          setCount(count + 1);
                      }
                  })
              );
          });

          class App {
                 ref = React.createRef();
                 mount() {
                     ReactDOM.render(<Counter ref={this.ref} />document.querySelector('#app'));
                 }
                 increment() {
                     this.ref.current.increment();
                 }
             }

          數(shù)據(jù)解耦

          我們的組件不應(yīng)該依賴于特定格式的數(shù)據(jù),組件中避免出現(xiàn) data.xxx 這種數(shù)據(jù)。你可以通過(guò) render props 的模式將要處理的對(duì)象傳到外面,讓使用者自行操作。舉個(gè)栗子:我設(shè)計(jì)了一個(gè) Tabs 組件,我需要?jiǎng)e人給我傳入這樣的結(jié)構(gòu):

          [
              {
                  key'Tab1',
                  content'這是 Tab 1',
                  title'Tab1'
              },
              {},
              {}
          ]

          這個(gè) key 是我們用來(lái)關(guān)聯(lián)所有 Tab 和當(dāng)前選中的 Tab 關(guān)系的。比如我選中了 Tab1,當(dāng)前的 Tab1 會(huì)有高亮顯示,就通過(guò) key 來(lái)關(guān)聯(lián)。而我們的組件可能會(huì)這樣設(shè)計(jì):

          <Tabs data={data} currentTab={'Tab1'} />

          這樣的設(shè)計(jì)不夠靈活,一個(gè)是耦合了數(shù)據(jù)的結(jié)構(gòu),大多數(shù)時(shí)候,接口不會(huì)返回上圖中的 key 這種字段,title 也很可能沒(méi)有,這就需要我們自己做一下數(shù)據(jù)格式化。另一個(gè)是封裝了 DOM 結(jié)構(gòu),如果我們想定制化傳入的 Tab 結(jié)構(gòu)就會(huì)變得非常困難。我們不妨轉(zhuǎn)換一下思路,當(dāng)設(shè)計(jì)一個(gè)通用組件的時(shí)候,一定要只有一個(gè)組件嗎?一定要把數(shù)據(jù)傳給組件嗎?那么來(lái)一起看看業(yè)界知名的組件庫(kù) Ant Design 是如何設(shè)計(jì) Tabs 組件的。

          <Tabs defaultActiveKey="1" onChange={callback}>
              <TabPane tab="Tab 1" key="1">
                  Content of Tab Pane 1
              </TabPane>

              <TabPane tab="Tab 2" key="2">
                  Content of Tab Pane 2
              </TabPane>

              <TabPane tab="Tab 3" key="3">
                  Content of Tab Pane 3
              </TabPane>

          </Tabs>

          Ant Design 將數(shù)據(jù)和結(jié)構(gòu)進(jìn)行了解耦,我們不再傳列表數(shù)據(jù)給 Tabs 組件,而是自行在外部渲染了所有的 TabPane,再將其作為 Children 傳給 Tabs,這樣的好處就是組件的結(jié)構(gòu)更加靈活,TabPane 里面隨便傳什么結(jié)構(gòu)都可以。

          結(jié)構(gòu)自由

          一個(gè)好的組件,結(jié)構(gòu)應(yīng)當(dāng)是靈活自由的,不應(yīng)該對(duì)其內(nèi)部結(jié)構(gòu)做過(guò)度封裝。我們上面講的 Tabs 組件其實(shí)就是結(jié)構(gòu)自由的一種代表。

          考慮到這樣一種業(yè)務(wù)場(chǎng)景,我們頁(yè)面上有多個(gè)輸入框,但這些輸入框前面的 Icon 都是不一樣的,代表著不同的含義。我相信肯定不會(huì)有人會(huì)對(duì)每個(gè) Icon 都實(shí)現(xiàn)一個(gè) Input 組件。

          image_1e5jq2o13qj0qmele81aahh6l13.png-10.7kB

          你可能會(huì)想到我們可以把圖片的地址當(dāng)做 props 傳給組件,這樣不就行了嗎?但萬(wàn)一前面不是 Icon 呢?而是一個(gè)文字、一個(gè)符號(hào)呢?

          那我們是不是可以把元素當(dāng)做 props 傳給組件呢?組件來(lái)負(fù)責(zé)渲染,但渲染后長(zhǎng)什么樣還是使用者來(lái)控制的。這就是 Ant Design 的實(shí)現(xiàn)思路。

          code.png-111.5kB

          在前面數(shù)據(jù)解耦中我們就講過(guò)了類似的思路,實(shí)際上數(shù)據(jù)解耦和結(jié)構(gòu)自由是相輔相成的。在設(shè)計(jì)一個(gè)組件的時(shí)候,很多人往往會(huì)陷入一種怪圈,那就是我該怎么才能封裝更多功能?怎么才能兼容不同的渲染?

          這時(shí)候我們就不妨換一種思路,如果將渲染交給使用者來(lái)控制呢?渲染成什么樣都由用戶來(lái)決定,這樣的組件結(jié)構(gòu)是非常靈活自由的。

          當(dāng)然,如果你把什么都交給用戶來(lái)渲染,這個(gè)組件的使用復(fù)雜度就大大提高了,所以我們也應(yīng)當(dāng)提供一些默認(rèn)的渲染,即使用戶什么都不傳也可以渲染默認(rèn)的結(jié)構(gòu)。

          總結(jié)

          組件設(shè)計(jì)是一項(xiàng)重要的工作,好的組件我們直接拿來(lái)復(fù)用可以大大提高效率,不好的組件只會(huì)增加我們的復(fù)雜度。

          在組件設(shè)計(jì)的學(xué)習(xí)中,你需要多探索、實(shí)踐,多去參考社區(qū)知名的組件庫(kù),比如 Ant Design、Element UI、iview 等等,去思考他們?yōu)槭裁磿?huì)這樣設(shè)計(jì),有沒(méi)有更好的設(shè)計(jì)?如果是自己來(lái)設(shè)計(jì)會(huì)怎么樣?

          Node 社群



          我組建了一個(gè)氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對(duì)Node.js學(xué)習(xí)感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。



          如果你覺得這篇內(nèi)容對(duì)你有幫助,我想請(qǐng)你幫我2個(gè)小忙:

          1. 點(diǎn)個(gè)「在看」,讓更多人也能看到這篇文章
          2. 訂閱官方博客 www.inode.club 讓我們一起成長(zhǎng)

          點(diǎn)贊和在看就是最大的支持??

          瀏覽 34
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  狠狠干视频在线 | 在线无码视频蜜桃 | 小泽玛利亚大战黑人喷水 | 2024国产无码 | 我要毛片毛片毛片毛片毛 |