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變得可預測,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.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的菜鳥閱讀和學習,希望能幫助到有需要的同學。