前端組件化思想與實踐


 前端組件化思想與實踐

image.png

 

組件化思想

 

  • 什么是組件化?
    • 簡單的說組件就是:將一段UI樣式和其對應的功能作為獨立的整體去看待,無論這個整體放在哪里去使用,它都具有一樣的功能和樣式,從而實現復用,這種整體化的思想就是組件化
  • 為什么要組件化?
    • 增加復用性靈活性,提高系統設計,從而提高開發效率。

 

蓋房子

 

要想理解這些概念是什么以及如何使用它們,我們先來理解一個小示例。就先蓋個房子

image.png

 

組件化

 

將 UI 分解成多個組件。例如,我們可以這樣來拆分房子:

 

image.png

 

將房子拆分成多個組件,分別完成各個組件后,通過組合便成蓋好了房子

 

1 <div>
2   <Roof />     // 房頂
3   <Wall />     //4   <Window />   //5   <Door />     //6 </div>

  

 

組件化實踐

 

理解了組件化思想,接下來我們進一步學習組件化實踐

 

組件分類

 

React 組件有很多種分類方式,常見的分類方式有:

  • 函數組件和類組件
  • 無狀態組件和有狀態組件
  • 展示型組件和容器型組件
  • 受控組件和非受控組件
  • 高階組件

 

真正弄明白這幾種分類方式,對於頁面的組件划分、組件之間的解耦是大有裨益的。

 

函數組件和類組件

 

函數組件(Functional Component )和類組件(Class Component),划分依據是根據組件的定義方式。函數組件使用函數定義組件,類組件使用ES6 class定義組件。下面是函數組件和類組件的簡單示例:

 

 1 // 函數組件(Functional Component )
 2 function Welcome(props) {
 3   return <h1>Hello, {props.name}</h1>;
 4 }
 5 
 6 // 類組件(Class Component)
 7 class Welcome extends React.Component {
 8   render() {
 9     return <h1>Hello, {this.props.name}</h1>;
10   }
11 }

 

 

兩種寫法等價,主要區別是:

 

  1. 函數組件的寫法要比類組件簡潔,
  2. 類組件比函數組件功能更加強大。類組件可以維護自身的狀態變量,即組件的state,類組件還有不同的生命周期方法,可以讓開發者能夠在組件的不同階段(掛載、更新、卸載),對組件做更多的控制。

 

類組件有這么多優點,是不是我們在開發中應該首選使用類組件呢?

 

  1. 其實不然。函數組件更加專注和單一,承擔的職責也更加清晰,它只是一個返回React 元素的函數,只關注對應UI的展現。函數組件接收外部傳入的props,返回對應UI的DOM描述,僅此而已。
  2. 當然,如上面例子所示,使用只包含一個render方法的類組件,可以實現和函數組件相同的效果。但函數組件的使用可以從思想上迫使你在設計組件時多做思考,更加關注邏輯和顯示的分離,設計出更加合理的頁面上組件樹的結構。

 

實際操作上,當一個組件不需要管理自身狀態時,可以把它設計成函數組件,當你有足夠的理由發現它需要“升級”為類組件時,再把它改造為類組件。因為函數組件“升級”為類組件是有一定成本的,這樣就會要求你做這個改造前更認真地思考其合理性,而不是僅僅為了一時的方便就使用類組件。(畫外:新特性hooks很強大可以彌補函數組件的不足)

 

無狀態組件和有狀態組件

 

無狀態組件(Stateless Component )和有狀態組件(Stateful Component),划分依據是根據組件內部是否維護state。無狀態組件內部不使用state,只根據外部組件傳入的props返回待渲染的React 元素。有狀態組件內部使用state,維護自身狀態的變化,有狀態組件根據外部組件傳入的props和自身的state,共同決定最終返回的React 元素。

很容易知道,函數組件(不使用hooks的情況下)一定是無狀態組件,類組件則既可以充當無狀態組件,也可以充當有狀態組件。但如上文所述,當一個組件不需要管理自身狀態時,也就是無狀態組件,應該優先設計為函數組件。

 

展示型組件和容器型組件

 

展示型組件(Presentational Component)和容器型組件(Container Component),划分依據是根據組件的職責。

 

展示型組件的職責是:

組件UI長成什么樣。展示型組件不關心組件使用的數據是如何獲取的,以及組件數據應該如何修改,它只需要知道有了這些數據后,組件UI是什么樣子的即可。外部組件通過props傳遞給展示型組件所需的數據和修改這些數據的回調函數,展示型組件只是它們的使用者。展示型組件一般是無狀態組件,不需要state,因為展示型組件不需要管理數據,但當展示型組件需要管理自身的UI狀態時,例如控制組件內部彈框的顯示與隱藏,是可以使用state的,這時的state屬於UI state。既然大部分情況下展示型組件不需要state,應該優先考慮使用函數組件實現展示型組件。

 

容器型組件的職責是:

組件數據如何工作。容器型組件需要知道如何獲取子組件所需數據,以及這些數據的處理邏輯,並把數據和邏輯通過props提供給子組件使用。容器型組件一般是有狀態組件,因為它們需要管理頁面所需數據。

 

例如,下面的例子中,UserListContainer是一個容器型組件,它獲取用戶列表數據,然后把用戶列表數據傳遞給展示型組件UserList,由UserList負責UI的展現。

 1 class UserListContainer extends React.Component{
 2   constructor(props){
 3     super(props);
 4     this.state = {
 5       users: []
 6     }
 7   }
 8   
 9   componentDidMount() {
10     var that = this;
11     fetch('/path/to/user-api').then(function(response) {
12       response.then(function(data) {
13         that.setState({users: data})
14       });
15     });
16   }
17 
18   render() {
19     return (
20       <UserList users={this.state.users} />
21     )
22   }
23 }
24 
25 function UserList(props) {
26   return (
27     <div>
28       <ul className="user-list">
29         {props.users.map((user) => {
30           return (
31             <li key={user.id}>
32               <span>{user.name}</span>
33             </li>
34           );
35         })}
36       </ul>
37     </div>
38   )  
39 }

 

 

另外展示型組件和容器型組件是可以互相嵌套的,展示型組件的子組件既可以包含展示型組件,也可以包含容器型組件,容器型組件也是如此。例如,當一個容器型組件承擔的數據管理工作過於復雜時,可以在它的子組件中定義新的容器型組件,由新組件分擔數據的管理。展示型組件和容器型組件的划分完全取決於組件所做的事情。

 

 

 

組件通信

 

深入了解組件分類后我們開始學習組件間的通信方法

 

  • 父組件向子組件傳值
  • 父組件調用子組件的方法
  • 子組件傳值調用父組件的方法
  • 公共store調用組件內的方法

 

父組件向子組件傳值

 

父組件傳值

 

class App extends Component {

  public render() {
    return (
      <div className="App">
        <ChildComponent
          num={1}
        />
      </div>
    );
  }
  
}

export default App;

 

 

子組件通過props接收父組件傳遞的值

 

interface IChildComponent {
  num:number
}

class ChildComponent extends Component<IChildComponent> {

  public render() {
    const {num} = this.props;
    return (
      <div className="App">
        {num}
      </div>
    );
  }
  
}

export default ChildComponent;

 

 

 

父組件調用子組件的方法

 

父組件通過綁定子組件 this 指向到child上,獲取調用子組件方法的能力

 

class App extends Component {
  
  public child: any = {};

  public handleChild = () => {
    this.child.handleSelect()
  }
  
  public render() {
    return (
      <div className="App">
        <ChildComponent
          onRef={e => this.child = e}
          num={1}
        />
            
        <button onClick={this.handleChild.bind(this)}>調用子組件事件</button>
      </div>
    );
  }
  
}

export default App;

 

 

子組件掛載上this

 

interface IChildComponent {
  num:number;
  onRef?:any;
}

class ChildComponent extends Component<IChildComponent> {
  
  public constructor(props) {
    super(props);
    if (this.props.hasOwnProperty('onRef')) {
      // 存在則執行
      this.props.onRef(this);
    }
  }
  
  public handleSelect = () => {
    console.log('handleSelect');
  }
  
  public render() {
    const {num} = this.props;
    return (
      <div className="App">
        {num}
      </div>
    );
  }

}

export default ChildComponent;

 

 

 

子組件傳值調用父組件的方法

 

父組件

 

class App extends Component {
  
  public child: any = {};

  public handleChild = () => {
    this.child.handleSelect()
  }
  
  public handleShow = (data) => {
    console.log(data)
  }
  
  public render() {
    return (
      <div className="App">
        <ChildComponent 
          onShow={this.handleShow.bind(this)}
          onRef={e => this.child = e}
          num={1}
        />
            
        <button onClick={this.handleChild.bind(this)}>調用子組件事件</button>
      </div>
    );
  }
  
}

export default App;

 

 

子組件通過調用props的函數並傳遞參數,實現子組件傳值調用父組件方法

 

interface IChildComponent {
  num:number;
  onRef?:(e)=>{};
  onShow?:(data)=>{};
}

class ChildComponent extends Component<IChildComponent> {
  
  public constructor(props) {
    super(props);
    if (this.props.hasOwnProperty('onRef')) {
      // 存在則執行
      this.props.onRef(this);
    }
  }

  public handleSelect = (data) => {
    console.log('handleSelect');
  }
  
  public handleShow = (data) => {
    this.props.onShow(data);
  }
  
  public render() {
    const {num} = this.props;
    return (
      <div className="App">
        {num}
        <button onClick={this.handleShow.bind(this,'hello')}>調用父組件onShow事件</button> 
      </div>
    );
  }

}

export default ChildComponent;

 

 

 

公共store調用組件內的方法

 

父組件

 

export const handleOnSearch = async() => {
  // @ts-ignore
  await App.handleOnSearch()
};

class App extends Component {
  
  public child: any = {};

  public constructor(props) {
    super(props);
    // @ts-ignore
    App.handleOnSearch = this.handleOnSearch.bind(this)
  }

  public handleOnSearch = async() => {
    await this.child.handleSelect();
  };  

  public handleChild = () => {
    this.child.handleSelect()
  }
  
  public handleShow = (data) => {
    console.log(data)
  }
  
  public render() {
    return (
      <div className="App">
        <ChildComponent 
          onShow={this.handleShow.bind(this)}
          onRef={e => this.child = e}
          num={1}
        />
            
        <button onClick={this.handleChild.bind(this)}>調用子組件事件</button>
      </div>
    );
  }
  
}

export default App;

 

 

store調用組件內的handleOnSearch方法

 

import { handleOnSearch } from '@/pages/Publish/Demo';
import { action, observable } from 'mobx';

class DemoStore {
  
  @action.bound
  public async handleDemo(){
    await handleOnSearch()
  }
  
}

export default new DemoStore();

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM