React Demo Two - TodoList 升級

jsliang發表於2019-04-07

create by jsliang on 2019-3-26 09:26:53
Recently revised in 2019-4-7 03:04:01

Hello 小夥伴們,如果覺得本文還不錯,記得給個 star , 小夥伴們的 star 是我持續更新的動力!GitHub 地址

一 目錄

不折騰的前端,和鹹魚有什麼區別

目錄
一 目錄
二 前言
三 初始化專案
四 使用 Ant Design
五 使用 Redux
六 Redux 探索
6.1 Redux 外掛
6.2 Redux 知識點講解
七 Redux 探索
7.1 Input 輸入資料
7.2 Button 提交資料
7.3 刪除 TodoItem 列表項
八 優化:抽取 ActionType
九 優化:抽取整個 action
十 優化:UI 元件和容器元件
十一 優化:無狀態元件
十二 結尾:呼叫 Axios,Redux-Base 完成
十三 進階:Redux 中介軟體
十四 進階:Redux-Thunk 中介軟體進行 ajax 請求管理
十五 進階:Redux-Saga 中介軟體進行 Ajax 請求管理
十六 進階:React-Redux
十七 總結

二 前言

返回目錄

宣告:該系列文章主要參考慕課網的 React 實戰視訊,並結合個人理解進行編寫:

本次 Demo 基於 ReactDemoOne 進行了 Redux 的升級,同時會講解到中介軟體 Redux-Thunk 以及 Redux-Saga,最終會使用 React-Redux 進行專案重構。

所以,沒有看第一篇的小夥伴可以檢視:

如果小夥伴想對照這原始碼一起看文章,可以前往下面地址下載:

注意,本文程式碼在 TodoListUpgrade 目錄,並且它四個資料夾,分別對應:

  • Redux-Base —— 記錄 Redux 基礎內容
  • Redux-Thunk —— 在 Redux-Base 基礎上進行 redux-thunk 的中介軟體配置
  • Redux-Saga —— 在 Redux-Base 基礎上進行 redux-saga 的中介軟體配置
  • React-Redux —— 對 TodoList 進行 react-redux 重新改造

Redux-Base 專案最終目錄如下,小夥伴可以先建立空檔案放著,後續將使用到:

React Demo Two - TodoList 升級

三 初始化專案

返回目錄

獲取 React 系列 中 Simpify 目錄中的程式碼,將其 Copy 到 Redux-Base 中,並將 App 修改為 TodoList,進行 TodoList 小改造。

下面我們開始修改:

  1. src/index.js
程式碼詳情
import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './TodoList';

ReactDOM.render(<TodoList />, document.getElementById('root'));
複製程式碼
  1. src/App.js TodoList.js
程式碼詳情
import React, { Component } from 'react';
import './index.css';

class TodoList extends Component {
  render() {
    return (
      <div className="App">
        Hello TodoList!
      </div>
    );
  }
}

export default TodoList;
複製程式碼
  1. src/index.css
程式碼詳情
/* 尚未進行編寫 */
複製程式碼

此時我們在終端執行 npm run start,顯示結果為:

React Demo Two - TodoList 升級

四 使用 Ant Design

返回目錄

下面開始在 TodoList 專案中使用 Ant Design:

  • 安裝:npm i antd -S
  • 使用:
  1. src/TodoList.js
程式碼詳情
import React, { Component } from 'react'; // 引入 React 及其 Component
import './index.css'; // 引入 index.css
import { Input, Button, List } from 'antd'; // 1. 引入 antd 的列表
import 'antd/dist/antd.css'; // 2. 引入 antd 的樣式

// 3. 定義資料
const data = [
  '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第一條 TodoList',
  '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第二條 TodoList',
  '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第三條 TodoList',
  '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第四條 TodoList',
];

class TodoList extends Component {
  render() {
    return (
      <div className="todo">
        <div className="todo-title">
          <h1>TodoList</h1>
        </div>
        {/* 4. 使用 Input、Button 元件 */}
        <div className="todo-action">
          <Input placeholder='todo' className="todo-input" />
          <Button type="primary" className="todo-submit">提交</Button>
        </div>
        {/* 5. 使用 List 元件 */}
        <div className="todo-list">
          <List
            size="large"
            bordered
            dataSource={data}
            renderItem={(item, index) => (<List.Item>{index + 1} - {item}</List.Item>)}
          />
        </div>
      </div>
    );
  }
}

export default TodoList;
複製程式碼
  1. src/index.css
程式碼詳情
.todo {
  width: 1000px;
  margin: 20px auto 0;
  padding: 30px;
  border: 1px solid #ccc;
  border-radius: 10px;
}
.todo-title {
  text-align: center;
}
.todo-action .todo-input {
  width: 200px;
}
.todo-action .todo-submit {
  margin-left: 10px;
}
.todo-list {
  margin-top: 30px;
}
複製程式碼

在這裡,我們引用 Ant Design 的步驟大致為:

  1. 引入 antd 的 Input、Button、List 元件
  2. 引入 antd 的樣式
  3. 定義資料
  4. 使用 Input、Button 元件
  5. 使用 List 元件

此時頁面顯示為:

React Demo Two - TodoList 升級

五 使用 Redux

返回目錄

我覺得有必要在講解 Redux 之前,我們先使用 Redux 體驗一下:

  • 安裝 Redux:npm i redux -S
  • 下面我們按照 Redux 的使用方法先試試:

在 src 目錄下建立 store 目錄用來儲存 Redux 資料,該目錄下有 reducer.js 以及 index.js 兩個檔案

首先,我們編寫 reducer.js 檔案,該檔案的作用是定義並處理資料:

  1. src/store/reducer.js
程式碼詳情
// 1. 我們定義一個資料 defaultState
const defaultState = {
  inputValue: '',
  todoList: [
    '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第一條 TodoList',
    '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第二條 TodoList',
    '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第三條 TodoList',
    '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第四條 TodoList',
  ]
}

// 2. 我們將資料 defaultState 最終以 state 形式匯出去
export default (state = defaultState, action) => {
  return state;
}
複製程式碼

然後,我們編寫 index.js 檔案,該檔案的作用是通過 createStore 方法建立資料倉儲並匯出去給 TodoList.js 使用。

  1. src/store/index.js
程式碼詳情
import { createStore } from 'redux'; // 3. 我們引用 redux 這個庫中的 createStore
import reducer from './reducer'; // 4. 我們引用 reducer.js 中匯出的資料

// 5. 我們通過 redux 提供的方法 reducer 來構建一個資料儲存倉庫
const store = createStore(reducer);

// 6. 我們將 store 匯出去
export default store;
複製程式碼

最後,我們在 TodoList.js 中引用 store/index.js 併到列表中進行使用,以及列印出來 store 傳遞給我們的資料:

  1. src/TodoList.js
程式碼詳情
import React, { Component } from 'react'; // 引入 React 及其 Component
import './index.css'; // 引入 index.css
import { Input, Button, List } from 'antd'; // 引入 antd 的元件
import 'antd/dist/antd.css'; // 引入 antd 的樣式
import store from './store'; // 7. 引入 store,你可以理解為 store 提供資料。./store 是 ./store/index.js 的縮寫

class TodoList extends Component {

  // 8. 在 constructor 中通過 store.getState() 方法來獲取資料,並賦值為 state
  constructor(props) {
    super(props);
    // 9. 我們嘗試在 Console 中列印 store.getState()
    console.log(store.getState());
    this.state = store.getState();
  }

  render() {
    return (
      <div className="todo">
        <div className="todo-title">
          <h1>TodoList</h1>
        </div>
        {/* 使用 Input、Button 元件 */}
        <div className="todo-action">
          <Input placeholder='todo' className="todo-input" />
          <Button type="primary" className="todo-submit">提交</Button>
        </div>
        {/* 使用 List 元件 */}
        {/* 10. 將原先的 data 換成 state 中的 todoList */}
        <div className="todo-list">
          <List
            size="large"
            bordered
            dataSource={this.state.todoList}
            renderItem={(item, index) => (<List.Item>{index + 1} - {item}</List.Item>)}
          />
        </div>
      </div>
    );
  }
}

export default TodoList;
複製程式碼

這時候,我們檢視 Chrome 控制檯和頁面,發現它的確起作用了:

React Demo Two - TodoList 升級

這樣我們就完成了 Redux 中資料的【查詢】,那麼我們如何【修改】 Redux 中的資料,以及 Redux 是什麼呢?我們一一道來。

六 Redux 探索

返回目錄

如果小夥伴覺得自己看小冊或者官網理解比較通透,請自行查閱,下面觀點僅供參考。

6.1 Redux 外掛

返回目錄

  1. 安裝:科學上網找到對應的 Chrome 外掛,或者百度下載一個,或者通過 npm install --save-dev redux-devtools 安裝它的開發者工具。
  2. 使用:
    1. 關閉瀏覽器,並重新開啟,再開啟控制檯(F12),進入 Redux 欄,提示你尚未安裝程式碼
    2. 前往 index.js 安裝程式碼。
    3. 檢視 state 中發現存有資料,此時 Redux 外掛安裝完畢。

src/store/index.js

程式碼詳情
import { createStore } from 'redux';
import reducer from './reducer';

// 如果安裝了 Redux 工具,則在這裡可以直接使用該工具
const store = createStore(
  reducer,
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

export default store;
複製程式碼

6.2 Redux 知識點講解

返回目錄

React Demo Two - TodoList 升級

由於 React 關於自身的定義:“用於構建使用者介面的 JavaScript 庫”。

所以,當我們在 React 中,如果兄弟元件需要通訊,例如上圖中左側的深色圓圈傳送資料到頂部(第一排)的圓圈,我們就需要兜很多彎子,導致資料通訊非常麻煩。

而 Redux 的出現,是為了彌補這種麻煩的通訊方式:建立起一箇中央機制,方便各元件之間的通訊。從而就有了上圖中右側的排程方式。

那麼,Redux 是怎麼個執行/工作機制呢?我們通過下面圖片進行分析:

React Demo Two - TodoList 升級

在上面圖中,我們假設:

  • 藍色(React Component):借書人
  • 黃色(Action Creators):借書動作
  • 橙色(Store):圖書管理員
  • 紫色(Reducers):系統

它的流程可以理解為:首先借書人走到前臺(借書動作)跟圖書管理員申請借書,圖書管理員幫它在系統中查詢書籍資料,然後拿到電腦返回資訊,最後告訴他去哪借/怎麼使用。

換回正常話,即:當元件(React Components)需要呼叫資料的時候,它就向創造器(Action Creators)發起請求,創造器通知管理者(Store),管理者就去查詢相關資料(Reducers),拿到返回的資訊後,再告訴元件。

而在這過程中,我們會使用到 Redux 的一些常用/核心 API:

  1. createStore:建立 store
  2. store.dispatch:派發 action
  3. store.getState:獲取 store 所有資料內容
  4. store.subscribe:監控 store 改變,store 改變了該方法就會被執行

下面我們通過 Input 輸入資料、Button 提交資料以及刪除 TodoItem 列表項進一步講解上面知識點。

七 Redux 資料修改

返回目錄

現在開始通過 Input 輸入資料、Button 提交資料以及刪除 TodoItem 列表項講解 Redux 資料修改。

7.1 Input 輸入資料

返回目錄

  1. src/TodoList.js
程式碼詳情
import React, { Component } from 'react'; // 引入 React 及其 Component
import './index.css'; // 引入 index.css
import { Input, Button, List } from 'antd'; // 引入 antd 的元件
import 'antd/dist/antd.css'; // 引入 antd 的樣式
import store from './store'; // 引入 store,你可以理解為 store 提供資料。./store 是 ./store/index.js 的縮寫

class TodoList extends Component {

  // 在 constructor 中通過 store.getState() 方法來獲取資料,並賦值為 state
  constructor(props) {
    super(props);
    // 我們嘗試在 Console 中列印 store.getState()
    // console.log(store.getState());
    this.state = store.getState();
    
    // 2. 定義 handleInputChange
    this.handleInputChange = this.handleInputChange.bind(this);

    // 7. 繫結方法 handleStoreChange 來處理 Redux 返回回來的資料
    this.handleStoreChange = this.handleStoreChange.bind(this);
    store.subscribe(this.handleStoreChange);
  }

  render() {
    return (
      <div className="todo">
        <div className="todo-title">
          <h1>TodoList</h1>
        </div>
        {/* 使用 Input、Button 元件 */}
        <div className="todo-action">
          {/* 1. Input 繫結 handleInputChange 事件 */}
          <Input 
            placeholder='todo' 
            className="todo-input" 
            value={this.state.inputValue}
            onChange={this.handleInputChange}
          />
          <Button 
            type="primary" 
            className="todo-submit"
          >
            提交
          </Button>
        </div>
        {/* 使用 List 元件 */}
        {/* 將原先的 data 換成 state 中的 todoList */}
        <div className="todo-list">
          <List
            size="large"
            bordered
            dataSource={this.state.todoList}
            renderItem={(item, index) => (<List.Item>{index + 1} - {item}</List.Item>)}
          />
        </div>
      </div>
    );
  }

  // 3. 編寫 handleInputChange
  handleInputChange(e) {
    // 4. 通過 action,將資料傳遞給 store
    const action = {
      type: 'change_input_value',
      value: e.target.value
    }
    store.dispatch(action);
  }

  // 8. 在 handleStoreChange 中處理資料
  handleStoreChange() {
    this.setState(store.getState());
  }

}

export default TodoList;
複製程式碼
  1. src/store/reducer.js
程式碼詳情
// 定義一個資料 defaultState
const defaultState = {
  inputValue: '',
  todoList: [
    // '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第一條 TodoList',
    // '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第二條 TodoList',
    // '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第三條 TodoList',
    // '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第四條 TodoList',
  ]
}

// 將資料 defaultState 最終以 state 形式匯出去
export default (state = defaultState, action) => {
  // 5. 列印 state 和 action
  console.log(state);
  console.log(action);

  // 6. 在 reducer.js 中獲取資料,並 return 回去處理結果
  if(action.type === 'change_input_value') {
    const newState = JSON.parse(JSON.stringify(state));
    newState.inputValue = action.value;
    return newState
  }
  
  return state;
}
複製程式碼

此時,我們開啟控制檯,一邊在 Input 輸入框輸入內容,一邊檢視 Console 輸出,會發現:

React Demo Two - TodoList 升級

現在我們來分析下(或者小夥伴看著程式碼的備註理解下),我們修改程式碼的時候做了什麼:

  1. 在 Input 元件中,我們為其 onChange 時繫結 handleInputChange 事件。
  2. 定義 handleInputChange 方法。
  3. 編寫 handleInputChange 方法。
  4. 我們在 handleInputChange 中編寫 action,通過 dispatchaction 從 TodoList.js 傳遞給 Redux 中的 reducer.js。
  5. 在 reducer.js 中列印 stateaction
  6. Redux 在 reducer.js 中接收到 stateaction,然後我們將新的 newState 返回回去(先返回到 src/store/index.js,再返回到 src/TodoList.js),期望 TodoList.js 能接受到反饋。
  7. 在 TodoList 的 constructor 中通過 store.subscribe 繫結處理 Redux 傳回來的資料的處理方法 handleStoreChange
  8. handleStoreChange 中,我們直接 setState Redux 返回的 state,即 store.getState()

這時候,我們再檢視章節 6.2 的 Redux 知識點講解,就會發現知識點通暢了。

參考:計數器

程式碼詳情
import { createStore } from 'redux';

/**
 * 這是一個 reducer,形式為 (state, action) => state 的純函式。
 * 描述了 action 如何把 state 轉變成下一個 state。
 *
 * state 的形式取決於你,可以是基本型別、陣列、物件、
 * 甚至是 Immutable.js 生成的資料結構。惟一的要點是
 * 當 state 變化時需要返回全新的物件,而不是修改傳入的引數。
 *
 * 下面例子使用 `switch` 語句和字串來做判斷,但你可以寫幫助類(helper)
 * 根據不同的約定(如方法對映)來判斷,只要適用你的專案即可。
 */
function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1;
  case 'DECREMENT':
    return state - 1;
  default:
    return state;
  }
}

// 建立 Redux store 來存放應用的狀態。
// API 是 { subscribe, dispatch, getState }。
let store = createStore(counter);

// 可以手動訂閱更新,也可以事件繫結到檢視層。
store.subscribe(() =>
  console.log(store.getState())
);

// 改變內部 state 惟一方法是 dispatch 一個 action。
// action 可以被序列化,用日記記錄和儲存下來,後期還可以以回放的方式執行
store.dispatch({ type: 'INCREMENT' });
// 1
store.dispatch({ type: 'INCREMENT' });
// 2
store.dispatch({ type: 'DECREMENT' });
// 1
複製程式碼

7.2 Button 提交資料

返回目錄

下面,我們為 Input 提供回車事件,以及使用 Button 的提交事件,小夥伴們可以參照 Input 的輸入事件,先自行編寫,寫完再檢視這個章節收穫會更大。

src/TodoList.js

程式碼詳情
import React, { Component } from 'react'; // 引入 React 及其 Component
import './index.css'; // 引入 index.css
import { Input, Button, List } from 'antd'; // 引入 antd 的元件
import 'antd/dist/antd.css'; // 引入 antd 的樣式
import store from './store'; // 引入 store,你可以理解為 store 提供資料。./store 是 ./store/index.js 的縮寫

class TodoList extends Component {

  // 在 constructor 中通過 store.getState() 方法來獲取資料,並賦值為 state
  constructor(props) {
    super(props);
    // 我們嘗試在 Console 中列印 store.getState()
    // console.log(store.getState());
    this.state = store.getState();
    
    // 處理 handleInputChange 方法
    this.handleInputChange = this.handleInputChange.bind(this);

    // 繫結方法 handleStoreChange 來處理 Redux 返回回來的資料
    this.handleStoreChange = this.handleStoreChange.bind(this);
    store.subscribe(this.handleStoreChange);

    // 2. 處理 handleAddItem 方法
    this.handleAddItem = this.handleAddItem.bind(this);

    // 7. 處理 handleInputKeyUp 方法
    this.handleInputKeyUp = this.handleInputKeyUp.bind(this);
  }

  render() {
    return (
      <div className="todo">
        <div className="todo-title">
          <h1>TodoList</h1>
        </div>
        {/* 使用 Input、Button 元件 */}
        <div className="todo-action">
          {/* Input 繫結 handleInputChange 事件 */}
          {/* 6. Input 繫結回車事件:handleInputKeyUp */}
          <Input 
            placeholder='todo' 
            className="todo-input" 
            value={this.state.inputValue}
            onChange={this.handleInputChange}
            onKeyUp={this.handleInputKeyUp}
          />
          {/* 1. 為 Button 定義點選執行 handleAddItem 方法 */}
          <Button 
            type="primary" 
            className="todo-submit"
            onClick={this.handleAddItem}
          >
            提交
          </Button>
        </div>
        {/* 使用 List 元件 */}
        {/* 將原先的 data 換成 state 中的 todoList */}
        <div className="todo-list">
          <List
            size="large"
            bordered
            dataSource={this.state.todoList}
            renderItem={(item, index) => (<List.Item>{index + 1} - {item}</List.Item>)}
          />
        </div>
      </div>
    );
  }

  // 編寫 handleInputChange 方法
  handleInputChange(e) {
    // 通過 dispatch(action),將資料傳遞給 store
    const action = {
      type: 'change_input_value',
      value: e.target.value
    }
    store.dispatch(action);
  }

  // 在 handleStoreChange 中處理資料
  handleStoreChange() {
    this.setState(store.getState());
  }

  // 3. 編寫 handleAddItem 方法
  handleAddItem() {
    // 4. 通過 dispatch(action),將資料傳遞給 store
    const action = {
      type: 'add_todo_item'
    }
    store.dispatch(action);
  }

  // 8. 為 Input 的 keyUp 方法 handleInputKeyUp 繫結 handleAddItem
  handleInputKeyUp(e) {
    if(e.keyCode === 13) {
      this.handleAddItem();
    }
  }

}

export default TodoList;
複製程式碼

src/store/reducer.js

程式碼詳情
// 定義一個資料 defaultState
const defaultState = {
  inputValue: '',
  todoList: [
    // '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第一條 TodoList',
    // '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第二條 TodoList',
    // '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第三條 TodoList',
    // '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第四條 TodoList',
  ]
}

// 將資料 defaultState 最終以 state 形式匯出去
export default (state = defaultState, action) => {
  // 列印 state 和 action
  // console.log(state);
  // console.log(action);

  // 在 reducer.js 中獲取資料,並 return 回去處理結果
  if(action.type === 'change_input_value') {
    const newState = JSON.parse(JSON.stringify(state));
    newState.inputValue = action.value;
    return newState;
  }

  // 5. 在 reducer.js 中獲取資料,並 return 回去處理結果
  if(action.type === 'add_todo_item') {
    const newState = JSON.parse(JSON.stringify(state));
    newState.todoList.push(newState.inputValue);
    newState.inputValue = '';
    return newState;
  }

  return state;
}
複製程式碼

這時候,我們的 Button 提交事件都處理完畢了,此時頁面的功能實現:

React Demo Two - TodoList 升級

OK,我們再來梳理一遍流程:

  1. 為 Button 定義點選執行 handleAddItem 方法
  2. 處理 handleAddItem 方法
  3. 編寫 handleAddItem 方法
  4. 通過 dispatch(action),將資料傳遞給 store
  5. 在 reducer.js 中獲取資料,並 return 回去處理結果
  6. Input 繫結回車事件:handleInputKeyUp
  7. 處理 handleInputKeyUp 方法
  8. 為 Input 的 keyUp 方法 handleInputKeyUp 繫結 handleAddItem

值得注意的是,我們在 Input 的時候,就做過 handleStoreChange 的處理,所以我們就沒有再寫 store.subscribe() 來監控資料的改變,所以小夥伴們要注意整體流程。

7.3 刪除 TodoItem 列表項

返回目錄

那麼接下來,我們再給列表項點選新增刪除事件。

src/TodoList.js

程式碼詳情
import React, { Component } from 'react'; // 引入 React 及其 Component
import './index.css'; // 引入 index.css
import { Input, Button, List } from 'antd'; // 引入 antd 的元件
import 'antd/dist/antd.css'; // 引入 antd 的樣式
import store from './store'; // 引入 store,你可以理解為 store 提供資料。./store 是 ./store/index.js 的縮寫

class TodoList extends Component {

  // 在 constructor 中通過 store.getState() 方法來獲取資料,並賦值為 state
  constructor(props) {
    super(props);
    // 我們嘗試在 Console 中列印 store.getState()
    // console.log(store.getState());
    this.state = store.getState();
    
    // 處理 handleInputChange 方法
    this.handleInputChange = this.handleInputChange.bind(this);

    // 繫結方法 handleStoreChange 來處理 Redux 返回回來的資料
    this.handleStoreChange = this.handleStoreChange.bind(this);
    store.subscribe(this.handleStoreChange);

    // 處理 handleAddItem 方法
    this.handleAddItem = this.handleAddItem.bind(this);

    // 處理 handleInputKeyUp 方法
    this.handleInputKeyUp = this.handleInputKeyUp.bind(this);
  }

  render() {
    return (
      <div className="todo">
        <div className="todo-title">
          <h1>TodoList</h1>
        </div>
        {/* 使用 Input、Button 元件 */}
        <div className="todo-action">
          {/* Input 繫結 handleInputChange 事件 */}
          {/* Input 繫結回車事件:handleInputKeyUp */}
          <Input 
            placeholder='todo' 
            className="todo-input" 
            value={this.state.inputValue}
            onChange={this.handleInputChange}
            onKeyUp={this.handleInputKeyUp}
          />
          {/* 為 Button 定義點選執行 handleAddItem 方法 */}
          <Button 
            type="primary" 
            className="todo-submit"
            onClick={this.handleAddItem}
          >
            提交
          </Button>
        </div>
        {/* 使用 List 元件 */}
        {/* 將原先的 data 換成 state 中的 todoList */}
        {/* 1. 列表點選事件繫結 handleDeleteItem 方法 */}
        <div className="todo-list">
          <List
            size="large"
            bordered
            dataSource={this.state.todoList}
            renderItem={(item, index) => (
              <List.Item onClick={this.handleDeleteItem.bind(this, index)}>{index + 1} - {item}</List.Item>
            )}
          />
        </div>
      </div>
    );
  }

  // 編寫 handleInputChange 方法
  handleInputChange(e) {
    // 通過 dispatch(action),將資料傳遞給 store
    const action = {
      type: 'change_input_value',
      value: e.target.value
    }
    store.dispatch(action);
  }

  // 在 handleStoreChange 中處理資料
  handleStoreChange() {
    this.setState(store.getState());
  }

  // 編寫 handleAddItem 方法
  handleAddItem() {
    // 通過 dispatch(action),將資料傳遞給 store
    const action = {
      type: 'add_todo_item'
    }
    store.dispatch(action);
  }

  // 為 Input 的 keyUp 方法 handleInputKeyUp 繫結 handleAddItem
  handleInputKeyUp(e) {
    if(e.keyCode === 13) {
      this.handleAddItem();
    }
  }

  // 2. 編寫 handleDeleteItem 方法
  handleDeleteItem(index) {
    console.log(index);
    // 3. 通過 dispatch(action),將資料傳遞給 store
    const action = {
      type: 'delete_todo_item',
      index
    }
    store.dispatch(action);
  }

}

export default TodoList;
複製程式碼

src/store/reducer.js

程式碼詳情
// 定義一個資料 defaultState
const defaultState = {
  inputValue: '',
  todoList: [
    // '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第一條 TodoList',
    // '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第二條 TodoList',
    // '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第三條 TodoList',
    // '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第四條 TodoList',
  ]
}

// 將資料 defaultState 最終以 state 形式匯出去
export default (state = defaultState, action) => {
  // 列印 state 和 action
  // console.log(state);
  // console.log(action);

  // 在 reducer.js 中獲取資料,並 return 回去處理結果
  if(action.type === 'change_input_value') {
    const newState = JSON.parse(JSON.stringify(state));
    newState.inputValue = action.value;
    return newState;
  }

  // 在 reducer.js 中獲取資料,並 return 回去處理結果
  if(action.type === 'add_todo_item') {
    const newState = JSON.parse(JSON.stringify(state));
    newState.todoList.push(newState.inputValue);
    newState.inputValue = '';
    return newState;
  }

  // 4. 在 reducer.js 中獲取資料,並 return 回去處理結果
  if(action.type === 'delete_todo_item') {
    const newState = JSON.parse(JSON.stringify(state));
    newState.todoList.splice(action.index, 1);
    return newState;
  }

  return state;
}
複製程式碼

現在我們先進行功能演示:

React Demo Two - TodoList 升級

再來檢視下我們的程式設計思路:

  1. 列表點選事件繫結 handleDeleteItem 方法。此時,由於需要繫結 this,並且傳遞值 index,即兩個值,所以我們直接在程式碼中:this.handleDeleteItem.bind(this, index)
  2. 編寫 handleDeleteItem 方法
  3. 通過 dispatch(action),將資料傳遞給 store
  4. 在 reducer.js 中獲取資料,並 return 回去處理結果

這樣,我們就完成了列表項的刪除。

至此,我們就熟悉了 Reudx 的資料獲取以及修改方法。

八 優化:抽取 action 中的 type

返回目錄

在上面章節中,我們已經完成了 TodoList 的建設,可以說我們已經搞定了。

但是,你懂的,本篇文章名為:【React Demo Two - TodoList 升級】

就是說,我們不僅要升級到 Redux,還要進一步地升級,為大型專案的開發做鋪墊。

所以,本章節開始進行優化處理。

在上面程式碼中,我們有沒有發現我們 actiontype 是寫到 TodoList.js 中的,多了後不好處理?

  • change_input_value
  • add_todo_item
  • delete_todo_item

所以我們需要進行下 type 的處理,我們在 store 目錄下新增一個 actionTypes.js:

src/store/actionTypes.js

程式碼詳情
// 1. 定義 actionTypes
export const CHANGE_INPUT_VALUE = 'change_input_value';
export const ADD_TODO_ITEM =  'add_todo_item';
export const DELETE_TODO_ITEM = 'delete_todo_item';
複製程式碼

然後在 TodoList.js 和 reducer.js 中使用:

src/TodoList.js

程式碼詳情
import React, { Component } from 'react'; // 引入 React 及其 Component
import './index.css'; // 引入 index.css
import { Input, Button, List } from 'antd'; // 引入 antd 的元件
import 'antd/dist/antd.css'; // 引入 antd 的樣式
import store from './store'; // 引入 store,你可以理解為 store 提供資料。./store 是 ./store/index.js 的縮寫
import { CHANGE_INPUT_VALUE, ADD_TODO_ITEM, DELETE_TODO_ITEM } from './actionTypes'; // 2. 引用 actionTypes

class TodoList extends Component {

  // 在 constructor 中通過 store.getState() 方法來獲取資料,並賦值為 state
  constructor(props) {
    super(props);
    // 我們嘗試在 Console 中列印 store.getState()
    // console.log(store.getState());
    this.state = store.getState();
    
    // 處理 handleInputChange 方法
    this.handleInputChange = this.handleInputChange.bind(this);

    // 繫結方法 handleStoreChange 來處理 Redux 返回回來的資料
    this.handleStoreChange = this.handleStoreChange.bind(this);
    store.subscribe(this.handleStoreChange);

    // 處理 handleAddItem 方法
    this.handleAddItem = this.handleAddItem.bind(this);

    // 處理 handleInputKeyUp 方法
    this.handleInputKeyUp = this.handleInputKeyUp.bind(this);
  }

  render() {
    return (
      <div className="todo">
        <div className="todo-title">
          <h1>TodoList</h1>
        </div>
        {/* 使用 Input、Button 元件 */}
        <div className="todo-action">
          {/* Input 繫結 handleInputChange 事件 */}
          {/* Input 繫結回車事件:handleInputKeyUp */}
          <Input 
            placeholder='todo' 
            className="todo-input" 
            value={this.state.inputValue}
            onChange={this.handleInputChange}
            onKeyUp={this.handleInputKeyUp}
          />
          {/* 為 Button 定義點選執行 handleAddItem 方法 */}
          <Button 
            type="primary" 
            className="todo-submit"
            onClick={this.handleAddItem}
          >
            提交
          </Button>
        </div>
        {/* 使用 List 元件 */}
        {/* 將原先的 data 換成 state 中的 todoList */}
        {/* 列表點選事件繫結 handleDeleteItem 方法 */}
        <div className="todo-list">
          <List
            size="large"
            bordered
            dataSource={this.state.todoList}
            renderItem={(item, index) => (
              <List.Item onClick={this.handleDeleteItem.bind(this, index)}>{index + 1} - {item}</List.Item>
            )}
          />
        </div>
      </div>
    );
  }

  // 編寫 handleInputChange 方法
  handleInputChange(e) {
    // 通過 dispatch(action),將資料傳遞給 store
    // 3. 使用 actionTypes
    const action = {
      type: CHANGE_INPUT_VALUE,
      value: e.target.value
    }
    store.dispatch(action);
  }

  // 在 handleStoreChange 中處理資料
  handleStoreChange() {
    this.setState(store.getState());
  }

  // 編寫 handleAddItem 方法
  handleAddItem() {
    // 通過 dispatch(action),將資料傳遞給 store
    // 3. 使用 actionTypes
    const action = {
      type: ADD_TODO_ITEM
    }
    store.dispatch(action);
  }

  // 為 Input 的 keyUp 方法 handleInputKeyUp 繫結 handleAddItem
  handleInputKeyUp(e) {
    if(e.keyCode === 13) {
      this.handleAddItem();
    }
  }

  // 編寫 handleDeleteItem 方法
  handleDeleteItem(index) {
    console.log(index);
    // 通過 dispatch(action),將資料傳遞給 store
    // 3. 使用 actionTypes
    const action = {
      type: DELETE_TODO_ITEM,
      index
    }
    store.dispatch(action);
  }

}

export default TodoList;
複製程式碼

src/store/reducer.js

程式碼詳情
import { CHANGE_INPUT_VALUE, ADD_TODO_ITEM, DELETE_TODO_ITEM } from './actionTypes'; // 2. 引用 actionTypes

// 定義一個資料 defaultState
const defaultState = {
  inputValue: '',
  todoList: [
    // '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第一條 TodoList',
    // '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第二條 TodoList',
    // '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第三條 TodoList',
    // '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第四條 TodoList',
  ]
}

// 將資料 defaultState 最終以 state 形式匯出去
export default (state = defaultState, action) => {
  // 列印 state 和 action
  // console.log(state);
  // console.log(action);

  // 在 reducer.js 中獲取資料,並 return 回去處理結果
  // 3. 使用 actionTypes
  if(action.type === CHANGE_INPUT_VALUE) {
    const newState = JSON.parse(JSON.stringify(state));
    newState.inputValue = action.value;
    return newState;
  }

  // 在 reducer.js 中獲取資料,並 return 回去處理結果
  // 3. 使用 actionTypes
  if(action.type === ADD_TODO_ITEM) {
    const newState = JSON.parse(JSON.stringify(state));
    newState.todoList.push(newState.inputValue);
    newState.inputValue = '';
    return newState;
  }

  // 在 reducer.js 中獲取資料,並 return 回去處理結果
  // 3. 使用 actionTypes
  if(action.type === DELETE_TODO_ITEM) {
    const newState = JSON.parse(JSON.stringify(state));
    newState.todoList.splice(action.index, 1);
    return newState;
  }

  return state;
}
複製程式碼

另外,抽取 actionTypes.js 的意義在於,固定 action.type 值,從而不會因為在兩處不同地方使用,導致報錯。

九 優化:抽取整個 action

返回目錄

隨著程式碼量的增多,我們發現註釋逐漸增長,所以在這裡,我們先去掉所有註釋,請小夥伴們自行熟悉上面章節的程式碼流程。

清除完畢後,我們可以發現:雖然 actionType 抽取出來了,但是當頁面足夠複雜的時候,我們的 action 管理起來還是非常複雜,所以我們嘗試將整個 action 抽取出來。

我們在 store 目錄中新建一個 actionCreators.js:

src/store/actionCreators.js

程式碼詳情
// 1. 引入 actionTypes
import { CHANGE_INPUT_VALUE, ADD_TODO_ITEM, DELETE_TODO_ITEM } from './actionTypes'

// 2. 匯出相應 action
export const getInputChangeAction = (value) => ({
  type: CHANGE_INPUT_VALUE,
  value
})

export const getAddItemAction = () => ({
  type: ADD_TODO_ITEM
})

export const getItemDeleteAction = (index) => ({
  type: DELETE_TODO_ITEM,
  index
})
複製程式碼

機智的小夥伴,看到這裡,應該就明白我們的意圖了,所以,我們再修改下 TodoList.js 即可:

src/TodoList.js

程式碼詳情
import React, { Component } from 'react';
import './index.css';
import { Input, Button, List } from 'antd';
import 'antd/dist/antd.css';
import store from './store';
import { getChangeInputValue, getAddTodoItem, getDeleteTodoItem } from './store/actionCreators'; // 3. 引入 actionCreators

class TodoList extends Component {

  constructor(props) {
    super(props);
    this.state = store.getState();
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleStoreChange = this.handleStoreChange.bind(this);
    store.subscribe(this.handleStoreChange);
    this.handleAddItem = this.handleAddItem.bind(this);
    this.handleInputKeyUp = this.handleInputKeyUp.bind(this);
  }

  render() {
    return (
      <div className="todo">
        <div className="todo-title">
          <h1>TodoList</h1>
        </div>
        <div className="todo-action">
          <Input 
            placeholder='todo' 
            className="todo-input" 
            value={this.state.inputValue}
            onChange={this.handleInputChange}
            onKeyUp={this.handleInputKeyUp}
          />
          <Button 
            type="primary" 
            className="todo-submit"
            onClick={this.handleAddItem}
          >
            提交
          </Button>
        </div>
        <div className="todo-list">
          <List
            size="large"
            bordered
            dataSource={this.state.todoList}
            renderItem={(item, index) => (
              <List.Item onClick={this.handleDeleteItem.bind(this, index)}>{index + 1} - {item}</List.Item>
            )}
          />
        </div>
      </div>
    );
  }

  handleInputChange(e) {
    // 4. 使用 actionCreators 中的 getChangeInputValue
    const action = getChangeInputValue(e.target.value);
    store.dispatch(action);
  }

  handleStoreChange() {
    this.setState(store.getState());
  }

  handleAddItem() {
    // 4. 使用 actionCreators 中的 getAddTodoItem
    const action = getAddTodoItem();
    store.dispatch(action);
  }

  handleInputKeyUp(e) {
    if(e.keyCode === 13) {
      this.handleAddItem();
    }
  }

  handleDeleteItem(index) {
    // 4. 使用 actionCreators 中的 getAddTodoItem
    const action = getDeleteTodoItem(index);
    store.dispatch(action);
  }

}

export default TodoList;
複製程式碼

這樣,我們就把整個 action 抽取出來了,在大型專案中,對我們的工作會非常方便。

十 優化:UI 元件和容器元件

返回目錄

現在,先丟擲兩個定義:

  • UI 元件 —— 傻瓜元件,做頁面的渲染
  • 容器元件 —— 聰明元件,做頁面的邏輯

我們先不多解釋,進行程式碼拆分,再來講解為什麼會有這兩個定義。

在這裡,我們進行元件的拆分:

src/TodoList.js

程式碼詳情
import React, { Component } from 'react';
import './index.css';
import 'antd/dist/antd.css';
import store from './store';
import { getChangeInputValue, getAddTodoItem, getDeleteTodoItem } from './store/actionCreators';
// 1. 將 Input 等 antd 的元件引入遷移到 TodoListUI,並引入 TodoListUI
import TodoListUI from './TodoListUI';

class TodoList extends Component {

  constructor(props) {
    super(props);
    this.state = store.getState();
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleAddItem = this.handleAddItem.bind(this);
    this.handleInputKeyUp = this.handleInputKeyUp.bind(this);
    this.handleDeleteItem = this.handleDeleteItem.bind(this);

    this.handleStoreChange = this.handleStoreChange.bind(this);
    store.subscribe(this.handleStoreChange);
  }

  render() {
    return (
      // 2. 編寫 TodoListUI,傳遞引數到 TodoListUI 中
      <TodoListUI
        inputValue={this.state.inputValue}
        todoList={this.state.todoList}
        handleInputChange={this.handleInputChange}
        handleInputKeyUp={this.handleInputKeyUp}
        handleAddItem={this.handleAddItem}
        handleDeleteItem={this.handleDeleteItem}
      />
    );
  }

  handleInputChange(e) {
    // 解決 Antd 中的 bug
    e.persist();
    const action = getChangeInputValue(e.target.value);
    store.dispatch(action);
  }

  handleStoreChange() {
    this.setState(store.getState());
  }

  handleAddItem() {
    const action = getAddTodoItem();
    store.dispatch(action);
  }

  handleInputKeyUp(e) {
    // 解決 Antd 中的 bug
    e.persist();
    if(e.keyCode === 13) {
      this.handleAddItem();
    }
  }

  handleDeleteItem(index) {
    // 解決 Antd 中的 bug
    index.persist();
    const action = getDeleteTodoItem(index);
    store.dispatch(action);
  }

}

export default TodoList;
複製程式碼

在這裡,我們將 render 中的內容抽取到子元件,該子元件在 src 目錄下,叫 TodoListUI,我們將 TodoList.js 當成容器元件中,只需要將資料傳遞給 TodoListUI 就行了,然後我們編寫 UI 元件內容:

src/TodoListUI.js

程式碼詳情
// 3. 引入 Input 等元件
import React, { Component } from 'react';
import { Input, Button, List } from 'antd';

class TodoListUI extends Component {
  render() {
    return (
      // 4. 接收 TodoList.js 中傳遞的資料
      <div className="todo">
        <div className="todo-title">
          <h1>TodoList</h1>
        </div>
        <div className="todo-action">
          <Input 
            placeholder='todo' 
            className="todo-input" 
            value={this.props.inputValue}
            onChange={this.props.handleInputChange}
            onKeyUp={this.props.handleInputKeyUp}
          />
          <Button 
            type="primary" 
            className="todo-submit"
            onClick={this.props.handleAddItem}
          >
            提交
          </Button>
        </div>
        <div className="todo-list">
        {/* 5. 在處理 handleDeleteItem 的時候需要注意,index 的值需要再進行處理 */}
          <List
            size="large"
            bordered
            dataSource={this.props.todoList}
            renderItem={(item, index) => (
              <List.Item onClick={() => {this.props.handleDeleteItem(index)}}>
                {index + 1} - {item}
              </List.Item>
            )}
          />
        </div>
      </div>
    );
  }
}

export default TodoListUI;
複製程式碼

這樣,我們就完成了 UI 元件和容器元件的拆分。

我們所做的內容有:

  1. 將 Input 等 antd 的元件引入遷移到 TodoListUI,並引入 TodoListUI
  2. 編寫 TodoListUI,傳遞引數到 TodoListUI 中
  3. 引入 Input 等元件
  4. 接收 TodoList.js 中傳遞的資料
  5. 在處理 handleDeleteItem 的時候需要注意,index 的值需要再進行處理

這樣,我們就完成了頁面的抽取,當我們頁面過多的時候,我們就將內容獨立到 UI 元件中。而容器元件,則可以包含無數個 UI 元件。所以:

容器元件是聰明元件,它對整體進行了一個把控;而 UI 元件是傻瓜元件,只需要執行容器元件傳遞過來的事件並渲染頁面即可。

十一 優化:無狀態元件

返回目錄

當一個元件中,只有 render() 函式,而不做其他事情的時候,我們就把它叫做無狀態元件。

在 TodoList 這個專案中,我們的 TodoListUI 就只做了 render() 工作,所以可以將 TodoListUI 作為一個無狀態元件:

src/TodoListUI

程式碼詳情
// 1. 我們不需要 react 中的 Component 了
import React from 'react';
import { Input, Button, List } from 'antd';

// class TodoListUI extends Component {

// 2. 進行無狀態元件定義,然後父元件傳遞過來的資料,通過 props 獲取
const TodoListUI = (props) => {
  // 3. 我們不需要進行 render 了,直接 return 就可以了
  return (
    // 4. 接收 TodoList.js 中傳遞的資料
    <div className="todo">
      <div className="todo-title">
        <h1>TodoList</h1>
      </div>
      <div className="todo-action">
        {/* 5. 我們修改 this.props 為 props */}
        <Input 
          placeholder='todo' 
          className="todo-input" 
          value={props.inputValue}
          onChange={props.handleInputChange}
          onKeyUp={props.handleInputKeyUp}
        />
        <Button 
          type="primary" 
          className="todo-submit"
          onClick={props.handleAddItem}
        >
          提交
        </Button>
      </div>
      <div className="todo-list">
        <List
          size="large"
          bordered
          dataSource={props.todoList}
          renderItem={(item, index) => (
            <List.Item onClick={() => {props.handleDeleteItem(index)}}>
              {index + 1} - {item}
            </List.Item>
          )}
        />
      </div>
    </div>
  );
}

export default TodoListUI;
複製程式碼

在這裡,大致做了 5 項工作:

  1. 我們不需要 react 中的 Component 了,所以我們去掉了 Component
  2. 進行無狀態元件定義,然後父元件傳遞過來的資料,通過 props 獲取
  3. 我們不需要進行 render 了,直接 return 就可以了
  4. 接收 TodoList.js 中傳遞的資料
  5. 我們修改 this.props 為 props

十二 結尾:呼叫 Axios,Redux-Base 完成

返回目錄

終於來到最終環節,我們需要獲取後端提供的介面,來進一步開發。

  • 引入 Axios:cnpm i axios -S
  • componentDidMount 中獲取介面資料,並走流程,最終渲染到頁面上:

TodoList.js

程式碼詳情
import React, { Component } from 'react';
import './index.css';
import 'antd/dist/antd.css';
import store from './store';
// 7. 從 actionCreators 中引入 initListAction
import { getChangeInputValue, getAddTodoItem, getDeleteTodoItem, initListAction } from './store/actionCreators';
import TodoListUI from './TodoListUI';
import axios from 'axios'; // 1. 引入 axios

class TodoList extends Component {

  constructor(props) {
    super(props);
    this.state = store.getState();
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleAddItem = this.handleAddItem.bind(this);
    this.handleInputKeyUp = this.handleInputKeyUp.bind(this);
    this.handleDeleteItem = this.handleDeleteItem.bind(this);

    this.handleStoreChange = this.handleStoreChange.bind(this);
    store.subscribe(this.handleStoreChange);
  }

  render() {
    return (
      <TodoListUI
        inputValue={this.state.inputValue}
        todoList={this.state.todoList}
        handleInputChange={this.handleInputChange}
        handleInputKeyUp={this.handleInputKeyUp}
        handleAddItem={this.handleAddItem}
        handleDeleteItem={this.handleDeleteItem}
      />
    );
  }

  // 2. 在 componentDidMount() 中進行 axios 介面呼叫
  componentDidMount() {
    axios.get('https://www.easy-mock.com/mock/5ca803587e5a246db3d100cb/todolist').then( (res) => {
      console.log(res.data.todolist);
      // 3. 將介面資料 dispatch 到 action 中,所以需要先前往 actionCreators.js 中建立 action
      // 8. 建立 action 並 dispatch 到 reducer.js 中
      const action = initListAction(res.data.todolist);
      store.dispatch(action);
    })
  }

  handleInputChange(e) {
    // 解決 Antd 中的 bug
    e.persist();
    const action = getChangeInputValue(e.target.value);
    store.dispatch(action);
  }

  handleStoreChange() {
    this.setState(store.getState());
  }

  handleAddItem() {
    const action = getAddTodoItem();
    store.dispatch(action);
  }

  handleInputKeyUp(e) {
    // 解決 Antd 中的 bug
    e.persist();
    if(e.keyCode === 13) {
      this.handleAddItem();
    }
  }

  handleDeleteItem(index) {
    // 解決 Antd 中的 bug
    index.persist();
    const action = getDeleteTodoItem(index);
    store.dispatch(action);
  }

}

export default TodoList;
複製程式碼

actionCreators.js

程式碼詳情
// 5. 從 actionTypes 引入 INIT_LIST_ACTION
import { CHANGE_INPUT_VALUE, ADD_TODO_ITEM, DELETE_TODO_ITEM, INIT_LIST_ACTION } from './actionTypes';

export const getChangeInputValue = (value) => ({
  type: CHANGE_INPUT_VALUE,
  value
})

export const getAddTodoItem = () => ({
  type: ADD_TODO_ITEM
})

export const getDeleteTodoItem = (index) => ({
  type: DELETE_TODO_ITEM,
  index
})

// 4. 編寫匯出的 initListAction,所以需要先在 actionTypes 中引入 INIT_LIST_ACTION
export const initListAction = (data) => ({
  type: INIT_LIST_ACTION,
  data
})
複製程式碼

actionTypes.js

程式碼詳情
export const CHANGE_INPUT_VALUE = 'change_input_value';
export const ADD_TODO_ITEM =  'add_todo_item';
export const DELETE_TODO_ITEM = 'delete_todo_item';
// 6. 匯出 INIT_LIST_ACTION
export const INIT_LIST_ACTION = 'init_list_action';
複製程式碼

reducer.js

程式碼詳情
// 9. 從 actionTypes 引用 INIT_LIST_ACTION
import { CHANGE_INPUT_VALUE, ADD_TODO_ITEM, DELETE_TODO_ITEM, INIT_LIST_ACTION } from './actionTypes';

const defaultState = {
  inputValue: '',
  todoList: []
}

export default (state = defaultState, action) => {

  if(action.type === CHANGE_INPUT_VALUE) {
    const newState = JSON.parse(JSON.stringify(state));
    newState.inputValue = action.value;
    return newState;
  }

  if(action.type === ADD_TODO_ITEM) {
    const newState = JSON.parse(JSON.stringify(state));
    newState.todoList.push(newState.inputValue);
    newState.inputValue = '';
    return newState;
  }

  if(action.type === DELETE_TODO_ITEM) {
    const newState = JSON.parse(JSON.stringify(state));
    newState.todoList.splice(action.index, 1);
    return newState;
  }

  // 10. 接受 TodoList 傳遞過來的資料,並進行處理與返回
  if(action.type === INIT_LIST_ACTION) {
    const newState = JSON.parse(JSON.stringify(state));
    newState.todoList = action.data;
    return newState;
  }

  return state;
}
複製程式碼

這樣,我們就完成了 axios 的呼叫,並渲染到頁面上,整理出來,思路為:

  1. TodoList.js —— 引入 axios
  2. TodoList.js —— 在 componentDidMount() 中進行 axios 介面呼叫
  3. TodoList.js —— 將介面資料 dispatchaction 中,所以需要先前往 actionCreators.js 中建立 action
  4. actionCreators.js —— 編寫匯出的 initListAction,所以需要先在 actionTypes 中引入 INIT_LIST_ACTION
  5. actionCreators.js —— 從 actionTypes 引入 INIT_LIST_ACTION
  6. actionTypes.js —— 匯出 INIT_LIST_ACTION 到 actionCreators
  7. TodoList.js —— 從 actionCreators 中引入 initListAction
  8. TodoList.js —— 建立 actiondispatch 到 reducer.js 中
  9. reducer.js —— 從 actionTypes 引用 INIT_LIST_ACTION
  10. reducer.js —— 接受 TodoList 傳遞過來的資料,並進行處理與返回

如此,我們就完成了介面的呼叫,此時頁面顯示如下:

React Demo Two - TodoList 升級

到此,我們就完成了 Redux-Base。

但是,這只是簡單的 Redux 的使用,我們可以感受到,僅僅使用 Redux 對於專案來說還是複雜的,所以我們需要 Redux 的中介軟體 Redux-Thunk 以及 Redux-Saga。並在最後嘗試使用下 React-Redux。

十三 進階:Redux 中介軟體

返回目錄

  • 什麼是中介軟體?

中介軟體即是安排在誰與誰之間的外掛。

  • 什麼是 Redux 中介軟體?

看圖:

React Demo Two - TodoList 升級

在上面圖中我們可以看出,我們通過 Dispatch 將 Action 派發到 Store 的時候,我們在 Dispatch 中引用了中介軟體做處理。它對 Dispatch 做了封裝升級,從而使得我們不僅可以在 Dispatch 使用物件,而且可以使用方法函式。

這樣,當我們傳遞給 Dispatch 一個物件的時候,跟我們正常使用 redux 沒區別。但是,當我們傳遞給 Dispatch 一個函式的時候,如果我們使用了 Redux-Thunk 或者 Redux-Saga 的時候,它們就會對此進行處理,從而讓我們也可以呼叫函式。

因此,簡單來說,Redux 的中介軟體,就是對 Dispatch 的封裝升級。

十四 進階:Redux-Thunk 中介軟體進行 ajax 請求管理

返回目錄

在第十二章節中,我們在 TodoList 中進行了 Ajax 請求,這是可以的。

但是,隨著 Ajax 請求越來越多,如果我們都在頁面中編寫,那麼就會讓頁面顯得臃腫。

這時候,就需要 Redux-Thunk 了。Redux-Thunk 可以把非同步請求及複雜業務邏輯抽取到其他地方處理。

我們拷貝一份 Redux-Base 程式碼到 Redux-Thunk 目錄中,並執行:

注意:不需要拷貝 node_modules 資料夾

  • 安裝依賴:npm i
  • 執行專案:npm run start

然後,我們開始引用 Redux-Thunk:

  • Redux Thunk:Github 地址
  • 安裝:npm i redux-thunk -S
  • 教程小例子:

test.js

程式碼詳情
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';

const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
)
複製程式碼

很好看上去非常 easy 有木有,那麼我們在專案中嘗試一下。

src/store/index.js

程式碼詳情
// 2. 從 redux 中引入 applyMiddleware,applyMiddleware 的作用是應用 redux 中介軟體
// 3. 引入 compose 函式,因為我們用到了兩個中介軟體:redux-thunk 以及 redux-devtools-extension,需要 compose 輔助
import { createStore, applyMiddleware, compose } from 'redux';
import reducer from './reducer';
// 1. 從 redux-thunk 中引入 thunk
import thunk from 'redux-thunk';

// 3. 使用 redux-devtools-extension 中介軟體
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;

// 4. 使用 applyMiddleware 對此進行擴充套件
const enhancer = composeEnhancers(
  applyMiddleware(thunk),
);

// 5. 在 createStore 進行 enhancer 呼叫
const store = createStore(
  reducer,
  enhancer
);

export default store;
複製程式碼

在這裡,我們做了幾件事:

  1. redux-thunk 中引入 thunk
  2. redux 中引入 applyMiddlewareapplyMiddleware 的作用是應用多個 redux 中介軟體
  3. 引入 compose 函式,因為我們用到了兩個中介軟體:redux-thunk 以及 redux-devtools-extension,需要 compose 輔助
  4. 使用 redux-devtools-extension 中介軟體
  5. 使用 applyMiddleware 對此進行擴充套件,即 redux-thunk 中介軟體加上 redux-devtools-extension 中介軟體
  6. createStore 進行 enhancer 呼叫

這樣,我們就同時在一個專案中使用了 redux-thunk 中介軟體加上 redux-devtools-extension 中介軟體,從而做到了 redux-thunk 的引用。

接下來,我們就要使用 redux-thunk

src/store/actionCreators.js

程式碼詳情
import { CHANGE_INPUT_VALUE, ADD_TODO_ITEM, DELETE_TODO_ITEM, INIT_LIST_ACTION } from './actionTypes';
// 1. 把 axios 從 TodoList.js 中剪下到 actionCreators.js 中
import axios from 'axios';

export const getChangeInputValue = (value) => ({
  type: CHANGE_INPUT_VALUE,
  value
})

export const getAddTodoItem = () => ({
  type: ADD_TODO_ITEM
})

export const getDeleteTodoItem = (index) => ({
  type: DELETE_TODO_ITEM,
  index
})

export const initListAction = (data) => ({
  type: INIT_LIST_ACTION,
  data
})

// 2. 把 TodoList 檔案中 componentDidMount() 的 axios.get() 挪到 actionCreators.js 中
// 3. 在沒使用 redux-thunk 之前,我們僅可以在 actionCreators.js 中使用物件,現在我們也可以使用函式了。
export const getTodoList = () => {
  // 7. 當我們使用 getTodoList 的時候,我們也能傳遞 store 的 dispatch,從而在下面程式碼中使用
  return (dispatch) => {
    axios.get('https://www.easy-mock.com/mock/5ca803587e5a246db3d100cb/todolist').then( (res) => {
      // 8. 直接使用 actionCreators.js 中的 initListAction方法,並 dispatch 該 action
      const action = initListAction(res.data.todolist);
      dispatch(action);
    })
  }
}
複製程式碼

src/TodoList.js

程式碼詳情
import React, { Component } from 'react';
import './index.css';
import 'antd/dist/antd.css';
import store from './store';
// 4. 在 TodoList.js 中引用 actionCreators.js 中的 getTodoList
import { getChangeInputValue, getAddTodoItem, getDeleteTodoItem, getTodoList } from './store/actionCreators';
import TodoListUI from './TodoListUI';

class TodoList extends Component {

  constructor(props) {
    super(props);
    this.state = store.getState();
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleAddItem = this.handleAddItem.bind(this);
    this.handleInputKeyUp = this.handleInputKeyUp.bind(this);
    this.handleDeleteItem = this.handleDeleteItem.bind(this);

    this.handleStoreChange = this.handleStoreChange.bind(this);
    store.subscribe(this.handleStoreChange);
  }

  render() {
    return (
      <TodoListUI
        inputValue={this.state.inputValue}
        todoList={this.state.todoList}
        handleInputChange={this.handleInputChange}
        handleInputKeyUp={this.handleInputKeyUp}
        handleAddItem={this.handleAddItem}
        handleDeleteItem={this.handleDeleteItem}
      />
    );
  }

  componentDidMount() {
    // 5. 在 componentDidMount 中呼叫 getTodoList。如果我們沒使用 redux-thunk,我們只能使用物件,但是現在我們可以使用函式了。
    const action = getTodoList();
    // 6. 當我們 dispatch 了 action 的時候,我們就呼叫了步驟 1 的 getTodoList(),從而獲取了資料
    store.dispatch(action);
  }

  handleInputChange(e) {
    const action = getChangeInputValue(e.target.value);
    store.dispatch(action);
  }

  handleStoreChange() {
    this.setState(store.getState());
  }

  handleAddItem() {
    const action = getAddTodoItem();
    store.dispatch(action);
  }

  handleInputKeyUp(e) {
    if(e.keyCode === 13) {
      this.handleAddItem();
    }
  }

  handleDeleteItem(index) {
    const action = getDeleteTodoItem(index);
    store.dispatch(action);
  }

}

export default TodoList;
複製程式碼

看到這裡,我們或許已經懵逼,所以先瞅瞅思路:

  1. axios 從 TodoList.js 中剪下到 actionCreators.js 中
  2. 把 TodoList 檔案中 componentDidMount()axios.get() 挪到 actionCreators.js 中
  3. 在沒使用 redux-thunk 之前,我們僅可以在 actionCreators.js 中使用物件,現在我們也可以使用函式了。
  4. 在 TodoList.js 中引用 actionCreators.js 中的 getTodoList(),並去除沒再引用的 initListAction
  5. componentDidMount() 中呼叫 getTodoList()。如果我們沒使用 redux-thunk,我們只能使用物件,但是現在我們可以使用函式了。
  6. 當我們 dispatchaction 的時候,我們就呼叫了步驟 1 的 getTodoList(),從而獲取了資料
  7. 當我們使用 getTodoList() 的時候,我們也能傳遞 storedispatch,從而在下面程式碼中使用
  8. 直接使用 actionCreators.js 中的 initListAction 方法,並 dispatchaction

如此,我們就通過 redux-thunk,將 axios 的介面呼叫抽取到了 actionCreators.js 中了。

為什麼我們原本在 TodoList.js 中用的好好的,到了這裡要走那麼多步驟把它抽取出來?

其實我們需要知道的是,當頁面足夠複雜,專案足夠大,程式碼越來越多的時候,如果我們的介面呼叫都在容器元件中,我們就不方便對介面進行管理,最後如果我們需要改動某個介面,我們就要在頁面中慢慢查詢。

通過 redux-thunk 的呼叫,我們就把介面程式碼從容器元件中抽取出來,從而做到:介面程式碼邏輯是介面程式碼邏輯,業務程式碼邏輯是業務程式碼邏輯。

而且,通過 redux-thunk 的抽取,可以方便我們的自動化測試。當然,自動化測試長啥樣子,我們還不清楚,但是我們可以安慰自己的是:這樣子始終是有道理的。

總結:至此,我們就完成了 Redux-Thunk 的引用及其使用,小夥伴們可以多進行嘗試,進一步熟悉 Redux-Thunk。

十五 進階:Redux-Saga 中介軟體進行 Ajax 請求管理

返回目錄

有了 Redux-Thunk 的經驗,我們也可以瞭解下 Redux-Saga 了。

首先我們還是從 Redux-Base 中拷貝一份檔案到 Redux-Saga 目錄中。

注意:不需要拷貝 node_modules 資料夾

  • 安裝依賴:npm i
  • 執行專案:npm run start

然後,我們開始引用 Redux-Saga:

  • Redux Saga:Github 地址
  • 安裝:npm i redux-saga -S
  • 教程小例子:

test.js

程式碼詳情
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

