总结

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

## 总结 在我去年底发布的上一篇[博客](https://hackhands.com/building-instagram-clone-angularjs-satellizer-nodejs-mongodb/)中,我说道: > 祝贺你成功坚持到了最后!这是我发布过最长的博客了。有趣的是,在TV Show Tracker博客中我也说过同样的话。 但是现在,这篇文章比那一篇还要长!我的确没有想到会写这么长,也绝不是为了打破记录而特意这么做。但我的确希望这篇教程对读者有帮助,并且内容丰富。如果你从本文中学到任何一点东西,那么我的辛苦就没有白费。 如果你喜欢这个项目,可以考虑扩展它,甚至基于New Eden Faces创建一个全新的应用。所有这些代码都放在[Github](http://github.com/sahat/newedenfaces-react)上并且是完全免费的,所以你可以按照你的想法使用或修改它。下面是我想到的一些主意: * 为重置统计数据、修正性别、删除角色创建一个后台管理界面。 * 为每周统计数据创建一个邮件订阅程序,类似[Fitbit Weekly Progress Report](https://www.google.com/search?q=fitbit+weekly+progress+report&source=lnms&tbm=isch&sa=X&ved=0CAgQ_AUoAmoVChMItIX17r_oxgIVCVyICh2NUQhh&biw=964&bih=656)。 * 为两个角色创建一对一的竞选投票。 * 更智能的匹配算法,比如高胜率角色应该匹配别的高胜率角色。 * 使用分页列出[所有角色](http://www.newedenfaces.com/#browse)。 * 将图片存储到Amazon S3或者MongoDB [GridFS](http://docs.mongodb.org/manual/core/gridfs/)来避免每次都请求EVE Online API。 * 研发图片处理算法,以拒绝添加新角色的[默认角色形象](http://image.eveonline.com/Character/1_512.jpg)。 * 每进行X轮后自动重置统计。 * 在角色资料页面显示投票历史。 * 一个归档页面,以显示之前几轮投票的Top 100人物角色。 从我发布的[TV Show Tracker](http://sahatyalkabov.com/create-a-tv-show-tracker-using-angularjs-nodejs-and-mongodb/)教程所收到的邮件,我很高兴看到这些文章几乎对所有水平的人都有用。无论对刚开始编程的初学者,还是对资深的JavaScript专家,或者两者中间的人。 最后是我的一些学习经验,送给那些还在迷茫的人。 如果你还在迷茫是否要学习JavaScript: * 相信我,我也经历过这个阶段。在学校学习C++和Java后,我难以理解JS中的异步和回调那一套东西。我曾经感到如此愤怒和挫败,以致于我以为以后再也不会用JavaScript了。当然,最后我还是学会了它。这里的技巧是,不要假装你会JavaScript,而是以一个开放的心态从头开始扎实的学习它。 如果你还在迷茫是否要学习和使用ES6: * 我曾经讨厌ES6.它根本不像我在过去2-3年里逐渐爱上的JavaScript。尽管ES6从很大程度上不过是一套语法糖,但它对我来说就像外星人一样。你需要的是给它一些时间,最终你会爱上它的。并且,不管你喜不喜欢它,它就是JavaScript发展的方向。 如果你还在迷茫是否要使用React: * 我记得第一次使用React时的想法是:“这些HTML跑我JavaScript里干嘛?去死吧,我还是坚持用AngularJS。”不过现在是2015年了,我想不用花时间去说服你React是一个很棒的库了。1年前还没什么人用它,但是现在瞅瞅这个[使用React的网站](https://github.com/facebook/react/wiki/Sites-Using-React)的长长的列表。React并不需要你用一种新思维方式去构建应用,而一旦你跨过最初的学习障碍,使用React构建应用其实是很有意思的。我读过很多React和Flux教程,但老实说,直到我开始构建自己的应用,我才真正的理解了它。这里我只想再次重复我的想法:搭建一个小项目是学习任何技术最好的方式,而不是被动的阅读一堆教程和书籍,也不是观看录屏或教学视频。 如果你还在为如何学习编程而挣扎: * 你应该学习如何坚持,并且应对学习路上一定会产生的沮丧和挫败感,不要放弃。如果2009年我放弃了,我也不会进入大学主修计算机专业;如果2012年我放弃了,我不会获得大学学位;如果2014年我放弃了Hacker School项目,也不会有后来的Satellizer,到现在被全世界的数千开发者使用。挣扎和挫败始终存在,特别是在这个发展特别迅速的行业。不管你怎么想,我不认为我是一个专家,我仍然和大多数人一样每天都有迷茫和挣扎。我很少有走进办公室,并且清楚知道需要做什么、怎么去做的时候,如果工作对于我来说很轻松,那说明我不再进步,该考虑换个工作了。 如果你是一个寻求建议的大学生: * 现在就开始打造你的代表作品。去创建一个GitHub账号,并且开始为开源项目做贡献或者开发你的个人项目。不要期望学校会教你市场所需要的所有技能。如果你的GPA成绩不好也不要担心,只要你拥有一个好的代表作品,或者对知名开源项目做了重大贡献,那就没什么。对GPA成绩和学校知名度过于重视的公司过于拘泥于传统,你可能不太想为它们工作,除非你也很重视这些。为生活确定一个长期目标,并且为之而努力。我今天所获得的成绩并不是因为我有多聪明或有多天才,我也并不是那些幸运儿。我能取得成绩仅仅是因为那是我想要的,并且为了获得它而持续的努力工作。 (全文完)
';

第二十步: 附加资源

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

## 第二十步: 附加资源 下面是一些我在学习React、Flux和ES6过程中找到的一些资源,大部分很用,有一些则很有趣。 | Link | Description | | --- | --- | | [Elemental UI](http://elemental-ui.com/) | 漂亮的React UI组件库,包含按钮、表单、旋钮、模态框等等 | | [Navigating the React Ecosystem](http://www.toptal.com/react/navigating-the-react-ecosystem) | 非常棒的博文,由Tomas Holas所作,探索了ES6、Generator、Babel、React、React Router、Alt、Flux、React表单、Typeahead等等,从很多层面上说它补足了这篇教程,非常推荐。 | | [A Quick Tour Of ES6](http://jamesknelson.com/es6-the-bits-youll-actually-use/) | 关于ES6新特性的追加资源,非常注重实践并且易于阅读的博文 | | [Atomic CSS](http://acss.io/) | 一个激进的新方式来设置你的App的样式。它需要花时间适应,不过一旦你适应,它的优点就显现出来了。你不用再使用CSS类,而是直接在组件中使用“原子的”CSS来设置样式。 | | [classnames](https://github.com/JedWatson/classnames) | 一个React插件用于优雅的设置类名 | | [Iso](https://github.com/goatslacker/iso) | Alt的辅助类,用于从服务器传递原始数据到客户端 |
';

第十九步:部署

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

## 第十九步:部署 现在我们的项目已经完成,而我们终于可以开始部署它了。网上有不少的托管服务提供商,不过如果你关注过我之前的项目或者教程的话,就会知道我为什么这么喜欢[Heroku](https://www.heroku.com/)了,不过其它托管商的部署流程应该和它差不太多。 让我们先在根目录创建一个.gitignore文件。然后添加下面的内容,其中大多数来自于Github的[gitignore](https://github.com/github/gitignore)仓库。 ~~~ # Logs logs *.log # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directory # Commenting this out is preferred by some people, see # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- node_modules bower_components # Users Environment Variables .lock-wscript # Project .idea *.iml .DS_Store # Compiled files public/css/* public/js/* ~~~ > 注意:我们仅签入源代码到Git中,不包括编译后的CSS和Gulp生成的JavaScript代码。 你还需要在package.json的`"scripts"`中添加下列代码: ~~~ "postinstall": "bower install && gulp build" ~~~ 因为我们没有签入编译后的CSS和JavaScript,以及第三方库,我们需要使用`postinstall`命令,让Heroku在部署后编译应用并下载Bower包,否则它将不包含main.css、vendor.js、vendor.bundle.js和bundle.js文件。 下一步,让我们在项目根目录下初始化一个新Git仓库: ~~~ $ git init $ git add .gitignore $ git commit -m 'initial commit' ~~~ 现在我们已经准备好将代码推送到Heroku了,不过,我们需要先在Heroku上新建一个应用。在新建应用后顺着下面这个页面的指南操作: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f644c4c5dca.jpg) 准备完毕后,现在运行下面的命令,这里newedenfaces是我所建应用的名称,把它替换为你在Heroku上新建的应用名称: ~~~ $ heroku git:remote -a newedenfaces ~~~ 然后,点击Settings标签,顺次点击Reveal Config Vars和Edit按钮,添加下面的环境变量,和我们在config.js中的设置相匹配: | KEY | VALUE | | --- | --- | | `MONGO_URI` | mongodb://admin:1234@ds061757.mongolab.com:61757/newedenfaces-tutorial | 上面是我为这个教程提供的沙箱数据库,但如果你想创建自己的数据库的话,可以从[MongoLab](https://mongolab.com/)或[Compose](https://www.compose.io/)甚至直接从[Heroku Addons](https://addons.heroku.com/)免费获取。 运行下面的命令,然后我们就大功告成! ~~~ $ git push heroku master ~~~ 现在,你可以从`http://<app_name>.herokuapp.com`这样的链接看到你的应用了。
';

第十八步:Stats组件

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

## 第十八步:Stats组件 我们最后一个组件非常简单,仅仅是一个包含一般统计的表格,比如角色总数,按种族、性别、总投票等等统计出来的数据。这些代码甚至都无需解释,因为它们很简单。 ### Component 在app/components新建文件*Stats.js*: ~~~ import React from 'react'; import StatsStore from '../stores/StatsStore' import StatsActions from '../actions/StatsActions'; class Stats extends React.Component { constructor(props) { super(props); this.state = StatsStore.getState(); this.onChange = this.onChange.bind(this); } componentDidMount() { StatsStore.listen(this.onChange); StatsActions.getStats(); } componentWillUnmount() { StatsStore.unlisten(this.onChange); } onChange(state) { this.setState(state); } render() { return ( <div className='container'> <div className='panel panel-default'> <table className='table table-striped'> <thead> <tr> <th colSpan='2'>Stats</th> </tr> </thead> <tbody> <tr> <td>Leading race in Top 100</td> <td>{this.state.leadingRace.race} with {this.state.leadingRace.count} characters</td> </tr> <tr> <td>Leading bloodline in Top 100</td> <td>{this.state.leadingBloodline.bloodline} with {this.state.leadingBloodline.count} characters </td> </tr> <tr> <td>Amarr Characters</td> <td>{this.state.amarrCount}</td> </tr> <tr> <td>Caldari Characters</td> <td>{this.state.caldariCount}</td> </tr> <tr> <td>Gallente Characters</td> <td>{this.state.gallenteCount}</td> </tr> <tr> <td>Minmatar Characters</td> <td>{this.state.minmatarCount}</td> </tr> <tr> <td>Total votes cast</td> <td>{this.state.totalVotes}</td> </tr> <tr> <td>Female characters</td> <td>{this.state.femaleCount}</td> </tr> <tr> <td>Male characters</td> <td>{this.state.maleCount}</td> </tr> <tr> <td>Total number of characters</td> <td>{this.state.totalCount}</td> </tr> </tbody> </table> </div> </div> ); } } export default Stats; ~~~ ### Actions 在app/actions目录新建*Stats.js*: ~~~ import alt from '../alt'; class StatsActions { constructor() { this.generateActions( 'getStatsSuccess', 'getStatsFail' ); } getStats() { $.ajax({ url: '/api/stats' }) .done((data) => { this.actions.getStatsSuccess(data); }) .fail((jqXhr) => { this.actions.getStatsFail(jqXhr); }); } } export default alt.createActions(StatsActions); ~~~ ### Store 在app/store目录新建*Stats.js*: ~~~ import {assign} from 'underscore'; import alt from '../alt'; import StatsActions from '../actions/StatsActions'; class StatsStore { constructor() { this.bindActions(StatsActions); this.leadingRace = { race: 'Unknown', count: 0 }; this.leadingBloodline = { bloodline: 'Unknown', count: 0 }; this.amarrCount = 0; this.caldariCount = 0; this.gallenteCount = 0; this.minmatarCount = 0; this.totalVotes = 0; this.femaleCount = 0; this.maleCount = 0; this.totalCount = 0; } onGetStatsSuccess(data) { assign(this, data); } onGetStatsFail(jqXhr) { toastr.error(jqXhr.responseJSON.message); } } export default alt.createStore(StatsStore); ~~~ 打开routes.js并添加新路由`/stats`。我们必须将它放在`:category`路由之前,这样它会被优先执行。 ~~~ import React from 'react'; import {Route} from 'react-router'; import App from './components/App'; import Home from './components/Home'; import AddCharacter from './components/AddCharacter'; import Character from './components/Character'; import CharacterList from './components/CharacterList'; import Stats from './components/Stats'; export default ( <Route handler={App}> <Route path='/' handler={Home} /> <Route path='/add' handler={AddCharacter} /> <Route path='/characters/:id' handler={Character} /> <Route path='/shame' handler={CharacterList} /> <Route path='/stats' handler={Stats} /> <Route path=':category' handler={CharacterList}> <Route path=':race' handler={CharacterList}> <Route path=':bloodline' handler={CharacterList} /> </Route> </Route> </Route> ); ~~~ 刷新浏览器,你应该看到如下的新Stats组件: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6449fd55f8.jpg)
';

