在前幾天的一篇文章中總結部分提到了學習過程當中基礎的重要性。固然,並非不支持你們學習新的框架,這篇文章就分享一下react+redux工程實例。javascript
一直在學習研究react.js,前先後後作了幾回分享。並在我參與的公司產品私信項目也使用了這套技術棧 。學習過程期間,感受react+redux初級DEMO很少,社區上可能是用爛了的todolist教程,未免乏味。前端
這篇文章主要實現一個簡單的例子,難度不大,可是貫穿了react+redux基本思想。
他將會是一個連續教程,這只是第一篇,不涉及redux中間件,redux處理異步等內容,也不涉及react性能優化,不可變數據immutable.js的內容。但這些不涉及到的內容,都會隨着這個demo的複雜度一步一步提高,在後續章節有分析和使用。java
整個項目的代碼,你能夠在GitHub中找到。react
百度經驗我的中心(WAP端)是經驗流量較多的頁面羣,其中的我的定製頁面是重要的頁面之一,請用手機點擊這裏查看效果,頁面的截圖以下:webpack
功能一目瞭然。主要分爲兩大塊:
1)能夠在『選擇分類』區塊中選擇本身喜歡的經驗分類條目來訂閱。在該區塊中,咱們能夠點擊『換一換』按鈕來切換分類條目。
2)已選結果會再『已選分類』區塊裏面展現。在『已選分類』區塊裏,咱們能夠點擊相關經驗分類條目來取消訂閱。git
在如今線上版本中,咱們採用了傳統的操做DOM方式(zepto類庫)來實現這一系列交互。使用react,又是一種全新的思想。孰優孰劣,能夠在結尾處你們本身總結。
好了,廢話很少說,咱們立刻進入正題。es6
爲了和咱們線上代碼保持一致,我採用了fisp來作工程化組織。如今社區上大多都是採用webpack,其實這些工具用哪一個都同樣,解決的問題也都相似,這裏再也不展開。即便你不懂FIS,也不妨礙繼續閱讀。github
action.es定義了頁面交互中dispatch的全部actions;
app.jsx是頁面入口腳本;
component.jsx定義了頁面的組件;
reducer.jsx接收action,該文件定義了全部用到的reducer。web
這兩個文件夾是咱們要用的框架源碼,好比react.js+redux.js等等;
該項目用到的是react15.3.1版本未壓縮版,這個版本比較穩定。
採用未壓縮版的緣由是想使用react addons 的perf,由於在後續章節中,會有性能優化部分的分析;
咱們知道,react和redux其實獨立存在,咱們使用流行的react-redux.js庫來實現二者的鏈接。chrome
其餘還有 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:
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是一個純函數,他接受兩個參數,一個是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進行性能優化,請進一步關注。