【React】React全棧腳手架搭建-蘋果籃子示例
接著上一章,腳手架已經搭建完畢,接下來便可以編寫元件,先上效果圖:
對蘋果籃子頁面進行元件切割
根據檢視,我們可以將其切割為兩部分
AppleItem:裡面的蘋果列表項
AppleBasket:包著蘋果列表項的籃子
解析蘋果籃子的資料結構,應該有一個蘋果列表 apples(每個蘋果具有id,重量,是否被吃掉的標誌),是否在採摘蘋果狀態標記等,如下
const basket = { isPicker:false, apples:[ { id:1, weight:230, isEaten:false }, { id:2, weight:120, isEaten:true }, ...... ] }
看一下我們的專案結構:紅色圈起來的是這次元件編寫需要動到的部分
具體元件如何編寫,直接貼出程式碼
AppleItem:這是一個用於展示蘋果的列表項,props只需傳遞進來具體一個蘋果的資料apple和吃掉蘋果的處理函式eatApple
import React from 'react'; import './index.less'; import '../../styles/common.less'; import appleImg from '../../assets/apple.png' class AppleItem extends React.Component { render(){ let {apple,eatApple} = this.props; return( <div className="apple-item row"> <div className='row'> <div className="apple"><img src={appleImg} alt="蘋果圖片"/></div> <div className="info"> <div className="name">紅蘋果 - {apple.id}號</div> <div className="weight">{apple.weight}克</div> </div> </div> <div className="btn-div"><button onClick={eatApple.bind(this,apple.id)}>吃掉</button></div> </div> ) } } export default AppleItem;
AppleBasket:這個稍微複雜點,但是也沒那麼複雜。props接收外界傳遞進來的就是整個蘋果籃子的資料,以及所有的蘋果處理函式的集合
拿到 整個蘋果籃子的資料appleBasket,跟檢視的資料結構是不符合,所以就需要將其轉化為我們檢視所需要需要的資料結構。map遍歷appleBasket.apples,統計已吃跟未吃的蘋果數量,重量,將未吃蘋果push進新陣列。未知蘋果的陣列就是我們要的資料。
import React from 'react'; import './index.less'; import '../../styles/common.less' import AppleItem from '../AppleItem'; class AppleBasket extends React.Component { render(){ const {appleBasket,actions} = this.props; let stats = { isPicker:appleBasket.isPicker, eat:{num:0,weight:0}, noeat:{num:0,weight:0}, apples:[] } appleBasket.apples.map(elem => { let name = elem.isEaten?"eat":"noeat"; stats[name].num++; stats[name].weight += elem.weight; if (!elem.isEaten) { stats.apples.push(elem); } }) function getNoApple(){ if (stats.apples.length===0 && !stats.isPicker) { return <div className='no-apple'>籃子空空如也,快去摘蘋果吧</div> } return; } const that = this; function getApples(){ let data = []; if (!stats.isPicker) { data.push(stats.apples.map((apple,index) => <AppleItem key={index} apple={apple} eatApple={that.props.actions.eatApple}/> )) }else{ return <div className='no-apple'>正在採摘蘋果...</div> } return data; } return( <div className="apple-basket"> <div className="title">蘋果籃子</div> <div className="stats row"> <div className='col current'> <div className="label">當前</div> <div><span>{stats.noeat.num}</span>個蘋果,<span>{stats.noeat.weight}</span>克</div> </div> <div className='col eat'> <div className="label">已吃掉</div> <div><span>{stats.eat.num}</span>個蘋果,<span>{stats.eat.weight}</span>克</div> </div> </div> <div className="apple-list col"> {getApples()} {getNoApple()} </div> <div className="btn-panel row"><button className={stats.isPicker ? 'disabled' : ''} onClick={actions.pickApple}>摘蘋果</button></div> </div> ) } } export default AppleBasket;
元件的編寫就完成了。寫完元件,可能有人會疑問,那我的props資料從哪裡來,我怎麼展示我已經寫好的元件,這時候就需要我們寫一個容器級(或頁面級)元件,然後將應用的state作為props傳遞進去給子元件,當然,這些都是後話。就算沒有這些操作,我們有storybook,這是一個可以對元件進行單元測試的有利工具。
啟動storybook:npm run storybook
要想在storybook看到我們編寫的元件就需要在 stories/index檔案裡面add我們的元件
語法十分簡單,模仿storybook提供的demo(Welcome/Button),照貓畫虎,很容易就可以引入我們的元件
storiesOf('Apple',module)
.add("AppleItem",()=>(<AppleItem apple={apple} eatApple={action("eatApple")}/>))
.add("AppleBasket",()=>(<AppleBasket appleBasket={basket} actions={appleActions}/>))
需要傳遞給元件的props資料需要我們事先編寫,可以理解為我們的測試資料。最終完整的引入如下
import AppleItem from '../src/components/AppleItem/index'
import AppleBasket from '../src/components/AppleBasket/index'
const basket = {
isPicker:false,
apples:[
{
id:1,
weight:230,
isEaten:false
},
{
id:2,
weight:120,
isEaten:true
},
{
id:3,
weight:290,
isEaten:false
},
{
id:4,
weight:118,
isEaten:false
},
{
id:5,
weight:280,
isEaten:true
}
]
}
const apple = {id:3,weight:280,isEaten:false};
const appleActions = {
eatApple: (id) => action("eatApple")(id),
pickApple: () => action("pickApple")('摘蘋果')
};
storiesOf('Apple',module)
.add("AppleItem",()=>(<AppleItem apple={apple} eatApple={action("eatApple")}/>))
.add("AppleBasket",()=>(<AppleBasket appleBasket={basket} actions={appleActions}/>))
元件的編寫便告一段落了
接下來就是如何在我們的頁面裡面展示蘋果籃子例項以及資料處理 redux
2. 蘋果籃子 redux處理
2.1 整個應用就是store,而應用資料的來源就是store裡面的state,redux的作用就是處理state。redux主要分為兩部分:
actions:actions分同步action跟非同步action。同步action返回一個物件,非同步action返回一個函式,在函式裡面進行非同步請求
reducers:根據actions型別的不同,分別進入到不同的處理函式,返回新的state
使用者不能直接修改資料,只能觸發通過觸發action來修改資料,action就是一個定義好的普通物件,type表示動作型別,payload用來負載使用者觸發action攜帶的資料。
{
type:'PICK_APPLE',
payload:445
}
2.2 actions
蘋果籃子例項,分別有吃蘋果,摘蘋果的動作,對應有 eatApple / pickApple 的action,還需注意有蘋果資料初始化的隱藏action
在這裡我將 pickApple作為非同步處理,即摘到的蘋果資料由後臺返回,將eatApple做同步處理。這樣通過比較同步和非同步action就能很清楚的知道兩者的區別。
同步action直接返回物件即可。但是非同步action需要進行action切割:通知非同步開始的action,非同步成功的action,非同步失敗的action。
最終程式碼如下
import Apples from '../services'; //Apples 是已經封裝好的非同步請求介面
let actions = {
initApplesStart:function(){
return function(dispatch,getState){
Apples.init().then(function(res){
dispatch(actions.initApplesSuccess(res));
}, function(err){
dispatch(actions.initApplesFail(err));
});
}
},
initApplesSuccess:(data)=>({
type:'apple/INIT_APPLE_SUCCESS',
payload:data
}),
initApplesFail:(data)=>({
type:'apple/INIT_APPLE_FAIL',
payload:data
}),
eatApple:(id) => ({
type:'apple/EAT_APPLE',
payload:id
}),
pickApple:function(){
return function(dispatch,getState){
if(getState().appleBasket.isPicker){
return;
}
dispatch(actions.beginPickApple());
Apples.pick().then(function(res){
dispatch(actions.donePickApple(res.apples));
}, function(err){
dispatch(actions.donePickApple(err));
});
}
},
beginPickApple:() => ({
type:'apple/BEGIN_PICK_APPLE'
}),
donePickApple:appleWeight => ({
type:'apple/DONE_PICK_APPLE',
payload:appleWeight
}),
failPickApple:err => ({
type:'apple/FAIL_PICK_APPLE',
payload:err
})
}
export default actions;
2.3 reducers
reducers相對來說,簡單點,其接收action,通過判斷action型別的不同對state做不同的處理,然後返回新的state即可
const initialState = {
isPicker:false,
apples:[]
};
const appleReducer = (state=initialState,action) =>{
switch (action.type) {
case 'apple/INIT_APPLE_SUCCESS':
state = action.payload;
return {...state};
case 'apple/INIT_APPLE_FAIL':
return state;
case 'apple/EAT_APPLE':
state.apples.map(elem => {
if (elem.id===action.payload) {
elem.isEaten = true;
}
})
return {...state};
case 'apple/BEGIN_PICK_APPLE':
state.isPicker = true;
return {...state};
case 'apple/DONE_PICK_APPLE':
state.isPicker = false;
state.apples.push(...action.payload);
return {...state};
case 'apple/FAIL_PICK_APPLE':
state.isPicker = false;
return {...state};
default:
return state;
}
}
export default appleReducer;
2.4 通常我們的應用不止一個 reducers,這時候需要將reducers進行整合。
import { combineReducers } from 'redux';
import appleReducer from './appleReducer';
//import todoReducers from './todoReducers';
const rootReducer = combineReducers({
// todoItems:todoReducers,
appleBasket: appleReducer
});
export default rootReducer;
2.5 store
有了 reducers就可以建立 store了
在入口檔案 src/index.js裡createStore,然後將store注入我們的應用裡面即可
import React from 'react';
import ReactDOM from 'react-dom';
import {createStore,applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import registerServiceWorker from './registerServiceWorker';
import reducer from './redux/reducers'
import Root from './pages/Root'
let store = createStore(reducer,applyMiddleware(thunk))
ReactDOM.render(
<Root store={store}/>,
document.getElementById('root')
);
registerServiceWorker();
2.6 編寫頁面級元件 Apples.js
通過 redux的connect函式可以將state對映到props,將actions和dispatch對映到props
import React from 'react'
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import AppleBasket from '../components/AppleBasket/index';
import actions from '../redux/actions/appleAction';
class Apples extends React.Component {
constructor(props){ //初始化資料
super(props);
this.props.dispatch(this.props.actions.initApplesStart)
}
render(){
let {appleBasket,actions,dispatch} = this.props;
return(
<div className="apples">
<AppleBasket appleBasket={appleBasket} actions={actions}/>
</div>
)
}
}
const mapStateToProps = state => ({
appleBasket: state.appleBasket
});
const mapDispatchToProps = dispatch => ({
dispatch: dispatch,
actions: bindActionCreators(actions, dispatch)
});
export default connect(mapStateToProps, mapDispatchToProps)(Apples);
2.7 編寫專案根元件 root
import React from 'react';
import {Provider} from 'react-redux';
import '../styles/common.less'
import Apples from '../pages/Apples'
export default class Root extends React.Component{
render(){
const { store } = this.props;
return(
<Provider store={store}>
<Apples/>
</Provider>
)
}
}
最終 npm run start
可以在 localhost:3000看到蘋果籃子例項成功跑起來了。