import reducer from './reducers'
import mySaga from './sagas'

// create the saga middleware
const sagaMiddleware = createSagaMiddleware()
// mount it on the Store
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)

// then run the saga
sagaMiddleware.run(mySaga)

// render the application
複製程式碼

噗呲,可以看出,Redux-Saga 的引用方式跟 Redux-Thunk 一樣簡單。但是,請抱著接受一定複雜性的形式繼續學習。

下面我們操作 store 目錄下的 index.js 檔案,進行 Redux-Saga 的引用:

src/store/index.js

程式碼詳情
// 1. 引入 applyMiddleware 和 compose 進行多箇中介軟體的處理
import { createStore, applyMiddleware, compose } from 'redux';
import reducer from './reducer';
// 2. 引入 redux-saga 的 createSagaMiddleware
import createSagaMiddleware from 'redux-saga';
// 6. 建立並引用 store 下的 sagas.js 檔案
import todoSaga from './sagas';

// 3. 呼叫 createSagaMiddleware 方法
const sagaMiddleware = createSagaMiddleware();

// 4. 定義 composeEnhancers
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;

// 5. 呼叫 composeEnhancers 進行多中介軟體處理
const enhancer = composeEnhancers(
  applyMiddleware(sagaMiddleware),
);

const store = createStore(
  reducer,
  enhancer
);

// 7. 使用 todoSaga
sagaMiddleware.run(todoSaga);

export default store;
複製程式碼

src/store/sagas.js

程式碼詳情
// 8. 使用 generator 函式定義 todoSaga
function* todoSaga() {

}

// 9. 將 generator 函式匯出去
export default todoSaga;
複製程式碼

如此,我們就完成了 Redux-Saga 的引用,大致做了如下步驟:

  1. 引入 applyMiddlewarecompose 進行多箇中介軟體的處理
  2. 引入 redux-sagacreateSagaMiddleware
  3. 呼叫 createSagaMiddleware 方法
  4. 定義 composeEnhancers
  5. 呼叫 composeEnhancers 進行多中介軟體處理
  6. 建立並引用 store 下的 sagas.js 檔案的 todoSaga
  7. 通過 sagaMiddleware 使用 todoSaga
  8. 使用 generator 函式定義 sagas.js 檔案
  9. generator 函式匯出去

同時我們觀察下頁面,也不存在報錯,說明我們引用對了。

下面我們將 componentDidMount() 方法中的 axios.get() 這些非同步介面提取到 src/store/sagas.js 中進行處理:

  1. src/TodoList.js
程式碼詳情
import React, { Component } from 'react';
import './index.css';
import 'antd/dist/antd.css';
import store from './store';
// 1. 刪除 initListAction 以及下面的 axios,並引入 actionCreators.js 中的 getInitList
import { getChangeInputValue, getAddTodoItem, getDeleteTodoItem, getInitList } from './store/actionCreators';
import TodoListUI from './TodoListUI';

class TodoList extends Component {

  constructor(props) {
    super(props);
    this.state = store.getState();
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleAddItem = this.handleAddItem.bind(this);
    this.handleInputKeyUp = this.handleInputKeyUp.bind(this);
    this.handleDeleteItem = this.handleDeleteItem.bind(this);

    this.handleStoreChange = this.handleStoreChange.bind(this);
    store.subscribe(this.handleStoreChange);
  }

  render() {
    return (
      <TodoListUI
        inputValue={this.state.inputValue}
        todoList={this.state.todoList}
        handleInputChange={this.handleInputChange}
        handleInputKeyUp={this.handleInputKeyUp}
        handleAddItem={this.handleAddItem}
        handleDeleteItem={this.handleDeleteItem}
      />
    );
  }

  componentDidMount() {
    // 5. 呼叫 getInitList,並使用 dispatch 將 action 派發出去。這時候不僅 reducer.js 可以接收到這個 action,我們的 sagas.js 也可以接收到這個 action。
    const action = getInitList();
    store.dispatch(action);
  }

  handleInputChange(e) {
    const action = getChangeInputValue(e.target.value);
    store.dispatch(action);
  }

  handleStoreChange() {
    this.setState(store.getState());
  }

  handleAddItem() {
    const action = getAddTodoItem();
    store.dispatch(action);
  }

  handleInputKeyUp(e) {
    if(e.keyCode === 13) {
      this.handleAddItem();
    }
  }

  handleDeleteItem(index) {
    const action = getDeleteTodoItem(index);
    store.dispatch(action);
  }

}

export default TodoList;
複製程式碼
  1. src/store/actionCreators.js
程式碼詳情
// 2. 匯入 actionTypes.js 中的 GET_INIT_LIST
import { CHANGE_INPUT_VALUE, ADD_TODO_ITEM, DELETE_TODO_ITEM, INIT_LIST_ACTION, GET_INIT_LIST } from './actionTypes';

export const getChangeInputValue = (value) => ({
  type: CHANGE_INPUT_VALUE,
  value
})

export const getAddTodoItem = () => ({
  type: ADD_TODO_ITEM
})

export const getDeleteTodoItem = (index) => ({
  type: DELETE_TODO_ITEM,
  index
})

export const initListAction = (data) => ({
  type: INIT_LIST_ACTION,
  data
})

// 3. 使用 GET_INIT_LIST
export const getInitList = () => ({
  type: GET_INIT_LIST
});
複製程式碼
  1. src/store/actionTypes.js
程式碼詳情
export const CHANGE_INPUT_VALUE = 'change_input_value';
export const ADD_TODO_ITEM =  'add_todo_item';
export const DELETE_TODO_ITEM = 'delete_todo_item';
export const INIT_LIST_ACTION = 'init_list_action';
// 4. 定義 GET_INIT_LIST 並匯出給 actionTypes.js 使用
export const GET_INIT_LIST = 'get_init_list';
複製程式碼
  1. src/store/sagas.js
程式碼詳情
// 6. 引用 redux-saga/effets 中的 takeEvery
// 13. 由於我們在 sagas.js 中沒有引用到 store,所以不能使用 store.dispatch(),但是 redux-saga 給我們提供了 put 方法來代替 store.dispatch() 方法
import { takeEvery, put } from 'redux-saga/effects';
// 7. 引入 GET_INIT_LIST 型別
import { GET_INIT_LIST } from './actionTypes';
// 11. 將 TodoList.js 的 axios 引入遷移到 sagas.js 中
import axios from 'axios';
// 12. 引入 actionCreator.js 中的 initListAction
import { initListAction } from './actionCreators'

// 8. 使用 generator 函式
function* todoSaga() {
  // 9. 這行程式碼表示,只要我們接收到 GET_INIT_LIST 的型別,我們就執行 getInitList 方法
  yield takeEvery(GET_INIT_LIST, getInitList);
}

// 10. 定義 getInitList 方法
function* getInitList() {
  try {
    // 14. 在 sagas.js 中處理非同步函式
    const res = yield axios.get('https://www.easy-mock.com/mock/5ca803587e5a246db3d100cb/todolis');
    const action = initListAction(res.data.todolist);
    // 15. 等 action 處理完之後,在執行 put 方法
    yield put(action);
  } catch (error) {
    console.log("介面請求失敗,請檢查 todolist 介面。");
  }
  
}

export default todoSaga;
複製程式碼

這樣,我們就把呼叫介面的非同步請求函式,抽取到了 sagas.js 檔案中,期間我們做了:

  1. TodoList.js —— 刪除 initListAction 以及下面的 axios,並引入 actionCreators.js 中的 getInitList
  2. actionCreators.js —— 匯入 actionTypes.js 中的 GET_INIT_LIST
  3. actionTypes.js —— 使用 GET_INIT_LIST
  4. actionTypes.js —— 定義 GET_INIT_LIST 並匯出給 actionTypes.js 使用
  5. TodoList.js —— 呼叫 getInitList,並使用 dispatchaction 派發出去。這時候不僅 reducer.js 可以接收到這個 action,我們的 sagas.js 也可以接收到這個 action
  6. 引用 redux-saga/effets 中的 takeEvery
  7. 引入 GET_INIT_LIST 型別
  8. 使用 generator 函式
  9. 通過 takeEvery,表示只要我們接收到 GET_INIT_LIST 的型別,我們就執行 getInitList 方法
  10. 定義 getInitList 方法
  11. 將 TodoList.js 的 axios 引入遷移到 sagas.js 中
  12. 引入 actionCreator.js 中的 initListAction
  13. 由於我們在 sagas.js 中沒有引用到 store,所以不能使用 store.dispatch(),但是 redux-saga 給我們提供了 put 方法來代替 store.dispatch() 方法,所以我們引用 put 方法。
  14. 在 sagas.js 中處理非同步函式
  15. action 處理完之後,在執行 put 方法:yield put(action)

如此,我們就成功將 TodoList 中非同步請求介面抽取到了 sagas.js 中,從而對介面進行統一管理。

src/store/sagas.js 中,我們還通過 try...catch... 方法,對介面進行處理,當介面不存在或者請求異常的時候,我們將知道該介面出錯了。

總結:至此,我們就完成了 Redux-Saga 的引用及其使用,小夥伴們可以多進行嘗試,進一步熟悉 Redux-Saga。

十六 進階:React-Redux

返回目錄

在之前的章節中,我們使用了 React,也使用了 Redux,以及接觸了 Redux 的中介軟體:Redux-Thunk 和 Redux-Saga。

那麼,本章節講解下 React-Redux。

  • 什麼是 React-Redux。

它是一個第三方模組,更方便我們在 React 中使用 Redux。

在這裡,由於 React-Base 目錄是 React 與 Redux 分開的,所以我們複製一份 Simplify 目錄的基礎程式碼到 React-Redux 目錄中,並進行 TodoList 改造,從而開始我們的 React-Redux 之旅。

將 Simplify 改造成 TodoList 的方法可參考 第三章 初始化專案第四章 使用 Ant Design 以及 第五章 使用 Redux

下面 jsliang 貼出自己的初始化後的程式碼:

  1. src/index.js
程式碼詳情
import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './TodoList';

ReactDOM.render(<TodoList />, document.getElementById('root'));
複製程式碼
  1. src/TodoList.js
