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 的方法就是觸發
action,action是一個用于描述已發生事件的普通物件,redux不會直接修改state,而是在狀態發生更改時,回傳一個全新的狀態,舊的狀態并沒有進行更改,得以保留,可以使用
redux-devtools-extension工具進行可視化查看, -
狀態修改由純函式完成
Reducer 只是一些純函式,它接收先前的 state 和 action,并回傳新的 state,
3.2 基礎
3.2.1 Store
Redux的核心是 Store ,Store 由 createStore方法創建,
createStore(reducer, [initState])//reducer表示一個根reducer,initState是一個初始化狀態
store提供方法來操作state
- 維持應用的 state;
- 提供
getState()方法獲取 state; - 提供
dispatch(action)方法更新 state; - 通過
subscribe(listener)注冊監聽器,在state狀體發生變化后會被呼叫, - 通過
subscribe(listener)回傳的函式注銷監聽器,
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就是一個函式,這個函式接受兩個引數 當前state 和 action,然后根據 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() 中最核心的兩個方法是:mapActionToProps 和 mapDispatchToProps ,通過容器組件,可以在 展示組件和 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

如圖,在之前的結構下,新增了 actions 、reducers 、containers 這三個檔案夾,
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
createStore:createStore 方法可接受兩個引數,第一個是專案的根 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.valuehttps://www.cnblogs.com/CherishTheYouth/p/= ''
}}
>
<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 參考資料
-
redux中文檔案
-
展示組件和容器組件相分離(英)
-
展示組件和容器組件相分離(中)
-
react-redux Provider組件
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/7185.html
標籤:JavaScript
上一篇:關于釘釘考勤機使用的注意事項
