## 第九步:Footer和Navbar組件
Navbar和Footer都是相對簡單的組件。Footer組件獲取并展示Top5人物角色,Navbar組件獲取并展示所有角色數量,然后還初始化一個Socket.IO事件監聽器,用以跟蹤在線訪客的數量。
> 注意:這一節會比別的小節要稍長些,因為我會在這里談到一些新概念,而其它小節將基于它們進行開發。
### Footer組件
在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='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語法而暈頭轉向,在這里我將最后一次展示這段代碼用ES5是如何寫的,另外你也可以參看[Using Alt with ES5](http://alt.js.org/guides/es5/)指南來了解創建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='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架構那一節的內容,這些代碼看上去應該挺熟悉。當組件加載后,將初始組件狀態設置為FooterStore中的值,然后初始化store監聽器。同樣,當組件被卸載(比如導航至另一頁面),store監聽器也被移除。當store更新,`onChange`函數被調用,然后反過來又更新Footer的狀態。
如果你之前用過React,在這里你需要注意的是,當使用ES6 class創建React組件,組件方法不再自動綁定`this`。也就是說,當你調用組件內部方法時,你需要手動綁定`this`,在之前,`React.createClass()`會幫我們自動綁定:
> 自動綁定:當在JavaScript中創建回調時,你經常需要手動綁定方法到它的實例以保證this的值正確,使用React,所有方法都自動綁定到組件實例。
以上出自于官方文檔。不過在ES6中我們要這么做:
~~~
this.onChange = this.onChange.bind(this);
~~~
下面是關于這個問題更詳細的例子:
~~~
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;
}
}
~~~
現在你需要了解JavaScript中的`map()`方法,即使你之前用過,也還是可能搞不清楚它在JSX中是怎么用的(React官方教程并沒有很好的解釋它)。
它基本上是一個for-each循環,和Jade和Handlebars中的類似,但在這里你可以將結果分配給一個變量,然后你就可以在JSX里使用它了,就和用其它變量一樣。它在React中很常見,你會經常用到。
> 注意:當渲染[動態子組件](https://facebook.github.io/react/docs/multiple-components.html#dynamic-children)時,如上面的`leaderboardCharacters`,React會要求你使用`key`屬性來指定每一個子組件。
[`Link`](http://rackt.github.io/react-router/#Link)組件當指定合適的*href*屬性時會渲染一個鏈接標簽,它還知道鏈接的目標是否可用,從而給鏈接加上`active`的類。如果你使用React Router,你需要使用Link模塊在應用內部進行導航。
### Actions
下面,我們將為Footer組件創建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);
~~~
首先,注意我們從第七步創建的alt.js中導入了一個Alt的實例,而不是從我們安裝的Alt模塊中。它是一個Alt的實例,實現了Flux dispatcher并提供創建Alt action和store的方法。你可以把它想象為我們的store和action之間的膠水。
這里我們有3個action,一個使用ajax獲取數據,另外兩個用來通知store獲取數據是成功還是失敗。在這個例子里,知道`getTopCharacters`何時被觸發并沒有什么用,我們真正想知道的是action執行成功(更新store然后重新渲染組件)還是失敗(顯示一個錯誤通知)。
Action可以很復雜,也可以很簡單。有些action我們不關心它們做了什么,我們只關心它們是否被觸發,比如這里的`ajaxInProgress`和`ajaxComplete`被用來通知store,AJAX請求是正在進行還是已經完成。
> 注意:Alt的action能通過`generateActions`方法創建,只要它們直接通向dispatch。具體可參看[官方文檔](http://alt.js.org/docs/createActions/)。
下面的兩種創建action方式是等價的,可依據你的喜好進行選擇:
~~~
getTopCharactersSuccess(payload) {
this.dispatch(payload);
}
getTopCharactersFail(payload) {
this.dispatch(payload);
}
// Equivalent to this...
this.generateActions(
'getTopCharactersSuccess',
'getTopCharactersFail'
);
~~~
最后,我們通過`alt.createActions`將FooterActions封裝并暴露出來,然后我們可以在Footer組件里導入并使用它。
### Store
下面,在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中創建的變量,比如`this`所賦值的變量,都將成為狀態的一部分。當Footer組件初始化并調用`FooterStore.getState()`,它會獲取在構造函數中指定的當前狀態(在一開始只是一個空數組,而遍歷空數組會返回另一個空數組,所以在Footer組件第一次加載時并沒有渲染任何內容)。
[`bindActions`](http://alt.js.org/docs/createStore/#storemodelbindactions)用于將action綁定到store中定義的相應處理函數。比如,一個命名為`foo`的action會匹配store中叫做`onFoo`或者`foo`的處理函數,不過需要注意它不會同時匹配兩者。因此我們在FooterActions.js中定義的action`getTopCharactersSuccess`和`getTopCharactersFail`會匹配到這里的處理函數`onGetTopCharactersSuccess`和`onGetTopCharactersFail`。
> 注意:如需更精細的控制store監聽的action以及它們綁定的處理函數,可參看文檔中的[`bindListeners`](http://alt.js.org/docs/createStore/#storemodelbindlisteners)方法。
在`onGetTopCharactersSuccess`處理函數中我們更新了store的數據,現在它包含Top 5角色,并且我們在Footer組件中初始化了store監聽器,當FooterStore更新后組件會自動的重新渲染。
我們會使用[Toastr庫](http://codeseven.github.io/toastr/demo.html)來處理通知。也許你會問為什么不使用純React通知組件呢?也許你以前看到過為React設計的通知組件,但我個人認為這是少數不太適合用React的地方(還有一個是tooltips)。我認為要從應用的任何地方顯示一個通知,使用命令方式遠比聲明式要簡單,我以前曾經構建過使用React和Flux的通知組件,但老實說,用來它處理顯隱狀態、動畫以及z-index位置等,非常痛苦。
打開app/components下的*App.js*并導入Footer組件:
~~~
import Footer from './Footer';
~~~
然后將`<Footer />`添加到`<RouterHandler / >`組件后面:
~~~
<div>
<RouteHandler />
<Footer />
</div>
~~~
刷新瀏覽器你應該看到新的底部:

我們稍后會實現Express API以及添加人物角色數據庫,不過現在讓我們還是繼續構建Navbar組件。因為之前已經講過了alt action和store,這里將會盡量簡略的說明Navbar組件如何構建。
### Navbar組件
在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;
~~~
必須承認,這里使用循環的話可以少寫一些代碼,但現在這樣對我來說更直觀。
你可能立刻注意到的一個東西是class變量`contextTypes`。我們需要它來引用router的實例,從而讓我們能訪問當前路徑、請求參數、路由參數以及到其它路由的變換。我們不在Navbar組件里直接使用它,而是將它作為一個參數傳遞給Navbar action,以使它能導航到特定character資料頁面。

`componentDidMount`是我們發起與Socket.IO的連接,并初始化`ajaxStart`和`ajaxComplete`時間監聽器地方,我們會在AJAX請求時在NEF logo旁邊顯示加載指示。
`handleSubmit`是用來處理表單提交的程序,在按下Enter鍵或點擊Search圖標時執行。它會做一些輸入清理和驗證工作,然后觸發`findCharacter`?action。另外我們還傳遞了搜索區域的DOM節點給action,以便當搜索結果為0時加載一個震動動畫。
### Actions
在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);
~~~
我想大多數action的命名應該能夠自我解釋,不過為了更清楚的理解,在下面簡單的描述一下它們是干什么的:
| Action | Description |
| --- | --- |
| `updateOnlineUsers` | 當Socket.IO事件更新時設置在線用戶數 |
| `updateAjaxAnimation` | 添加”fadeIn”或”fadeOut”類到加載指示器 |
| `updateSearchQuery` | 當使用鍵盤時設置搜索請求 |
| `getCharacterCount` | 從服務器獲取總角色數 |
| `getCharacterCountSuccess` | 返回角色總數 |
| `getCharacterCountFail` | 返回jQuery jqXhr對象 |
| `findCharacter` | 根據名稱查找角色 |
### Store
在app/stores目錄下創建*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} />
~~~
因為[`onChange`](https://facebook.github.io/react/docs/forms.html#interactive-props)方法返回一個event對象,所以這里我們在`onUpdateSearchQuery`使用`event.target.value`來獲取輸入框的值。
再次打開App.js并導入Navbar組件:
~~~
import Navbar from './Navbar';
~~~
然后在`<RouterHandler />`添加`<Navbar />`組件:
~~~
<div>
<Navbar />
<RouteHandler />
<Footer />
</div>
~~~

由于我們還沒有設置服務器端的Socke.IO,也沒有實現任何API,所以現在你應該看不到在線訪問人數或總的character數。
- 前言
- 概述
- 第一步:新建Express項目
- 第二步:構建系統
- 第三步:項目結構
- 第四步: ES6速成教程
- 第五步: React速成教程
- 第六步:Flux架構速成教程
- 第七步:React路由(客戶端)
- 第八步:React路由(服務端)
- 第九步:Footer和Navbar組件
- 第十步:Socke.IO – 實時用戶數
- 第十一步:添加Character的組件
- 第十二步:數據庫模式
- 第十三步:Express API 路由(1/2)
- 第十五步:Home組件
- 第十四步:Express API 路由(2/2)
- 第十六步:角色(資料)組件
- 第十七步:Top 100 組件
- 第十八步:Stats組件
- 第十九步:部署
- 第二十步: 附加資源
- 總結