程式碼詳情
import React, { Component } from 'react';
import './index.css';
import { Input, Button, List } from 'antd';
import 'antd/dist/antd.css';
import store from './store';

class TodoList extends Component {

  constructor(props) {
    super(props);
    this.state = store.getState();
  }

  render() {
    return (
      <div className="todo">
        <div className="todo-title">
          <h1>TodoList</h1>
        </div>
        <div className="todo-action">
          <Input placeholder='todo' className="todo-input" />
          <Button type="primary" className="todo-submit">提交</Button>
        </div>
        <div className="todo-list">
          <List
            size="large"
            bordered
            dataSource={this.state.list}
            renderItem={(item, index) => (<List.Item>{index + 1} - {item}</List.Item>)}
          />
        </div>
      </div>
    );
  }
}

export default TodoList;
複製程式碼
  1. src/index.css
程式碼詳情
.todo {
  width: 1000px;
  margin: 20px auto 0;
  padding: 30px;
  border: 1px solid #ccc;
  border-radius: 10px;
}
.todo-title {
  text-align: center;
}
.todo-action .todo-input {
  width: 200px;
}
.todo-action .todo-submit {
  margin-left: 10px;
}
.todo-list {
  margin-top: 30px;
}
複製程式碼
  1. src/store/index.js
程式碼詳情
import { createStore } from 'redux';
import reducer from './reducer';

const store = createStore(reducer);

export default store;
複製程式碼
  1. src/store/reducer.js
程式碼詳情
const defaultState = {
  inputValue: '',
  list: [
    '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第一條 TodoList',
    '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第二條 TodoList',
    '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第三條 TodoList',
    '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第四條 TodoList',
  ]
}

export default (state = defaultState, action) => {
  return state;
}
複製程式碼

此時頁面展示為第四章最後的頁面樣子:

React Demo Two - TodoList 升級

  • React Redux:GitHub 地址
  • 安裝 react-reduxnpm i react-redux -S

是時候展現真正的技術了!

我們在 src/index.js 中引用 react-redux

src/index.js

程式碼詳情
import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './TodoList';
// 1. 引入 react-redux 的 Provider
import { Provider } from 'react-redux';
// 3. 引入 store
import store from './store';

// 2. 使用 Provider 重新定義 App
const App = (
  // 4. Provider 連線了 store,那麼 Provider 裡面的元件,都可以獲取和使用 store 中的內容
  <Provider store={store}>
    <TodoList />
  </Provider>
)

// 5. 直接渲染 App
ReactDOM.render(App, document.getElementById('root'));
複製程式碼

接著可以在 src/TodoList.js 中使用:

src/TodoList.js

程式碼詳情
import React, { Component } from 'react';
import './index.css';
import { Input, Button, List } from 'antd';
import 'antd/dist/antd.css';
// 6. 在 TodoList 中,我們就不需要使用 import store from store 以及定義 constructor 獲取 store 了,而是通過 react-redux 的 connect 來獲取
import { connect } from 'react-redux';

class TodoList extends Component {
  render() {
    return (
      <div className="todo">
        <div className="todo-title">
          <h1>TodoList</h1>
        </div>
        <div className="todo-action">
          {/* 10. 使用 inputValue */}
          <Input 
            placeholder='todo' 
            className="todo-input" 
            value={this.props.inputValue}
          />
          <Button type="primary" className="todo-submit">提交</Button>
        </div>
        <div className="todo-list">
          {/* 12. 使用 list */}
          <List
            size="large"
            bordered
            dataSource={this.props.list}
            renderItem={(item, index) => (<List.Item>{index + 1} - {item}</List.Item>)}
          />
        </div>
      </div>
    );
  }
}

// 8. 定義 mapStateToProps 方法,把 store 裡面的資料,對映成元件裡面的 props,其中引數 state 就是 store 裡面的資料
const mapStateToProps = (state) => {
  return {
    // 9. 定義 inputValue
    inputValue: state.inputValue,
    // 11. 定義 list
    list: state.list
  }
}

// 7. 匯出 connect 方法,讓 TodoList 和 store 做連線,需要對應兩個規則,即:mapStateToProps 和
export default connect(mapStateToProps, null)(TodoList);
複製程式碼

現在,我們發現程式碼仍能正常執行,我們分析下我們做了什麼步驟:

  1. 引入 react-reduxProvider
  2. 使用 Provider 重新定義 App
  3. 引入 store
  4. Provider 連線了 store,那麼 Provider 裡面的元件,都可以獲取和使用 store 中的內容
  5. 直接渲染 App
  6. 在 TodoList.js 中,我們就不需要使用 import store from store 以及定義 constructor 獲取 store 了,而是通過 react-reduxconnect 來獲取
  7. 匯出 connect 方法,讓 TodoList.js 和 store 做連線,需要對應兩個規則,即:mapStateToProps 和 **
  8. 定義 mapStateToProps 方法,把 store 裡面的資料,對映成元件裡面的 props,其中引數 state 就是 store 裡面的資料
  9. 定義 inputValue
  10. 使用 inputValue
  11. 定義 list
  12. 使用 list

如此,我們就完成了 store 通過 react-reduxTodoList.js 中的引用。

下面我們再試試修改 store 的值:

src/TodoList.js

程式碼詳情
import React, { Component } from 'react';
import './index.css';
import { Input, Button, List } from 'antd';
import 'antd/dist/antd.css';
import { connect } from 'react-redux';

class TodoList extends Component {
  render() {
    return (
      <div className="todo">
        <div className="todo-title">
          <h1>TodoList</h1>
        </div>
        <div className="todo-action">
          {/* 3. 給 Input 繫結 onChange 事件 handleInputChange,此時我們通過 this.props 來繫結方法 */}
          <Input 
            placeholder='todo' 
            className="todo-input" 
            value={this.props.inputValue}
            onChange={this.props.handleInputChange}
          />
          <Button type="primary" className="todo-submit">提交</Button>
        </div>
        <div className="todo-list">
          <List
            size="large"
            bordered
            dataSource={this.props.list}
            renderItem={(item, index) => (<List.Item>{index + 1} - {item}</List.Item>)}
          />
        </div>
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    inputValue: state.inputValue,
    list: state.list
  }
}

// 2. 定義 mapDispatchToProps 方法,該方法即是 TodoList.js 將 store.dispatch 方法對映到 props 上,所以我們就可以通過 this.props 來定義方法
// 4. 這裡我們傳遞了 dispatch,所以就可以使用 store.dispatch 方法
const mapDispatchToProps = (dispatch) => {
  return {
    // 5. 定義 handleInputChange 方法
    handleInputChange(e) {
      const action = {
        type: 'change_input_value',
        value: e.target.value
      }
      // 6. 將 action 派發到 reducer.js
      dispatch(action);
    }
  }
}

// 1. 使用 mapDispatchToProps 方法
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
複製程式碼

在修改 src/reducer.js

src/reducer.js

程式碼詳情
const defaultState = {
  inputValue: '',
  list: [
    '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第一條 TodoList',
    '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第二條 TodoList',
    '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第三條 TodoList',
    '這是非常非常非常長的讓人覺得不可思議的但是它語句通順的第四條 TodoList',
  ]
}

export default (state = defaultState, action) => {
  // 7. 判斷傳遞過來的 action.type 是哪個,進行深拷貝,獲取 action.value 的值,並返回 newState
  if(action.type === 'change_input_value') {
    const newState = JSON.parse(JSON.stringify(state));
    newState.inputValue = action.value;
    return newState;
  }

  return state;
}
複製程式碼

這時候,我們做了 7 個步驟:

  1. 在 TodoList.js 中使用了 mapDispatchToProps 方法
  2. 定義 mapDispatchToProps 方法,該方法即是 TodoList.js 將 store.dispatch 方法對映到 props 上,所以我們就可以通過 this.props 來定義方法
  3. Input 繫結 onChange 事件 handleInputChange,此時我們通過 this.props 來繫結方法
  4. mapDispatchToProps 中我們傳遞了 dispatch,所以就可以使用 store.dispatch 方法
  5. 定義 handleInputChange 方法
  6. action 派發到 reducer.js
  7. 判斷傳遞過來的 action.type 是哪個,進行深拷貝,獲取 action.value 的值,並返回 newState

至此,我們就簡單過了一遍 React-Redux 的使用,下面我們的 Button 按鈕點選提交,以及點選 Item 項進行 TodoList 的 list 項刪除功能,我們就不一一講解了,感興趣的小夥伴可以自行實現下,並通過下載 jsliang 的程式碼進行參照:

十七 總結

返回目錄

現在,我們完成了所有的知識點、程式碼及其講解,是時候可以放鬆聊聊了:這篇文章中我們學會了啥:

  1. Ant Design 的使用
  2. Redux 的引入及使用
  3. UI 元件、容器元件、無狀態元件以及為了一些大型專案進行的程式碼抽取封裝
  4. Axios 在 React 中的使用
  5. 為了方便管理 Axios 介面程式碼,我們使用了 Redux 的中介軟體 Redux-Thunk 和 Redux-Thunk
  6. 使用 React-Redux 再過了遍 Redux 的使用,並學習 React-Redux 的使用

至此,我們就成功完結這篇文章,進入到 React 下個環節的升級進階了。

如果小夥伴們感覺 jsliang 寫得不錯,記得給個 【贊】 或者給 jsliang 的文件庫點個 【star】,你們的 【贊】 或者 【star】 是我滿滿的動力,哈哈,React 系列下篇再見!


jsliang 廣告推送:
也許小夥伴想了解下雲伺服器
或者小夥伴想買一臺雲伺服器
或者小夥伴需要續費雲伺服器
歡迎點選 雲伺服器推廣 檢視!

React Demo Two - TodoList 升級
React Demo Two - TodoList 升級

知識共享許可協議
jsliang 的文件庫樑峻榮 採用 知識共享 署名-非商業性使用-相同方式共享 4.0 國際 許可協議進行許可。
基於github.com/LiangJunron…上的作品創作。
本許可協議授權之外的使用許可權可以從 creativecommons.org/licenses/by… 處獲得。

相關文章