前端組件化思想與實踐

組件化思想
- 什么是組件化?
- 簡單的說組件就是:將一段UI樣式和其對應的功能作為獨立的整體去看待,無論這個整體放在哪里去使用,它都具有一樣的功能和樣式,從而實現復用,這種整體化的思想就是組件化。
- 為什么要組件化?
- 增加復用性,靈活性,提高系統設計,從而提高開發效率。
蓋房子
要想理解這些概念是什么以及如何使用它們,我們先來理解一個小示例。就先蓋個房子

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

將房子拆分成多個組件,分別完成各個組件后,通過組合便成蓋好了房子
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 }
兩種寫法等價,主要區別是:
- 函數組件的寫法要比類組件簡潔,
- 類組件比函數組件功能更加強大。類組件可以維護自身的狀態變量,即組件的state,類組件還有不同的生命周期方法,可以讓開發者能夠在組件的不同階段(掛載、更新、卸載),對組件做更多的控制。
類組件有這么多優點,是不是我們在開發中應該首選使用類組件呢?
- 其實不然。函數組件更加專注和單一,承擔的職責也更加清晰,它只是一個返回React 元素的函數,只關注對應UI的展現。函數組件接收外部傳入的props,返回對應UI的DOM描述,僅此而已。
- 當然,如上面例子所示,使用只包含一個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();
