上週,基於 React 的 React Native 釋出,結果一天之內,就獲得了 5000 顆星,受矚目程度可見一斑。
React 起源於 Facebook 的內部專案,因為該公司對市場上所有 JavaScript MVC 框架,都不滿意,就決定自己寫一套,用來架設 Instagram 的網站。做出來以後,發現這套東西很好用,就在2013年5月開源了。
由於 React 的設計思想極其獨特,屬於革命性創新,效能出眾,程式碼邏輯卻非常簡單。所以,越來越多的人開始關注和使用,認為它可能是將來 Web 開發的主流工具。
這個專案本身也越滾越大,從最早的UI引擎變成了一整套前後端通吃的 Web App 解決方案。衍生的 React Native 專案,目標更是宏偉,希望用寫 Web App 的方式去寫 Native App。如果能夠實現,整個網際網路行業都會被顛覆,因為同一組人只需要寫一次 UI ,就能同時執行在伺服器、瀏覽器和手機(參見《也許,DOM 不是答案》)。
既然 React 這麼熱門,看上去充滿希望,當然應該好好學一下。從技術角度,可以滿足好奇心,提高技術水平;從職業角度,有利於求職和晉升,有利於參與潛力大的專案。但是,好的 React 教程卻不容易找到,這一方面因為這項技術太新,剛剛開始走紅,大家都沒有經驗,還在摸索之中;另一方面因為 React 本身還在不斷變動,API 一直在調整,至今沒釋出1.0版。
我學習 React 時,就很苦惱。有的教程討論一些細節問題,對入門沒幫助;有的教程寫得不錯,但比較短,無助於看清全貌。我斷斷續續學了幾個月,看過二十幾篇教程,在這個過程中,將對自己有幫助的 Demo 都收集下來,做成了一個庫 React Demos 。
下面,我就根據這個庫,寫一篇全面又易懂的 React 入門教程。你只需要跟著每一個 Demo 做一遍,就能初步掌握 React 。當然,前提是你必須擁有基本 JavaScript 和 DOM 知識,但是你讀完就會發現,React 所要求的預備知識真的很少。
零、安裝
React 的安裝包,可以到官網下載。不過,React Demos
已經自帶 React 原始碼,不用另外安裝,只需把這個庫複製到你的硬碟就行了。
$ git clone [email protected]:ruanyf/react-demos.git
如果你沒安裝 git, 那就直接下載 zip 壓縮包。
下面要講解的12個例子在各個 Demo
子目錄,每個目錄都有一個 index.html
檔案,在瀏覽器開啟這個檔案(大多數情況下雙擊即可),就能立刻看到效果。
需要說明的是,React 可以在瀏覽器執行,也可以在伺服器執行,但是本教程只涉及瀏覽器。一方面是為了儘量保持簡單,另一方面 React 的語法是一致的,伺服器的用法與瀏覽器差別不大。Demo13
是伺服器首屏渲染的例子,有興趣的朋友可以自己去看原始碼。
一、HTML 模板
使用 React 的網頁原始碼,結構大致如下。
<!DOCTYPE html> <html> <head> <script src="../build/react.js"></script> <script src="../build/react-dom.js"></script> <script src="../build/browser.min.js"></script> </head> <body> <div id="example"></div> <script type="text/babel"> // ** Our code goes here! ** </script> </body> </html>
上面程式碼有兩個地方需要注意。首先,最後一個 <script>
標籤的 type
屬性為 text/babel
。這是因為 React 獨有的 JSX 語法,跟 JavaScript 不相容。凡是使用 JSX 的地方,都要加上 type="text/babel"
。
其次,上面程式碼一共用了三個庫: react.js
、react-dom.js
和 Browser.js
,它們必須首先載入。其中,react.js
是 React 的核心庫,react-dom.js
是提供與 DOM 相關的功能,Browser.js
的作用是將 JSX 語法轉為 JavaScript 語法,這一步很消耗時間,實際上線的時候,應該將它放到伺服器完成。
$ babel src --out-dir build
上面命令可以將 src
子目錄的 js
檔案進行語法轉換,轉碼後的檔案全部放在 build
子目錄。
二、ReactDOM.render()
ReactDOM.render 是 React 的最基本方法,用於將模板轉為 HTML 語言,並插入指定的 DOM 節點。
ReactDOM.render( <h1>Hello, world!</h1>, document.getElementById('example') );
上面程式碼將一個 h1
標題,插入 example
節點(檢視 demo01
),執行結果如下。
三、JSX 語法
上一節的程式碼, HTML 語言直接寫在 JavaScript 語言之中,不加任何引號,這就是 JSX 的語法,它允許 HTML 與 JavaScript 的混寫(檢視 Demo02
)。
var names = ['Alice', 'Emily', 'Kate']; ReactDOM.render( <div> { names.map(function (name) { return <div>Hello, {name}!</div> }) } </div>, document.getElementById('example') );
上面程式碼體現了 JSX 的基本語法規則:遇到 HTML 標籤(以 <
開頭),就用 HTML 規則解析;遇到程式碼塊(以 {
開頭),就用 JavaScript 規則解析。上面程式碼的執行結果如下。
JSX 允許直接在模板插入 JavaScript 變數。如果這個變數是一個陣列,則會展開這個陣列的所有成員(檢視 demo03
)。
var arr = [ <h1>Hello world!</h1>, <h2>React is awesome</h2>, ]; ReactDOM.render( <div>{arr}</div>, document.getElementById('example') );
上面程式碼的arr
變數是一個陣列,結果 JSX 會把它的所有成員,新增到模板,執行結果如下。
四、元件
React 允許將程式碼封裝成元件(component),然後像插入普通 HTML 標籤一樣,在網頁中插入這個元件。React.createClass 方法就用於生成一個元件類(檢視 demo04
)。
var HelloMessage = React.createClass({ render: function() { return <h1>Hello {this.props.name}</h1>; } }); ReactDOM.render( <HelloMessage name="John" />, document.getElementById('example') );
上面程式碼中,變數 HelloMessage
就是一個元件類。模板插入 <HelloMessage />
時,會自動生成 HelloMessage
的一個例項(下文的"元件"都指元件類的例項)。所有元件類都必須有自己的 render
方法,用於輸出元件。
注意,元件類的第一個字母必須大寫,否則會報錯,比如HelloMessage
不能寫成helloMessage
。另外,元件類只能包含一個頂層標籤,否則也會報錯。
var HelloMessage = React.createClass({ render: function() { return <h1> Hello {this.props.name} </h1><p> some text </p>; } });
上面程式碼會報錯,因為HelloMessage
元件包含了兩個頂層標籤:h1
和p
。
元件的用法與原生的 HTML 標籤完全一致,可以任意加入屬性,比如 <HelloMessage name="John">
,就是 HelloMessage
元件加入一個 name
屬性,值為 John
。元件的屬性可以在元件類的 this.props
物件上獲取,比如 name
屬性就可以透過 this.props.name
讀取。上面程式碼的執行結果如下。
新增元件屬性,有一個地方需要注意,就是 class
屬性需要寫成 className
,for
屬性需要寫成 htmlFor
,這是因為 class
和 for
是 JavaScript 的保留字。
五、this.props.children
this.props
物件的屬性與元件的屬性一一對應,但是有一個例外,就是 this.props.children
屬性。它表示元件的所有子節點(檢視 demo05
)。
var NotesList = React.createClass({ render: function() { return ( <ol> { React.Children.map(this.props.children, function (child) { return <li>{child}</li>; }) } </ol> ); } }); ReactDOM.render( <NotesList> <span>hello</span> <span>world</span> </NotesList>, document.body );
上面程式碼的 NoteList
元件有兩個 span
子節點,它們都可以透過 this.props.children
讀取,執行結果如下。
這裡需要注意, this.props.children
的值有三種可能:如果當前元件沒有子節點,它就是 undefined
;如果有一個子節點,資料型別是 object
;如果有多個子節點,資料型別就是 array
。所以,處理 this.props.children
的時候要小心。
React 提供一個工具方法 React.Children
來處理 this.props.children
。我們可以用 React.Children.map
來遍歷子節點,而不用擔心 this.props.children
的資料型別是 undefined
還是 object
。更多的 React.Children
的方法,請參考官方文件。
六、PropTypes
元件的屬性可以接受任意值,字串、物件、函式等等都可以。有時,我們需要一種機制,驗證別人使用元件時,提供的引數是否符合要求。
元件類的PropTypes
屬性,就是用來驗證元件例項的屬性是否符合要求(檢視 demo06
)。
var MyTitle = React.createClass({ propTypes: { title: React.PropTypes.string.isRequired, }, render: function() { return <h1> {this.props.title} </h1>; } });
上面的Mytitle
元件有一個title
屬性。PropTypes
告訴 React,這個 title
屬性是必須的,而且它的值必須是字串。現在,我們設定 title
屬性的值是一個數值。
var data = 123; ReactDOM.render( <MyTitle title={data} />, document.body );
這樣一來,title
屬性就通不過驗證了。控制檯會顯示一行錯誤資訊。
Warning: Failed propType: Invalid prop `title` of type `number` supplied to `MyTitle`, expected `string`.
更多的PropTypes
設定,可以檢視官方文件。
此外,getDefaultProps
方法可以用來設定元件屬性的預設值。
var MyTitle = React.createClass({ getDefaultProps : function () { return { title : 'Hello World' }; }, render: function() { return <h1> {this.props.title} </h1>; } }); ReactDOM.render( <MyTitle />, document.body );
上面程式碼會輸出"Hello World"。
七、獲取真實的DOM節點
元件並不是真實的 DOM 節點,而是存在於記憶體之中的一種資料結構,叫做虛擬 DOM (virtual DOM)。只有當它插入文件以後,才會變成真實的 DOM 。根據 React 的設計,所有的 DOM 變動,都先在虛擬 DOM 上發生,然後再將實際發生變動的部分,反映在真實 DOM上,這種演算法叫做 DOM diff ,它可以極大提高網頁的效能表現。
但是,有時需要從元件獲取真實 DOM 的節點,這時就要用到 ref
屬性(檢視 demo07 )。
var MyComponent = React.createClass({ handleClick: function() { this.refs.myTextInput.focus(); }, render: function() { return ( <div> <input type="text" ref="myTextInput" /> <input type="button" value="Focus the text input" onClick={this.handleClick} /> </div> ); } }); ReactDOM.render( <MyComponent />, document.getElementById('example') );
上面程式碼中,元件 MyComponent
的子節點有一個文字輸入框,用於獲取使用者的輸入。這時就必須獲取真實的 DOM 節點,虛擬 DOM 是拿不到使用者輸入的。為了做到這一點,文字輸入框必須有一個 ref
屬性,然後 this.refs.[refName]
就會返回這個真實的 DOM 節點。
需要注意的是,由於 this.refs.[refName]
屬性獲取的是真實 DOM ,所以必須等到虛擬 DOM 插入文件以後,才能使用這個屬性,否則會報錯。上面程式碼中,透過為元件指定 Click
事件的回撥函式,確保了只有等到真實 DOM 發生 Click
事件之後,才會讀取 this.refs.[refName]
屬性。
React 元件支援很多事件,除了 Click
事件以外,還有 KeyDown
、Copy
、Scroll
等,完整的事件清單請檢視官方文件。
八、this.state
元件免不了要與使用者互動,React 的一大創新,就是將元件看成是一個狀態機,一開始有一個初始狀態,然後使用者互動,導致狀態變化,從而觸發重新渲染 UI (檢視 demo08
)。
var LikeButton = React.createClass({ getInitialState: function() { return {liked: false}; }, handleClick: function(event) { this.setState({liked: !this.state.liked}); }, render: function() { var text = this.state.liked ? 'like' : 'haven\'t liked'; return ( <p onClick={this.handleClick}> You {text} this. Click to toggle. </p> ); } }); ReactDOM.render( <LikeButton />, document.getElementById('example') );
上面程式碼是一個 LikeButton
元件,它的 getInitialState
方法用於定義初始狀態,也就是一個物件,這個物件可以透過 this.state
屬性讀取。當使用者點選元件,導致狀態變化,this.setState
方法就修改狀態值,每次修改以後,自動呼叫 this.render
方法,再次渲染元件。
由於 this.props
和 this.state
都用於描述元件的特性,可能會產生混淆。一個簡單的區分方法是,this.props
表示那些一旦定義,就不再改變的特性,而 this.state
是會隨著使用者互動而產生變化的特性。
九、表單
使用者在表單填入的內容,屬於使用者跟元件的互動,所以不能用 this.props
讀取(檢視 demo9
)。
var Input = React.createClass({ getInitialState: function() { return {value: 'Hello!'}; }, handleChange: function(event) { this.setState({value: event.target.value}); }, render: function () { var value = this.state.value; return ( <div> <input type="text" value={value} onChange={this.handleChange} /> <p>{value}</p> </div> ); } }); ReactDOM.render(<Input/>, document.body);
上面程式碼中,文字輸入框的值,不能用 this.props.value
讀取,而要定義一個 onChange
事件的回撥函式,透過 event.target.value
讀取使用者輸入的值。textarea
元素、select
元素、radio
元素都屬於這種情況,更多介紹請參考官方文件。
十、元件的生命週期
元件的生命週期分成三個狀態:
- Mounting:已插入真實 DOM
- Updating:正在被重新渲染
- Unmounting:已移出真實 DOM
React 為每個狀態都提供了兩種處理函式,will
函式在進入狀態之前呼叫,did
函式在進入狀態之後呼叫,三種狀態共計五種處理函式。
- componentWillMount()
- componentDidMount()
- componentWillUpdate(object nextProps, object nextState)
- componentDidUpdate(object prevProps, object prevState)
- componentWillUnmount()
此外,React 還提供兩種特殊狀態的處理函式。
- componentWillReceiveProps(object nextProps):已載入元件收到新的引數時呼叫
- shouldComponentUpdate(object nextProps, object nextState):元件判斷是否重新渲染時呼叫
這些方法的詳細說明,可以參考官方文件。下面是一個例子(檢視 demo10
)。
var Hello = React.createClass({ getInitialState: function () { return { opacity: 1.0 }; }, componentDidMount: function () { this.timer = setInterval(function () { var opacity = this.state.opacity; opacity -= .05; if (opacity < 0.1) { opacity = 1.0; } this.setState({ opacity: opacity }); }.bind(this), 100); }, render: function () { return ( <div style={{opacity: this.state.opacity}}> Hello {this.props.name} </div> ); } }); ReactDOM.render( <Hello name="world"/>, document.body );
上面程式碼在hello
元件載入以後,透過 componentDidMount
方法設定一個定時器,每隔100毫秒,就重新設定元件的透明度,從而引發重新渲染。
另外,元件的style
屬性的設定方式也值得注意,不能寫成
style="opacity:{this.state.opacity};"
而要寫成
style={{opacity: this.state.opacity}}
這是因為 React 元件樣式是一個物件,所以第一重大括號表示這是 JavaScript 語法,第二重大括號表示樣式物件。
十一、Ajax
元件的資料來源,通常是透過 Ajax 請求從伺服器獲取,可以使用 componentDidMount
方法設定 Ajax 請求,等到請求成功,再用 this.setState
方法重新渲染 UI (檢視 demo11
)。
var UserGist = React.createClass({ getInitialState: function() { return { username: '', lastGistUrl: '' }; }, componentDidMount: function() { $.get(this.props.source, function(result) { var lastGist = result[0]; if (this.isMounted()) { this.setState({ username: lastGist.owner.login, lastGistUrl: lastGist.html_url }); } }.bind(this)); }, render: function() { return ( <div> {this.state.username}'s last gist is <a href={this.state.lastGistUrl}>here</a>. </div> ); } }); ReactDOM.render( <UserGist source="https://api.github.com/users/octocat/gists" />, document.body );
上面程式碼使用 jQuery 完成 Ajax 請求,這是為了便於說明。React 本身沒有任何依賴,完全可以不用jQuery,而使用其他庫。
我們甚至可以把一個Promise物件傳入元件,請看Demo12
。
ReactDOM.render( <RepoList promise={$.getJSON('https://api.github.com/search/repositories?q=javascript&sort=stars')} />, document.body );
上面程式碼從Github的API抓取資料,然後將Promise物件作為屬性,傳給RepoList
元件。
如果Promise物件正在抓取資料(pending狀態),元件顯示"正在載入";如果Promise物件報錯(rejected狀態),元件顯示報錯資訊;如果Promise物件抓取資料成功(fulfilled狀態),元件顯示獲取的資料。
var RepoList = React.createClass({ getInitialState: function() { return { loading: true, error: null, data: null}; }, componentDidMount() { this.props.promise.then( value => this.setState({loading: false, data: value}), error => this.setState({loading: false, error: error})); }, render: function() { if (this.state.loading) { return <span>Loading...</span>; } else if (this.state.error !== null) { return <span>Error: {this.state.error.message}</span>; } else { var repos = this.state.data.items; var repoList = repos.map(function (repo) { return ( <li> <a href={repo.html_url}>{repo.name}</a> ({repo.stargazers_count} stars) <br/> {repo.description} </li> ); }); return ( <main> <h1>Most Popular JavaScript Projects in Github</h1> <ol>{repoList}</ol> </main> ); } } });
十二、參考連結
- React's official site
- React's official examples
- React (Virtual) DOM Terminology, by Sebastian Markbåge
- The React Quick Start Guide, by Jack Callister
- Learning React.js: Getting Started and Concepts, by Ken Wheeler
- Getting started with React, by Ryan Clark
- React JS Tutorial and Guide to the Gotchas, by Justin Deal
- React Primer, by Binary Muse
- jQuery versus React.js thinking, by zigomir
(完)