第十七步:Top 100 组件

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

## 第十七步:Top 100 组件 这个组件使用Bootstrap的[Media控件](http://getbootstrap.com/components/#media)作为主要的界面,下面是它看起来的样子: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6447f33276.jpg) ### Component 在app/components目录新建文件*CharacterList.js*: ~~~ import React from 'react'; import {Link} from 'react-router'; import {isEqual} from 'underscore'; import CharacterListStore from '../stores/CharacterListStore'; import CharacterListActions from '../actions/CharacterListActions'; class CharacterList extends React.Component { constructor(props) { super(props); this.state = CharacterListStore.getState(); this.onChange = this.onChange.bind(this); } componentDidMount() { CharacterListStore.listen(this.onChange); CharacterListActions.getCharacters(this.props.params); } componentWillUnmount() { CharacterListStore.unlisten(this.onChange); } componentDidUpdate(prevProps) { if (!isEqual(prevProps.params, this.props.params)) { CharacterListActions.getCharacters(this.props.params); } } onChange(state) { this.setState(state); } render() { let charactersList = this.state.characters.map((character, index) => { return ( <div key={character.characterId} className='list-group-item animated fadeIn'> <div className='media'> <span className='position pull-left'>{index + 1}</span> <div className='pull-left thumb-lg'> <Link to={'/characters/' + character.characterId}> <img className='media-object' src={'http://image.eveonline.com/Character/' + character.characterId + '_128.jpg'} /> </Link> </div> <div className='media-body'> <h4 className='media-heading'> <Link to={'/characters/' + character.characterId}>{character.name}</Link> </h4> <small>Race: <strong>{character.race}</strong></small> <br /> <small>Bloodline: <strong>{character.bloodline}</strong></small> <br /> <small>Wins: <strong>{character.wins}</strong> Losses: <strong>{character.losses}</strong></small> </div> </div> </div> ); }); return ( <div className='container'> <div className='list-group'> {charactersList} </div> </div> ); } } CharacterList.contextTypes = { router: React.PropTypes.func.isRequired }; export default CharacterList; ~~~ 鉴于角色数组已经按照胜率进行了排序,我们可以使用`index + 1`(从1到100)来作为数组下标直接输出角色。 ### Actions 在app/actions目录新建*CharacterListActions.js*: ~~~ import alt from '../alt'; class CharacterListActions { constructor() { this.generateActions( 'getCharactersSuccess', 'getCharactersFail' ); } getCharacters(payload) { let url = '/api/characters/top'; let params = { race: payload.race, bloodline: payload.bloodline }; if (payload.category === 'female') { params.gender = 'female'; } else if (payload.category === 'male') { params.gender = 'male'; } if (payload.category === 'shame') { url = '/api/characters/shame'; } $.ajax({ url: url, data: params }) .done((data) => { this.actions.getCharactersSuccess(data); }) .fail((jqXhr) => { this.actions.getCharactersFail(jqXhr); }); } } export default alt.createActions(CharacterListActions); ~~~ 这里的`payload`包含React Router参数,这些参数我们将在routes.js中指定。 ~~~ <Route path=':category' handler={CharacterList}> <Route path=':race' handler={CharacterList}> <Route path=':bloodline' handler={CharacterList} /> </Route> </Route> ~~~ 比如,如果我们访问[http://localhost:3000/female/gallente/intaki](http://localhost:3000/female/gallente/intaki) ,则`payload`对象将包括下列数据: ~~~ { category: 'female', race: 'gallente', bloodline: 'intaki' } ~~~ ### Store 在app/store目录下新建文件*CharacterListStore.js*: ~~~ import alt from '../alt'; import CharacterListActions from '../actions/CharacterListActions'; class CharacterListStore { constructor() { this.bindActions(CharacterListActions); this.characters = []; } onGetCharactersSuccess(data) { this.characters = data; } onGetCharactersFail(jqXhr) { toastr.error(jqXhr.responseJSON.message); } } export default alt.createStore(CharacterListStore);/pre> 打开route.js并添加下列路由。所有内嵌路由都使用动态区段,所以不用重复输入。确保它们在路由的最后面,否则:category将会覆盖其它路由如/stats、/add和/shame。不要忘了导入CharacterList组件: ~~~ ~~~ import React from 'react'; import {Route} from 'react-router'; import App from './components/App'; import Home from './components/Home'; import AddCharacter from './components/AddCharacter'; import Character from './components/Character'; import CharacterList from './components/CharacterList'; export default ( <Route handler={App}> <Route path='/' handler={Home} /> <Route path='/add' handler={AddCharacter} /> <Route path='/characters/:id' handler={Character} /> <Route path='/shame' handler={CharacterList} /> <Route path=':category' handler={CharacterList}> <Route path=':race' handler={CharacterList}> <Route path=':bloodline' handler={CharacterList} /> </Route> </Route> </Route> ); ~~~ 下面是所有动态区段可以取的值: * `:category` — male, female, top. * `:race` — caldari, gallente, minmatar, amarr. * `:bloodline` — civire, deteis, achura, intaki, gallente, jin-mei, amarr, ni-kunni, khanid, brutor, sebiestor, vherokior. 可以看到,如果我们使用硬编码的话,将如此多的路由包含进去将使route.js变得很长很长。
';

第十六步:角色(资料)组件

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

## 第十六步:角色(资料)组件 在这一节里我们将为角色创建资料页面。它和其它组件有些不同,其不同之处在于: 1. 它有一个覆盖全页面的背景图片。 2. 从一个角色页面导航至另一个角色页面并不会卸载组件,因此,在`componentDidMount`内部的`getCharacter` action仅会被调用一次,比如它更新了URL但并不获取新数据。 ### Component 在app/components目录新建文件*Character.js*: ~~~ import React from 'react'; import CharacterStore from '../stores/CharacterStore'; import CharacterActions from '../actions/CharacterActions' class Character extends React.Component { constructor(props) { super(props); this.state = CharacterStore.getState(); this.onChange = this.onChange.bind(this); } componentDidMount() { CharacterStore.listen(this.onChange); CharacterActions.getCharacter(this.props.params.id); $('.magnific-popup').magnificPopup({ type: 'image', mainClass: 'mfp-zoom-in', closeOnContentClick: true, midClick: true, zoom: { enabled: true, duration: 300 } }); } componentWillUnmount() { CharacterStore.unlisten(this.onChange); $(document.body).removeClass(); } componentDidUpdate(prevProps) { // Fetch new charachter data when URL path changes if (prevProps.params.id !== this.props.params.id) { CharacterActions.getCharacter(this.props.params.id); } } onChange(state) { this.setState(state); } render() { return ( <div className='container'> <div className='profile-img'> <a className='magnific-popup' href={'https://image.eveonline.com/Character/' + this.state.characterId + '_1024.jpg'}> <img src={'https://image.eveonline.com/Character/' + this.state.characterId + '_256.jpg'} /> </a> </div> <div className='profile-info clearfix'> <h2><strong>{this.state.name}</strong></h2> <h4 className='lead'>Race: <strong>{this.state.race}</strong></h4> <h4 className='lead'>Bloodline: <strong>{this.state.bloodline}</strong></h4> <h4 className='lead'>Gender: <strong>{this.state.gender}</strong></h4> <button className='btn btn-transparent' onClick={CharacterActions.report.bind(this, this.state.characterId)} disabled={this.state.isReported}> {this.state.isReported ? 'Reported' : 'Report Character'} </button> </div> <div className='profile-stats clearfix'> <ul> <li><span className='stats-number'>{this.state.winLossRatio}</span>Winning Percentage</li> <li><span className='stats-number'>{this.state.wins}</span> Wins</li> <li><span className='stats-number'>{this.state.losses}</span> Losses</li> </ul> </div> </div> ); } } Character.contextTypes = { router: React.PropTypes.func.isRequired }; export default Character; ~~~ 在`componentDidMount`里我们将当前Character ID(从URL获取)传递给`getCharacter` action并且初始化Magnific Popup lightbox插件。 > 注意:我从未成功使用`ref="magnificPopup"`进行插件初始化,这也是我采用代码中方法的原因。这也许不是最好的办法,但它能正常工作。 另外你需要注意,角色组件包含一个全页面背景图片,并且在`componentWillUnmount`时移除,因为其它组件不包含这样的背景图。它又是什么时候添加上去的呢?在store中当成功获取到角色数据时。 最后值得一提的是在`componentDidUpdate`中发生了什么。如果我们从一个角色页面跳转至另一个角色页面,我们仍然处于角色组件内,它不会被卸载掉。而因为它没有被卸载,`componentDidMount`不会去获取新角色数据,所以我们需要在`componentDidUpdate`中获取新数据,只要我们仍然处于同一个角色组件且URL是不同的,比如从/characters/1807823526跳转至/characters/467078888。`componentDidUpdate`在组件的生命周期中,每一次组件状态变化后都会触发。 ### Actions 在app/actions目录新建文件*CharacterActions.js*: ~~~ import alt from '../alt'; class CharacterActions { constructor() { this.generateActions( 'reportSuccess', 'reportFail', 'getCharacterSuccess', 'getCharacterFail' ); } getCharacter(characterId) { $.ajax({ url: '/api/characters/' + characterId }) .done((data) => { this.actions.getCharacterSuccess(data); }) .fail((jqXhr) => { this.actions.getCharacterFail(jqXhr); }); } report(characterId) { $.ajax({ type: 'POST', url: '/api/report', data: { characterId: characterId } }) .done(() => { this.actions.reportSuccess(); }) .fail((jqXhr) => { this.actions.reportFail(jqXhr); }); } } export default alt.createActions(CharacterActions); ~~~ ### Store 在app/store目录新建文件*CharacterStore.js*: ~~~ import {assign, contains} from 'underscore'; import alt from '../alt'; import CharacterActions from '../actions/CharacterActions'; class CharacterStore { constructor() { this.bindActions(CharacterActions); this.characterId = 0; this.name = 'TBD'; this.race = 'TBD'; this.bloodline = 'TBD'; this.gender = 'TBD'; this.wins = 0; this.losses = 0; this.winLossRatio = 0; this.isReported = false; } onGetCharacterSuccess(data) { assign(this, data); $(document.body).attr('class', 'profile ' + this.race.toLowerCase()); let localData = localStorage.getItem('NEF') ? JSON.parse(localStorage.getItem('NEF')) : {}; let reports = localData.reports || []; this.isReported = contains(reports, this.characterId); // If is NaN (from division by zero) then set it to "0" this.winLossRatio = ((this.wins / (this.wins + this.losses) * 100) || 0).toFixed(1); } onGetCharacterFail(jqXhr) { toastr.error(jqXhr.responseJSON.message); } onReportSuccess() { this.isReported = true; let localData = localStorage.getItem('NEF') ? JSON.parse(localStorage.getItem('NEF')) : {}; localData.reports = localData.reports || []; localData.reports.push(this.characterId); localStorage.setItem('NEF', JSON.stringify(localData)); toastr.warning('Character has been reported.'); } onReportFail(jqXhr) { toastr.error(jqXhr.responseJSON.message); } } export default alt.createStore(CharacterStore); ~~~ 这里我们使用了Underscore的两个辅助函数[`assign`](http://underscorejs.org/#extendOwn)和[`contains`](http://underscorejs.org/#contains),来合并两个对象并检查数组是否包含指定值。 > 注意:在我写本教程时Babel.js还不支持[`Object.assign`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)方法,并且我觉得`contains`比相同功能的`Array.indexOf() > -1`可读性要好得多。 就像我在前面解释过的,这个组件在外观上和其它组件有显著的不同。添加`profile`类到`<body>`改变了页面整个外观和感觉,至于第二个CSS类,可能是`caldari`、`gallente`、`minmatar`、`amarr`其中的一个,将决定使用哪一个背景图片。我一般会避免与组件`render()`之外的DOM直接交互,但这里为简单起见还是允许例外一次。最后,在`onGetCharacterSuccess`方法里我们需要检查角色在之前是否已经被该用户举报过。如果举报过,举报按钮将设置为disabled。*因为这个限制很容易被绕过,所以如果你想严格对待举报的话,你可以在服务端执行一个IP检查。* 如果角色是第一次被举报,相关信息会被存储到Local Storage里,因为我们不能在Local Storage存储对象,所以我们需要先用`JSON.stringify()`转换一下。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f644556ee10.jpg) 最后,打开routes.js并且为`/characters/:id`添加一个新路由。这个路由使用了动态区段`id`来匹配任意有效Character ID,同时,别忘了导入Character组件。 ~~~ import React from 'react'; import {Route} from 'react-router'; import App from './components/App'; import Home from './components/Home'; import AddCharacter from './components/AddCharacter'; import Character from './components/Character'; export default ( <Route handler={App}> <Route path='/' handler={Home} /> <Route path='/add' handler={AddCharacter} /> <Route path='/characters/:id' handler={Character} /> </Route> ); ~~~ 刷新浏览器,点击一个角色,现在你应该能看到新的角色资料页面。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f64455ebce8.jpg) 下一节我们将介绍如何为Top100角色构建CharacterList组件,并且能够根据性别、种族、血统进行过滤。耻辱墙(Hall of Shame)同样也是该组件的一部分。
';

第十四步:Express API 路由(2/2)

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

## 第十四步:Express API 路由(2/2) 让我们回到server.js。我希望现在你已经明白下面这些路由该放在哪里——在Express中间件后面和React中间件前面。 > 注意:请理解我们这里将所有的路由都放在server.js,是为了这个教程的方便。在我工作期间所构建的仪表盘项目里,所有的路由都被拆开分散到不同的文件,并放在routes目录下面,并且,所有的路由处理程序也都被打散,分成不同的文件放到controllers目录下。 让我们以获取Home组件中两个角色的路由作为开始。 ### GET /api/characters ~~~ /** * GET /api/characters * Returns 2 random characters of the same gender that have not been voted yet. */ app.get('/api/characters', function(req, res, next) { var choices = ['Female', 'Male']; var randomGender = _.sample(choices); Character.find({ random: { $near: [Math.random(), 0] } }) .where('voted', false) .where('gender', randomGender) .limit(2) .exec(function(err, characters) { if (err) return next(err); if (characters.length === 2) { return res.send(characters); } var oppositeGender = _.first(_.without(choices, randomGender)); Character .find({ random: { $near: [Math.random(), 0] } }) .where('voted', false) .where('gender', oppositeGender) .limit(2) .exec(function(err, characters) { if (err) return next(err); if (characters.length === 2) { return res.send(characters); } Character.update({}, { $set: { voted: false } }, { multi: true }, function(err) { if (err) return next(err); res.send([]); }); }); }); }); ~~~ 别忘了在最顶部添加[Underscore.js](http://underscorejs.org/)模块,因为我们需要使用它的几个函数`_.sample()`、`_.first()`和`_.without()`。 ~~~ var _ = require('underscore'); ~~~ 我已经尽力让这段代码易于理解,所以你应该很清楚如何获取两个随机角色。它将随机选择Male或Female性别并查询数据库以获取两个角色,如果获得的角色少于2个,它将尝试用另一个性别进行查询。比如,如果我们有10个男性角色但其中9个已经被投票过了,只显示一个角色没有意义。如果无论是男性还是女性角色查询返回都不足两个角色,说明我们已经耗尽了所有未投票的角色,应该重置投票计数,通过设置所有角色的`voted:false`即可办到。 ### PUT /api/characters 这个路由和前一个相关,它会分别更新获胜的`wins`字段和失败角色的`losses`字段。 ~~~ /** * PUT /api/characters * Update winning and losing count for both characters. */ app.put('/api/characters', function(req, res, next) { var winner = req.body.winner; var loser = req.body.loser; if (!winner || !loser) { return res.status(400).send({ message: 'Voting requires two characters.' }); } if (winner === loser) { return res.status(400).send({ message: 'Cannot vote for and against the same character.' }); } async.parallel([ function(callback) { Character.findOne({ characterId: winner }, function(err, winner) { callback(err, winner); }); }, function(callback) { Character.findOne({ characterId: loser }, function(err, loser) { callback(err, loser); }); } ], function(err, results) { if (err) return next(err); var winner = results[0]; var loser = results[1]; if (!winner || !loser) { return res.status(404).send({ message: 'One of the characters no longer exists.' }); } if (winner.voted || loser.voted) { return res.status(200).end(); } async.parallel([ function(callback) { winner.wins++; winner.voted = true; winner.random = [Math.random(), 0]; winner.save(function(err) { callback(err); }); }, function(callback) { loser.losses++; loser.voted = true; loser.random = [Math.random(), 0]; loser.save(function(err) { callback(err); }); } ], function(err) { if (err) return next(err); res.status(200).end(); }); }); }); ~~~ 这里我们使用[`async.parallel`](https://github.com/caolan/async#paralleltasks-callback)来同时进行两个数据库查询,因为这两个查询并不相互依赖。不过,因为我们有两个独立的MongoDB文档,还要进行两个独立的异步操作,因此我们还需要另一个`async.parallel`。一般来说,我们仅在两个角色都完成更新并没有错误后给出一个success的响应。 ### GET /api/characters/count MOngoDB有一个内建的`count()`方法,可以返回所匹配的查询结果的数量。 ~~~ /** * GET /api/characters/count * Returns the total number of characters. */ app.get('/api/characters/count', function(req, res, next) { Character.count({}, function(err, count) { if (err) return next(err); res.send({ count: count }); }); }); ~~~ > 注意:从这个返回总数量的一次性路由上,你可能注意到我们开始与RESTful API设计模式背道而驰。很不幸这就是现实。我还没有在一个能完美实现RESTful API的项目中工作过,你可以参看Apigee写的[这篇文章](https://blog.apigee.com/detail/restful_api_design_what_about_counts)来进一步了解为什么会这样。 ### GET /api/characters/search 我上次检查时MongoDB还不支持大小写不敏感的查询,所以这里我们需要使用正则表达式,不过还好MongoDB提供了[`$regex`](http://docs.mongodb.org/manual/reference/operator/query/regex/)操作符。 ~~~ /** * GET /api/characters/search * Looks up a character by name. (case-insensitive) */ app.get('/api/characters/search', function(req, res, next) { var characterName = new RegExp(req.query.name, 'i'); Character.findOne({ name: characterName }, function(err, character) { if (err) return next(err); if (!character) { return res.status(404).send({ message: 'Character not found.' }); } res.send(character); }); }); ~~~ ### GET /api/characters/:id 这个路由是供角色资料页面使用的(我们将在下一节创建角色组件),教程最开始的图片就是这个页面。 ~~~ /** * GET /api/characters/:id * Returns detailed character information. */ app.get('/api/characters/:id', function(req, res, next) { var id = req.params.id; Character.findOne({ characterId: id }, function(err, character) { if (err) return next(err); if (!character) { return res.status(404).send({ message: 'Character not found.' }); } res.send(character); }); }); ~~~ 当我开始构建这个项目时,我大概有7-9个几乎相同的路由来检索Top 100的角色。在经过一些代码重构后我仅留下了下面这一个: ~~~ /** * GET /api/characters/top * Return 100 highest ranked characters. Filter by gender, race and bloodline. */ app.get('/api/characters/top', function(req, res, next) { var params = req.query; var conditions = {}; _.each(params, function(value, key) { conditions[key] = new RegExp('^' + value + '$', 'i'); }); Character .find(conditions) .sort('-wins') // Sort in descending order (highest wins on top) .limit(100) .exec(function(err, characters) { if (err) return next(err); // Sort by winning percentage characters.sort(function(a, b) { if (a.wins / (a.wins + a.losses) < b.wins / (b.wins + b.losses)) { return 1; } if (a.wins / (a.wins + a.losses) > b.wins / (b.wins + b.losses)) { return -1; } return 0; }); res.send(characters); }); }); ~~~ 比如,如果我们对男性、种族为Caldari、血统为Civire的Top 100角色感兴趣,你可以构造这样的URL路径: ~~~ GET /api/characters/top?race=caldari&bloodline=civire&gender=male ~~~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6442ee5332.jpg) 如果你还不清楚如何构造`conditions`对象,这段经过注释的代码应该可以解释: ~~~ // Query params object req.query = { race: 'caldari', bloodline: 'civire', gender: 'male' }; var params = req.query; var conditions = {}; // This each loop is equivalent... _.each(params, function(value, key) { conditions[key] = new RegExp('^' + value + '$', 'i'); }); // To this code conditions.race = new RegExp('^' + params.race + '$', 'i'); // /caldari$/i conditions.bloodline = new RegExp('^' + params.bloodline + '$', 'i'); // /civire$/i conditions.gender = new RegExp('^' + params.gender + '$', 'i'); // /male$/i // Which ultimately becomes this... Character .find({ race: /caldari$/i, bloodline: /civire$/i, gender: /male$/i }) ~~~ 在我们取回获胜数最多的角色后,我们会对胜率进行一个排序,不让最老的角色始终显示在前面。 ### GET /api/characters/shame 和前一个路由差不多,这个路由会取回失败最多的100个角色: ~~~ /** * GET /api/characters/shame * Returns 100 lowest ranked characters. */ app.get('/api/characters/shame', function(req, res, next) { Character .find() .sort('-losses') .limit(100) .exec(function(err, characters) { if (err) return next(err); res.send(characters); }); }); ~~~ ### POST /api/report 有些角色没有一个有效的avatar(一般是灰色轮廓),另有些角色的avatar是漆黑一片,它们在一开始就不应该添加到数据库中。但因为任何人都能添加任何角色,因此有些时候你需要从数据库移除一些异常角色。这里设置当一个角色被访问者举报4次后将被删除。 ~~~ /** * POST /api/report * Reports a character. Character is removed after 4 reports. */ app.post('/api/report', function(req, res, next) { var characterId = req.body.characterId; Character.findOne({ characterId: characterId }, function(err, character) { if (err) return next(err); if (!character) { return res.status(404).send({ message: 'Character not found.' }); } character.reports++; if (character.reports > 4) { character.remove(); return res.send({ message: character.name + ' has been deleted.' }); } character.save(function(err) { if (err) return next(err); res.send({ message: character.name + ' has been reported.' }); }); }); }); ~~~ ### GET /api/stats 最后,为角色的统计创建一个路由。是的,下面的代码可以用[`async.each`](https://github.com/caolan/async#each)或[`promises`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)来简化,不过记住,我在两年前开始创建New Eden Faces时对这些方案还不熟悉,到现在绝大部分的后端代码没怎么动过。不过即使这样,这些代码还是足够鲁棒,最少它很明确并且易读。 ~~~ /** * GET /api/stats * Returns characters statistics. */ app.get('/api/stats', function(req, res, next) { async.parallel([ function(callback) { Character.count({}, function(err, count) { callback(err, count); }); }, function(callback) { Character.count({ race: 'Amarr' }, function(err, amarrCount) { callback(err, amarrCount); }); }, function(callback) { Character.count({ race: 'Caldari' }, function(err, caldariCount) { callback(err, caldariCount); }); }, function(callback) { Character.count({ race: 'Gallente' }, function(err, gallenteCount) { callback(err, gallenteCount); }); }, function(callback) { Character.count({ race: 'Minmatar' }, function(err, minmatarCount) { callback(err, minmatarCount); }); }, function(callback) { Character.count({ gender: 'Male' }, function(err, maleCount) { callback(err, maleCount); }); }, function(callback) { Character.count({ gender: 'Female' }, function(err, femaleCount) { callback(err, femaleCount); }); }, function(callback) { Character.aggregate({ $group: { _id: null, total: { $sum: '$wins' } } }, function(err, totalVotes) { var total = totalVotes.length ? totalVotes[0].total : 0; callback(err, total); } ); }, function(callback) { Character .find() .sort('-wins') .limit(100) .select('race') .exec(function(err, characters) { if (err) return next(err); var raceCount = _.countBy(characters, function(character) { return character.race; }); var max = _.max(raceCount, function(race) { return race }); var inverted = _.invert(raceCount); var topRace = inverted[max]; var topCount = raceCount[topRace]; callback(err, { race: topRace, count: topCount }); }); }, function(callback) { Character .find() .sort('-wins') .limit(100) .select('bloodline') .exec(function(err, characters) { if (err) return next(err); var bloodlineCount = _.countBy(characters, function(character) { return character.bloodline; }); var max = _.max(bloodlineCount, function(bloodline) { return bloodline }); var inverted = _.invert(bloodlineCount); var topBloodline = inverted[max]; var topCount = bloodlineCount[topBloodline]; callback(err, { bloodline: topBloodline, count: topCount }); }); } ], function(err, results) { if (err) return next(err); res.send({ totalCount: results[0], amarrCount: results[1], caldariCount: results[2], gallenteCount: results[3], minmatarCount: results[4], maleCount: results[5], femaleCount: results[6], totalVotes: results[7], leadingRace: results[8], leadingBloodline: results[9] }); }); }); ~~~ 最后使用`aggregate()`方法的操作比较令人费解。必须承认,到这一步我也曾去寻求过帮助。在MongoDB里,聚合(aggregation)操作处理数据记录并且返回计算后的结果。在这里它通过将所有`wins`数量相加,来计算所有投票的总数。因为投票是一个零和游戏,获胜总数总是和失败总数相同,所以我们同样也可以使用`losses`数量来计算。 项目到这里基本就完成了。在教程的最后我还将给项目添加更多特性,给它稍稍扩展一下。
';

第十五步:Home组件

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

## 第十五步:Home组件 这是一个稍微简单些的组件,它唯一的职责就是显示两张图片并且处理点击事件,用于告知哪个角色胜出。 ### 组件 在components目录下新建文件*Home.js*: ~~~ import React from 'react'; import {Link} from 'react-router'; import HomeStore from '../stores/HomeStore' import HomeActions from '../actions/HomeActions'; import {first, without, findWhere} from 'underscore'; class Home extends React.Component { constructor(props) { super(props); this.state = HomeStore.getState(); this.onChange = this.onChange.bind(this); } componentDidMount() { HomeStore.listen(this.onChange); HomeActions.getTwoCharacters(); } componentWillUnmount() { HomeStore.unlisten(this.onChange); } onChange(state) { this.setState(state); } handleClick(character) { var winner = character.characterId; var loser = first(without(this.state.characters, findWhere(this.state.characters, { characterId: winner }))).characterId; HomeActions.vote(winner, loser); } render() { var characterNodes = this.state.characters.map((character, index) => { return ( <div key={character.characterId} className={index === 0 ? 'col-xs-6 col-sm-6 col-md-5 col-md-offset-1' : 'col-xs-6 col-sm-6 col-md-5'}> <div className='thumbnail fadeInUp animated'> <img onClick={this.handleClick.bind(this, character)} src={'http://image.eveonline.com/Character/' + character.characterId + '_512.jpg'}/> <div className='caption text-center'> <ul className='list-inline'> <li><strong>Race:</strong> {character.race}</li> <li><strong>Bloodline:</strong> {character.bloodline}</li> </ul> <h4> <Link to={'/characters/' + character.characterId}><strong>{character.name}</strong></Link> </h4> </div> </div> </div> ); }); return ( <div className='container'> <h3 className='text-center'>Click on the portrait. Select your favorite.</h3> <div className='row'> {characterNodes} </div> </div> ); } } export default Home; ~~~ 2015年7月27日更新:修复“Cannot read property ‘characterId’ of undefined”错误,我更新了在`handleClick()`方法里获取“失败”的Character ID。它使用[`_.findWhere`](http://underscorejs.org/#findWhere)在数组里查找“获胜”的角色对象,然后使用[`_.without`](http://underscorejs.org/#without)获取不包含“获胜”角色的数组,因为数组只包含两个角色,所以这就是我们需要的,然后使用[`_.first`](http://underscorejs.org/#first)获取数组第一个元素,也就是我们需要的对象。 鉴于角色数组只有两个元素,其实没有必要非要使用map方法不可,虽然这也能达到我们的目的。另一种做法是为`characters[0]`和`characters[1]`各自创建标记。 ~~~ render() { return ( <div className='container'> <h3 className='text-center'>Click on the portrait. Select your favorite.</h3> <div className='row'> <div className='col-xs-6 col-sm-6 col-md-5 col-md-offset-1'> <div className='thumbnail fadeInUp animated'> <img onClick={this.handleClick.bind(this, characters[0])} src={'http://image.eveonline.com/Character/' + characters[0].characterId + '_512.jpg'}/> <div className='caption text-center'> <ul className='list-inline'> <li><strong>Race:</strong> {characters[0].race}</li> <li><strong>Bloodline:</strong> {characters[0].bloodline}</li> </ul> <h4> <Link to={'/characters/' + characters[0].characterId}><strong>{characters[0].name}</strong></Link> </h4> </div> </div> </div> <div className='col-xs-6 col-sm-6 col-md-5'> <div className='thumbnail fadeInUp animated'> <img onClick={this.handleClick.bind(this, characters[1])} src={'http://image.eveonline.com/Character/' + characters[1].characterId + '_512.jpg'}/> <div className='caption text-center'> <ul className='list-inline'> <li><strong>Race:</strong> {characters[1].race}</li> <li><strong>Bloodline:</strong> {characters[1].bloodline}</li> </ul> <h4> <Link to={'/characters/' + characters[1].characterId}><strong>{characters[1].name}</strong></Link> </h4> </div> </div> </div> </div> </div> ); } ~~~ 第一张图片使用Bootstrap中的`col-md-offset-1`位移,所以两张图片是完美居中的。 注意我们在点击事件上绑定的不是`this.handleClick`,而是`this.handleClick.bind(this, character)`。简单的传递一个事件对象是不够的,它不会给我们任何有用的信息,不像文本字段、单选、复选框元素等。 [MSDN文档](https://msdn.microsoft.com/en-us/library/ff841995%28v=vs.94%29.ASPx?f=255&MSPPError=-2147217396)中的解释: ~~~ function.bind(thisArg[, arg1[, arg2[, ...]]]) ~~~ * thisARG(必须) – 使用this的一个对象,能在新函数内部指向当前对象的上下文 * arg1, arg2, … (可选) – 传递给新函数的一系列参数 简单的来说,因为我们需要在`handleClick`方法里引用`this.state`,所以需要将`this`上下文传递进去。另外我们还传递了被点击的角色对象,而不是当前的event对象。 `handleClick`方法里的`character`参数代表的是获胜的角色,因为它是被点击的那一个。因为我们仅有两个角色需要判断,所以不难分辨谁是输的那个。接下来将获胜和失败的角色Character ID传递给`Character ID` action。 ### Actions 在actions目录下新建*HomeActions.js*: ~~~ import alt from '../alt'; class HomeActions { constructor() { this.generateActions( 'getTwoCharactersSuccess', 'getTwoCharactersFail', 'voteFail' ); } getTwoCharacters() { $.ajax({ url: '/api/characters' }) .done(data => { this.actions.getTwoCharactersSuccess(data); }) .fail(jqXhr => { this.actions.getTwoCharactersFail(jqXhr.responseJSON.message); }); } vote(winner, loser) { $.ajax({ type: 'PUT', url: '/api/characters' , data: { winner: winner, loser: loser } }) .done(() => { this.actions.getTwoCharacters(); }) .fail((jqXhr) => { this.actions.voteFail(jqXhr.responseJSON.message); }); } } export default alt.createActions(HomeActions); ~~~ 这里我们不需要`voteSuccess` action,因为`getTwoCharacters`已经满足了我们的需求。换句话说,在一次成功的投票之后,我们需要从数据库获取两个新的随机角色显示出来。 ### Store 在stores目录下新建文件*HomeStore.js*: ~~~ import alt from '../alt'; import HomeActions from '../actions/HomeActions'; class HomeStore { constructor() { this.bindActions(HomeActions); this.characters = []; } onGetTwoCharactersSuccess(data) { this.characters = data; } onGetTwoCharactersFail(errorMessage) { toastr.error(errorMessage); } onVoteFail(errorMessage) { toastr.error(errorMessage); } } export default alt.createStore(HomeStore); ~~~ 下一步,让我们实现剩下的Express路由,来获取并更新Home组件中的两个角色、获得总角色数量等等。
';

第十三步:Express API 路由(1/2)

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

