接着上一章,脚手架已经搭建完毕,接下来便可以编写组件,先上效果图:
对苹果篮子页面进行组件切割
根据视图,我们可以将其切割为两部分
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
打开 http://localhost:9009便可以看到页面
要想在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看到苹果篮子实例成功跑起来了。