現(xiàn)在最熱門的前端框架饿凛,毫無疑問是 React 。
上周剖笙,基于 React 的 React Native 發(fā)布卵洗,結果一天之內,就獲得了 5000 顆星弥咪,受矚目程度可見一斑过蹂。
React 起源于 Facebook 的內部項目,因為該公司對市場上所有 JavaScript MVC 框架聚至,都不滿意酷勺,就決定自己寫一套,用來架設 Instagram 的網站扳躬。做出來以后脆诉,發(fā)現(xiàn)這套東西很好用,就在2013年5月開源了贷币。
由于 React 的設計思想極其獨特击胜,屬于革命性創(chuàng)新,性能出眾役纹,代碼邏輯卻非常簡單偶摔。所以,越來越多的人開始關注和使用促脉,認為它可能是將來 Web 開發(fā)的主流工具辰斋。
這個項目本身也越滾越大信不,從最早的UI引擎變成了一整套前后端通吃的 Web App 解決方案。衍生的 React Native 項目亡呵,目標更是宏偉抽活,希望用寫 Web App 的方式去寫 Native App。如果能夠實現(xiàn)锰什,整個互聯(lián)網行業(yè)都會被顛覆下硕,因為同一組人只需要寫一次 UI ,就能同時運行在服務器汁胆、瀏覽器和手機(參見《也許梭姓,DOM 不是答案》)。
既然 React 這么熱門嫩码,看上去充滿希望誉尖,當然應該好好學一下。從技術角度铸题,可以滿足好奇心铡恕,提高技術水平;從職業(yè)角度丢间,有利于求職和晉升探熔,有利于參與潛力大的項目。但是烘挫,好的 React 教程卻不容易找到诀艰,這一方面因為這項技術太新,剛剛開始走紅饮六,大家都沒有經驗其垄,還在摸索之中;另一方面因為 React 本身還在不斷變動卤橄,API 一直在調整绿满,至今沒發(fā)布1.0版。
我學習 React 時虽风,就很苦惱棒口。有的教程討論一些細節(jié)問題寄月,對入門沒幫助辜膝;有的教程寫得不錯,但比較短漾肮,無助于看清全貌厂抖。我斷斷續(xù)續(xù)學了幾個月,看過二十幾篇教程克懊,在這個過程中忱辅,將對自己有幫助的 Demo 都收集下來七蜘,做成了一個庫 React Demos 。
下面墙懂,我就根據(jù)這個庫橡卤,寫一篇全面又易懂的 React 入門教程。你只需要跟著每一個 Demo 做一遍损搬,就能初步掌握 React 碧库。當然,前提是你必須擁有基本 JavaScript 和 DOM 知識巧勤,但是你讀完就會發(fā)現(xiàn)嵌灰,React 所要求的預備知識真的很少。
零颅悉、安裝
React 的安裝包沽瞭,可以到官網下載。不過剩瓶,React Demos
已經自帶 React 源碼驹溃,不用另外安裝,只需把這個庫拷貝到你的硬盤就行了延曙。
$ git clone git@github.com:ruanyf/react-demos.git
如果你沒安裝 git吠架, 那就直接下載 zip 壓縮包。
下面要講解的12個例子在各個 Demo
子目錄搂鲫,每個目錄都有一個 index.html
文件傍药,在瀏覽器打開這個文件(大多數(shù)情況下雙擊即可),就能立刻看到效果魂仍。
需要說明的是拐辽,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 節(jié)點。
ReactDOM.render( <h1>Hello, world!</h1>, document.getElementById('example'));
上面代碼將一個 h1
標題绊寻,插入 example
節(jié)點(查看 demo01
)花墩,運行結果如下。
三澄步、JSX 語法
上一節(jié)的代碼冰蘑, 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'));
上面代碼體現(xiàn)了 JSX 的基本語法規(guī)則:遇到 HTML 標簽(以 <
開頭)梯皿,就用 HTML 規(guī)則解析仇箱;遇到代碼塊(以 {
開頭),就用 JavaScript 規(guī)則解析东羹。上面代碼的運行結果如下剂桥。
JSX 允許直接在模板插入 JavaScript 變量。如果這個變量是一個數(shù)組百姓,則會展開這個數(shù)組的所有成員(查看 demo03
)渊额。
var arr = [ <h1>Hello world!</h1>, <h2>React is awesome</h2>,];ReactDOM.render( <div>{arr}</div>, document.getElementById('example'));
上面代碼的arr
變量是一個數(shù)組,結果 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
屬性。它表示組件的所有子節(jié)點(查看 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
子節(jié)點墓怀,它們都可以通過 this.props.children
讀取,運行結果如下卫键。
這里需要注意傀履, this.props.children
的值有三種可能:如果當前組件沒有子節(jié)點,它就是 undefined
;如果有一個子節(jié)點莉炉,數(shù)據(jù)類型是 object
钓账;如果有多個子節(jié)點,數(shù)據(jù)類型就是 array
絮宁。所以梆暮,處理 this.props.children
的時候要小心。
React 提供一個工具方法 React.Children
來處理 this.props.children
绍昂。我們可以用 React.Children.map
來遍歷子節(jié)點啦粹,而不用擔心 this.props.children
的數(shù)據(jù)類型是 undefined
還是 object
。更多的 React.Children
的方法治专,請參考官方文檔卖陵。
六、PropTypes
組件的屬性可以接受任意值张峰,字符串泪蔫、對象、函數(shù)等等都可以喘批。有時撩荣,我們需要一種機制,驗證別人使用組件時饶深,提供的參數(shù)是否符合要求餐曹。
組件類的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
屬性是必須的俱两,而且它的值必須是字符串”タ瘢現(xiàn)在,我們設置 title
屬性的值是一個數(shù)值宪彩。
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節(jié)點
組件并不是真實的 DOM 節(jié)點块差,而是存在于內存之中的一種數(shù)據(jù)結構侵续,叫做虛擬 DOM (virtual DOM)倔丈。只有當它插入文檔以后,才會變成真實的 DOM 状蜗。根據(jù) React 的設計需五,所有的 DOM 變動,都先在虛擬 DOM 上發(fā)生轧坎,然后再將實際發(fā)生變動的部分宏邮,反映在真實 DOM上,這種算法叫做 DOM diff 缸血,它可以極大提高網頁的性能表現(xiàn)蜜氨。
但是,有時需要從組件獲取真實 DOM 的節(jié)點捎泻,這時就要用到 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
的子節(jié)點有一個文本輸入框笆豁,用于獲取用戶的輸入郎汪。這時就必須獲取真實的 DOM 節(jié)點,虛擬 DOM 是拿不到用戶輸入的闯狱。為了做到這一點煞赢,文本輸入框必須有一個 ref
屬性,然后 this.refs.[refName]
就會返回這個真實的 DOM 節(jié)點哄孤。
需要注意的是照筑,由于 this.refs.[refName]
屬性獲取的是真實 DOM ,所以必須等到虛擬 DOM 插入文檔以后瘦陈,才能使用這個屬性凝危,否則會報錯。上面代碼中双饥,通過為組件指定 Click
事件的回調函數(shù)媒抠,確保了只有等到真實 DOM 發(fā)生 Click
事件之后,才會讀取 this.refs.[refName]
屬性咏花。
React 組件支持很多事件趴生,除了 Click
事件以外阀趴,還有 KeyDown
、Copy
苍匆、Scroll
等刘急,完整的事件清單請查看官方文檔。
八浸踩、this.state
組件免不了要與用戶互動叔汁,React 的一大創(chuàng)新,就是將組件看成是一個狀態(tài)機检碗,一開始有一個初始狀態(tài)据块,然后用戶互動,導致狀態(tài)變化折剃,從而觸發(fā)重新渲染 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
方法用于定義初始狀態(tài)怕犁,也就是一個對象边篮,這個對象可以通過 this.state
屬性讀取。當用戶點擊組件奏甫,導致狀態(tài)變化戈轿,this.setState
方法就修改狀態(tài)值,每次修改以后阵子,自動調用 this.render
方法思杯,再次渲染組件。
由于 this.props
和 this.state
都用于描述組件的特性款筑,可能會產生混淆智蝠。一個簡單的區(qū)分方法是,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
事件的回調函數(shù),通過event.target.value
讀取用戶輸入的值谱俭。textarea
元素奉件、select
元素宵蛀、radio
元素都屬于這種情況,更多介紹請參考官方文檔县貌。
十术陶、組件的生命周期
組件的生命周期分成三個狀態(tài):
Mounting:已插入真實 DOM
Updating:正在被重新渲染
Unmounting:已移出真實 DOM
React 為每個狀態(tài)都提供了兩種處理函數(shù),will
函數(shù)在進入狀態(tài)之前調用煤痕,did
函數(shù)在進入狀態(tài)之后調用梧宫,三種狀態(tài)共計五種處理函數(shù)。
componentWillMount()
componentDidMount()
componentWillUpdate(object nextProps, object nextState)
componentDidUpdate(object prevProps, object prevState)
componentWillUnmount()
此外摆碉,React 還提供兩種特殊狀態(tài)的處理函數(shù)塘匣。
componentWillReceiveProps(object nextProps):已加載組件收到新的參數(shù)時調用
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毫秒,就重新設置組件的透明度历谍,從而引發(fā)重新渲染现拒。
另外,組件的style
屬性的設置方式也值得注意望侈,不能寫成
style="opacity:{this.state.opacity};"
而要寫成
style={{opacity: this.state.opacity}}
這是因為 React 組件樣式是一個對象印蔬,所以第一重大括號表示這是 JavaScript 語法,第二重大括號表示樣式對象。
十一、Ajax
組件的數(shù)據(jù)來源谆吴,通常是通過 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抓取數(shù)據(jù)滩愁,然后將Promise對象作為屬性,傳給RepoList
組件辫封。
如果Promise對象正在抓取數(shù)據(jù)(pending狀態(tài))硝枉,組件顯示"正在加載"玖瘸;如果Promise對象報錯(rejected狀態(tài)),組件顯示報錯信息檀咙;如果Promise對象抓取數(shù)據(jù)成功(fulfilled狀態(tài))雅倒,組件顯示獲取的數(shù)據(jù)。
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)
{repo.description} </li> ); }); return ( <main> <h1>Most Popular JavaScript Projects in Github</h1> <ol>{repoList}</ol> </main> ); } }});