打造一個 redux 數據流方案 --- 名爲 demacia

亞瑟

打造一個 redux 數據流方案 --- 名爲 demacia

目的:打造一個簡單的 redux 數據流方案,實現功能相似與 dva,但僅僅只是對 redux 進行封裝,簡化 redux 使用流程和難度。最終目的確定是爲了提高開發效率和加深本身對 redux 源碼的理解能力和運用能力css

若是你對 redux 理解還不夠深刻,想要徹底理解它,能夠看一下這篇文章:徹底理解 redux(從零實現一個 redux)html

名稱介紹

倉庫名稱叫 demacia,有沒有熟悉的既視感,對,就是德瑪西亞,命名原因:沒啥原因,英雄聯盟只玩過德瑪西亞,玩過幾回,王者榮耀常玩英雄-亞瑟(2016 年剛畢業連續玩了兩百把 😂)。react

先講使用

編寫 redux 部分的方式和 dva 相似,主要是引入方式和使用方式有所區別css3

快速上手

進入本身的 react 項目,經過 npm 安裝 demaciagit

npm install demacia -S
複製代碼

項目中使用

1. 建立 store

在 src 下建立一個 store 文件用於建立倉庫github

// src/store/index.js
import { demacia } from 'demacia'
// 這裏引入了一個名爲global的model
import global from './global'

// 須要初始化建立的model
const initialModels = {
  global
}

// 設置state初始值,用於全局初始化數據,好比當須要持久化存儲時,會很方便
const initialState = {
  global: {
    counter: 2
  }
}

// 調用demacia並傳入初始參數,返回了redux的store
const store = demacia({
  initialModels,
  initialState,
  middlewares: [], // 加入中間件
  effectsExtraArgument: {} // 額外參數
})

export default store
複製代碼

上面的代碼中,咱們引入了 demacia 函數,並調用它,而後返回了 store,這個 store 就是調用 reduxcreateStore 函數生成的,咱們在調用 demacia 函數時傳入了一個對象做爲參數,幷包含了兩個初始化屬性,initialModels 用於注入 model 數據,initialState 用於設置 redux 初始 statenpm

模塊 global.js 代碼以下redux

// src/store/模塊global.js
export default {
  namespace: 'global',
  state: {
    counter: 0
  },
  reducers: {
    increment(state, { payload }) {
      return {
        ...state,
        counter: state.counter + 1
      }
    },
    decrement(state, { payload }) {
      return {
        ...state,
        counter: state.counter - 1
      }
    }
  },
  effects: {
    async add({ dispatch }, { payload }) {
      const res = await new Promise(resolve => {
        setTimeout(() => {
          resolve({ code: 1, success: true })
        }, 1000)
      })
      dispatch({
        type: 'increment',
        payload: res
      })
    }
  }
}
複製代碼

2. 頁面引入

使用react-redux把 store 加入項目,這裏跟 redux 同樣api

// src/App.js
import React from 'react'
import { Provider } from 'react-redux'
import { HashRouter } from 'react-router-dom'
import store from './store'
import routes from './routes'

function App() {
  return (
    <Provider store={store}> <HashRouter>{routes}</HashRouter> </Provider>
  )
}
複製代碼

頁面中使用react-reduxconnect方法獲取 state數組

// src/pages/home/index.js
import React from 'react'
import { Button } from 'antd'
import { connect } from 'react-redux'

const HomePage = props => {
  return (
    <div> <div>globalCounter: {props.global.counter}</div> <Button onClick={() => { props.dispatch({ type: 'global/increment' }) }} > 同步increment </Button> <Button onClick={() => { props.dispatch({ type: 'global/add' }) }} > 異步increment </Button> </div>
  )
}

export default connect(state => state)(HomePage)
複製代碼

觸發同步或者異步操做,都經過dispatch來分發對應模塊對應的actioneffects action

上面使用的都是全局的 state,若是某個頁面或者某個組件想有直接的狀態呢,或者說是動態的向 store 添加 state 和 reducer,這時候能夠引入 model 來進行處理。

下面建一個頁面 Todos,實現一個 todo list

編寫輸入 Todos 的 model:

// src/pages/todos/model.js
import { model } from 'demacia'
export default model({
  namespace: 'Todos',
  // 至關於react-redux中的connect的第一個參數,會傳入state,返回的對象會返回給組件的props
  selectors: function(state) {
    return {
      todos: state.Todos.todos,
      loading: state.Todos.loading,
      total: state.Todos.todos.reduce((acc, item) => acc + (item.count || 0), 0)
    }
  },
  state: {
    todos: [{ name: '菠蘿', id: 0, count: 2 }]
  },
  reducers: {
    putTodos(state, { payload }) {
      return {
        ...state,
        todos: [...state.todos, ...payload]
      }
    },
    putAdd(state, { payload }) {
      return {
        ...state,
        todos: [...state.todos, payload]
      }
    }
  },
  effects: {
    async getTodos({ dispatch }) {
      const { datas } = await new Promise(resolve => {
        setTimeout(() => {
          resolve({
            code: 0,
            datas: [
              { name: '🍎', id: 1, count: 11 },
              { name: '🍆', id: 2, count: 22 }
            ]
          })
        }, 1000)
      })
      dispatch({ type: 'putTodos', payload: datas })
    },
    async add({ dispatch }, { payload }) {
      const { code } = await new Promise(resolve => {
        setTimeout(() => {
          resolve({
            code: 0
          })
        }, 200)
      })
      if (code === 0) {
        dispatch({ type: 'putAdd', payload: payload })
      }
    }
  }
})
複製代碼

上面須要注意的幾點:

一,model 函數接收了如下參數:

  • namespace reducer 的名稱,給 combineReducers 合併 reducers 用的
  • selectors 至關於 react-redux 中的 connect 的第一個參數(mapStateToProps),會傳入 state,須要返回一個對象,併合併到組件的 props
  • state 存儲的數據,從上方代碼selectors上能夠看出多 state 中出現了loading,loading是內置的,它是一個數組,存儲正在執行中的effects
  • reducers 存儲的是一個對象,對象的鍵是 action 的 type,值是一個函數,接收兩個參數:state 和 action 對象,執行 reducer 過程當中須要執行的部分,函數的返回值是新的 state
  • effects 處理反作用的地方,每個屬性都必須函數,相似前面的reducers,接收兩個參數:
    • 第一參數是一個對象,包含了強化後的dispatch和 state,以及一些擴展(初始化的時候傳入的effectsExtraArgument對象就會合併到這個參數裏面)
    • 第二個參數是也是一個對象,僅包含一個payload屬性

二,model 函數執行後返回了一個高階函數

組件引入 model,用 model 函數執行後返回的高階函數包裹組件,這裏會作三件事:

  1. selectors執行的結果返回的對象加入到組件的 props
  2. effects對象結構注入到組件的 props
  3. 把內置的 resetStoresetStore 方法注入到組件的 props,執行是分別會觸發兩個 action,resetStore的 action 會把數據重置爲最初的數據,setStore須要開發者在對應的reducers裏自定義一個setStore函數

下面是編寫頁面,引入 model 後能夠獲取數據並實現一些功能:

// src/pages/todos/index.js
import React, { useEffect, useState } from 'react'
import model from './model'

const Todos = props => {
  const { todos = [], total, getTodos, loading } = props
  const [input, setInput] = useState('')
  useEffect(() => {
    getTodos()
  }, [getTodos])

  return (
    <div> <h2>水果蔬菜(total: {total})</h2> <div> <input value={input} onChange={e => setInput(e.target.value)} /> <button onClick={async () => { await props.add({ name: input, id: Math.random() .toString(16) .slice(2), count: parseInt(Math.random() * 10) }) setInput('') }} > 添加 </button> </div> {loading.includes('getTodos') ? ( 'loading...' ) : ( <ul> {todos.map(fruit => ( <li key={fruit.id}>{fruit.name}</li> ))} </ul> )} <div> <button onClick={() => { props.resetStore() }} > resetStore </button> <button onClick={() => { props.setStore('haha') }} > setStore </button> </div> </div> ) } export default model(Todos) 複製代碼

實現介紹

從上面能夠看出,demacia只有兩個 api:

  • demacia 用於初始化 store 用的,能夠接收四個參數
    • initialState 初始化數據參數,寫法是:{ [model對應的namespace]: model對應的state初始值 }
    • initialModels 初始化 model,參考上面的建立 store部分
    • middlewares 中間件 這裏你能夠加入其餘的中間件進行擴展
    • effectsExtraArgument 額外參數,這裏的參數會在 effects 的屬性方法的第一個參數對象中出現
  • model 用於生成 reducer,並把新的 reducer 合併到項目的 reducers 中,使用方法上面有講到,參考上面的應該就能夠了

擴展:

  • selectors 能夠結合 reselect 來優化性能

功能主要是有

  • 把 redux 繁瑣的使用過程給封裝起來
  • 動態注入 reducer
  • 每一個 model 內置了 loading 數組,loading 會收集正在執行過程當中的 effects,讓在項目能夠獲取 effects 的執行 狀態
  • 添加了一些內置的 action 可讓實際開發中更簡單,好比resetStore重置 state

源碼地址: github.com/ht113158958…

Demo 地址: github.com/ht113158958…

最後

附上一些以前寫過的相關文章地址:

React系列之Redux 源碼探索

Mobx 使用總結

實現過程參考的資料:

redux中文文檔

relaxjs

相關文章
相關標籤/搜索