## 第十三步:Express API 路由(1/2) 在这一节我们将实现一个Express路由,以获取角色信息并存储进数据库。我们将使用[EVE Online API](http://wiki.eve-id.net/APIv2_Page_Index)来获取给定character name的Character ID,Race以及Bloodline。 > 注意:角色性别并不是公开数据,它需要一个API key。在我看来,让New Eden > Faces变得非常棒的是它的开放生态:用户并不需要登录即可添加、查看EVE中的角色。这也是为什么我在表单里添加了性别选项让用户自己填写的缘故,虽 > 然它的准确性的确依赖于用户的诚信。 下面的表格列出了每个路由的职责。不过,我们不会实现所有的路由,如果需要的话你可以自己实现它们。 | Route | POST | GET | PUT | DELETE | | --- | --- | --- | --- | --- | | /api/characters | 添加新角色 | 获取随机两个角色 | 更新角色投票胜负信息 | 删除所有角色 | | /api/characters/:id | N/A | 获取角色 | 更新角色 | 删除角色 | 在server.js文件前面添加下列依赖: ~~~ var async = require('async');var request = require('request'); ~~~ 我们将使用[`async.waterfall`](https://github.com/caolan/async#waterfalltasks-callback)来管理多异步操作,使用request来向EVE Online API发起HTTP请求。 将我们的第一个路由添加到Express中间件后面,在第8步创建的React中间件前面: ~~~ /** * POST /api/characters * Adds new character to the database. */ app.post('/api/characters', function(req, res, next) { var gender = req.body.gender; var characterName = req.body.name; var characterIdLookupUrl = 'https://api.eveonline.com/eve/CharacterID.xml.aspx?names=' + characterName; var parser = new xml2js.Parser(); async.waterfall([ function(callback) { request.get(characterIdLookupUrl, function(err, request, xml) { if (err) return next(err); parser.parseString(xml, function(err, parsedXml) { if (err) return next(err); try { var characterId = parsedXml.eveapi.result[0].rowset[0].row[0].$.characterID; Character.findOne({ characterId: characterId }, function(err, character) { if (err) return next(err); if (character) { return res.status(409).send({ message: character.name + ' is already in the database.' }); } callback(err, characterId); }); } catch (e) { return res.status(400).send({ message: 'XML Parse Error' }); } }); }); }, function(characterId) { var characterInfoUrl = 'https://api.eveonline.com/eve/CharacterInfo.xml.aspx?characterID=' + characterId; request.get({ url: characterInfoUrl }, function(err, request, xml) { if (err) return next(err); parser.parseString(xml, function(err, parsedXml) { if (err) return res.send(err); try { var name = parsedXml.eveapi.result[0].characterName[0]; var race = parsedXml.eveapi.result[0].race[0]; var bloodline = parsedXml.eveapi.result[0].bloodline[0]; var character = new Character({ characterId: characterId, name: name, race: race, bloodline: bloodline, gender: gender, random: [Math.random(), 0] }); character.save(function(err) { if (err) return next(err); res.send({ message: characterName + ' has been added successfully!' }); }); } catch (e) { res.status(404).send({ message: characterName + ' is not a registered citizen of New Eden.' }); } }); }); } ]); }); ~~~ > 注意:我一般在路由上面写块级注释,包括完整路径和简介,这样我就能使用查找功能(Command+F)来快速寻找路由,如下图所示: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f643ecdf702.jpg) 下面就来按部就班的看一下它是如何工作的: 1. 利用Character Name获取Character ID。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f643ed7c76e.jpg) 2. 解析XML响应。 3. 查询数据库看这个角色是否已经存在。 4. 将Character ID传递给`async.waterfall`中的下一个函数。 5. 利用Character ID获取基本的角色信息。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f643ef6d840.jpg) 6. 解析XML响应。 7. 添加新角色到数据库。 在浏览器打开[http://localhost:3000/add](http://localhost:3000/add) 并添加一些角色,你可以使用下面的名字: * Daishan Auergni * CCP Falcon * Celeste Taylor ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f643eff052d.jpg) 或者,你也可以下载这个MongoDB文件dump并将它导入到你的数据库,它包含4000以上个角色。如果你之前添加了其中的一些角色,可能会出现“duplicate key errors”错误,不用管它: * [newedenfaces.bson](https://dl.dropboxusercontent.com/u/14131013/newedenfaces.bson) 下载之后使用下面的命令将它导入到数据库: ~~~ $ mongorestore newedenfaces.bson ~~~ 鉴于我们还没有实现相关的API,现在你还不能看到总的角色数,我们将在下下节来实现。 下面让我们先创建Home组件,这是一个初始页面,上面会并排显示两个角色。 (*译者注:下面会先第15节再第14节,原文如此*)
';

第十二步:数据库模式

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

## 第十二步:数据库模式 在根目录新建目录models,然后进入目录并新建文件*character.js*: ~~~ var mongoose = require('mongoose'); var characterSchema = new mongoose.Schema({ characterId: { type: String, unique: true, index: true }, name: String, race: String, gender: String, bloodline: String, wins: { type: Number, default: 0 }, losses: { type: Number, default: 0 }, reports: { type: Number, default: 0 }, random: { type: [Number], index: '2d' }, voted: { type: Boolean, default: false } }); module.exports = mongoose.model('Character', characterSchema); ~~~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f643b4dab4b.jpg) 一个模式(schema)是你的MongoDB数据库中的数据的一个表示,你能强迫某些字段必须为特定的类型,甚至决定该字段是否必需、唯一或者仅包含指定的元素。 和抽象的模式相比,一个模型(model)是和实践更接近的对象,包含添加、删除、查询、更新数据的方法,在上面,我们创建了一个Character模型并将它暴露出来。 > 注意:为什么这个教程仍然使用MongoDB?为什么不使用MySQL、PostgreSQL、CouchDB甚至[RethinkDB](http://rethinkdb.com/)?这是因为对于要构建的应用来说,我并不真正关心数据库层到底是什么样的。我更关注在前端的技术栈,因为这是我最感兴趣的部分。MongoDB也许并适合所有的使用场景,但它是一个合适的通用数据库,并且过去3年来我和它相处良好。 这里大多数字段都能自我解释,不过`random`和`voted`也许需要更多解释: * `random` – 从`[Math.random(), 0]`生成的包含两个数字的数组,这是一个MongoDB相关的[地理](http://docs.mongodb.org/manual/applications/geospatial-indexes/)标记,为了从数据库随机抓取一些角色,我们将使用[`$near`](http://docs.mongodb.org/manual/reference/operator/query/near/)操作符,我是从StackOverflow上[Random record from MongoDB](http://stackoverflow.com/questions/2824157/random-record-from-mongodb)学到这个技巧。 * `voted` – 一个布尔值,为确定角色是否已被投票。如果不设置的话,人们可能会给同一角色反复刷票,现在当请求两个角色时,只有那些没有被投票的角色会被获取。即使有人直接使用API,已投票的角色也不会再次被投票。 回到server.js,在文件开头添加下面的代码: ~~~ var mongoose = require('mongoose'); var Character = require('./models/character'); ~~~ 为了保证一致性和系统性,我经常按照下面的顺序导入模块: 1. 核心Node.js模块——path、querystring、http 2. 第三方NPM库——mongoose、express、request 3. 应用本身文件——controllers、models、config 最后,为链接到数据库,在依赖模块和Express中间件之间添加下面的代码,它将在我们启动Express app的时候发起一个到MongoDB的连接池: ~~~ mongoose.connect(config.database); mongoose.connection.on('error', function() { console.info('Error: Could not connect to MongoDB. Did you forget to run `mongod`?'); }); ~~~ > 注意:我们将在config.js中设置数据库的hostname以避免硬编码。 在根目录新建另一个文件*config.js*: ~~~ module.exports = { database: process.env.MONGO_URI || 'localhost' }; ~~~ 它将使用一个环境变量(如果可用)或降级到localhost,这将允许我们在本地开发时使用一个hostname,而在生产环境使用另一个,同时无需修改任何代码。这种方法对于[处理OAuth客户端key和secret](https://github.com/sahat/hackathon-starter/blob/master/config/secrets.js)时特别有用。 现在让我们将它导入到server.js中: ~~~ var config = require('./config'); ~~~ 在终端中打开一个新的标签并运行`mongod`。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f643b558bcb.jpg)
';

第十一步:添加Character的组件

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

## 第十一步:添加Character的组件 这个组件包含一个简单的表单。成功或失败的消息会显示在输入框下的`help-block`里。 ### 组件 在app/components目录新建文件*AddCharacter.js*: ~~~ import React from 'react'; import AddCharacterStore from '../stores/AddCharacterStore'; import AddCharacterActions from '../actions/AddCharacterActions'; class AddCharacter extends React.Component { constructor(props) { super(props); this.state = AddCharacterStore.getState(); this.onChange = this.onChange.bind(this); } componentDidMount() { AddCharacterStore.listen(this.onChange); } componentWillUnmount() { AddCharacterStore.unlisten(this.onChange); } onChange(state) { this.setState(state); } handleSubmit(event) { event.preventDefault(); var name = this.state.name.trim(); var gender = this.state.gender; if (!name) { AddCharacterActions.invalidName(); this.refs.nameTextField.getDOMNode().focus(); } if (!gender) { AddCharacterActions.invalidGender(); } if (name && gender) { AddCharacterActions.addCharacter(name, gender); } } render() { return ( <div className='container'> <div className='row flipInX animated'> <div className='col-sm-8'> <div className='panel panel-default'> <div className='panel-heading'>Add Character</div> <div className='panel-body'> <form onSubmit={this.handleSubmit.bind(this)}> <div className={'form-group ' + this.state.nameValidationState}> <label className='control-label'>Character Name</label> <input type='text' className='form-control' ref='nameTextField' value={this.state.name} onChange={AddCharacterActions.updateName} autoFocus/> <span className='help-block'>{this.state.helpBlock}</span> </div> <div className={'form-group ' + this.state.genderValidationState}> <div className='radio radio-inline'> <input type='radio' name='gender' id='female' value='Female' checked={this.state.gender === 'Female'} onChange={AddCharacterActions.updateGender}/> <label htmlFor='female'>Female</label> </div> <div className='radio radio-inline'> <input type='radio' name='gender' id='male' value='Male' checked={this.state.gender === 'Male'} onChange={AddCharacterActions.updateGender}/> <label htmlFor='male'>Male</label> </div> </div> <button type='submit' className='btn btn-primary'>Submit</button> </form> </div> </div> </div> </div> </div> ); } } export default AddCharacter; ~~~ 现在你可以看到这些组件的一些共同点: 1. 设置组件的初始状态为store中的值。 2. 在`componentDidMount`中添加store监听者,在`componentWillUnmount`中移除。 3. 添加`onChange`方法,无论何时当store改变后更新组件状态。 `handleSubmit`方法的作用和你想的一样——处理添加新角色的表单提交。当它为真时我们能在`addCharacter` action里完成表单验证,不过这样做的话,需要我们将输入区的DOM节点传到action,因为当`nameTextField`无效时,需要focus在输入框,这样用户可以直接输入而无需点击一下输入框。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f64389a1e14.jpg) ### Actions 在app/actions目录新建*AddCharacterActions.js*: ~~~ import alt from '../alt'; class AddCharacterActions { constructor() { this.generateActions( 'addCharacterSuccess', 'addCharacterFail', 'updateName', 'updateGender', 'invalidName', 'invalidGender' ); } addCharacter(name, gender) { $.ajax({ type: 'POST', url: '/api/characters', data: { name: name, gender: gender } }) .done((data) => { this.actions.addCharacterSuccess(data.message); }) .fail((jqXhr) => { this.actions.addCharacterFail(jqXhr.responseJSON.message); }); } } export default alt.createActions(AddCharacterActions); ~~~ 当角色被成功加入数据库后触发`addCharacterSuccess`,当失败时触发`addCharacterFail`,失败的原因可能是无效的名字,或角色已经在数据库中存在了。当角色的Name字段和Gender单选框改变时由`onChange`触发`updateName`和`updateGender`,同样的,当输入的名字无效或没有选择性别时触发`invalidName`和`invalidGender`。 ### Store 在app/stores目录新建*AddCharacterStore.js*: ~~~ import alt from '../alt'; import AddCharacterActions from '../actions/AddCharacterActions'; class AddCharacterStore { constructor() { this.bindActions(AddCharacterActions); this.name = ''; this.gender = ''; this.helpBlock = ''; this.nameValidationState = ''; this.genderValidationState = ''; } onAddCharacterSuccess(successMessage) { this.nameValidationState = 'has-success'; this.helpBlock = successMessage; } onAddCharacterFail(errorMessage) { this.nameValidationState = 'has-error'; this.helpBlock = errorMessage; } onUpdateName(event) { this.name = event.target.value; this.nameValidationState = ''; this.helpBlock = ''; } onUpdateGender(event) { this.gender = event.target.value; this.genderValidationState = ''; } onInvalidName() { this.nameValidationState = 'has-error'; this.helpBlock = 'Please enter a character name.'; } onInvalidGender() { this.genderValidationState = 'has-error'; } } export default alt.createStore(AddCharacterStore); ~~~ `nameValidationState`和`genderValidationState`指向Bootstrap提供的代表验证状态的表单控件。 `helpBlock`是在输入框下显示的状态信息,如“Character has been added successfully”。 `onInvalidName`方法当Character Name字段为空时触发。如果name在EVE中不存在,将由`onAddCharacterFail`输出另一个错误信息。 最后,打开routes.js并添加新的路由`/add`,以及`AddCharacter`组件方法: ~~~ import React from 'react'; import {Route} from 'react-router'; import App from './components/App'; import Home from './components/Home'; import AddCharacter from './components/AddCharacter'; export default ( <Route handler={App}> <Route path='/' handler={Home} /> <Route path='/add' handler={AddCharacter} /> </Route> ); ~~~ 这里简单总结了从你输入角色名称开始的整个流程: 1. 触发`updateName` action,传递event对象。 2. 调用`onUpdateName` store处理程序。 3. 使用新的名称更新状态。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f64389d5e20.jpg) 在下一节,我们将实现添加和保存新character到数据库的后端代码。
';

