React已經成了一整套前後端通吃的 Web App 解決方案。衍生的 React Native 項目,目標更是宏偉,希望用寫 Web App 的方式去寫 Native App。如果能夠實現,整個互連網行業都會被顛覆,因為同一組人只需要寫一次 UI ,就能同時運行在伺服器、瀏覽器和手機
既然 React 這麼熱門,看上去充滿希望,當然應該好好學一下。從技術角度,可以滿足好奇心,提高技術水平;從職業角度,有利於求職和晉陞,有利於參與潛力大的項目。但是,好的 React 教程卻不容易找到,這一方面因為這項技術太新,剛剛開始走紅,大家都沒有經驗,還在摸索之中;另一方面因為 React 本身還在不斷變動,API 一直在調整,至今沒發布1.0版。
我學習 React 時,就很苦惱。有的教程討論一些細節問題,對入門沒協助;有的教程寫得不錯,但比較短,無助於看清全貌。我斷斷續續學了幾個月,看過二十幾篇教程,在這個過程中,將對自己有協助的 Demo 都收集下來,做成了一個庫 React Demos 。
下面,我就根據這個庫,寫一篇全面又易懂的 React 入門教程。你只需要跟著每一個 Demo 做一遍,就能初步掌握 React 。當然,前提是你必須擁有基本 JavaScript 和 DOM 知識,但是你讀完就會發現,React 所要求的預備知識真的很少。
零、安裝
React 的安裝包,可以到官網 https://facebook.github.io/react/downloads.html 下載。不過,React Demos 已經內建 React 源碼,不用另外安裝,只需把這個庫拷貝到你的硬碟就行了。
$ git clone git@github.com:ruanyf/react-demos.git
如果你沒安裝 git, 那就直接下載 zip 壓縮包。
下面要講解的10個例子在各個 Demo 子目錄,每個目錄都有一個 index.html 檔案,在瀏覽器開啟這個檔案(大多數情況下雙擊即可),就能立刻看到效果。
需要說明的是,React 可以在瀏覽器運行,也可以在伺服器運行,但是本教程只涉及瀏覽器。一方面是為了盡量保持簡單,另一方面 React 的文法是一致的,伺服器的用法與瀏覽器差別不大。Demo11 是伺服器首屏渲染的例子,有興趣的朋友可以自己去看源碼。
一、HTML 範本
使用 React 的網頁源碼,結構大致如下。
<!DOCTYPE html>
<html>
<head>
<script src="../build/react.js"></script>
<script src="../build/JSXTransformer.js"></script>
</head>
<body>
<div id="example"></div>
<script type="text/jsx">
// ** Our code goes here! **
</script>
</body>
</html>
上面代碼有兩個地方需要注意。首先,最後一個 script 標籤的 type 屬性為 text/jsx 。這是因為 React 專屬的 JSX 文法,跟 JavaScript 不相容。凡是使用 JSX 的地方,都要加上 type="text/jsx" 。
其次,React 提供兩個庫: react.js 和 JSXTransformer.js ,它們必須首先載入。其中,JSXTransformer.js 的作用是將 JSX 文法轉為 JavaScript 文法。這一步很消耗時間,實際上線的時候,應該將它放到伺服器完成。
$ jsx src/ build/
上面命令可以將 src 子目錄的 js 檔案進行文法轉換,轉碼後的檔案全部放在 build 子目錄。
二、React.render()
React.render 是 React 的最基本方法,用於將模板轉為 HTML 語言,並插入指定的 DOM 節點。
React.render(
<h1>Hello, world!</h1>,
document.getElementById('example')
);
上面代碼將一個 h1 標題,插入 example 節點(查看 demo01),運行結果如下。
三、JSX 文法
上一節的代碼, HTML 語言直接寫在 JavaScript 語言之中,不加任何引號,這就是 JSX 的文法,它允許 HTML 與 JavaScript 的混寫(查看 Demo02 )。
var names = ['Alice', 'Emily', 'Kate'];
React.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>,
];
React.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>;
}
});
React.render(
<HelloMessage name="John" />,
document.getElementById('example')
);
上面代碼中,變數 HelloMessage 就是一個組件類。模板插入 <HelloMessage /> 時,會自動產生 HelloMessage 的一個執行個體(下文的"組件"都指組件類的執行個體)。所有組件類都必須有自己的 render 方法,用於輸出組件。
組件的用法與原生的 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>
{
this.props.children.map(function (child) {
return <li>{child}</li>
})
}
</ol>
);
}
});
React.render(
<NotesList>
<span>hello</span>
<span>world</span>
</NotesList>,
document.body
);
上面代碼的 NoteList 組件有兩個 span 子節點,它們都可以通過 this.props.children 讀取,運行結果如下。
這裡需要注意,只有當子節點多餘1個時,this.props.children 才是一個數組,否則是不能用 map 方法的, 會報錯。
六、React.findDOMNode()
組件並不是真實的 DOM 節點,而是存在於記憶體之中的一種資料結構,叫做虛擬 DOM (virtual DOM)。只有當它插入文檔以後,才會變成真實的 DOM 。根據 React 的設計,所有的 DOM 變動,都先在虛擬 DOM 上發生,然後再將實際發生變動的部分,反映在真實 DOM上,這種演算法叫做 DOM diff ,它可以極大提高網頁的效能表現。
但是,有時需要從組件擷取真實 DOM 的節點,這時就要用到 React.findDOMNode 方法(查看 demo06 )。
var MyComponent = React.createClass({
handleClick: function() {
React.findDOMNode(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>
);
}
});
React.render(
<MyComponent />,
document.getElementById('example')
);
上面代碼中,組件 MyComponent 的子節點有一個文本輸入框,用於擷取使用者的輸入。這時就必須擷取真實的 DOM 節點,虛擬 DOM 是拿不到使用者輸入的。為了做到這一點,文本輸入框必須有一個 ref 屬性,然後 this.refs.[refName] 就指向這個虛擬 DOM 的子節點,最後通過 React.findDOMNode 方法擷取真實 DOM 的節點。
需要注意的是,由於 React.findDOMNode 方法擷取的是真實 DOM ,所以必須等到虛擬 DOM 插入文檔以後,才能使用這個方法,否則會返回 null 。上面代碼中,通過為組件指定 Click 事件的回呼函數,確保了只有等到真實 DOM 發生 Click 事件之後,才會調用 React.findDOMNode 方法。
React 組件支援很多事件,除了 Click 事件以外,還有 KeyDown 、Copy、Scroll 等,完整的事件清單請查看官方文檔。
http://facebook.github.io/react/docs/events.html#supported-events
七、this.state
組件免不了要與使用者互動,React 的一大創新,就是將組件看成是一個狀態機器,一開始有一個初始狀態,然後使用者互動,導致狀態變化,從而觸發重新渲染 UI (查看 demo07 )。
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>
);
}
});
React.render(
<LikeButton />,
document.getElementById('example')
);
上面代碼是一個 LikeButton 組件,它的 getInitialState 方法用於定義初始狀態,也就是一個對象,這個對象可以通過 this.state 屬性讀取。當使用者點擊組件,導致狀態變化,this.setState 方法就修改狀態值,每次修改以後,自動調用 this.render 方法,再次渲染組件。
由於 this.props 和 this.state 都用於描述組件的特性,可能會產生混淆。一個簡單的區分方法是,this.props 表示那些一旦定義,就不再改變的特性,而 this.state 是會隨著使用者互動而產生變化的特性。
八、表單
使用者在表單填入的內容,屬於使用者跟組件的互動,所以不能用 this.props 讀取(查看 demo08 )。
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>
);
}
});
React.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):組件判斷是否重新渲染時調用
這些方法的詳細說明,可以參考官方文檔。下面是一個例子(查看 demo09 )。
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>
);
}
});
React.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 (查看 demo10 )。
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>
);
}
});
React.render(
<UserGist source="https://api.github.com/users/octocat/gists" />,
document.body
);
上面代碼使用 jQuery 完成 Ajax 請求,這是為了便於說明。React 沒有任何依賴,完全可以使用其他庫。