在這一節(jié)里我們將為角色創(chuàng)建資料頁(yè)面。它和其它組件有些不同,其不同之處在于:
componentDidMount
內(nèi)部的getCharacter
action僅會(huì)被調(diào)用一次,比如它更新了URL但并不獲取新數(shù)據(jù)。在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
里我們將當(dāng)前Character ID(從URL獲取)傳遞給getCharacter
action并且初始化Magnific Popup lightbox插件。
注意:我從未成功使用
ref="magnificPopup"
進(jìn)行插件初始化,這也是我采用代碼中方法的原因。這也許不是最好的辦法,但它能正常工作。
另外你需要注意,角色組件包含一個(gè)全頁(yè)面背景圖片,并且在componentWillUnmount
時(shí)移除,因?yàn)槠渌M件不包含這樣的背景圖。它又是什么時(shí)候添加上去的呢?在store中當(dāng)成功獲取到角色數(shù)據(jù)時(shí)。
最后值得一提的是在componentDidUpdate
中發(fā)生了什么。如果我們從一個(gè)角色頁(yè)面跳轉(zhuǎn)至另一個(gè)角色頁(yè)面,我們?nèi)匀惶幱诮巧M件內(nèi),它不會(huì)被卸載掉。而因?yàn)樗鼪](méi)有被卸載,componentDidMount
不會(huì)去獲取新角色數(shù)據(jù),所以我們需要在componentDidUpdate
中獲取新數(shù)據(jù),只要我們?nèi)匀惶幱谕粋€(gè)角色組件且URL是不同的,比如從/characters/1807823526跳轉(zhuǎn)至/characters/467078888。componentDidUpdate
在組件的生命周期中,每一次組件狀態(tài)變化后都會(huì)觸發(fā)。
在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);
在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的兩個(gè)輔助函數(shù)assign
和contains
,來(lái)合并兩個(gè)對(duì)象并檢查數(shù)組是否包含指定值。
注意:在我寫(xiě)本教程時(shí)Babel.js還不支持
Object.assign
方法,并且我覺(jué)得contains
比相同功能的Array.indexOf() > -1
可讀性要好得多。
就像我在前面解釋過(guò)的,這個(gè)組件在外觀上和其它組件有顯著的不同。添加profile
類(lèi)到<body>
改變了頁(yè)面整個(gè)外觀和感覺(jué),至于第二個(gè)CSS類(lèi),可能是caldari
、gallente
、minmatar
、amarr
其中的一個(gè),將決定使用哪一個(gè)背景圖片。我一般會(huì)避免與組件render()
之外的DOM直接交互,但這里為簡(jiǎn)單起見(jiàn)還是允許例外一次。最后,在onGetCharacterSuccess
方法里我們需要檢查角色在之前是否已經(jīng)被該用戶(hù)舉報(bào)過(guò)。如果舉報(bào)過(guò),舉報(bào)按鈕將設(shè)置為disabled。因?yàn)檫@個(gè)限制很容易被繞過(guò),所以如果你想嚴(yán)格對(duì)待舉報(bào)的話,你可以在服務(wù)端執(zhí)行一個(gè)IP檢查。
如果角色是第一次被舉報(bào),相關(guān)信息會(huì)被存儲(chǔ)到Local Storage里,因?yàn)槲覀儾荒茉贚ocal Storage存儲(chǔ)對(duì)象,所以我們需要先用JSON.stringify()
轉(zhuǎn)換一下。
更多建議: