第九步:Footer和Navbar组件
第九步: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://www.w3cschool.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语法而晕头转向,在这里我将最后一次展示这段代码用ES5是如何写的,另外你也可以参看Using Alt with 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://www.w3cschool.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架构那一节的内容,这些代码看上去应该挺熟悉。当组件加载后,将初始组件状态设置为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中很常见,你会经常用到。
注意:当渲染动态子组件时,如上面的
leaderboardCharacters
,React会要求你使用key
属性来指定每一个子组件。
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。具体可参看官方文档。
下面的两种创建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
用于将action绑定到store中定义的相应处理函数。比如,一个命名为foo
的action会匹配store中叫做onFoo
或者foo
的处理函数,不过需要注意它不会同时匹配两者。因此我们在FooterActions.js中定义的actiongetTopCharactersSuccess
和getTopCharactersFail
会匹配到这里的处理函数onGetTopCharactersSuccess
和onGetTopCharactersFail
。
注意:如需更精细的控制store监听的action以及它们绑定的处理函数,可参看文档中的
bindListeners
方法。
在onGetTopCharactersSuccess
处理函数中我们更新了store的数据,现在它包含Top 5角色,并且我们在Footer组件中初始化了store监听器,当FooterStore更新后组件会自动的重新渲染。
我们会使用Toastr库来处理通知。也许你会问为什么不使用纯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
方法返回一个event对象,所以这里我们在onUpdateSearchQuery
使用event.target.value
来获取输入框的值。
再次打开App.js并导入Navbar组件:
import Navbar from './Navbar';
然后在<RouterHandler />
添加<Navbar />
组件:
<div>
<Navbar />
<RouteHandler />
<Footer />
</div>
更多建议: