redux 入門到實踐

前言

以前沒太理解redux,在使用時老是照葫蘆畫瓢,看項目裏別人如何使用,本身就如何使用,這一次完全學習了下官方文檔,記錄。css

在學習redux初時,有三個概念須要瞭解。react

  • action
  • reducer
  • store

Action

類型是一個Object 更改storestate的惟一方法,它經過store.dispatchaction傳到storegit

一個簡單的actiones6

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}
複製代碼
dispatch(addTodo(text))
複製代碼

Reducer

根據action,來指定store中的state如何改變。github

store

存儲stateshell

store.getState();
複製代碼
  • 提供getState()方法獲取state
  • 提供dispatch(action)更新state
  • subscribe(listener)來註冊、取消監聽器

更新store的步驟

1.建立action,action中必需要有type 2.建立reducer,根據action中的type來更新store中的state 3.初始化storeexpress

理解不可變性

在reducer更新state時,不能改變原有的state,只能從新建立一個新的state。這裏提供了幾個方法能夠來建立一個不一樣的對象。redux

  • 使用immutable-js建立不可變的數據結構
  • 使用JavaScript庫(如Loadsh)來執行不可變的操做
  • 使用ES6語法執行不可變操做

以前並不瞭解immutable-js,因此仍是使用es6的語法來執行不可變操做。segmentfault

let a = [1, 2, 3];              // [1, 2, 3]
let b = Object.assign([], a);   // [1, 2, 3]

// a !== b
複製代碼

上面和下面是相同的api

// es6語法
let a = [1,  2, 3]; // [1, 2, 3]
let b = [...a];     // [1, 2, 3]

// a !== b
複製代碼

初始化store

在建立store時要將注意傳入開發者工具相關參數

import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import { createLogger } from 'redux-logger'
import api from '../middleware/api'
import rootReducer from '../reducers'
import DevTools from '../containers/DevTools'

const configureStore = preloadedState => {
  const store = createStore(
    rootReducer,
    preloadedState,
    compose(
      applyMiddleware(thunk, api, createLogger()),
      DevTools.instrument()
    )
  )
  
  // ..省略相關代碼
  return store
}

export default configureStore
複製代碼

createStore

參數

  • reducer (Function,必選):用於返回新的state,給出當前的stateaction
  • preloadedState (Any,可選):初始化state, 你能夠選擇將其指定爲通用應用程序中的服務器狀態,或者還原之前序列化的用戶會話,若是使用combineReducers生成reducer,則它必須是一個普通對象,其形狀與傳遞給它的鍵相同。不然,您能夠自由地傳遞reducer只要可以理解。
  • enhancer (Function,可選),能夠指定它使用第三方功能加強store,例如中間件等等。隨Redux一塊兒提供的enhancer只有applyMiddleware(),傳入的enhancer只能是一個。

返回值

(Store): 保存應用完整state的對象,只要dispatching actions才能改變它的state。你能夠用subscribestate的改變來更新UI。

Tips

  • 最多建立一個store在一個應用當中,使用combineReducers來建立根reducer
  • 你能夠選擇狀態的格式,能夠選擇普通對象或相似Immutable,若是不肯定,先從普通對象開始
  • 若是state是個普通對象,請肯定永遠不要改變它,例如從reducers返回對象時,不要使用Object.assign(state, newData),而是返回Object.assign({}, state, newData)。這樣就不會覆蓋之前的狀態,或者使用return {...state, ...newData}
  • 要使用多個enhancer可使用compose()
  • 建立store時,Redux會發送一個虛擬的action用來初始化storestate,初始化時第一個參數未定義,那麼store的state會返回undefined

Enhancer

加強器

Middleware

官方文檔中有提到,中間件是用來包裝dispatch

這裏看一個官方的例子,從這個例子中就能夠看到,傳入參數是action,隨後能夠對這個action進行一些操做。

import { createStore, applyMiddleware } from 'redux'
import todos from './reducers'

function logger({ getState }) {
  return next => action => {
    console.log('will dispatch', action)

    // Call the next dispatch method in the middleware chain.
    const returnValue = next(action)

    console.log('state after dispatch', getState())

    // This will likely be the action itself, unless
    // a middleware further in chain changed it.
    return returnValue
  }
}

const store = createStore(todos, ['Use Redux'], applyMiddleware(logger))

store.dispatch({
  type: 'ADD_TODO',
  text: 'Understand the middleware'
})
// (These lines will be logged by the middleware:)
// will dispatch: { type: 'ADD_TODO', text: 'Understand the middleware' }
// state after dispatch: [ 'Use Redux', 'Understand the middleware' ]
複製代碼

使用applyMiddleware參數可使多箇中間件,最後返回的是一個enhancer

相關提示
  • 有一些中間件可能只在某個特定環境下使用,好比日誌中間件,可能在生成環境就不須要了。須要注意引用。
let middleware = [a, b]
if (process.env.NODE_ENV !== 'production') {
  const c = require('some-debug-middleware')
  const d = require('another-debug-middleware')
  middleware = [...middleware, c, d]
}

const store = createStore(
  reducer,
  preloadedState,
  applyMiddleware(...middleware)
)
複製代碼

Provider與connect

須要額外安裝

yarn add react-redux
複製代碼

provider和connect必須一塊兒使用,這樣store能夠做爲組件的props傳入。關於Providerconnect,這裏有一篇淘寶的文章能夠看下Provider和connect

大體使用以下,在root container當中,會加入Provider

const App = () => {
  return (
    <Provider store={store}> <Comp/> </Provider>
  )
};
複製代碼

在根佈局下的組件當中,須要使用到connect

mapStateToProps

connect方法第一個參數mapStateToProps是能夠將store中的state變換爲組件內部的props來使用。

const mapStateToProps = (state, ownProps) => {
  // state 是 {userList: [{id: 0, name: '王二'}]}
  // 將user加入到改組件中的props當中
  return {
    user: _.find(state.userList, {id: ownProps.userId})
  }
}

class MyComp extends Component {
  
  static PropTypes = {
    userId: PropTypes.string.isRequired,
    user: PropTypes.object
  };
  
  render(){
    return <div>用戶名:{this.props.user.name}</div>
  }
}

const Comp = connect(mapStateToProps)(MyComp);

複製代碼

mapDispatchToProps

connect方法的第二個參數,它的功能是將action做爲組件的props

const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    increase: (...args) => dispatch(actions.increase(...args)),
    decrease: (...args) => dispatch(actions.decrease(...args))
  }
}

class MyComp extends Component {
  render(){
    const {count, increase, decrease} = this.props;
    return (<div> <div>計數:{this.props.count}次</div> <button onClick={increase}>增長</button> <button onClick={decrease}>減小</button> </div>)
  }
}

const Comp = connect(mapStateToProps, mapDispatchToProps)(MyComp);

複製代碼

利用props使用store

import { setUser } from 'action';
// 在使用了connect的組件中 store在它的props當中
const { dispatch } = this.porps;

const user = ...;
// 直接分發設置user
dispatch(setUser(user));
複製代碼

異步場景下更新store

  • Thunk middleware
  • redux-promise
  • redux-observable
  • redux-saga
  • redux-pack
  • 自定義...

Redux-thunk

在沒有使用Redux-thunk以前,當咱們須要改變store中的state,只能使用使用dispath傳入action的形式,這裏有個官方的例子可以說明它的使用場景。

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';

// Note: this API requires redux@>=3.1.0
const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
);

function fetchSecretSauce() {
  return fetch('https://www.google.com/search?q=secret+sauce');
}

// These are the normal action creators you have seen so far.
// The actions they return can be dispatched without any middleware.
// However, they only express 「facts」 and not the 「async flow」.

function makeASandwich(forPerson, secretSauce) {
  return {
    type: 'MAKE_SANDWICH',
    forPerson,
    secretSauce
  };
}

