react-redux 學習以及模組化配置方案

不吃早餐發表於2018-09-15

rudux

redux 執行流程圖:

react-redux 學習以及模組化配置方案

簡單概述:click -> store.dispatch(action) -> reduer -> newState -> viewUpdate

react-readux 中 通過 connect 連結元件和 redux , this.props.dispatch() 呼叫

後面將會講到...

redux 依賴包也是十分的簡潔

react-redux 學習以及模組化配置方案
先來個demo

const redux = require('redux')
const createStore = redux.createStore

const types = {
  UPDATE_NAME: 'UPDATE_NAME'
}

const defaultStore = {
  user: 'tom'
}

/**
 * reducer 純函式 接收一個state,返回一個新的state
 * @param {Object} state
 * @param {Object} action [type] 必選引數
 * @return newState
 * */
function getUser(state = defaultStore, action) {
  const { type, payload } = action
  let res = Object.assign({}, defaultStore)
  switch (type) {
    case types.UPDATE_NAME:
      res.user = payload.name
      break
    default:
      return res
  }
  return res
}

const store = createStore(getUser)

/**
 * listener
 * */
store.subscribe(() => {
  console.log(store.getState())
})

/**
 * dispatch(action) action
 * */
store.dispatch({
  type: types.UPDATE_NAME,
  payload: {
    name: '大帥哥'
  }
})
//@log { name: '大帥哥' }
複製程式碼
  1. 使用者發出 actionstore.dispatch(action)
  2. Store 自動呼叫 Reducer , 返回新的 statelet nextState = getUser(previousState, action)
  3. State 一旦有變化,Store 就會呼叫監聽函式 【store.subscribe(listener)

執行過程如下:

react-redux 學習以及模組化配置方案

store

Store 就是儲存資料的地方,你可以把它看成一個容器。整個應用只能有一個 Store 常用方法:

  • store.dispatch() :分發 action 較為常用
  • store.subscribe() : state 發生變化後立即執行
  • store.getState() : 獲取 store 中存著的 state

createStore

createStore 如其名,建立 store 下面是該方法的部分原始碼:

/**
 * @param {Function} reducer 函式
 * @param {any} [preloadedState] The initial state
 * @param {Function} [enhancer] The store enhancer
 * @returns {Store}
 * */
export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    return enhancer(createStore)(reducer, preloadedState)
  }
  // ...
  return {
    dispatch, // 分發 action
    subscribe, // 監聽器
    getState, // 獲取 store 的 state 值
    replaceReducer,
    [$$observable]: observable // 供Redux內部使用
  }
}
複製程式碼
  • preloadedState: 初始化的initialState,第二個引數不是Object,而是FunctioncreateStore會認為你忽略了preloadedState而傳入了一個enhancer
  • createStore會返回enhancer(createStore)(reducer, preloadedState)的呼叫結果,這是常見高階函式的呼叫方式。在這個呼叫中enhancer接受createStore作為引數,對createStore的能力進行增強,並返回增強後的createStore

dispatch(action)

diapatch 是 store 物件的方法,主要用來分發 action ,

redux 規定 action 一定要包含一個 type 屬性,且 type 屬性也要唯一

dispatch 是 store 非常核心的一個方法,也是我們在應用中最常使用的方法,下面是 dispatch 的原始碼 :

function dispatch(action) {
  if (!isPlainObject(action)) {
    // 校驗了action是否為一個原生js物件
    throw new Error('Actions must be plain objects. ' + 'Use custom middleware for async actions.')
  }

  if (typeof action.type === 'undefined') {
    // action物件是否包含了必要的type欄位
    throw new Error('Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?')
  }

  if (isDispatching) {
    // 判斷當前是否處於某個action分發過程中, 主要是為了避免在reducer中分發action
    throw new Error('Reducers may not dispatch actions.')
  }

  try {
    isDispatching = true
    currentState = currentReducer(currentState, action)
  } finally {
    isDispatching = false
  }

  const listeners = (currentListeners = nextListeners)
  for (let i = 0; i < listeners.length; i++) {
    const listener = listeners[i]
    listener()
  }
  // 在一系列檢查完畢後,若均沒有問題,將當前的狀態和action傳給當前reducer,用於生成新的state
  return action
}
複製程式碼

reducer && store.replaceReducer

Redux 中負責響應 action 並修改資料的角色就是reducerreducer的本質實際上是一個函式 replaceReducer:

/**
 * @desc 替換當前的reducer的函式
 * @param {Function}
 * @return {void}
 */
function replaceReducer(nextReducer) {
  if (typeof nextReducer !== 'function') {
    throw new Error('Expected the nextReducer to be a function.')
  }

  currentReducer = nextReducer
  dispatch({ type: ActionTypes.REPLACE })
}
複製程式碼

