第九步:Footer和Navbar组件

最后更新于:2022-04-01 03:19:21

## 第九步: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> ~~~ 刷新浏览器你应该看到新的底部: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6433978049.jpg) 我们稍后会实现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资料页面。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f64339daa8f.jpg) `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> ~~~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6433a5dd94.jpg) 由于我们还没有设置服务器端的Socke.IO,也没有实现任何API,所以现在你应该看不到在线访问人数或总的character数。
';