redux 入門到實踐

粑啦霸粑粑~發表於2018-12-10

前言

之前沒太理解redux,在使用時總是照葫蘆畫瓢,看專案裡別人如何使用,自己就如何使用,這一次徹底學習了下官方文件,記錄。

在學習redux初時,有三個概念需要了解。

  • action
  • reducer
  • store

Action

型別是一個Object 更改storestate的唯一方法,它通過store.dispatchaction傳到store

一個簡單的action

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

Reducer

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

store

儲存state

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

更新store的步驟

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

理解不可變性

在reducer更新state時,不能改變原有的state,只能重新建立一個新的state。這裡提供了幾個方法可以來建立一個不同的物件。

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

之前並不瞭解immutable-js,所以還是使用es6的語法來執行不可變操作。

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

// a !== b
複製程式碼

上面和下面是相同的

// 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 入門到實踐

實戰

我們需要的要使用redux需要

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

同時,為了方便

  • 需要有Provider

專案目錄

專案目錄如下所示

redux 入門到實踐

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);

複製程式碼

學習文章

相關文章