1. 程式人生 > 實用技巧 >Redux入門實戰——todo-list2.0實現

Redux入門實戰——todo-list2.0實現

1.前言

在之前的部落格中,我寫了一篇關於todo-list實現的部落格,一步一步詳細的記錄瞭如何使用基礎的React知識實現一個React單頁面應用,通過該篇文章,能夠對React入門開發有一個直觀的認識和粗淺的理解。

近期,個人學習了一下Redux,又將該專案使用 React+Redux的方式進行了實現。本片內容記錄以下實踐的過程。通過本例項,可以學習到:

  • Redux的核心思想;
  • Redux的三大概念;
  • React+Redux的開發方法和流程;

下面將從以下幾個方面展開講解和記錄。

2.專案演示

3.Redux基礎知識

3.1 認識

3.1.1 動機

隨著 JavaScript 單頁面應用開發日趨複雜,JavaScript 需要管理比任何時候都要多的 state (狀態)

,管理不斷變化的 state 非常困難,state 在什麼時候,由於什麼原因,如何變化已然不受控制。當系統變得錯綜複雜的時候,想重現問題或者新增新功能就會變得舉步維艱。

因此,需要一種更可控的方式來管理系統的state,讓系統的state變得可預測,redux就是用來管理系統state的工具。

3.1.2 三大原則

  • 單一資料來源

    整個應用的狀態都儲存在一個物件中,一個應用只有一個唯一的state,儲存在store中,通過store統一管理。

  • 狀態是隻讀的

    唯一改變 state 的方法就是觸發 actionaction 是一個用於描述已發生事件的普通物件。

    redux不會直接修改state,而是在狀態發生更改時,返回一個全新的狀態,舊的狀態並沒有進行更改,得以保留。可以使用 redux-devtools-extension

    工具進行視覺化檢視。

  • 狀態修改由純函式完成

    Reducer 只是一些純函式,它接收先前的 state 和 action,並返回新的 state。

3.2 基礎

3.2.1 Store

Redux的核心是 Store ,StorecreateStore方法建立,

createStore(reducer, [initState])//reducer表示一個根reducer,initState是一個初始化狀態

store提供方法來操作state

3.2.2 Action

action 是把資料從應用傳到 store 的有效載荷。它是 store 資料的唯一來源。通過 store.dispatch() 將 action 傳到 store。如果有資料需要新增,在action中一併傳過來。

action需要action建立函式進行建立,如下是一個action建立函式:

/*
 * action 型別
 */

export const ADD_TODO = 'ADD_TODO';
export const TOGGLE_TODO = 'TOGGLE_TODO'
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'

/*
 * 其它的常量
 */

export const VisibilityFilters = {
  SHOW_ALL: 'SHOW_ALL',
  SHOW_COMPLETED: 'SHOW_COMPLETED',
  SHOW_ACTIVE: 'SHOW_ACTIVE'
}

/*
 * action 建立函式
 */

export function addTodo(text) {
  return { type: ADD_TODO, text }
}

export function toggleTodo(index) {
  return { type: TOGGLE_TODO, index }
}

export function setVisibilityFilter(filter) {
  return { type: SET_VISIBILITY_FILTER, filter }
}

返回一個物件,改物件由reducer獲取,根據 action 型別進行相應操作。

3.2.3 Reducer

store通過 store.dispatch(某action(引數)) 來給reducer安排任務。

簡單理解,一個reducer就是一個函式,這個函式接受兩個引數 當前stateaction,然後根據 action 來對當前 state 進行操作,如果有需要更改的地方,就返回一個 新的 state ,而不會對舊的 state進行操作,任何一個階段的 state 都可以進行檢視和監測,這讓 state 的管理變得可控,可以實時追蹤 state的變化。

React中使用Redux時,需要有一個根 Reducer,這個根 Reducer 通過 conbineReducer() 將多個子 Reducer 組合起來。

根reducer:

import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'
//根reducer
// rootReducer 根reducer,把子reducer組合在一起
export default combineReducers({
  todos, //子state
  visibilityFilter //子state
})

子reducer:

//這裡的state = []為state的當前值
const todos = (state = [], action) => {
    switch (action.type) {
      case 'ADD_TODO':
        return [
          ...state,     // Object.assign() 新建了一個副本
          {
            id: action.id,
            text: action.text,
            completed: false
          }
        ]
      case 'TOGGLE_TODO':
     //   console.log(state);
        return state.map((value,index) => {
            return (value.id === action.id) ? {...value,completed:!value.completed} : value;
        }) 
      default:
        return state;
    }
  }

export default todos;

3.2.4 資料流

3.3 展示元件和容器元件

3.3.1 展示元件和容器元件分離

本部分在筆者尚未深入研究,在此給出redux作者寫的深度解析文章連結及網上的譯文連結,讀者可自行檢視。

