在前幾天的一篇文章中總結部分提到了學習過程中基礎的重要性。當然,并不是不支持大家學習新的框架,這篇文章就分享一下react+redux工程實例。
一直在學習研究react.js,前前后后做了幾次分享。并在我參與的公司產品私信項目也使用了這套技術棧 。學習過程期間,感覺react+redux初級DEMO不多,社區上多是用爛了的todolist教程,未免乏味。
這篇文章主要實現一個簡單的例子,難度不大,但是貫穿了react+redux基本思想。
他將會是一個連續教程,這只是第一篇,不涉及redux中間件,redux處理異步等內容,也不涉及react性能優化,不可變數據immutable.js的內容。但這些不涉及到的內容,都會隨著這個demo的復雜度一步一步提升,在后續章節有分析和使用。
整個項目的代碼,你可以在GitHub中找到。
簡介
百度經驗個人中心(WAP端)是經驗流量較多的頁面群,其中的個人定制頁面是重要的頁面之一,請用手機點擊這里查看效果,頁面的截圖如下:
功能一目了然。主要分為兩大塊:
1)可以在『選擇分類』區塊中選擇自己喜歡的經驗分類條目來訂閱。在該區塊中,我們可以點擊『換一換』按鈕來切換分類條目。
2)已選結果會再『已選分類』區塊里面展示。在『已選分類』區塊里,我們可以點擊相關經驗分類條目來取消訂閱。
在現在線上版本中,我們采用了傳統的操作DOM方式(zepto類庫)來實現這一系列交互。使用react,又是一種全新的思想。孰優孰劣,可以在結尾處大家自己總結。
好了,廢話不多說,我們馬上進入正題。
架構
為了和我們線上代碼保持一致,我采用了fisp來做工程化組織。現在社區上大多都是采用webpack,其實這些工具用哪個都一樣,解決的問題也都類似,這里不再展開。即使你不懂FIS,也不妨礙繼續閱讀。
app/ app文件夾下
action.es定義了頁面交互中dispatch的所有actions;
app.jsx是頁面入口腳本;
component.jsx定義了頁面的組件;
reducer.jsx接收action,該文件定義了所有用到的reducer。
lib + lib-nomod 文件夾下
這兩個文件夾是我們要用的框架源碼,比如react.js+redux.js等等;
該項目用到的是react15.3.1版本未壓縮版,這個版本比較穩定。
采用未壓縮版的原因是想使用react addons 的perf,因為在后續章節中,會有性能優化部分的分析;
我們知道,react和redux其實獨立存在,我們使用流行的react-redux.js庫來實現兩者的連接。
其他相關文件
其他還有 fis-conf.js文件:這是用來做fis配置的,比如打包規則,發布規則,編譯配置等;
同時,我們配置了babel來編譯es6和jsx等,還配置了autoprefixer;
server.conf是fis的附屬文件,用來做數據mock;
build.sh和BCLOUD是上線腳本相關,這里我們并不上線,只是學習react的用法。
頁面數據
我們部門后端是PHP,采用Smarty模板。這個頁面會在請求時同步給出一些數據,比如用戶信息等,輸出在模版里。我們的同步數據如截圖(由于機密性原因,數據進行了精簡、重命名和重新設計):
我們關心selectList和likedList:
1)likedList給出當前用戶已經選則的訂閱分類條目;
2)selectList給出所有可選的分類條目,一共從1-127,127個可選條目,數據格式如上。
具體實現
說了這么多,終于可以進入具體代碼層面了。如果上邊的內容你似懂非懂,也沒有關系。因為涉及了一些項目組織上的內容。下邊的內容,就是具體的代碼分析。
數據設計
react+redux開發前端的思想是頁面由數據驅動。
上邊已經分析到我們的頁面主要由兩種數據:
1)一個是selectList,我們姑且叫做選擇池數據;
2)另一個是likedList,我們叫做已選數據。
這兩處數據初始由reducer拿到,設置為容器組件的初始狀態,并由容器組件傳遞給相應展示組件。
組件設計
組件設計如下截圖:
按照react-redux思想,組件分為:
1)容器組件,負責接收數據;
2)展示(木偶)組件負責向上接收數據,根據數據展現組件UI。
其實很明顯,我們主要就是兩個展示組件,叫做:
1)SelectedBlock,負責展示用戶已選已訂閱內容;
2)SelectListBlock,展示頁面選擇池可供選擇的內容。
他們一起被套在叫做DemoApp的父組件里面。
有了以上劃分,我們有了:
1)SelectedBlock組件需要關心已選數據likedList;
2)SelectListBlock則選擇池數據和已選數據都需要關心。
你可能會問『SelectListBlock關心選擇池數據不就夠了嗎?』
但是,產品經理要求在選擇池里,當渲染用戶已選條目時,需要樣式置灰,并且在點擊已選分類條目時不在觸發action。所以選擇池SelectListBlock組件也要依賴已選數據,進而做出相應的變化。
這兩項數據由react-redux派分給容器組件,并由容器組件按需分給展示組件;
有了以上基礎,我們看最外層的DemoApp組件全部代碼:
class DemoApp extends React.Component {
constructor(props) {
super(props);
}
render() {
const { dispatch } = this.props;
return (
<div>
<SelectedBlock
likedList={this.props.likedList}
onDeleteLikeItem={(item)=>{dispatch(action.deleteLikeItem(item))}}>
</SelectedBlock>
<SelectListBlock
selectList={this.props.selectList}
likedList={this.props.likedList}
onAddLikeItem={(index, item) =>{dispatch(action.addLikeItem(index, item))}}>
</SelectListBlock>
</div>
)
}
}
我們看他的render()部分,很明顯,他平行嵌套了:
1)SelectedBlock組件,并把likedList數據作為屬性向其傳遞;
2)同時,包含了SelectListBlock,并把selectList,likedList數據作為屬性向其傳遞。
那么SelectedBlock設計如下:
class SelectedBlock extends React.Component {
constructor(props) {
super(props);
}
deleteItem(event, index) {
this.props.onDeleteLikeItem(index);
}
render() {
let likedList = this.props.likedList;
let likedListArray = [];
let likedListKey = Object.keys(likedList);
likedListKey.forEach(function(index){
likedListArray.push(likedList[index]);
})
return (
<div>
<h2>已選分類(<em id="f-num">{likedListArray.length}</em>)</h2>
<div className="selected-list" style={{overflow: 'auto'}}>
<ul className="feed-list">
{
likedListArray.length > 0 ?
likedListArray.map((item, index) => {
return (
<li style={{position: 'relative'}}>
<span>{item}</span>
<a style={deleteIconStyle}
onClick={event=>{this.deleteItem(event, likedListKey[index])}}>
</a>
</li>
)
})
:
<li className="empty-list">還沒有任何訂閱<br />請從下方選擇訂閱</li>
}
</ul>
</div>
</div>
)
}
}
我們把likedList轉換成likedListArray數組,在render()里面,直接使用map循環輸出;
當用戶刪除某一條目時,觸發deleteItem(event, index)方法,該方法向上傳遞,并在DemoApp父組件中,觸發相應action。這個刪除過程并不是一個單純組件內行為,因為這個action會使得已選數據發生變化,進而影響SelectListBlock組件。所以一系列邏輯需要在reducer中處理,處理完后重置已選數據,進而頁面更新。
SelectListBlock組件也很好理解:
class SelectListBlock extends React.Component {
constructor(props) {
super(props);
this.state = {
flag: 0
}
}
onChangeGroup(event) {
event.stopPropagation();
let flagNow = this.state.flag;
if (flagNow == 117) {
this.setState({
flag: 0
});
}
else {
this.setState({
flag: flagNow + 9
});
}
}
onSelectItem(index, item) {
var likedList = this.props.likedList;
var likedListKey = Object.keys(likedList);
if ( likedListKey.indexOf(index.toString()) >= 0 ) {
return;
}
this.props.onAddLikeItem(index, item);
}
render() {
let selectListArray = [];
for (var i in this.props.selectList) {
selectListArray.push(this.props.selectList[i])
}
let likedList = this.props.likedList;
let likedListKey = Object.keys(likedList);
return (
<div>
<h2 className="clr">
<span onClick={event=>{this.onChangeGroup(event)}}>換一換</span>
選擇分類</h2>
<ul className="feed-list clr">
{
selectListArray.slice(this.state.flag, this.state.flag+9).map((item, index)=>{
return (
<li onClick={event=>{this.onSelectItem((index + this.state.flag), item)}}
key={index + this.state.flag}>
{(likedListKey.indexOf((index + this.state.flag).toString()) >= 0 ?
<span className='disable'>{item}</span>
:
<span>{item}</span>
)}
</li>
)
})
}
</ul>
</div>
)
}
}
我們首先把后端打過來的127條可供選擇的數據轉換為selectListArray數組。這127個分類內容都對應一個index(1-127)。
在render()時候,因為頁面一次只展示9項待選項,所以我們把selectListArray用slice方法按順序切割出來9項輸出。
點擊『換一換』按鈕時,觸發onChangeGroup()方法,這個方法是個組件內方法,他負責將slice參數+9,當到127(一共127項分類)時,還原回0。
我們知道,點擊『換一換』觸發的onChangeGroup方法改變flag時,因為flag為該組件內部state,他的變化,將會引起該組件重新render(),所以數據池就會毫無壓力的切換了。
同時,我們給數據池里每一項分類都綁定onSelectItem方法,該方法會向上傳遞給父組件,由父組件發出相應action。因為這個動作將會改變已選數據,從而影響平行的SelectedBlock組件。因此需要在reducer中處理。
action設計
有了以上組件的設計,很明顯我們需要定義兩個action:
1)第一個是添加某一條目到已選分類
export const ADD_LIKE_ITEM = 'ADD_LIKE_ITEM';
對應action creator:
export function addLikeItem (index, item) {
return {
type: ADD_LIKE_ITEM,
obj: {
index: index,
item: item
}
}
}
返回action對象,包括type命名為ADD_LIKE_ITEM和負載數據:條目名item及其index。
2)另一個是在已選分類刪除某一條目:
export const DELETE_LIKE_ITEM = 'DELETE_LIKE_ITEM';
對應action creator:
export function deleteLikeItem (index) {
return {
type: DELETE_LIKE_ITEM,
index
}
}
返回action對象,包括type和負載數據。
到此為止,action腳本只需要定義action,不需要進一步處理,對所有action的處理都會由reducer接受。
reducer設計
再次強調reducer是一個純函數,他接受兩個參數,一個是state,一個是action;并對相應的action,返回一個新的state,從而促使頁面里訂閱相關state的組件再次render();
我們把同步模板數據initialLikeBlockState設為初始state:
var initialLikeBlockState = F.context('likedList');
function likeBlockReducer (state = initialLikeBlockState, action) {
switch (action.type) {
case actionType.ADD_LIKE_ITEM: {
var addIndex = action.obj.index;
var newLikedList = Object.assign({}, state, {
[addIndex]: action.obj.item
})
return newLikedList;
}
case actionType.DELETE_LIKE_ITEM: {
var newLikedList = {};
for (var key in state) {
var val = state[key];
newLikedList[key] = val;
}
var index = action.index;
delete newLikedList[index];
return newLikedList;
}
default: {
return state;
}
}
}
當匹配ADD_LIKE_ITEM action時,我們把當前的state和action帶來的數據(item,index)進行merge,從而return 一個新的已選數據狀態,即添加了新分類item的state;
當匹配DELETE_LIKE_ITEM action時,我們把action負載帶來要刪除item的index刪除掉。返回刪除該條目之后的新state。
總結
截至目前,我們介紹了基本設計和開發思路。教程里面已經基本包含了全部代碼。
對比線上已有代碼
1)和線上的zepto實現對比完全是兩種思路,經過比較,用react設計的代碼代碼量上有明顯的優勢。
2)開發思路上,是個蘿卜青菜各有所愛的問題。但是對于寫慣了$()的我來說,這種全新的開發方式還是帶來了很大的驚喜。
3)線上實現這一套邏輯,可能對于一個簡單的UI交互,我們都需要選取很多dom元素,進行處理。整體上看,比較復雜且凌亂,不是很容易進行維護。
接下來...
當然,這只是第一步。后邊還有更多的路要走。比如:
1)我們在選擇或刪除一個條目時,如何給后端發異步請求并沒有涉及。因此,redux異步流程并沒有展現。后續章節會進一步講解。
2)我們的數據都是后端模板通過同步的方式傳遞過來的,數據量也不大,結構也不復雜,因此這一章為了簡單并未使用immutable.js。當然,后續章節會進一步講解。
3)這里我并沒有介紹使用redux dev tool,這真的是一個很漂亮的利器。
尤其在數據復雜時候,對于調試能幫上很大作用。后面我會單獨介紹一下關于這個工具的使用。
4)最后,這么簡單的交互還并不會涉及頁面性能的問題。在后續章節,我會構造出極端CASE進行一些邊緣測試,并使用一些方法結合chrome dev tool進行性能優化,請進一步關注。