replaceReducer 使用場景:

  • 當你的程式要進行程式碼分割的時候
  • 當你要動態的載入不同的 reducer 的時候
  • 當你要實現一個實時 reloading 機制的時候

中介軟體 middleware

以上介紹了 redux 的實現流的過程,應用場景無非於

button -- click --> disptch -- action --> reducer -- newState --> view

但是這種實現方式是基於同步的方式的,日常開發中當然少不了 http 這些非同步請求,這種情況下必須等到伺服器資料返回後才重新渲染 view, 顯然某些時候回阻塞頁面的展示。

舉例來說,要新增日誌功能,把 ActionState 列印出來,可以對 store.dispatch 進行如下改造。

let next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
  console.log('dispatching', action)
  next(action)
  console.log('next state', store.getState())
}
複製程式碼

上面程式碼中,對 store.dispatch 進行了重定義,在傳送 Action 前後新增了列印功能。這就是中介軟體的雛形。

中介軟體就是一個函式,對 store.dispatch 方法進行了改造,在發出 Action 和執行 Reducer 這兩步之間,新增了其他功能。

applyMiddleware

Redux 提供了applyMiddleware來裝載middleware: 它是 Redux 的原生方法,**作用是將所有中介軟體組成一個陣列,依次執行。**下面是它的原始碼。

/**
 * @param {...Function} middlewares
 * returns {Function} A store enhancer applying the middleware
 */
export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}
複製程式碼

所有中介軟體被放進了一個陣列 chain,然後巢狀執行,最後執行 store.dispatch。可以看到,中介軟體內部(middlewareAPI)可以拿到getStatedispatch這兩個方法

compose 實際上是函數語言程式設計中的組合,接收多個函式體並且將其組合成一個新的函式,例如compose 後 [fn1, fn2...] 依次從右到左巢狀執行函式 而compose用於applyMiddleware 也是為了組合中介軟體 dispatch = compose(...chain)(store.dispatch) ==> dispatch=fn1(fn2(fn3(store.dispatch)))

/**
 * @param {...Function} funcs The functions to compose.
 * @returns {Function} A function obtained by composing the argument functions
 */
export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
複製程式碼

redux-thunk

上面的中介軟體的介紹可以知道 redux 通過 applyMiddleware 來裝載中介軟體,通過 compose 方法可以組合函式

非同步的問題可以通過 redux-thunk 解決,用法也不難 react 元件中使用相關如下:

// 配置 redux 加上這個...
import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
// ...
const store = createStore(getUser, compose(applyMiddleware(thunk)))

// react 中使用
import { connect } from 'react-redux'

handleClick = () => {
  this.props.dispatch(dispatch => {
    return axios.get('https://randomuser.me/api/').then(res => {
      dispatch({
        type: types.CHANGE_ARRAY,
        payload: {
          name: res.data.results[0].name.title
        }
      })
    })
  })
}

const mapStateToProps = (state, props) => {
  return {
    name: state.demo.name
  }
}

export default connect(mapStateToProps)(Demo)
複製程式碼

處理非同步的還有很多外掛 如 redux-soga 等,樓主並未實踐過,所以不做延伸...

react-redux

下面是在 react 中使用的程式碼的雛形:

import { createStore } from 'redux'

let defaultState = {
  count: 1
}

/**
 * Reducer
 * */
function demoReducer(state = defaultState, action = {}) {
  const { type, payload } = action
  const res = Object.assign({}, state)
  if (type === 'changeCount') {
    res.count = payload.count
  }
  return res
}

/**
 * @Store 存資料的地方,你可以把它看成一個容器。整個應用只能有一個 Store。
 * combineReducers({ ...reducers }) 可以組合多個reducer
 * */
const store = createStore(
  demoReducer,
  window.devToolsExtension && window.devToolsExtension() // 配置redux 開發工具
)

// ... 根元素下配置下 Provider
import { Provider } from 'react-redux'

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

// 元件中使用
import { connect } from 'react-redux'

//use
this.dispatch({
  type: 'changeCount',
  payload: {
    count: 22
  }
})

const mapStateToProps = (state, props) => {
  return {
    name: state.demo.name
  }
}

export default connect(mapStateToProps)(Demo)
複製程式碼

mapStateToProps

  • 用於建立元件跟 store 的 state 的對映關係作為一個函式,它可以傳入兩個引數,結果一定要返回一個 object
  • 傳入mapStateToProps之後,會訂閱 store 的狀態改變,在每次 store 的 state 發生變化的時候,都會被呼叫
  • 如果寫了第二個引數 props,那麼當 props 發生變化的時候,mapStateToProps 也會被呼叫

