## 第十六步:角色(资料)组件
在这一节里我们将为角色创建资料页面。它和其它组件有些不同,其不同之处在于:
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://box.kancloud.cn/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://box.kancloud.cn/2015-09-14_55f64455ebce8.jpg)
下一节我们将介绍如何为Top100角色构建CharacterList组件,并且能够根据性别、种族、血统进行过滤。耻辱墙(Hall of Shame)同样也是该组件的一部分。
- 前言
- 概述
- 第一步:新建Express项目
- 第二步:构建系统
- 第三步:项目结构
- 第四步: ES6速成教程
- 第五步: React速成教程
- 第六步:Flux架构速成教程
- 第七步:React路由(客户端)
- 第八步:React路由(服务端)
- 第九步:Footer和Navbar组件
- 第十步:Socke.IO – 实时用户数
- 第十一步:添加Character的组件
- 第十二步:数据库模式
- 第十三步:Express API 路由(1/2)
- 第十五步:Home组件
- 第十四步:Express API 路由(2/2)
- 第十六步:角色(资料)组件
- 第十七步:Top 100 组件
- 第十八步:Stats组件
- 第十九步:部署
- 第二十步: 附加资源
- 总结