原文連結:展示元件和容器元件相分離

譯文連結:展示元件和容器元件相分離

3.3.2 展示元件和容器元件比較

展示元件 容器元件
作用 描述如何展示骨架、樣式 描述如何執行(資料獲取、狀態更新)
直接使用Redux
資料來源 props 監聽Redux state
資料修改 從props呼叫回撥函式 向Redux派發action
呼叫方式 手動 通常由React Redux生成

大部分的元件都應該是展示型的,但一般需要少數的幾個容器元件把它們和 Redux store 連線起來。

React Redux 的使用 connect() 方法來生成容器元件。

import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'

//mapStateToProps引數中的state是store的state.
// 在容器元件中,通過mapStateToProps方法,在展示元件和store中間傳遞資料和執行action
// ownProps表示的是元件自身的屬性,即父元件傳過來的屬性
const mapStateToProps = (state, ownProps) => {
    return {
        active: ownProps.filter === state.setVisibilityFilter
    }
}
// ownProps表示的是元件自身的屬性,即父元件傳過來的屬性
const mapDispatchToProps = (dispatch, ownProps) => {
    return {
        // 這裡寫方法名,在展示元件中通過這個方法名來執行裡面的action派遣函式
        onClick: () => {
            // 執行setVisibilityFilter這個action
            dispatch(setVisibilityFilter(ownProps.filter))
        }
    }
}
//通過connect讓Link元件得以連線store,從store中取得active資料和onClick方法的執行體。
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Link)

connect() 中最核心的兩個方法是:mapActionToPropsmapDispatchToProps ,通過容器元件,可以在 展示元件和 store之間傳遞資料和執行 action

4.基於Redux的React專案實戰

4.1 目錄結構

根據Redux的幾大組成部分,在進行開發時,將在之前基礎的React開發模式下,增加幾個資料夾,形成新的開發目錄結構,具體目錄結構如下圖:

│  App.css
│  App.js
│  App.test.js
│  index.css
│  index.js
│  logo.svg
│  readme.txt
│  serviceWorker.js
│  setupTests.js
├─actions      
├─components       
├─containers
└─reducers

如圖,在之前的結構下,新增了 actionsreducerscontainers 這三個資料夾。

4.2 配置React-Redux開發環境

4.2.1 步驟

在建好檔案目錄後就可以開始進行開發了,由於是基於Redux做React開發,所以首先一步當然需要把Redux的開發環境配置一下。

  • 安裝 react-redux
npm install --save react-redux
  • 編寫入口檔案 index.js

前文講到,redux使用一個唯一的 store 來對專案進行狀態管理,那麼首先我們需要建立這個 store ,並將這個 store 作為一個屬性,傳遞給下級子元件。

具體程式碼如下:

import React from 'react';
import ReactDOM, { render } from 'react-dom';

//redux ----------------------------------------------------
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { rootReducer } from './reducers';
//引入專案根元件App.jsx
import App from './App';

//建立store,將根Reducer傳入store中。redux應用只有一個單一的store
const store = createStore(rootReducer);

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

如上程式碼所示,使用Redux,需要引入的檔案有:

  • Provider 元件
  • createStore 方法
  • 根reducer
  • 專案根元件App.jsx

createStorecreateStore 方法可接受兩個引數,第一個是專案的根 reducer ,是必選的引數,另一個是可選的引數,可輸入專案的初始 state 值。通過該方法建立一個 store 例項,即為專案唯一的 store

Provider元件Provider元件包裹在跟元件App.jsx外層,將專案的 store作為屬性傳遞給 Provider。使用Provider 可以實現所有子元件直接對 store 進行訪問。在下文將深入講一下 Provider 的實現和工作原理。

根reducer:隨之專案的不斷增大,程式state的越來越複雜,只用一個 reducer 是很難滿足實際需求的,redux中採用將 reducer 進行拆分,最終在狀態改變之前通過 根 reducer 將 各個拆分的子 reducer 進行合併方式來進行處理。

App.jsx:專案的跟元件,將一級子元件寫在App.jsx中。

4.2.2 Provider

provider 包裹在根元件外層,使所有的子元件都可以拿到state。它接受store作為props,然後通過context往下傳,這樣react中任何元件都可以通過context獲取store。

Provider 原理:

原理是React元件的context屬性

元件原始碼如下:

原理是React元件的context屬性

export default class Provider extends Component {
  getChildContext() {
      //返回一個物件,這個物件就是context
    return { store: this.store }
  }

  constructor(props, context) {
    super(props, context)
    this.store = props.store
  }
  render() {
    return Children.only(this.props.children)
  }
}

Provider.propTypes = {
  store: storeShape.isRequired,
  children: PropTypes.element.isRequired
}

Provider.childContextTypes = {
  store: storeShape.isRequired
}

4.3 src目錄檔案列表