mapDispatchToProps

  • mapDispatchToProps用於建立元件跟 store.dispatch 的對映關係
  • 可以是一個 object,也可以傳入函式
  • 如果mapDispatchToProps是一個函式,它可以傳入 dispatch,props,定義 UI 元件如何發出 action,實際上就是要呼叫 dispatch 這個方法
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'

// 頁面中使用...
this.props.changeName()

const mapDispatchToProps = ({ changeName } = (dispatch, props) => {
  return bindActionCreators(
    {
      changeName: function() {
        return {
          type: types.UPDATE_NAME,
          payload: {
            name: '大大大'
          }
        }
      }
    },
    dispatch
  )
})

export default connect(mapDispatchToProps)(App)
複製程式碼

模組化配置

下面的配置僅供參考。實現的功能:

  • 整合 actiontypesreducer 到一個檔案
  • 根據開發/生成環境配置不同的 redux 中介軟體(開發環境配置 dev-tools )
  • 支援裝飾器模式
  • redux 熱載入配置(這裡面順便將 react 熱載入配置也加上了)

注意:專案基於 create-react-app eject 後的配置改造實現的。下面用了別名 @ ,需要改下 webpack 的配置,如果你配置不成功。詳情可以看我的 github 上面有原始碼. 連結入口

安裝

npm install redux react-redux redux-thunk --save
npm install redux-devtools-extension react-hot-loader -D
npm install @babel/plugin-proposal-decorators -D
複製程式碼

相關資料夾如圖:

react-redux 學習以及模組化配置方案

models/demo.js

demo 模組。

// types
const ADD_COUNT = 'ADD_COUNT'

// actions
export const addCount = () => {
  return { type: ADD_COUNT }
}

// state
const defaultState = {
  count: 11
}

// reducer
export const demoReducer = (state = defaultState, action) => {
  switch (action.type) {
    case ADD_COUNT:
      return { ...state, count: ++state.count }
    default:
      return state
  }
}

export default demoReducer
複製程式碼

models/index.js

模組的匯出口。

import { combineReducers } from 'redux'

import demo from './demo'

export default combineReducers({
  demo
})
複製程式碼

redux/index.js

redux 倉庫的總出口

import thunk from 'redux-thunk'
import { compose, createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'

import rootReducer from './models'

let storeEnhancers
if (process.env.NODE_ENV === 'production') {
  storeEnhancers = compose(thunk)
} else {
  storeEnhancers = compose(composeWithDevTools(applyMiddleware(thunk)))
}

const configureStore = (initialState = {}) => {
  const store = createStore(rootReducer, initialState, storeEnhancers)

  if (module.hot && process.env.NODE_ENV !== 'production') {
    // Enable Webpack hot module replacement for reducers
    module.hot.accept('./models', () => {
      console.log('replacing reducer...')
      const nextRootReducer = require('./models').default
      store.replaceReducer(nextRootReducer)
    })
  }

  return store
}

export default configureStore()
複製程式碼

src/index.js

react 專案的入口配置。

import React from 'react'
import ReactDOM from 'react-dom'
import { AppContainer } from 'react-hot-loader'
import App from './App'
import { Provider } from 'react-redux'
import store from '@/redux'

const render = Component => {
  ReactDOM.render(
    <AppContainer>
      <Provider store={store}>
        <Component />
      </Provider>
    </AppContainer>,
    document.getElementById('root')
  )
}

render(App)

if (module.hot) {
  module.hot.accept('./App', () => {
    render(App)
  })
}
複製程式碼

App.jsx

import React, { Component, Fragment } from 'react'
import { connect } from 'react-redux'
import { addCount } from '@/redux/models/demo'
import { Button } from 'antd'

const mapStateToProps = state => ({
  count: state.demo.count
})

@connect(
  mapStateToProps,
  { addCount }
)
class ReduxTest extends Component {
  render() {
    return (
      <Fragment>
        {this.props.count}
        <Button type="primary" onClick={this.props.addCount}>
          Click
        </Button>
        <hr />
      </Fragment>
    )
  }
}

export default ReduxTest
複製程式碼

.babelrc

配置 babel 裝飾器模式

{
  "presets": ["react-app"],
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }]
  ]
}
複製程式碼

vscode 裝飾器模式如果有報警的話,可以根目錄下新建 jsconfig.json

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "baseUrl": "./",
    "paths": {
      "@/*": [
        "src/*"
      ]
    },
    "jsx": "react"
  },
  "exclude": [
    "node_modules",
    "build",
    "config",
    "scripts"
  ]
}
複製程式碼

參考

相關文章