第十步:Socke.IO – 实时用户数

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

## 第十步:Socke.IO – 实时用户数 本节我们将聚焦在服务器端的Socket.IO。 打开server.js并找到下面的代码: ~~~ app.listen(app.get('port'), function() { console.log('Express server listening on port ' + app.get('port')); }); ~~~ 用下面的代码替换上面的: ~~~ /** * Socket.io stuff. */ var server = require('http').createServer(app); var io = require('socket.io')(server); var onlineUsers = 0; io.sockets.on('connection', function(socket) { onlineUsers++; io.sockets.emit('onlineUsers', { onlineUsers: onlineUsers }); socket.on('disconnect', function() { onlineUsers--; io.sockets.emit('onlineUsers', { onlineUsers: onlineUsers }); }); }); server.listen(app.get('port'), function() { console.log('Express server listening on port ' + app.get('port')); }); ~~~ 概括的来说,当发起一个WebSocket连接,它增加`onlineUsers`数量(一个全局变量)并发布一个广播——“嘿,我现在有这么多在线访问者啦!”当某人关闭浏览器离开,`onlineUsers`数量减少并再次发布广播“嘿,有人刚刚离开了,我现在有这么多在线访问者了。” > 注意:如果你从来没用过Socket.IO,那么这个[聊天室应用](http://socket.io/get-started/chat/)教程非常适合你。 打开views目录下的index.html并添加下面的代码到其它script标签下面: ~~~ <script src="/socket.io/socket.io.js"></script> ~~~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f64367c1c3d.jpg) 刷新浏览器,然后在不同的标签页打开[http://localhost:3000](http://localhost:3000/)以模拟不同的用户连接。现在你应该能在logo的圆点上看到访问者总数了。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f643681fd35.jpg) 到目前为止,我们既没有完成前端,也没有能用的API端点。我们可以在教程前半部分专注在前端,然后在后半部分专注于后端,或者反过来。但就我个人来说,我从来没像这样构建过任何App。在开发过程中,我一般在前端和后端之间切换着来做。
';

第九步: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数。
';

第八步:React路由(服务端)

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

## 第八步:React路由(服务端) 打开*server.js*并将下面的代码粘贴到文件最前面,我们需要导入这些模块: ~~~ var swig = require('swig'); var React = require('react'); var Router = require('react-router'); var routes = require('./app/routes'); ~~~ 然后,将下面的中间件也加入到*server.js*中去,放在现有的Express中间件之后。 ~~~ app.use(function(req, res) { Router.run(routes, req.path, function(Handler) { var html = React.renderToString(React.createElement(Handler)); var page = swig.renderFile('views/index.html', { html: html }); res.send(page); }); }); ~~~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f64304be166.jpg) 这个中间件在每次请求时都会执行。这里server.js中的`Router.run`和main.js中`Router.run`的主要区别是应用是如何渲染的。 在客户端,渲染完成的HTML标记将被插入到`<div id="app"></div>`,在服务器端,渲染完成的HTML标记被发到index.html模板,然后被[Swig](http://paularmstrong.github.io/swig/)模板引擎插入到`<div id="app">{{ html|safe }}</div>`中,我选择Swig是因为我想尝试下[Jade](http://jade-lang.com/)和[Handlerbars](http://handlebarsjs.com/)之外的选择。 但我们真的需要一个独立的模板吗?为什么不直接将内容渲染到App组件呢?是的,你可以这么做,只要你能接受[违反W3C规范](https://validator.w3.org/)的HMTL标记,以及不能在组件中直接包含内嵌的script标签,比如Google Analytics。不过即便这么说,好像现在不规范的HMTL标记也不再和SEO相关了,也有一些[绕过的办法](https://github.com/hzdg/react-google-analytics/blob/master/src/index.coffee)来包含内嵌script标签,所以要怎么做看你咯,不过为了这个教程的目的,让我们还是使用Swig模板引擎吧。 在项目根目录新建目录views,进入目录并新建文件*index.html*: ~~~ <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <meta name="viewport" content="width=device-width, initial-scale=1"/> <title>New Eden Faces</title> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,900"/> <link rel="stylesheet" href="/css/main.css"/> </head> <body> <div id="app">{{ html|safe }}</div> <script src="/js/vendor.js"></script> <script src="/js/vendor.bundle.js"></script> <script src="/js/bundle.js"></script> </body> </html> ~~~ 打开两个终端界面并进入根目录,在其中一个运行`gulp`,连接依赖文件、编译LESS样式并监视你的文件变化: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f64304ed427.jpg) 在另一个界面运行`npm run watch`来启动Node.js服务器并在文件变动时自动重启服务器: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f643052d54d.jpg) 在浏览器打开[http://localhost:3000](http://localhost:3000/),现在你应该能看到React应用成功渲染了。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f643054cced.jpg) 我们坚持到现在,做了大量的工作,结果就给我们显示了一个提示信息!不过还好,最艰难的部分已经结束了,从现在开始我们可以轻松一点,专注到创建React组件并实现REST API端点。 上面的两个命令`gulp`和`npm run watch`将为我们完成脏活累活,我们不用在添加React组件后的重新编译和Express服务器重启上费心啦。
';

第七步:React路由(客户端)

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

## 第七步:React路由(客户端) 在app/components目录下新建文件*App.js*,粘贴下面的代码: ~~~ import React from 'react'; import {RouteHandler} from 'react-router'; class App extends React.Component { render() { return ( <div> <RouteHandler /> </div> ); } } export default App; ~~~ `RouteHandler`是渲染当前子路由处理器的组件,它将根据URL渲染这些组件中的一个:Home、Top100、Profile,或Add Character。 > 注意:它和AngularJS中的`<div ng-view></div>`挺相似,会将当前路由中已渲染的模板包含进主布局中。 然后,打开app目录下的*routes.js*,粘贴下面的代码: ~~~ import React from 'react'; import {Route} from 'react-router'; import App from './components/App'; import Home from './components/Home'; export default ( <Route handler={App}> <Route path='/' handler={Home} /> </Route> ); ~~~ 之所以将路由像这样嵌套,是因为我们将在`RouteHandler`的前后添加Navbar和Footer组件。不像其它组件,路由改变的时候,Navbar和Footer组件会保持不变。 最后,我们需要添加一个URL监听程序,当URL改变时渲染应用。打开App目录下的*main.js*并添加下列代码: ~~~ import React from 'react'; import Router from 'react-router'; import routes from './routes'; Router.run(routes, Router.HistoryLocation, function(Handler) { React.render(<Handler />, document.getElementById('app')); }); ~~~ > 注意:*main.js*是我们的React应用的入口点,当Browserify将整个依赖树串起来并生成最终的bundle.js时会用到,这里我们填入初始化的内容后我们基本不用再动它了。 [React Router](http://rackt.github.io/react-router/)引导route.js中的路由,将它们和URL匹配,然后执行相应的callback处理器,在这里即意味着渲染一个React组件到`<div id="app"></div>`。它是怎么知道要渲染哪个组件呢?举例来说,如果我们在`/`URL路径,那么`<Handler />`将渲染Home组件,因为我们之前已经在route.js指定这个组件了。后面我们将添加更多的路由。 另外注意,为了让URL好看点,我们使用了[`HistoryLocation`](http://rackt.github.io/react-router/#HistoryLocation)来启用HMTL History API。比如它的URL看起来会是`http://localhost:3000/add`而不是`http://localhost:3000/#add`,因为我们构建的是一个同型React应用(在客户端和服务端都能渲染),所以我们不需要用一些[非正统的方式](https://github.com/sahat/tvshow-tracker/blob/master/server.js#L343-L345)在服务器上重定向以启用这项特性,它直接就能用。 接下来让我们创建这一节最后一个React组件。在app/components目录新建文件*Home.js*,并添上内容: ~~~ import React from 'react'; class Home extends React.Component { render() { return ( <div className='alert alert-info'> Hello from Home Component </div> ); } } export default Home; ~~~ 下面应该是我们在目前所创建的所有内容。现在是你检查代码的好时候了。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f642e01ef78.jpg) 哦,还有一个,打开app目录下的alt.js并粘贴下面的代码,我将会在第9步真正用到它的时候再解释这些代码的目的。 ~~~ import Alt from 'alt'; export default new Alt(); ~~~ 现在我们只需要在后端设置一些东西,就终于能将我们的应用运行起来了。
';

第六步:Flux架构速成教程

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