資料夾 檔案
src index.js
src/actions index.js
src/components(展示元件) App.jsx
TodoList.jsx
Footer.jsx
Todo.jsx
Link.jsx
src/containers(容器元件) AddTodo.js
FilterLink.js
VisibleTodoList.js
src/reducers index.js
todo.jsx
visibilityFilter.js

4.4 專案程式碼

注意:

  • 程式碼說明大部分寫在專案程式碼中,讀者在檢視時,建議對程式碼也要進行仔細閱讀。
  • 本專案功能較簡單,因此程式碼直接按照檔案目錄給出,而不按照功能模組陳列。

4.4.1 入口檔案 index.js

import React from 'react';
import ReactDOM, { render } from 'react-dom';
import './index.css';
import App from './components/App';

//redux
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './reducers';

//建立store,createStore()第一個引數是專案的根reducer,第二個引數是可選的,用於設定state的初始狀態
const store = createStore(rootReducer);

render(
  // Provider元件包裹在跟元件的外層,使所有的子元件都可以拿到state.
  // 它接受store作為props,然後通過context往下傳,這樣react中任何元件
  // 都可以通過context獲取store.
  <Provider store = {store}>
    {/* App 根元件 */}
    <App />
  </Provider>,
  document.getElementById('root')
)

4.4.2 actions檔案

  • index.js
let nextTodoId = 0;

// 定義action 常量 對於小型專案,可以將action常量和action建立函式寫在一起,對於複雜的專案,可將action常量和其他的常量抽取出來,放到單獨的某個常量資料夾中
const ADD_TODO = 'ADD_TODO';
const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER';
const TOGGLE_TODO = 'TOGGLE_TODO';

//這裡是幾個action建立函式,函式裡面的物件才是action,返回一個action
// text是跟隨action傳遞的資料
// 呼叫 dispatch(addTodo(text)),即代表派遣action,交給reducer處理
//action生成函式
// 大部分情況下,他簡單的從引數中收集資訊,組裝成一個action物件並返回,
// 但對於較為複雜的行為,他往往會容納較多的業務邏輯與副作用,包括與後端的互動等等。
export const addTodo = (text) => {
    return {
        type: ADD_TODO,
        id: nextTodoId ++,
        text
    }
}
export const setVisibilityFilter = (filter) => {
    return {
        type: SET_VISIBILITY_FILTER,
        filter
    }
}
export const toggleTodo = (id) => {
    return {
        type: TOGGLE_TODO,
        id
    }
}
//三個常量
export const VisibilityFilters = {
  SHOW_ALL: 'SHOW_ALL',
  SHOW_COMPLETED: 'SHOW_COMPLETED',
  SHOW_ACTIVE: 'SHOW_ACTIVE'
}

4.4.3 components檔案(展示元件)

  • App.jsx
import React from 'react'
import Footer from './Footer'
import AddTodo from '../containers/AddTodo'
import VisibleTodoList from '../containers/VisibleTodoList'
//應用的根元件
const App = () => {
  return (
    <div>
      {/* 容器元件 */}
      <AddTodo />
      {/* 容器元件 */}
      <VisibleTodoList />
      {/* 展示元件 */}
      <Footer />
    </div>
  )  
}
export default App
  • Footer.jsx
import React from 'react'
import FilterLink from '../containers/FilterLink'
import { VisibilityFilters } from '../actions'
//無狀態元件,這種寫法初學者可能難以理解,可以先補習下ES6,等價於
//function Footer(){
//	return (<div>XXX</div>)
//}
const Footer = () => (
  <div>
    <span>Show: </span>
    <FilterLink filter={VisibilityFilters.SHOW_ALL}>
      All
    </FilterLink>
    <FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>
      Active
    </FilterLink>
    <FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>
      Completed
    </FilterLink>
  </div>
)
export default Footer
  • Link.jsx
import React from 'react'
import PropTypes from 'prop-types'
//prop-types是一個元件屬性校驗包,匯入這個包可以資料進行格式等方面的校驗
const Link = (props) => {
    return (
     <button onClick={props.onClick} disabled={props.active} style={{marginLeft:'4px'}}>
       {props.children}
     </button>
    )
}

Link.propTypes = {
  active: PropTypes.bool.isRequired,
  children: PropTypes.node.isRequired,
  onClick: PropTypes.func.isRequired
}

export default Link
  • TodoList.jsx
import React, { createFactory } from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'

const TodoList = (props) => {
    return (
        <ul>
            {
                props.todos.map((value,index) => {
                    return <Todo key = {index} {...value} onClick = {() => props.toggleTodo(value.id)} />
                })
            }
        </ul>
    )
}

TodoList.propTypes = {
  todos: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.number.isRequired,
      completed: PropTypes.bool.isRequired,
      text: PropTypes.string.isRequired
    }).isRequired
  ).isRequired,
  toggleTodo: PropTypes.func.isRequired
}

