Navbar和Footer都是相對簡單的組件。Footer組件獲取并展示Top5人物角色,Navbar組件獲取并展示所有角色數(shù)量,然后還初始化一個Socket.IO事件監(jiān)聽器,用以跟蹤在線訪客的數(shù)量。
注意:這一節(jié)會比別的小節(jié)要稍長些,因?yàn)槲視谶@里談到一些新概念,而其它小節(jié)將基于它們進(jìn)行開發(fā)。
在components目錄下新建文件Footer.js:
import React from 'react';
import {Link} from 'react-router';
import FooterStore from '../stores/FooterStore'
import FooterActions from '../actions/FooterActions';
class Footer extends React.Component {
constructor(props) {
super(props);
this.state = FooterStore.getState();
this.onChange = this.onChange.bind(this);
}
componentDidMount() {
FooterStore.listen(this.onChange);
FooterActions.getTopCharacters();
}
componentWillUnmount() {
FooterStore.unlisten(this.onChange);
}
onChange(state) {
this.setState(state);
}
render() {
let leaderboardCharacters = this.state.characters.map((character) => {
return (
<li key={character.characterId}>
<Link to={'/characters/' + character.characterId}>
<img className='thumb-md' src={'http://image.eveonline.com/Character/' + character.characterId + '_128.jpg'} />
</Link>
</li>
)
});
return (
<footer>
<div className='container'>
<div className='row'>
<div className='col-sm-5'>
<h3 className='lead'><strong>Information</strong> and <strong>Copyright</strong></h3>
<p>Powered by <strong>Node.js</strong>, <strong>MongoDB</strong> and <strong>React</strong> with Flux architecture and server-side rendering.</p>
<p>You may view the <a href='http://www.15014759268.cn/targetlink?url=https://github.com/sahat/newedenfaces-react'>Source Code</a> behind this project on GitHub.</p>
<p>? 2015 Sahat Yalkabov.</p>
</div>
<div className='col-sm-7 hidden-xs'>
<h3 className='lead'><strong>Leaderboard</strong> Top 5 Characters</h3>
<ul className='list-inline'>
{leaderboardCharacters}
</ul>
</div>
</div>
</div>
</footer>
);
}
}
export default Footer;
為防止你還未熟悉ES6語法而暈頭轉(zhuǎn)向,在這里我將最后一次展示這段代碼用ES5是如何寫的,另外你也可以參看Using Alt with ES5指南來了解創(chuàng)建action和store時語法的不同。
var React = require('react');
var Link = require('react-router').Link;
var FooterStore = require('../stores/FooterStore');
var FooterActions = require('../actions/FooterActions');
var Footer = React.createClass({
getInitialState: function() {
return FooterStore.getState();
}
componentDidMount: function() {
FooterStore.listen(this.onChange);
FooterActions.getTopCharacters();
}
componentWillUnmount: function() {
FooterStore.unlisten(this.onChange);
}
onChange: function(state) {
this.setState(state);
}
render() {
var leaderboardCharacters = this.state.characters.map(function(character) {
return (
<li key={character.characterId}>
<Link to={'/characters/' + character.characterId}>
<img className='thumb-md' src={'http://image.eveonline.com/Character/' + character.characterId + '_128.jpg'} />
</Link>
</li>
);
});
return (
<footer>
<div className='container'>
<div className='row'>
<div className='col-sm-5'>
<h3 className='lead'><strong>Information</strong> and <strong>Copyright</strong></h3>
<p>Powered by <strong>Node.js</strong>, <strong>MongoDB</strong> and <strong>React</strong> with Flux architecture and server-side rendering.</p>
<p>You may view the <a href='http://www.15014759268.cn/targetlink?url=https://github.com/sahat/newedenfaces-react'>Source Code</a> behind this project on GitHub.</p>
<p>? 2015 Sahat Yalkabov.</p>
</div>
<div className='col-sm-7 hidden-xs'>
<h3 className='lead'><strong>Leaderboard</strong> Top 5 Characters</h3>
<ul className='list-inline'>
{leaderboardCharacters}
</ul>
</div>
</div>
</div>
</footer>
);
}
});
module.exports = Footer;
如果你還記得Flux架構(gòu)那一節(jié)的內(nèi)容,這些代碼看上去應(yīng)該挺熟悉。當(dāng)組件加載后,將初始組件狀態(tài)設(shè)置為FooterStore中的值,然后初始化store監(jiān)聽器。同樣,當(dāng)組件被卸載(比如導(dǎo)航至另一頁面),store監(jiān)聽器也被移除。當(dāng)store更新,onChange
函數(shù)被調(diào)用,然后反過來又更新Footer的狀態(tài)。
如果你之前用過React,在這里你需要注意的是,當(dāng)使用ES6 class創(chuàng)建React組件,組件方法不再自動綁定this
。也就是說,當(dāng)你調(diào)用組件內(nèi)部方法時,你需要手動綁定this
,在之前,React.createClass()
會幫我們自動綁定:
自動綁定:當(dāng)在JavaScript中創(chuàng)建回調(diào)時,你經(jīng)常需要手動綁定方法到它的實(shí)例以保證this的值正確,使用React,所有方法都自動綁定到組件實(shí)例。
以上出自于官方文檔。不過在ES6中我們要這么做:
this.onChange = this.onChange.bind(this);
下面是關(guān)于這個問題更詳細(xì)的例子:
class App extends React.Component {
constructor(props) {
super(props);
this.state = AppStore.getState();
this.onChange = this.onChange; // Need to add `.bind(this)`.
}
onChange(state) {
// Object `this` will be undefined without binding it explicitly.
this.setState(state);
}
render() {
return null;
}
}
現(xiàn)在你需要了解JavaScript中的map()
方法,即使你之前用過,也還是可能搞不清楚它在JSX中是怎么用的(React官方教程并沒有很好的解釋它)。
它基本上是一個for-each循環(huán),和Jade和Handlebars中的類似,但在這里你可以將結(jié)果分配給一個變量,然后你就可以在JSX里使用它了,就和用其它變量一樣。它在React中很常見,你會經(jīng)常用到。
注意:當(dāng)渲染動態(tài)子組件時,如上面的
leaderboardCharacters
,React會要求你使用key
屬性來指定每一個子組件。
Link
組件當(dāng)指定合適的href屬性時會渲染一個鏈接標(biāo)簽,它還知道鏈接的目標(biāo)是否可用,從而給鏈接加上active
的類。如果你使用React Router,你需要使用Link模塊在應(yīng)用內(nèi)部進(jìn)行導(dǎo)航。
下面,我們將為Footer組件創(chuàng)建action和store,在app/actions目錄新建FooterActions.js并添加:
import alt from '../alt';
class FooterActions {
constructor() {
this.generateActions(
'getTopCharactersSuccess',
'getTopCharactersFail'
);
}
getTopCharacters() {
$.ajax({ url: '/api/characters/top' })
.done((data) => {
this.actions.getTopCharactersSuccess(data)
})
.fail((jqXhr) => {
this.actions.getTopCharactersFail(jqXhr)
});
}
}
export default alt.createActions(FooterActions);
首先,注意我們從第七步創(chuàng)建的alt.js中導(dǎo)入了一個Alt的實(shí)例,而不是從我們安裝的Alt模塊中。它是一個Alt的實(shí)例,實(shí)現(xiàn)了Flux dispatcher并提供創(chuàng)建Alt action和store的方法。你可以把它想象為我們的store和action之間的膠水。
這里我們有3個action,一個使用ajax獲取數(shù)據(jù),另外兩個用來通知store獲取數(shù)據(jù)是成功還是失敗。在這個例子里,知道getTopCharacters
何時被觸發(fā)并沒有什么用,我們真正想知道的是action執(zhí)行成功(更新store然后重新渲染組件)還是失?。@示一個錯誤通知)。
Action可以很復(fù)雜,也可以很簡單。有些action我們不關(guān)心它們做了什么,我們只關(guān)心它們是否被觸發(fā),比如這里的ajaxInProgress
和ajaxComplete
被用來通知store,AJAX請求是正在進(jìn)行還是已經(jīng)完成。
注意:Alt的action能通過
generateActions
方法創(chuàng)建,只要它們直接通向dispatch。具體可參看官方文檔。
下面的兩種創(chuàng)建action方式是等價的,可依據(jù)你的喜好進(jìn)行選擇:
getTopCharactersSuccess(payload) {
this.dispatch(payload);
}
getTopCharactersFail(payload) {
this.dispatch(payload);
}
// Equivalent to this...
this.generateActions(
'getTopCharactersSuccess',
'getTopCharactersFail'
);
最后,我們通過alt.createActions
將FooterActions封裝并暴露出來,然后我們可以在Footer組件里導(dǎo)入并使用它。
下面,在app/stores目錄下新建文件FooterStore.js:
import alt from '../alt';
import FooterActions from '../actions/FooterActions';
class FooterStore {
constructor() {
this.bindActions(FooterActions);
this.characters = [];
}
onGetTopCharactersSuccess(data) {
this.characters = data.slice(0, 5);
}
onGetTopCharactersFail(jqXhr) {
// Handle multiple response formats, fallback to HTTP status code number.
toastr.error(jqXhr.responseJSON && jqXhr.responseJSON.message || jqXhr.responseText || jqXhr.statusText);
}
}
export default alt.createStore(FooterStore);
在store中創(chuàng)建的變量,比如this
所賦值的變量,都將成為狀態(tài)的一部分。當(dāng)Footer組件初始化并調(diào)用FooterStore.getState()
,它會獲取在構(gòu)造函數(shù)中指定的當(dāng)前狀態(tài)(在一開始只是一個空數(shù)組,而遍歷空數(shù)組會返回另一個空數(shù)組,所以在Footer組件第一次加載時并沒有渲染任何內(nèi)容)。
bindActions
用于將action綁定到store中定義的相應(yīng)處理函數(shù)。比如,一個命名為foo
的action會匹配store中叫做onFoo
或者foo
的處理函數(shù),不過需要注意它不會同時匹配兩者。因此我們在FooterActions.js中定義的actiongetTopCharactersSuccess
和getTopCharactersFail
會匹配到這里的處理函數(shù)onGetTopCharactersSuccess
和onGetTopCharactersFail
。
注意:如需更精細(xì)的控制store監(jiān)聽的action以及它們綁定的處理函數(shù),可參看文檔中的
bindListeners
方法。
在onGetTopCharactersSuccess
處理函數(shù)中我們更新了store的數(shù)據(jù),現(xiàn)在它包含Top 5角色,并且我們在Footer組件中初始化了store監(jiān)聽器,當(dāng)FooterStore更新后組件會自動的重新渲染。
我們會使用Toastr庫來處理通知。也許你會問為什么不使用純React通知組件呢?也許你以前看到過為React設(shè)計的通知組件,但我個人認(rèn)為這是少數(shù)不太適合用React的地方(還有一個是tooltips)。我認(rèn)為要從應(yīng)用的任何地方顯示一個通知,使用命令方式遠(yuǎn)比聲明式要簡單,我以前曾經(jīng)構(gòu)建過使用React和Flux的通知組件,但老實(shí)說,用來它處理顯隱狀態(tài)、動畫以及z-index位置等,非常痛苦。
打開app/components下的App.js并導(dǎo)入Footer組件:
import Footer from './Footer';
然后將<Footer />
添加到<RouterHandler / >
組件后面:
<div>
<RouteHandler />
<Footer />
</div>
刷新瀏覽器你應(yīng)該看到新的底部:
我們稍后會實(shí)現(xiàn)Express API以及添加人物角色數(shù)據(jù)庫,不過現(xiàn)在讓我們還是繼續(xù)構(gòu)建Navbar組件。因?yàn)橹耙呀?jīng)講過了alt action和store,這里將會盡量簡略的說明Navbar組件如何構(gòu)建。
在app/components目錄新建文件Navbar.js:
import React from 'react';
import {Link} from 'react-router';
import NavbarStore from '../stores/NavbarStore';
import NavbarActions from '../actions/NavbarActions';
class Navbar extends React.Component {
constructor(props) {
super(props);
this.state = NavbarStore.getState();
this.onChange = this.onChange.bind(this);
}
componentDidMount() {
NavbarStore.listen(this.onChange);
NavbarActions.getCharacterCount();
let socket = io.connect();
socket.on('onlineUsers', (data) => {
NavbarActions.updateOnlineUsers(data);
});
$(document).ajaxStart(() => {
NavbarActions.updateAjaxAnimation('fadeIn');
});
$(document).ajaxComplete(() => {
setTimeout(() => {
NavbarActions.updateAjaxAnimation('fadeOut');
}, 750);
});
}
componentWillUnmount() {
NavbarStore.unlisten(this.onChange);
}
onChange(state) {
this.setState(state);
}
handleSubmit(event) {
event.preventDefault();
let searchQuery = this.state.searchQuery.trim();
if (searchQuery) {
NavbarActions.findCharacter({
searchQuery: searchQuery,
searchForm: this.refs.searchForm.getDOMNode(),
router: this.context.router
});
}
}
render() {
return (
<nav className='navbar navbar-default navbar-static-top'>
<div className='navbar-header'>
<button type='button' className='navbar-toggle collapsed' data-toggle='collapse' data-target='#navbar'>
<span className='sr-only'>Toggle navigation</span>
<span className='icon-bar'></span>
<span className='icon-bar'></span>
<span className='icon-bar'></span>
</button>
<Link to='/' className='navbar-brand'>
<span ref='triangles' className={'triangles animated ' + this.state.ajaxAnimationClass}>
<div className='tri invert'></div>
<div className='tri invert'></div>
<div className='tri'></div>
<div className='tri invert'></div>
<div className='tri invert'></div>
<div className='tri'></div>
<div className='tri invert'></div>
<div className='tri'></div>
<div className='tri invert'></div>
</span>
NEF
<span className='badge badge-up badge-danger'>{this.state.onlineUsers}</span>
</Link>
</div>
<div id='navbar' className='navbar-collapse collapse'>
<form ref='searchForm' className='navbar-form navbar-left animated' onSubmit={this.handleSubmit.bind(this)}>
<div className='input-group'>
<input type='text' className='form-control' placeholder={this.state.totalCharacters + ' characters'} value={this.state.searchQuery} onChange={NavbarActions.updateSearchQuery} />
<span className='input-group-btn'>
<button className='btn btn-default' onClick={this.handleSubmit.bind(this)}><span className='glyphicon glyphicon-search'></span></button>
</span>
</div>
</form>
<ul className='nav navbar-nav'>
<li><Link to='/'>Home</Link></li>
<li><Link to='/stats'>Stats</Link></li>
<li className='dropdown'>
<a href='#' className='dropdown-toggle' data-toggle='dropdown'>Top 100 <span className='caret'></span></a>
<ul className='dropdown-menu'>
<li><Link to='/top'>Top Overall</Link></li>
<li className='dropdown-submenu'>
<Link to='/top/caldari'>Caldari</Link>
<ul className='dropdown-menu'>
<li><Link to='/top/caldari/achura'>Achura</Link></li>
<li><Link to='/top/caldari/civire'>Civire</Link></li>
<li><Link to='/top/caldari/deteis'>Deteis</Link></li>
</ul>
</li>
<li className='dropdown-submenu'>
<Link to='/top/gallente'>Gallente</Link>
<ul className='dropdown-menu'>
<li><Link to='/top/gallente/gallente'>Gallente</Link></li>
<li><Link to='/top/gallente/intaki'>Intaki</Link></li>
<li><Link to='/top/gallente/jin-mei'>Jin-Mei</Link></li>
</ul>
</li>
<li className='dropdown-submenu'>
<Link to='/top/minmatar'>Minmatar</Link>
<ul className='dropdown-menu'>
<li><Link to='/top/minmatar/brutor'>Brutor</Link></li>
<li><Link to='/top/minmatar/sebiestor'>Sebiestor</Link></li>
<li><Link to='/top/minmatar/vherokior'>Vherokior</Link></li>
</ul>
</li>
<li className='dropdown-submenu'>
<Link to='/top/amarr'>Amarr</Link>
<ul className='dropdown-menu'>
<li><Link to='/top/amarr/amarr'>Amarr</Link></li>
<li><Link to='/top/amarr/ni-kunni'>Ni-Kunni</Link></li>
<li><Link to='/top/amarr/khanid'>Khanid</Link></li>
</ul>
</li>
<li className='divider'></li>
<li><Link to='/shame'>Hall of Shame</Link></li>
</ul>
</li>
<li className='dropdown'>
<a href='#' className='dropdown-toggle' data-toggle='dropdown'>Female <span className='caret'></span></a>
<ul className='dropdown-menu'>
<li><Link to='/female'>All</Link></li>
<li className='dropdown-submenu'>
<Link to='/female/caldari'>Caldari</Link>
<ul className='dropdown-menu'>
<li><Link to='/female/caldari/achura'>Achura</Link></li>
<li><Link to='/female/caldari/civire/'>Civire</Link></li>
<li><Link to='/female/caldari/deteis'>Deteis</Link></li>
</ul>
</li>
<li className='dropdown-submenu'>
<Link to='/female/gallente'>Gallente</Link>
<ul className='dropdown-menu'>
<li><Link to='/female/gallente/gallente'>Gallente</Link></li>
<li><Link to='/female/gallente/intaki'>Intaki</Link></li>
<li><Link to='/female/gallente/jin-mei'>Jin-Mei</Link></li>
</ul>
</li>
<li className='dropdown-submenu'>
<Link to='/female/minmatar'>Minmatar</Link>
<ul className='dropdown-menu'>
<li><Link to='/female/minmatar/brutor'>Brutor</Link></li>
<li><Link to='/female/minmatar/sebiestor'>Sebiestor</Link></li>
<li><Link to='/female/minmatar/vherokior'>Vherokior</Link></li>
</ul>
</li>
<li className='dropdown-submenu'>
<Link to='/female/amarr'>Amarr</Link>
<ul className='dropdown-menu'>
<li><Link to='/female/amarr/amarr'>Amarr</Link></li>
<li><Link to='/female/amarr/ni-kunni'>Ni-Kunni</Link></li>
<li><Link to='/female/amarr/khanid'>Khanid</Link></li>
</ul>
</li>
</ul>
</li>
<li className='dropdown'>
<a href='#' className='dropdown-toggle' data-toggle='dropdown'>Male <span className='caret'></span></a>
<ul className='dropdown-menu'>
<li><Link to='/male'>All</Link></li>
<li className='dropdown-submenu'>
<Link to='/male/caldari'>Caldari</Link>
<ul className='dropdown-menu'>
<li><Link to='/male/caldari/achura'>Achura</Link></li>
<li><Link to='/male/caldari/civire'>Civire</Link></li>
<li><Link to='/male/caldari/deteis'>Deteis</Link></li>
</ul>
</li>
<li className='dropdown-submenu'>
<Link to='/male/gallente'>Gallente</Link>
<ul className='dropdown-menu'>
<li><Link to='/male/gallente/gallente'>Gallente</Link></li>
<li><Link to='/male/gallente/intaki'>Intaki</Link></li>
<li><Link to='/male/gallente/jin-mei'>Jin-Mei</Link></li>
</ul>
</li>
<li className='dropdown-submenu'>
<Link to='/male/minmatar'>Minmatar</Link>
<ul className='dropdown-menu'>
<li><Link to='/male/minmatar/brutor'>Brutor</Link></li>
<li><Link to='/male/minmatar/sebiestor'>Sebiestor</Link></li>
<li><Link to='/male/minmatar/vherokior'>Vherokior</Link></li>
</ul>
</li>
<li className='dropdown-submenu'>
<Link to='/male/amarr'>Amarr</Link>
<ul className='dropdown-menu'>
<li><Link to='/male/amarr/amarr'>Amarr</Link></li>
<li><Link to='/male/amarr/ni-kunni'>Ni-Kunni</Link></li>
<li><Link to='/male/amarr/khanid'>Khanid</Link></li>
</ul>
</li>
</ul>
</li>
<li><Link to='/add'>Add</Link></li>
</ul>
</div>
</nav>
);
}
}
Navbar.contextTypes = {
router: React.PropTypes.func.isRequired
};
export default Navbar;
必須承認(rèn),這里使用循環(huán)的話可以少寫一些代碼,但現(xiàn)在這樣對我來說更直觀。
你可能立刻注意到的一個東西是class變量contextTypes
。我們需要它來引用router的實(shí)例,從而讓我們能訪問當(dāng)前路徑、請求參數(shù)、路由參數(shù)以及到其它路由的變換。我們不在Navbar組件里直接使用它,而是將它作為一個參數(shù)傳遞給Navbar action,以使它能導(dǎo)航到特定character資料頁面。
componentDidMount
是我們發(fā)起與Socket.IO的連接,并初始化ajaxStart
和ajaxComplete
時間監(jiān)聽器地方,我們會在AJAX請求時在NEF logo旁邊顯示加載指示。
handleSubmit
是用來處理表單提交的程序,在按下Enter鍵或點(diǎn)擊Search圖標(biāo)時執(zhí)行。它會做一些輸入清理和驗(yàn)證工作,然后觸發(fā)findCharacter
action。另外我們還傳遞了搜索區(qū)域的DOM節(jié)點(diǎn)給action,以便當(dāng)搜索結(jié)果為0時加載一個震動動畫。
在app/actions目錄下新建文件NavbarActions.js:
import alt from '../alt';
import {assign} from 'underscore';
class NavbarActions {
constructor() {
this.generateActions(
'updateOnlineUsers',
'updateAjaxAnimation',
'updateSearchQuery',
'getCharacterCountSuccess',
'getCharacterCountFail',
'findCharacterSuccess',
'findCharacterFail'
);
}
findCharacter(payload) {
$.ajax({
url: '/api/characters/search',
data: { name: payload.searchQuery }
})
.done((data) => {
assign(payload, data);
this.actions.findCharacterSuccess(payload);
})
.fail(() => {
this.actions.findCharacterFail(payload);
});
}
getCharacterCount() {
$.ajax({ url: '/api/characters/count' })
.done((data) => {
this.actions.getCharacterCountSuccess(data)
})
.fail((jqXhr) => {
this.actions.getCharacterCountFail(jqXhr)
});
}
}
export default alt.createActions(NavbarActions);
我想大多數(shù)action的命名應(yīng)該能夠自我解釋,不過為了更清楚的理解,在下面簡單的描述一下它們是干什么的:
Action | Description |
---|---|
updateOnlineUsers |
當(dāng)Socket.IO事件更新時設(shè)置在線用戶數(shù) |
updateAjaxAnimation |
添加”fadeIn”或”fadeOut”類到加載指示器 |
updateSearchQuery |
當(dāng)使用鍵盤時設(shè)置搜索請求 |
getCharacterCount |
從服務(wù)器獲取總角色數(shù) |
getCharacterCountSuccess |
返回角色總數(shù) |
getCharacterCountFail |
返回jQuery jqXhr對象 |
findCharacter |
根據(jù)名稱查找角色 |
在app/stores目錄下創(chuàng)建NavbarStore.js:
import alt from '../alt';
import NavbarActions from '../actions/NavbarActions';
class NavbarStore {
constructor() {
this.bindActions(NavbarActions);
this.totalCharacters = 0;
this.onlineUsers = 0;
this.searchQuery = '';
this.ajaxAnimationClass = '';
}
onFindCharacterSuccess(payload) {
payload.router.transitionTo('/characters/' + payload.characterId);
}
onFindCharacterFail(payload) {
payload.searchForm.classList.add('shake');
setTimeout(() => {
payload.searchForm.classList.remove('shake');
}, 1000);
}
onUpdateOnlineUsers(data) {
this.onlineUsers = data.onlineUsers;
}
onUpdateAjaxAnimation(className) {
this.ajaxAnimationClass = className; //fadein or fadeout
}
onUpdateSearchQuery(event) {
this.searchQuery = event.target.value;
}
onGetCharacterCountSuccess(data) {
this.totalCharacters = data.count;
}
onGetCharacterCountFail(jqXhr) {
toastr.error(jqXhr.responseJSON.message);
}
}
export default alt.createStore(NavbarStore);
回憶一下我們在Navbar組件中的代碼:
<input type='text' className='form-control' placeholder={this.state.totalCharacters + ' characters'} value={this.state.searchQuery} onChange={NavbarActions.updateSearchQuery} />
因?yàn)?a rel="external nofollow" target="_blank" target="_blank">onChange
方法返回一個event對象,所以這里我們在onUpdateSearchQuery
使用event.target.value
來獲取輸入框的值。
再次打開App.js并導(dǎo)入Navbar組件:
import Navbar from './Navbar';
然后在<RouterHandler />
添加<Navbar />
組件:
<div>
<Navbar />
<RouteHandler />
<Footer />
</div>
更多建議: