在第二篇文章 《新型前端開發方式》 中有說到 React 有很爽的一點就是給我們一種創造 HTML 標簽的能力,那么今天這篇文章就詳細講解下 React 是如何提供這種能力的,作為前端開發者如何來運用這種能力。
在第三篇文章 《JavaScript代碼里寫HTML一樣可以很優雅》 中介紹了 JavaScript 的擴展語法 JSX,相信大家已經知道了,所謂的創造新的 HTML 的能力,其實就是以極其類似 HTML 的 JSX 語法來使用基于 React 編寫的視圖層組件。所以說,要完成今天的任務,我們只需要搞清楚一個問題即可:如何基于 React 編寫視圖層組件。
內容摘要
- 定義組件兩種方式:類繼承組件、函數式組件。
- 類繼承組件有更豐富的特性,函數式組件書寫更簡潔,執行效率更高。
- 組件名稱首字母要大寫。
- 屬性是一個組件的外部輸入。
- 屬性值可以通過
{}
設置任意的 JS 表達式。 - 屬性是只讀的。
- 屬性可以設置默認值。
- 屬性可以設置類型,開發階段 React 會對屬性進行類型檢查。
- 為組件所有屬性設置類型檢查是個好習慣,有助于協作開發。
通過內容摘要可以讓你快速了解本文內容是否對你有用,從而決定是否繼續閱讀,節省你的時間也是一件很有意義的事情。
定義組件的幾種姿勢
下面介紹一下在 React 中定義組件的幾種方式。
1. 類繼承
有過 Java 等面向對象開發經驗的同學一定很容易接受這種方式。ES6 為 JavaScript 增加了類和類繼承的特性。子類會繼承父類的“基因”(成員方法、屬性),如果父類是一個組件,那子類自然而然也是一個組件。
React 提供了 Component
和 PureComponent
兩個父類,他們之間有一點點區別,我們在之后的文章中會詳細介紹,現在你可以將他們同等看待,暫且無須理會。
通過繼承自 React 提供的組件基類,我們可以這樣來創建一個組件:
import React, {Component} from 'react';class HelloMessage extends Component {render() {return <div>Hello world.</div>;}
}
通過類繼承的方式創建一個組件,就是這么簡單,只要繼承 Component
基類并實現 render
方法即可。然后就可以把 HelloMessage
當成一個新的“HTML 標簽”來用了,如下你可以把它渲染到頁面上:
ReactDOM.render(<HelloMessage />, document.querySelector('#root'));
你也可以用它來裝配其它組件,如:
import React, {Component} from 'react';class HelloMessageList extends React.Component {render() {return (<div><HelloMessage /><HelloMessage /><HelloMessage /></div>)}
}
當然,例子沒有任何實際意義,只是為了演示組件的定義及其用法。
演示代碼:https://codepen.io/Sarike/pen...
2. 函數式組件
顧名思義,函數式組件,就是以函數的形式來定義一個組件,如下所示:
import React from 'react';function HelloMessage() {return <div>Hello world.</div>;
}// 或者:const HelloMessage = () => <div>Hello world.</div>;
實際上就是只實現了類繼承方式中的 render
方法。
示例代碼:https://codepen.io/Sarike/pen...
類繼承 vs 函數式組件
這兩種定義組件的方式,在實際的開發中都經常會被用到,對大部分人來說類繼承的方式用得頻率會更高一些。
類繼承的方式,相較于函數式組件,雖然寫起來略繁瑣,但是它擁有更多的特性:
- 內部狀態:
state
- 生命周期函數
函數式組件雖然沒有 state 和生命周期函數等特性,但是它有更簡潔的書寫方式,另外還有更好的性能,不用處理一些復雜的特性,執行效率當然高了。
現在你可以無需關心 state
和生命周期函數的具體作用,下一篇文章我會詳細講解,等你看完下一篇文章之后,至于選擇哪種方式的問題就很好解決了。在開發一個組件的時候,我是這樣來做的:當我一開始就知道這個組件會用到 state
或者生命周期函數時,毫無疑問直接使用類繼承的方式;如果一開始用不到這些特性也不確定未來會不會用到,那我就先用函數式組件,如果隨著業務的演進,組件需要應用這些特性的時候,我會再把它重構成類繼承的方式。這個重構非常簡單,只需要將原來的函數變成組件類的 render
方法即可。
另外,還有一點需要注意,不管那種方式,組件的名稱首字母必須為大寫。嚴格來說,是 JSX 要求用戶自定義的組件名首字母必須為大寫,如果是小寫字母開頭,那么 React 會將其當成內置的組件直接將其渲染成一個 html 標簽,從而不會正確渲染用戶自定義的組件。
如果你非要將組件名稱以小寫字母開頭,那你在以 JSX 語法使用之前也必須將其賦值為一個大寫字母開頭的變量,如下所示:
function helloMessage() {return <div>Hello world.</div>
}const HelloMessage = helloMessage;ReactDOM.render(<HelloMessage />, mountNode);
但這有事何必呢,純粹是沒事兒找事兒,大家在實際項目開發時,直接將組件名以大寫字母開頭即可。
屬性
上面說完了在 React 中兩種定義組件的方式。在上面的例子中,我們定義的組件都是靜態的,然而在實際的開發中,視圖層組件往往會進行頻繁更新,或者需要從后端 API 獲取動態數據在組件中展示。這就需要組件擁有接收外部輸入的能力。
屬性是組件的輸入
在第二篇文章 《新型前端開發方式》 中有說到 “視圖是數據的映射”,那么其中說的數據指的就是屬性。
如果把組件理解為一個函數,那么屬性就是這個函數的參數,函數的返回值就是呈現到頁面上的視圖。而且通過上面部分的學習,在 React 中組件確實可以以函數的形式來定義,而且函數的參數就是一個包含當前組件接收到的所有屬性的對象。
如下所示帶有屬性 name
的組件定義:
import React, {Component} from 'react';class HelloMessage extends Component {render() {return <div>Hello {this.props.name}.</div>;}
}
函數式:
import React from 'react';function HelloMessage(props) {return <div>Hello {props.name}.</div>;
}// 或者:const HelloMessage = props => <div>Hello {props.name}.</div>;
屬性的傳遞也跟 HTML 一樣(在本文的最后一部分會有各種類型屬性的詳細介紹),如下所示:
import React, {Component} from 'react';
import ReactDOM from 'react-dom';class HelloMessageList extends Component {render() {return (<div><HelloMessage name="Lucy" /><HelloMessage name="Tom" /><HelloMessage name="Jack" /></div>)}
}ReactDOM.render(<HelloMessageList />, document.querySelector('#root'));
這樣頁面上會展示出:
Hello Lucy.
Hello Tom.
Hello Jack.
示例代碼:https://codepen.io/Sarike/pen...
屬性必須為只讀的
屬性必須為只讀的,這一點非常重要,請嚴格遵守。對應到上面說到的,如果把組件理解為一個函數,那么這個函數必須為一個純函數(Pure function),在純函數中不能修改其參數,確定的輸入必須有確定的輸出。
雖然有些時候,你修改了組件的屬性,貌似也能正常工作。沒錯,因為 JavaScript 語言特性的原因,沒人能阻止你這么做。但是請先相信我,嚴格遵守這條規則不僅能讓你少踩很多坑,而且能讓你的應用穩定性更強、維護性更強。如果你直接修改組件的屬性,React 并不會感知到此修改,從而不會重新渲染組件,就導致了當前組件的視圖展示與數據不一致,但這個被修改的屬性會隨著下一次組件的渲染被生效到視圖上,而且這次渲染的時機是不確定的,不難想象,如果一個規模較大的項目里充滿了這種不確定性是多么痛苦的一件事情。總之,如果你隨意修改組件的屬性,會很容易讓你的應用充滿許多難以排查的 BUG。
默認屬性
通常情況下,我們需要為組件的屬性設為默認值。就像 HTML 標簽的屬性也有默認值一樣,例如 form 標簽的 method 屬性默認值是 GET,input 標簽的 type 屬性默認值是 text 一樣。
還是上面 HelloMessage
組件,如果需求是當不傳入 name 屬性時,默認展示 Hello World.
,也就是說 name 屬性的默認值是 World。
一種很容易想到的做法:
<div>Hello {this.props.name || 'World'}.</div>
這樣確實可以解決當前這個需求,但是屬性可能還會是個 Object,也可能是個函數,你當然可以先判斷下這個屬性是否為 undefined
然后決定是否使用默認值,但是這樣會讓代碼顯得很不優雅,而且也會增加很多繁瑣的判斷邏輯。
因此,React 提供了相應的機制可以設置組件屬性的默認值,如下所示,你需要通過組件的靜態字段 defaultProps
來設置組件屬性的默認值。如下所示:
import React, {Component} from 'react';class HelloMessage extends Component {render() {return <div>Hello {this.props.name}.</div>;}
}
HelloMessage.defaultProps = {name: 'World'
}
這樣就可以了,<HelloMessage />
這樣不為組件設置任何屬性,那么它就會在頁面上展示Hello World.
。
示例代碼:https://codepen.io/Sarike/pen...
屬性的類型及校驗
在開發較復雜的前端應用時,我們經常會遇到許多因為類型檢查導致的問題,例如上面的 HelloMessage
組件,我期望其 name
屬性只能是字符串類型的,你要是給我一個 Object,我是沒法正確展示的。為了在開發過程中盡快的發現這類問題,React 為組件添加了類型檢查的機制,你需要給組件設置靜態字段 propTypes
來設置組件各個屬性的類型檢查器。
import React, {Component} from 'react';
import PropTypes from 'prop-types';class HelloMessage extends Component {render() {return <div>Hello {this.props.name}.</div>;}
}
HelloMessage.defaultProps = {name: 'World'
}
HelloMessage.propTypes = {name: PropTypes.string
}
這樣在開發過程中 React 就能校驗組件接收到的屬性值是否符合指定的類型,如果校驗不通過,將會拋出警告。React 只會在開發模式下進行屬性類型檢查,當代碼進行生產發布后,為了減少額外的性能開銷,類型檢查將會被略過。
其實,為每一個組件編寫完善的屬性類型是一個非常好的習慣,這不僅能及時發現問題,更重要的是配合幾句簡單額注釋,這將成為該組件一份非常好的文檔,一個完善的組件應該具有良好的封裝性和易復用性,在一個協作開發的項目中,其他開發者需要引用你開發的組件時,只需要看一下組件的屬性列表,大致就可以了解如何來使用這個組件,省去了很多不必要的溝通。
下面是 React 提供的可用的數據類型檢查器。
PropTypes.array
PropTypes.bool
PropTypes.func
PropTypes.number
PropTypes.object
PropTypes.string
PropTypes.symbol
-
PropTypes.element
元素,其實就是 JSX 表達式,上一篇文章有說過 JSX 是React.createElement
的語法糖,一個 JSX 表達式實際上會生成一個 JS 對象,在 React 中稱之為元素(Element)。 -
PropTypes.node
所有可以被渲染的數據類型,包括:數值, 字符串, 元素或者這些類型的數組。 -
PropTypes.instanceOf(Message)
某個類的實例 -
PropTypes.oneOf(['News', 'Photos'])
枚舉,屬性值必須為其中的某一個值。 -
PropTypes.oneOfType([PropTypes.string, PropTypes.number])
類型枚舉,屬性必須為其中某一個類型。 -
PropTypes.arrayOf(PropTypes.number)
屬性為一個數組,且數組中的元素必須符合指定類型。 -
PropTypes.objectOf(PropTypes.number)
屬性為一個對象,且對象中的各個字段的值必須符合指定類型。 -
PropTypes.any
任何類型
如果你想指定某些屬性為必需屬性
,你可以鏈式調動其 isRequired
來標識某個屬性對于當前組件來說是必需的。如果在使用組件時未指定則會拋出警告提醒。
另外你還可以通過一個函數自定義屬性驗證器,如果驗證不通過你需要返回一個 Error 實例,如下所示:
function(props, propName, componentName) {if (!/matchme/.test(props[propName])) {return new Error('Invalid prop `' + propName + '` supplied to' +' `' + componentName + '`. Validation failed.');}
}
設置組件的屬性值
上面咱們了解到組件的屬性有很多種類型,下面說一下各種類型的屬性是如何傳遞給組件的。其實很簡單,屬性的值可以用一對大括號 { }
來包圍,其中可以指定任意的 JavaScript 表達式。如下所示:
return (<Username="Tom" // 字符串age={18} // 數值isActivated={true} // 布爾值interests={['basketball', 'music']} // 數組address={{ city: 'Beijing', road: 'BeiWuHuan' }} // 對象/>
)
展開操作符
你也可以用展開操作符 ...
將一個對象的所有字段展開,依次作為屬性傳遞給組件,上面的代碼等價于:
const userInfo = {name: 'Tom',age: 18,isActivated: true,interests: ['basketball', 'music'],address: { city: 'Beijing', road: 'BeiWuHuan' }
}
return <User {...userInfo} />
值為 true
的屬性的簡寫
如果是屬性類型為布爾值,且當前屬性值為 true
可以只寫屬性名,如下所示:
<inputdisabled // 禁用該輸入框type="text"
/>
children
屬性
用戶自定義的組件內可以通過 this.props.children
來獲取一個特殊的屬性。該屬性與其它屬性的區別就是傳遞方式不同。
children
屬性的值是指一對閉合的 JSX 標簽中間的內容,如下所示:
<UserList><User name="Tom" /><User name="Lucy" />
</UserList>
那么在 UserList
內部可以通過 this.props.children
來獲取下面這個 JSX 片段:
<User name="Tom" />
<User name="Lucy" />
該示例中,獲取到的實際上是一個包含兩個 User
元素對象的數組。
總結
本文主要介紹了在 React 中組件的定義方式,以及幾個關鍵的注意事項。另外介紹了組件屬性的作用、屬性默認值、屬性類型校驗以及如何為組件傳遞屬性。
希望內容對大家有用,如有任何問題和建議可以給我留言,謝謝。