export default TodoList
  • Todo.jsx
import React from 'react'
import PropTypes from 'prop-types'

const Todo = ({ onClick, completed, text }) => (
  <li
    onClick={onClick}
    style={ {
      textDecoration: completed ? 'line-through' : 'none'
    }}
  >
    {text}
  </li>
)

Todo.propTypes = {
  onClick: PropTypes.func.isRequired,
  completed: PropTypes.bool.isRequired,
  text: PropTypes.string.isRequired
}

export default Todo

4.4.4 containers檔案(容器元件)

注意:本部分涉及 connect() 方法,程式碼註釋中有重要知識點,建議仔細檢視。對於connect()本文不做深入探討,後續會單獨成文分析。

  • FilterLink.js
import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'
import { createFactory } from 'react'

//mapStateToProps引數中的state是store的state.
// 在容器元件中,通過mapStateToProps方法,在展示元件和store中間傳遞資料和執行action
// ownProps表示的是元件自身的屬性,即父元件傳過來的屬性
const mapStateToProps = (state, ownProps) => {
    return {
        active: ownProps.filter === state.setVisibilityFilter
    }
}

// ownProps表示的是元件自身的屬性,即父元件傳過來的屬性
const mapDispatchToProps = (dispatch, ownProps) => {
    return {
        // 這裡寫方法名,在展示元件中通過這個方法名來執行裡面的action派遣函式
        onClick: () => {
            // 執行setVisibilityFilter這個action
            dispatch(setVisibilityFilter(ownProps.filter))
        }
    }
}

//通過connect讓Link元件得以連線store,從store中取得active資料和onClick方法的執行體。
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Link)

// //將Link元件的內容放到本頁面來結合起來理解,以下程式碼不是本元件的功能程式碼
// const Link = ({ active, children, onClick }) => (
//     <button
//        onClick={onClick}
//        disabled={active}
//        style={{
//            marginLeft: '4px',
//        }}
//     >
//       {children}
//     </button>
//   )
  
//   Link.propTypes = {
//     active: PropTypes.bool.isRequired,
//     children: PropTypes.node.isRequired,
//     onClick: PropTypes.func.isRequired
//   }

建議將容器元件和它對應的展示元件緊密結合起來理解。

  • AddTodo.js
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'

const AddTodo = ({ dispatch }) => {
  let input

  return (
    <div>
      <form
        onSubmit={e => {
          e.preventDefault()
          if (!input.value.trim()) {
            return
          }
          dispatch(addTodo(input.value))
          input.value = ''
        }}
      >
        <input ref={node => input = node} />
        <button type="submit">
          Add Todo
        </button>
      </form>
    </div>
  )
}

export default connect()(AddTodo);
  • VisibleTodoList.js
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'

//獲取符合條件的todo,
// todos state中的todo資料
// filter state中的過濾條件
const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case 'SHOW_COMPLETED':
      return todos.filter(t => t.completed)
    case 'SHOW_ACTIVE':
      return todos.filter(t => !t.completed)
    case 'SHOW_ALL':
    default:
      return todos
  }
}
const mapStateToProps = (state) => {
    return {
        todos: getVisibleTodos(state.todos, state.visibilityFilter)
    }
}
const mapDispatchToProps = (dispatch) => {
    return {
        toggleTodo: (id) => {
            dispatch(toggleTodo(id))
        }
    }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

4.4.5 reducer資料夾

  • 根reducer/index.js
import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'
// rootReducer 根reducer,把子reducer組合在一起
export default combineReducers({
  todos, //子state
  visibilityFilter //子state
})
  • todo.js
//這裡的state = []為state的當前值
const todos = (state = [], action) => {
    switch (action.type) {
      case 'ADD_TODO':
        return [
          ...state,     // Object.assign() 新建了一個副本
          {
            id: action.id,
            text: action.text,
            completed: false
          }
        ]
      case 'TOGGLE_TODO':
     //   console.log(state);
        return state.map((value,index) => {
            return (value.id === action.id) ? {...value,completed:!value.completed} : value;
        }) 
      default:
        return state;
    }
  }

export default todos;
  • visibilityFilter.js
const visibilityFilter = (state = 'SHOW_ALL', action) => {
    switch (action.type) {
      case 'SET_VISIBILITY_FILTER':
        return action.filter
      default:
        return state
    }
  }
  
export default visibilityFilter

5.總結

本文,菜雞本雞通過一個todo-list例項相對系統的介紹了redux的一些基礎概念,基本用法和如何如react進行結合,實現react的功能開發,主要內容包括redux基礎,redux於react結合,例項完成步驟,完整程式碼,專案演示等,比較適合剛接觸redux的菜鳥閱讀和學習,希望能幫助到有需要的同學。

6 參考資料