## 第六步:Flux架构速成教程 Flux是Facebook为可扩展的客户端web应用开发的应用架构。它利用单向数据流补全了React组件的一些不足。Flux更应该看做一种模式而非框架,不过,这里我们将使用一个叫做Alt的Flux实现,来减少我们写脚手架代码的时间。 以前你看过这个图解吗?你能理解它吗?我就不能理解,无论我看它多少遍。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f642be3e089.jpg) 现在我对Flux比较了解了,我只能说,真是服了他们(Flux作者),能将简单的架构用如此复杂的方式展现出来。不过需要说明的是,它们的[新Flux图解](http://idlelife.org/archives/%22https://facebook.github.io/flux/docs/overview.html#structure-and-)比以前好多了。 > 有趣事实:当我刚开始写这个教程时,我决定不在这个项目中使用Flux。我实在掌握不了这个东西,还是让别人去教它吧。不过谢天谢地,在Yahoo我能在上班时间把玩不同的技术并试验它们,所以花点功夫还是学会了。老实说,不用Flux我们也能构建这个app,并且写的代码还少些,因为这个项目并没有什么复杂的内嵌组件。但我相信,做一个全栈的React app,包括服务端渲染和Flux架构,看着不同的部分是如何组合到一起的,这本身有它的价值。 与其重复Flux那抽象的[官方教程](https://facebook.github.io/flux/docs/overview.html),不如让我们来看一个真实的用例,来展示Flux是如何工作的: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f642bec00d0.jpg) * 在`componentDidMount`中,三个action被触发: ~~~ OverviewActions.getSummary(); OverviewActions.getApps(); OverviewActions.getCompanies(); ~~~ * 每一个action都创建了一个AJAX请求向服务器获取数据。 * 获取到数据后,每一个action触发另一个“success”的action,并且将数据传递给它: ~~~ getSummary() { request .get('/api/overview/summary') .end((err, res) => { this.actions.getSummarySuccess(res.body); }); } ~~~ * 同时,Overview的store(我们存储Overview组件状态的地方)监听所有“success”的action。当`getSummarySuccess`被触发后,Overview的store中的`onGetSummarySuccess`方法被调用,store被更新: ~~~ class OverviewStore { constructor() { this.bindActions(OverviewActions); this.summary = {}; this.apps = []; this.companies = []; } onGetSummarySuccess(data) { this.summary = data; } onGetAppsSuccess(data) { this.apps = data; } onGetCompaniesSuccess(data) { this.companies = data; } } ~~~ * 一旦store更新,Overview组件将会知道,因为它订阅了Overview store,当store更新/改变后,组件将会安装store中的值更新自身状态。 ~~~ class Overview extends React.Component { constructor(props) { super(props); this.state = OverviewStore.getState(); this.onChange = this.onChange.bind(this); } componentDidMount() { OverviewStore.listen(this.onChange); } onChange() { this.setState(OverviewStore.getState()) } ... } ~~~ * 此时Overview组件已经根据新数据更新完成了。 * 在上面的截图上,当从下拉菜单选择不同的日期范围,将会重复整个流程。 > 注意:Action如何命名并无规定,你可自由按照自己的习惯命名,只要它是描述性并且有意义的。 让我们暂时先忽略*Dispatcher*一会,从上面的描述你看到了一条单向的数据流吗?如果没有也没什么大不了的,当我们开始构建应用的时候你自然就明白了。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f642bf3e417.jpg) ### Flux概要 Flux事实上不过是pub/sub架构的一个时髦说法,比如,应用的数据总是单向的,并被一路上的订阅者们接收。 在写这篇教程的时候,外面已经有超过一打的Flux实现,在这些实现当中,我只用过[RefluxJS](https://github.com/spoike/refluxjs)和[Alt](http://alt.js.org/),在这两者之间,我个人比较喜欢Alt,因为它的简洁,以及作者[goatslacker](https://github.com/goatslacker)的热心、支持服务端渲染、非常棒的文档,以及积极的维护。 我强烈建议你去读一下Alt的[Getting Started](http://alt.js.org/guide/),不超过10分钟你就能基本入门了。 如果你对于该选择哪个Flux库感到迷茫,可以考虑一下Hacker News上一个叫glenjamin的家伙的[评论](https://news.ycombinator.com/item?id=9833099),他花了大量时间试图弄清到底该用哪个Flux库: > 令人郁闷的事实是:它们(Flux库)都很好。你几乎不可能因为一个Flux库而让你的应用出现问题。即使某个Flux库停止维护了也不打紧,你要知道,大多数正常的Flux实现都非常小(大约100行代码),出不了什么致命问题,即使出了我想你也能搞定。总之,redux很灵巧,但不要在试图获得完美的Flux库上浪费时间,瞅着哪个还算顺眼就拿来用,赶紧将关注点转回到你的应用上去。 现在,我们已经过了一遍ES6、React、Flux的一些基础,现在该将注意力集中到我们的应用上来了。
';

第五步: React速成教程

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

## 第五步: React速成教程 React是一个用于构建UI的JS库。你可以说它的竞争对手有AngularJS,Ember.js,Backbone和Polymer,尽管React专注的领域要小得多。React仅仅是MVC架构中的V,即视图层。 那么,React有什么特殊的呢? React的组件使用特定的声明式样式书写,不像jQuery或其它传统JS库,你不与DOM直接交互。当背后的数据改变时,React接管所有的UI更新。 React还非常快,这归功于Virtual DOM和幕后的diff算法。当数据改变时,React计算所需要操作的最少的DOM,然后高效的重新渲染组件。比如,如果页面上有10000个已经渲染的元素,但只有一个元素改变,React将仅仅更新其中一个DOM,这是React为何能高效的重新渲染整个组件的原因。 React其它令人瞩目的特性包括: * 可组合性。小的组件可以组合成大的、复杂的组件。 * 相对易于学习。需要学习的并不多,并且它不像AngularJS或Ember.js那样有庞大的文档。 * 服务端渲染。让我们能轻松的构建[同型JS应用(Isomorphic JavaScript apps)](https://medium.com/@mjackson/universal-javascript-4761051b7ae9)。 * 最有帮助的错误和警告提示,是我从未在其它JS库中见到过的。 * 组件是自包含的。标记、行为(甚至[样式](http://blog.vjeux.com/2014/javascript/react-css-in-js-nationjs.html))都在同一个地方,让组件非常易于重用。 我非常喜欢[React v0.14 Beta 1发布](http://facebook.github.io/react/blog/2015/07/03/react-v0.14-beta-1.html)中的这段话,讲了React到底是什么: > 现在我们已经清楚,React的美妙和本质与浏览器或DOM无关,我们认为React的真正基础是关于组件和元素的质朴想法:用声明式的方式来描述任何你想渲染的东西。 在进入下一步之前,推荐你先观看这个了不起的视频[React in 7 Minutes](https://egghead.io/lessons/react-react-in-7-minutes),它的作者是John Lindquist,推荐你订阅PRO以获得更多的视频教程。 另外,也可以考虑Udemy上的这个广受好评的教程——[Build Web Apps with React JS and Flux](https://www.udemy.com/learn-and-understand-reactjs),作者是Stephen Grider。它包含超过71个视频和10小时以上的内容,涵盖了React,Flux,React Router,Firebase,Imgur API和其它。 当学习React时,我最大的挑战是使用完全不同的思考方式去构建UI。这也是为什么你必须阅读[Thinking in React](https://facebook.github.io/react/docs/thinking-in-react.html)([中文版](http://reactjs.cn/react/docs/thinking-in-react.html))这个官方指南的原因。 和Thinking in React中的产品列表风格类似,如果我们将*New Eden Faces* UI分开为潜在的组件,它将会是这样: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6429e3d12e.jpg) > 注意:每个组件都应该坚持单一职责原则,如果你发现你的组件做的事情太多,也许最好将它分成子组件。不过话虽如此,我首先还是编写了一个典型的的单块组件,当它能够工作后,然后将它重构为小的子组件。 在我们的项目中,顶级App组件包含Navbar,Homepage和Footer组件,Homepage组件包含两个Character组件。 所以,无论何时你想到一个UI设计,从将它分解为不同的组件开始,并且永远考虑你的数据如何在父-子、子-父以及同胞组件间传递,否则你会遇到这样的时刻:“WTF,这个到底在React里怎么实现的?这个用jQuery实现起来简单多了……” 所以,下次你决定用React构建一个新app时,在写代码之前,先画一个这样的层次大纲图。它帮你视觉化多个组件间的关系,并可以照着它来构建组件。 React中所有的组件都有`render()`方法,它总是返回一个单一的子元素。因此,下面的返回语句是错误的,因为它返回了3个子元素。 ~~~ render() { // Invalid JSX, return ( <li>Achura</li> <li>Civire</li> <li>Deteis</li> ); } ~~~ 上面的HTML标记叫做[JSX](https://facebook.github.io/react/docs/jsx-in-depth.html)。它的语法和HTML仅有些微的不同,比如用`className`代替`class`,在我们开始开发应用的时候你将会学到它的更多内容。 当我第一眼看到这样的语法,我的第一反应就是拒绝,在JavaScript中我习惯返回布尔值、数字、字符串、对象以及函数,但绝不是这种东西。但是,JSX不过是一个语法糖。使用一个`<ul>`标签包裹上面的返回内容后,下面是不使用JSX时的模样: ~~~ render() { return React.createElement('ul', null, React.createElement('li', null, 'Achura'), React.createElement('li', null, 'Civire'), React.createElement('li', null, 'Deteis') ); } ~~~ 我相信你会同意JSX远比普通的JavaScript的可读性更好,另外,[Babel](http://babeljs.io/)对JSX有内建支持,所以我们无需做任何额外的事情即可解析它。如果你用过AngularJS中的指令(directive)那么你将会欣赏React的做法,这样你就不必同时处理两个文件——*directive.js*(负责逻辑)和*template.html*(负责展现),你可以在同一个文件里同时处理逻辑和展现了。 React中的`componentDidMount`方法和jQuery中的`$(document).ready`非常相似,这个方法仅在组件第一次渲染后运行一次(只在客户端运行),这里经常用于初始化第三方库和jQuery插件,或者连接到Socket.IO。 在`render`方法中,你将经常使用三元运算符:当数据为空时隐藏元素、根据条件注入CSS类名、根据组件的状态切换元素的展示等等。 比如下面的例子展示如果根据props值作为条件将CSS类名设为text-danger或text-success: ~~~ render() { let delta = this.props.delta ? ( <strong className={this.props.delta > 0 ? 'text-success' : 'text-danger'}> {this.props.delta} </strong> ) : null; return ( <div className='card'> {delta} {this.props.title} </div> ); } ~~~ 这里我们仅仅浅尝辄止了React的内容,但这应该已经足以展示React的一般概念和它的优点了。 React本身是非常简单并且容易掌握的,但是,当我们谈起Flux架构时,可能会有些麻烦。
';

第四步: ES6速成教程

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