function apologize(fromPerson, toPerson, error) {
  return {
    type: 'APOLOGIZE',
    fromPerson,
    toPerson,
    error
  };
}

function withdrawMoney(amount) {
  return {
    type: 'WITHDRAW',
    amount
  };
}

// Even without middleware, you can dispatch an action:
store.dispatch(withdrawMoney(100));

// But what do you do when you need to start an asynchronous action,
// such as an API call, or a router transition?

// Meet thunks.
// A thunk is a function that returns a function.
// This is a thunk.

function makeASandwichWithSecretSauce(forPerson) {

  // Invert control!
  // Return a function that accepts `dispatch` so we can dispatch later.
  // Thunk middleware knows how to turn thunk async actions into actions.

  return function (dispatch) {
    return fetchSecretSauce().then(
      sauce => dispatch(makeASandwich(forPerson, sauce)),
      error => dispatch(apologize('The Sandwich Shop', forPerson, error))
    );
  };
}

// Thunk middleware lets me dispatch thunk async actions
// as if they were actions!

store.dispatch(
  makeASandwichWithSecretSauce('Me')
);

// It even takes care to return the thunk’s return value
// from the dispatch, so I can chain Promises as long as I return them.

store.dispatch(
  makeASandwichWithSecretSauce('My wife')
).then(() => {
  console.log('Done!');
});
複製代碼

thunk可讓咱們在dispatch執行時,能夠傳入方法,而不是本來的action

咱們能夠看一下thunk的源碼,當action是方法時,它會將action進行返回。

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    // action的類型是方法時,放回action
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;
複製代碼

通過這樣,咱們就能夠理解爲何在上述的官方例子當中能夠這麼使用。

store.dispatch(
  makeASandwichWithSecretSauce('My wife')
).then(() => {
  console.log('Done!');
});
複製代碼

makeASandwichWithSecretSauce實際會返回fetch().then()返回值,而fetch().then()返回的是Promise對象。

Redux-saga

在開始講述saga之前,先講下與它相關的ES6語法 Generator函數

function* helloWorldGenerator() {
  // 能夠將yield當作return,只不過yield時,還能繼續
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }
複製代碼

異步Generator函數

這裏有2個方法,一個是經過回調寫的,一個是經過generator來寫的

fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
  if (err) throw err;
  console.log(data);
});
複製代碼
function* asyncJob() {
  // ...其餘代碼
  var f = yield readFile(fileA);
  // ...其餘代碼
}
複製代碼

官方文檔的一個例子以下

function render() {
  ReactDOM.render(
    <Counter value={store.getState()} onIncrement={() => action('INCREMENT')} onDecrement={() => action('DECREMENT')} onIncrementAsync={() => action('INCREMENT_ASYNC')} />, document.getElementById('root') ) } 複製代碼

在使用saga時,都會創建一個saga.js,其他的都是和普通的redux同樣,須要建立action``reducerstore

import { delay } from 'redux-saga'
import { put, takeEvery } from 'redux-saga/effects'

// ...

// Our worker Saga: 將執行異步的 increment 任務
export function* incrementAsync() {
  yield delay(1000)
  yield put({ type: 'INCREMENT' })
}

// Our watcher Saga: 在每一個 INCREMENT_ASYNC action spawn 一個新的 incrementAsync 任務
export function* watchIncrementAsync() {
  yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}
複製代碼

當主動觸發了onIncrementAsync回調以後,就會發送一個INCREMENT_ASYNC,在saga接受到這個action時候,就會incrementAsync,在這個方法當中會延遲1000毫秒,隨後put(相似於dispatch)發送一個type爲increment的事件,在reducer當中,能夠根據這個action作出對storestate進行操做。

咱們能夠看到這裏yield的使用更像是await。

兩種其實都是經過不一樣的異步方式對store進行操做。thunk自己其實沒有異步的功能,可是它可以拓展dispath,加入傳入的是一個異步方法,那就讓它可以具備異步的功能。

設置開發者工具

在官方Example當中有提到,建立一個DevTools文件,ctrl-h打開顯示toggle,ctrl-w改變開發者工具的位置

import React from 'react'
import { createDevTools } from 'redux-devtools'
import LogMonitor from 'redux-devtools-log-monitor'
import DockMonitor from 'redux-devtools-dock-monitor'

export default createDevTools(
  <DockMonitor toggleVisibilityKey="ctrl-h" changePositionKey="ctrl-w"> <LogMonitor /> </DockMonitor>
)

複製代碼

而後將該組件放在根目錄

import React from 'react'
import PropTypes from 'prop-types'
import { Provider } from 'react-redux'
import DevTools from './DevTools'
import { Route } from 'react-router-dom'
import App from './App'
import UserPage from './UserPage'
import RepoPage from './RepoPage'

const Root = ({ store }) => (
  <Provider store={store}>
    <div>
      <Route path="/" component={App} />
      <Route path="/:login/:name"
             component={RepoPage} />
      <Route path="/:login"
             component={UserPage} />
      <DevTools />
    </div>
  </Provider>
)

Root.propTypes = {
  store: PropTypes.object.isRequired,
}

export default Root

複製代碼

最後在createStore時須要傳入

import DevTools from '../devtool'

const store = createStore(
    rootReducer,
    preloadedState,
    compose(
      applyMiddleware(thunk),
      DevTools.instrument()
    )
  )
複製代碼

效果圖以下

實戰

咱們須要的要使用redux須要

  • 創建action
  • 創建對應reducer
  • 建立store

同時,爲了方便

  • 須要有Provider

項目目錄

項目目錄以下所示

action/index.js

建立一個action,用於告知reducer,設置用戶信息,增長一個type,讓reducer根據type來更新store中的state

export const TYPE = {
  SET_USER: 'SET_USER'
};

export const setUser = (user) => ({
  type: 'SET_USER',
  user
});
複製代碼

reducer/user.js

建立一個關於userreducer

import {
  TYPE
} from '../action'

const createUser = (user) => user;

const user = (state = {}, action) => {
  console.log(action);
  switch (action.type) {
    case TYPE.SET_USER:
      // 根據type來更新用戶信息
      return {...state, ...createUser(action.user)};
    default:
      return state;
  }
}

export {
  user
}


複製代碼

reducers/index.js

reducer,用於將其餘不一樣業務的reducer合併。

import { combineReducers } from 'redux';

import { user } from './user';

export default combineReducers({
  user
});
複製代碼

store/config-store.dev.js

store中有不一樣的初始化store的方法,dev中有開發者工具,而pro中沒有。這裏作了個區分。

import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from '../reducers'
import DevTools from '../devtool'

const configureStore = preloadedState => {
  const store = createStore(
    rootReducer,
    preloadedState,
    compose(
      applyMiddleware(thunk),
      DevTools.instrument()
    )
  )

  if (module.hot) {
    // Enable Webpack hot module replacement for reducers
    module.hot.accept('../reducers', () => {
      store.replaceReducer(rootReducer)
    })
  }

  return store
}

export default configureStore

複製代碼

store/configure-store.prod.js

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from '../reducers'

const configureStore = preloadedState => createStore(
  rootReducer,
  preloadedState,
  applyMiddleware(thunk)
)

export default configureStore
複製代碼

store/configure-store.js

根據不一樣環境讀取不一樣的初始化store的文件。

if (process.env.NODE_ENV === 'production') {
  module.exports = require('./configure-store.prod')
} else {
  module.exports = require('./configure-store.dev')
}

複製代碼

devtool/index.js

開發者組件的配置文件。

import React from 'react'
import { createDevTools } from 'redux-devtools'
import LogMonitor from 'redux-devtools-log-monitor'
import DockMonitor from 'redux-devtools-dock-monitor'

export default createDevTools(
  <DockMonitor toggleVisibilityKey="ctrl-h" changePositionKey="ctrl-w"> <LogMonitor /> </DockMonitor>
)

複製代碼

index.js

在index.js中初始化store

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import configureStore from './store/store/configure-store';

const store = configureStore();

ReactDOM.render(

  <App store={store}/> , document.getElementById('root')); registerServiceWorker(); 複製代碼

app.jsx

在根文件中,建立provider

import React, { Component } from 'react'
import './App.css'
import './reset.css'
import 'antd/dist/antd.css'
import Auth from './pages/auth'
import Star from './pages/star/star'
import { BrowserRouter, Route, Redirect } from 'react-router-dom'
import DevTools from './store/devtool'
import { Provider } from 'react-redux'


class App extends Component {
  constructor(props) {
    super(props)

    this.onClickAuth = this.onClickAuth.bind(this)
  }

  onClickAuth() {}

  /** * 渲染開發者工具 */
  renderDevTools() {
    if (process.env.NODE_ENV === 'production') {
      return null;
    }
    return (<DevTools />)
  }

  render() {
    return (
      <Provider store={this.props.store}>
        <div className="App">
          <BrowserRouter basename="/">
            <div>
              <Route exact path="/" component={Auth} />
              <Route path="/auth" component={Auth} />
              <Route path="/star" component={Star} />
              { this.renderDevTools() }
            </div>
          </BrowserRouter>
        </div>
      </Provider>
    )
  }
}

export default App

複製代碼

更新用戶信息

import React, { Component } from 'react';
import './star.scss';
import globalData from '../../utils/globalData';
import StringUtils from '../../utils/stringUtils';
import { List, Avatar, Row, Col } from 'antd';
import Api from '../../utils/api';
import Head from '../../components/Head/Head';
import ResInfo from '../../components/resInfo/resInfo';
import ControlList from '../../components/control/control-list';
import StarList from '../../components/star-list/star-list';
import Eventbus from '@/utils/eventbus.js';
import { connect } from 'react-redux';
import { setUser } from '../../store/action';

class Star extends Component {
  constructor(props) {
    super(props);

    this.state = {
      tableData: [],
      originTableData: [],
      userInfo: {},
      rawMdData: ''
    };
  }

  componentDidMount() {
    this.getUserInfo();
  }

  componentWillUnmount() {
  }

  getUserInfo() {
    Api.getAuthenticatedUser()
      .then(data => {
        this.handleGetUserInfoSuccessResponse(data);
      })
      .catch(e => {
        console.log(e);
      });
  }

  /** * 獲取完用戶信息 */
  handleGetUserInfoSuccessResponse(res) {
    this.setState({
      userInfo: res.data
    });
    this.getStarFromWeb();
    this.refs.controlList.getTagsFromWeb();

    const { dispatch } = this.props;
    // 更新用戶信息
    dispatch(setUser(this.state.userInfo));
  }

  // ...省略一些代碼
  
  render() {
    return (
      <div className="star">
        <Head
          ref="head"
          head={this.state.userInfo.avatar_url}
          userName={this.state.userInfo.login}
        />
        <Row className="content-container">
          <Col span={3} className="control-list-container bg-blue-darkest">
            <ControlList
              ref="controlList"
              onClickRefresh={this.onClickRefresh}
              onClickAllStars={this.onClickAllStars}
              onClickUntaggedStars={this.onClickUntaggedStars}
            />
          </Col>
          <Col span={5} className="star-list-container">
            <StarList
              tableData={this.state.tableData}
              onClickResItem={this.onClickResItem.bind(this)}
            />
          </Col>
          <Col span={16}>
            <div className="md-container">
              <ResInfo resSrc={this.state.rawMdData} />
            </div>
          </Col>
        </Row>
      </div>
    );
  }
}

const mapStateToProps = (state, ownProps) => ({
  user: state.user
});

export default connect(mapStateToProps)(Star);

複製代碼

學習文章

相關文章
相關標籤/搜索