1. 程式人生 > >【React】React全棧腳手架搭建-蘋果籃子示例

【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看到蘋果籃子例項成功跑起來了。