## 第四步: ES6速成教程 最好的学习ES6的方法,是为每一个ES6示例提供一个等价的ES5实现。外面已经有不少介绍ES6的文章,本文将只讲其中一些。 ### Modules(Import) ~~~ // ES6 import React from 'react'; import {Route, DefaultRoute, NotFoundRoute} from 'react-router'; // ES5 var React = require('react'); var Router = require('react-router'); var Route = Router.Route; var DefaultRoute = Router.DefaultRoute; var NotFoundRoute = Router.NotFoundRoute; ~~~ 使用ES6中的[解构赋值(destructuring assignment)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment),我们能导入模块的子集,这对于像*react-router*和*underscore*这样不止输出一个函数的模块尤其有用。 需要注意的是ES6 import的优先级很高,所有的依赖模块都会在模块代码执行之前加载,也就是说,你无法像在CommonJS一样有条件的加载模块。之前我尝试在一个if-else条件里import模块,结果失败了。 想了解`import`的更多细节,可访问它的[MDN页面](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import)。 ### Modules(Export) ~~~ // ES6 function Add(x) { return x + x; } export default Add; // ES5 function Add(x) { return x + x; } module.exports = Add; ~~~ 想学习ES6模块的更多细节,这里有两篇文章[ECMAScript 6 modules](http://www.2ality.com/2014/09/es6-modules-final.html)和[Understanding ES6 Modules](http://www.sitepoint.com/understanding-es6-modules/)。 ### Classes ES6 class只不过是现有的基于原型继承机制的一层语法糖,了解这个事实之后,`class`关键字对你来说就不再像一个其它语言的概念了。 ~~~ // ES6 class Box { constructor(length, width) { this.length = length; this.width = width; } calculateArea() { return this.length * this.width; } } let box = new Box(2, 2); box.calculateArea(); // 4 // ES5 function Box(length, width) { this.length = length; this.width = width; } Box.prototype.calculateArea = function() { return this.length * this.width; } var box = new Box(2, 2); box.calculateArea(); // 4 ~~~ 另外,ES6中还可以用`extends`关键字来创建子类。 ~~~ class MyComponent extends React.Component { // Now MyComponent class contains all React component methods // such as componentDidMount(), render() and etc. } ~~~ 了解ES6 class更多信息可查看[Classes in ECMAScript 6](http://www.2ality.com/2015/02/es6-classes-final.html)这篇博文。 ### JS `var`与`let` 这两个关键字唯一的区别是,`var`的作用域在最近的函数块中,而`let`的作用域在最近的块语句中——它可以是一个函数、一个for循环,或者一个if语句块。 这里有个很好的[示例](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let),来自MDN: ~~~ var a = 5; var b = 10; if (a === 5) { let a = 4; // The scope is inside the if-block var b = 1; // The scope is inside the function console.log(a); // 4 console.log(b); // 1 } console.log(a); // 5 console.log(b); // 1 ~~~ 一般来说,`let`是块作用域,`var`是函数作用域。 ### 箭头函数(=> fat arrow) 一个箭头函数表达式与函数表达式相比有更简短的语法,以及从语法上绑定了`this`值。 ~~~ // ES6 [1, 2, 3].map(n => n * 2); // [2, 4, 6] // ES5 [1, 2, 3].map(function(n) { return n * 2; }); // [2, 4, 6] ~~~ > 注意:如果参数只有一个,圆括号是可选的,到底是否强制使用取决于你,不过有些人认为去掉括号是坏的实践,有些人则无所谓。 除了更短的语法,箭头函数还有什么用途呢? 考虑下面这个示例,它来自于我将这个项目转换为使用ES6之前的代码: ~~~ $.ajax({ type: 'POST', url: '/api/characters', data: { name: name, gender: gender } }) .done(function(data) { this.setState({ helpBlock: data.message }); }.bind(this)) .fail(function(jqXhr) { this.setState({ helpBlock: jqXhr.responseJSON.message }); }.bind(this)) .always(function() { this.setState({ name: '', gender: '' }); }.bind(this)); ~~~ 上面的每个函数都创建了自己的`this`作用域,不绑定外层`this`的话我们是无法在示例中调用`this.setState`的,因为函数作用域的`this`一般是*undefined*。 当然,它有绕过的方法,比如将`this`赋值给一个变量,比如`var self = this`,然后在闭包里用`self.setState`代替`this.setState`即可。 而使用等价的ES6代码的话,我们没有必要如此麻烦: ~~~ $.ajax({ type: 'POST', url: '/api/characters', data: { name: name, gender: gender } }) .done((data) => { this.setState({ helpBlock: data.message }); }) .fail((jqXhr) => { this.setState({ helpBlock: jqXhr.responseJSON.message }); }) .always(() => { this.setState({ name: '', gender: '' }); }); ~~~ ES6的讲解就到此为止了,下面让我们看看React,到底是什么让它如此特殊。
';

第三步:项目结构

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

## 第三步:项目结构 在public目录下创建四个目录: css,js,fonts,img。然后,下载这个[favicon.png](https://raw.githubusercontent.com/sahat/newedenfaces-react/master/public/favicon.png),也把它放到这里。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6425ce6f50.jpg) 在项目根目录,创建新目录app。 然后在app文件夹里新建四个目录:actions,components,stores,stylesheets,以及三个空文件*alt.js*,*routes.js*和*main.js*。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6426217766.jpg) 在stylesheets目录下新建文件*main.less*,我们将在里面填入样式。 回到根目录,创建新文件*bower.json*并粘贴下面的代码: ~~~ { "name": "newedenfaces", "dependencies": { "jquery": "^2.1.4", "bootstrap": "^3.3.5", "magnific-popup": "^1.0.0", "toastr": "^2.1.1" } } ~~~ > 注意:Bower是一个让你轻松下载JavaScript库的前端包管理器,通过命令行即可下载上面的库。 在终端运行`bower install`然后等待包下载并安装到bower_components目录。你能在[.bowerrc](http://bower.io/docs/config/#directory)文件改变该路径,不过本教程我们使用默认设置。 和node_modules相似,你可能不想将bower_components提交到Git仓库,但如果你不提交的话,当你部署应用的时候如何下载这些文件?我们将在教程的部署部分来解决这个问题。 复制bower_components/bootstrap/fonts下所有的字体图标(glyphicons fonts)到public/fonts目录。 下载并解压下面的背景图片到public/img目录。 * [Background Images.zip](http://sahatyalkabov.com/assets/Background%20Images.zip) > 有趣事实:三年前我使用Adobe Photoshop来创建高斯模糊效果,但它们今天能轻松的使用[CSS滤镜](http://codepen.io/aniketpant/pen/DsEve)实现。 打开*main.less*并粘贴下面的文件中的代码。鉴于代码的长度,我决定不将它放在文中。 * [main.less](https://github.com/sahat/newedenfaces-react/blob/master/app/stylesheets/main.less) 如果你以前用过[Bootstrap](http://getbootstrap.com/) CSS框架,那么你应该对里面的大部分代码都感到熟悉。 > 注意:我花了很长时间在这个UI上,调整fonts和颜色,添加精细的变换效果,如果你有时间的话,推荐在完成本教程之后继续探索一下样式的细节。 我不知道你是否知道最近的[趋势](https://speakerdeck.com/vjeux/react-css-in-js),是将样式直接放入React组件当中,但我不太确定我是否喜欢这项实践,也许当相关的工具完善之后我会喜欢吧,但在那之前我还是会使用附加的样式表文件。不过,如果你对使用模块化的CSS感兴趣,可以看看这个[css-modulesify](https://github.com/css-modules/css-modulesify)项目。 在我们开始构建React app之前,我决定先花三个章节的时间讲讲ES6、React、Flux基础,否则要想一下子全部学会它们会让人很崩溃。对我个人来说,我曾花了不少时间理解某些用ES6编写的React + Flux示例代码上,因为我相当于同时学习一个新语法、一个新框架,以及一个全新的应用架构。 由于三者的内容众多,显然我不能在一篇文章中全讲清楚,我将会只讲那些本教程中会用到的主题。
';

第二步:构建系统

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

## 第二步 构建系统 如果你经常参与web社区,那么你应该听说过[Browserify](http://browserify.org/)和[Webpack](http://webpack.github.io/)工具。如果不使用它们,你将面临手动的在HTML输入很多`<script>`标签,并且需要将JS代码放到合适的地方。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6422bd4f37.jpg) 并且,我们还不能直接在浏览器使用ES6,在将代码提供给用户之前,我们需要用Babel将它们转换为ES5代码。 我们将使用Gulp和Browserify而不用Webpack。我并不认为它们两个谁优谁劣,但我想说Gulp+Browserify比等价的Webpack文件要直观多了,我还没有在任何React boilerplate项目中看到一个易于理解的*webpack.config.js*文件。 创建文件*gulpfile.js*并粘贴下面的代码: ~~~ var gulp = require('gulp'); var gutil = require('gulp-util'); var gulpif = require('gulp-if'); var streamify = require('gulp-streamify'); var autoprefixer = require('gulp-autoprefixer'); var cssmin = require('gulp-cssmin'); var less = require('gulp-less'); var concat = require('gulp-concat'); var plumber = require('gulp-plumber'); var source = require('vinyl-source-stream'); var babelify = require('babelify'); var browserify = require('browserify'); var watchify = require('watchify'); var uglify = require('gulp-uglify'); var production = process.env.NODE_ENV === 'production'; var dependencies = [ 'alt', 'react', 'react-router', 'underscore' ]; /* |-------------------------------------------------------------------------- | Combine all JS libraries into a single file for fewer HTTP requests. |-------------------------------------------------------------------------- */ gulp.task('vendor', function() { return gulp.src([ 'bower_components/jquery/dist/jquery.js', 'bower_components/bootstrap/dist/js/bootstrap.js', 'bower_components/magnific-popup/dist/jquery.magnific-popup.js', 'bower_components/toastr/toastr.js' ]).pipe(concat('vendor.js')) .pipe(gulpif(production, uglify({ mangle: false }))) .pipe(gulp.dest('public/js')); }); /* |-------------------------------------------------------------------------- | Compile third-party dependencies separately for faster performance. |-------------------------------------------------------------------------- */ gulp.task('browserify-vendor', function() { return browserify() .require(dependencies) .bundle() .pipe(source('vendor.bundle.js')) .pipe(gulpif(production, streamify(uglify({ mangle: false })))) .pipe(gulp.dest('public/js')); }); /* |-------------------------------------------------------------------------- | Compile only project files, excluding all third-party dependencies. |-------------------------------------------------------------------------- */ gulp.task('browserify', ['browserify-vendor'], function() { return browserify('app/main.js') .external(dependencies) .transform(babelify) .bundle() .pipe(source('bundle.js')) .pipe(gulpif(production, streamify(uglify({ mangle: false })))) .pipe(gulp.dest('public/js')); }); /* |-------------------------------------------------------------------------- | Same as browserify task, but will also watch for changes and re-compile. |-------------------------------------------------------------------------- */ gulp.task('browserify-watch', ['browserify-vendor'], function() { var bundler = watchify(browserify('app/main.js', watchify.args)); bundler.external(dependencies); bundler.transform(babelify); bundler.on('update', rebundle); return rebundle(); function rebundle() { var start = Date.now(); return bundler.bundle() .on('error', function(err) { gutil.log(gutil.colors.red(err.toString())); }) .on('end', function() { gutil.log(gutil.colors.green('Finished rebundling in', (Date.now() - start) + 'ms.')); }) .pipe(source('bundle.js')) .pipe(gulp.dest('public/js/')); } }); /* |-------------------------------------------------------------------------- | Compile LESS stylesheets. |-------------------------------------------------------------------------- */ gulp.task('styles', function() { return gulp.src('app/stylesheets/main.less') .pipe(plumber()) .pipe(less()) .pipe(autoprefixer()) .pipe(gulpif(production, cssmin())) .pipe(gulp.dest('public/css')); }); gulp.task('watch', function() { gulp.watch('app/stylesheets/**/*.less', ['styles']); }); gulp.task('default', ['styles', 'vendor', 'browserify-watch', 'watch']); gulp.task('build', ['styles', 'vendor', 'browserify']); ~~~ > 如果你从未使用过Gulp,这里有一个很棒的教程《[An Introduction to Gulp.js](http://www.sitepoint.com/introduction-gulp-js/)》 下面简单介绍一下每个Gulp任务是干嘛的。 | Gulp Task | Description | | --- | --- | | `vendor` | 将所有第三方JS文件合并到一个文件 | | `browserify-vendor` | 因为性能原因,我们将NPM模块和前端模块分开编译和打包,因此每次重新编译将会快个几百毫秒 | | `browserify` | 仅将app文件编译并打包,不包括其它模块和库 | | `browserify-watch` | 包括上面的功能,并且监听文件改变,然后重新编译打包app文件 | | `watch` | 当文件改变时重新编译LESS文件 | | `default` | 运行上面所有任务并开始监听文件改变 | | `build` | 运行上面所有任务然后退出 | 下面,我们将注意力转移到项目结构上,我们将创建*gulpfile.js*需要的文件和文件夹。
';