Redux实战 5个月前

编程语言
668
Redux实战

前言

上一节我们了解了 Redux 基本的概念和特性后,本章我们要实际动手用 Redux、React Redux 结合 ImmutableJS 开发一个简单的 Todo 应用。话不多说,那就让让我们开始吧!

以下这张图表示了整个 React Redux App 的资料流程图(使用者与 View 互动 => dispatch 出 Action => Reducers 依据 action tyoe 分配到对应处理方式,回传新的 state => 透过 React Redux 传送给 React,React 重新绘制 View):

image

动手创作 React Redux ImmutableJS TodoApp

在开始创作之前我们先完成一些开发的前置作业,先透过以下指令在根目录产生 npm 设定档 package.json

$ npm init

安装相关套件(包含开发环境使用的套件):

$ npm install --save react react-dom redux react-redux immutable redux-actions redux-immutable
$ npm install --save-dev babel-core babel-eslint babel-loader babel-preset-es2015 babel-preset-react eslint eslint-config-airbnb eslint-loader eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react html-webpack-plugin webpack webpack-dev-server

安装好后我们可以设计一下我们的资料夹结构,首先我们在根目录建立 src,放置 scriptsource 。在 components 资料夹中我们会放置所有 components(个别组件资料夹中会用 index.js 输出组件,让引入组件更简洁)、containers(负责和 store 互动取得 state),另外还有 actionsconstantsreducersstore,其余设定档则放置于根目录下。

大致上的资料夹结构会长这样:

image

接下来我们参考上一章设定一下开发文档(.babelrc.eslintrcwebpack.config.js)。这样我们就完成了开发环境的设定可以开始动手实作 React Redux 应用程式了!

首先我们先用 Component 之眼感受一下我们应用程式,将它切成一个个 Component。在这边我们设计一个主要的 Main 包含两个子 Component:TodoHeaderTodoList

image

首先设计 HTML Markup:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Redux Todo</title>
</head>
<body>
    <div id="app"></div>
</body>
</html>

在撰写 src/index.js 之前,我们先说明整合 react-redux 的用法。从以下这张图可以看到 react-redux 是 React 和 Redux 间的桥梁,使用 Providerconnect 去连结 store 和 React View。

image

事实上,整合了 react-redux 后,我们的 React App 就可以解决传统跨 Component 之前传递 state 的问题和困难。只要透过 Provider 就可以让每个 React App 中的 Component 取用 store 中的 state,非常方便(接下来我们也会更详细说明 Container/Component、connect 的用法)。

image

以下是 src/index.js 完整程式码:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import Main from './components/Main';
import store from './store';

ReactDOM.render(
  <Provider store={store}>
    <Main />
  </Provider>,
  document.getElementById('app')
);

其中 src/components/Main/Main.js 是 Stateless Component,负责所有 View 的进入点。

import React from 'react';
import ReactDOM from 'react-dom';
import TodoHeaderContainer from '../../containers/TodoHeaderContainer';
import TodoListContainer from '../../containers/TodoListContainer';

const Main = () => (
  <div>
    <TodoHeaderContainer />
    <TodoListContainer />
  </div>
);

export default Main;

接下来我们定义一下 Actions 的部份,由于是范例 App 所以相对简单,这边只定义一个 todoActions。在这边我们使用了 redux-actions,它可以方便我们使用 Flux Standard Action 格式的 action。以下是 src/actions/todoActions.js 完整程式码:

import { createAction } from 'redux-actions';
import {
  CREATE_TODO,
  DELETE_TODO,
  CHANGE_TEXT,
} from '../constants/actionTypes';

export const createTodo = createAction('CREATE_TODO');
export const deleteTodo = createAction('DELETE_TODO');
export const changeText = createAction('CHANGE_TEXT');

我们在 src/actions/index.js 将所有 actions 输出

export * from './todoActions';

另外我们把 constants 放到 components 资料夹中方便管理,以下是 src/constants/actionTypes.js 程式码:

export const CREATE_TODO = 'CREATE_TODO';
export const DELETE_TODO = 'DELETE_TODO';
export const CHANGE_TEXT = 'CHANGE_TEXT';

/* 
或是可以考虑使用 keyMirror,方便产生与 key 相同的常数
import keyMirror from 'fbjs/lib/keyMirror';

export default keyMirror({
    ADD_ITEM: null,
    DELETE_ITEM: null,
    DELETE_ALL: null,
    FILTER_ITEM: null
});
*/

设定 Actions 后我们来讨论一下 Reducers 的部份。在讨论 Reducers 之前我们先来设定一下我们的前端的资料结构,在这边我们把所有资料结构(initialState)放到 src/constants/models.js 中。这边特别注意的是由于 Redux 中有一个重要特性是 State is read-only,也就是说更新当 reducers 进到 action 只会回传新的 state 不会更改到原有的 state。因此我们会在整个 Redux App 中使用 ImmutableJS 让整个资料流维持在 Immutable 的状态,也可以提升程式开发上的效能和避免不可预期的副作用。

以下是 src/constants/models.js 完整程式码,其设定了 TodoState 的资料结构并使用 fromJS() 转成 Immutable

import Immutable from 'immutable';

export const TodoState = Immutable.fromJS({
  'todos': [],
  'todo': {
    id: '',
    text: '',
    updatedAt: '',
    completed: false,
  }
});

接下来我们要讨论的是 Reducers 的部份,在 todoReducers 中我们会根据接收到的 action 进行 mapping 到对应的处理函式并传入夹带的 payload 资料(这边我们使用 redux-actions 来进行 mapping,使用上比传统的 switch 更为简洁)。Reducers 接收到 action 的处理方式为 (initialState, action) => newState,最终会回传一个新的 state,而非更改原来的 state,所以这边我们使用 ImmutableJS

import { handleActions } from 'redux-actions';
import { TodoState } from '../../constants/models';

import {
  CREATE_TODO,
  DELETE_TODO,
  CHANGE_TEXT,
} from '../../constants/actionTypes';

 const todoReducers = handleActions({
  CREATE_TODO: (state) => {
    let todos = state.get('todos').push(state.get('todo'));
    return state.set('todos', todos)
  },
  DELETE_TODO: (state, { payload }) => (
    state.set('todos', state.get('todos').splice(payload.index, 1))
  ),
  CHANGE_TEXT: (state, { payload }) => (
    state.merge({ 'todo': payload })
  )
}, TodoState);

export default todoReducers;
import { handleActions } from 'redux-actions';
import UiState from '../../constants/models';

export default handleActions({
  SHOW: (state, { payload }) => (
    state.set('todos', payload.todo)
  ),
}, UiState);

虽然 Redux 本身仅会有一个 store,但 redux 本身有提供了 combineReducers 可以让我们切割我们 state 方便维护和管理。实上,state 的规划也是一们学问,通常需要不断地实作和工作团队讨论才能找到比较好的方式。不过这边要注意的是我们改使用了 redux-immutablecombineReducers 这样可以确保我们的 state 维持在 Immutable 的状态。

由于 Redux 官方也没有特别明确或严谨的规范。在一般情况我会将 reducers 分为 data 和单纯和 UI 有关的 ui state。但由于这边是比较简单的例子,我们最终只使用到 src/reducers/data/todoReducers.js

import { combineReducers } from 'redux-immutable';
import ui from './ui/uiReducers';// import routes from './routes';
import todo from './data/todoReducers';// import routes from './routes';

const rootReducer = combineReducers({
  todo,
});

export default rootReducer;

还记得我们上面说明 React Redux 之前的桥梁时有提到的 store 吗?现在我们要更仔细地去设计 store,我们这边使用到了 redux 其中两个 API:applyMiddleware、createStore。分别可以产生 store 和挂载我们要使用的 middleware(这边我们只使用到 redux-logger 方便我们除错)。注意我们 initialState 也是维持在 Immutable 的状态。

import { createStore, applyMiddleware } from 'redux';
import createLogger from 'redux-logger';
import Immutable from 'immutable';
import rootReducer from '../reducers';

const initialState = Immutable.Map();

export default createStore(
  rootReducer,
  initialState,
  applyMiddleware(createLogger({ stateTransformer: state => state.toJS() }))
);

透过 src/store/index.js 输出 configureStore:

export { default } from './configureStore';

讲解完架构层面的议题,终于我们来到了 View 的部份。加油,距离我们终点也不远了! 在开始讨论 Component 的部份之前我们先来研究一下

react-redux 所提供的 API connect 将 props 传给 Component,其用法如下:

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

在我们的范例 App 中我们只会先用到前两个参数,第三个参数会在之后的例子里用到。第一个参数 mapStateToProps 是一个让开发者可以从 store 取出想要 state 并当做 props 往下传的功能,第二个参数则是将 dispatch 行为封装成函数顺着 props 可以方便往下传和呼叫。

以下是 src/components/TodoHeader/TodoHeader.js 的部份:

import React from 'react';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';
import TodoHeader from '../../components/TodoHeader';

// 将欲使用的 actions 引入
import {
  changeText,
  createTodo,
} from '../../actions';

const mapStateToProps = (state) => ({
    // 从 store 取得 todo state
    todo: state.getIn(['todo', 'todo'])
});

const mapDispatchToProps = (dispatch) => ({
    // 当使用者在 input 输入资料值即会触发这个函数,发出 changeText action 并附上使用者输入内容 event.target.value
    onChangeText: (event) => (
      dispatch(changeText({ text: event.target.value }))
    ),
    // 当使用者按下送出时,发出 createTodo action 并清空 input 
    onCreateTodo: () => {
      dispatch(createTodo());
      dispatch(changeText({ text: '' }));
    }
});

export default connect(
    mapStateToProps,
    mapDispatchToProps,
)(TodoHeader);

// 开始建设 Component 并使用 connect 进来的 props 并绑定事件(onChange、onClick)。注意我们的 state 因为是使用 `ImmutableJS` 所以要用 `get()` 取值
const TodoHeader = ({
  onChangeText,
  onCreateTodo,
  todo,
}) => (
  <div>
    <h1>TodoHeader</h1>
    <input type="text" value={todo.get('text')} onChange={onChangeText} />
    <button onClick={onCreateTodo}>送出</button>
  </div>
);

export default TodoHeader;

以下是 src/components/TodoList/TodoList.js 的部份:

import React from 'react';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';
import TodoList from '../../components/TodoList';

import {
  deleteTodo,
} from '../../actions';

const mapStateToProps = (state) => ({
  todos: state.getIn(['todo', 'todos'])
});

// 由 Component 传进欲删除元素的 index
const mapDispatchToProps = (dispatch) => ({
  onDeleteTodo: (index) => () => (
    dispatch(deleteTodo({ index }))
  )
});

export default connect(
    mapStateToProps,
    mapDispatchToProps,
)(TodoList);

// Component 部分值的注意的是 todos state 是透过 map function 去迭代出元素,由于要让 React JSX 可以渲染并保持传入触发 event state 的 immutable,所以需使用 toJS() 转换 component of array。
const TodoList = ({
  todos,
  onDeleteTodo,
}) => (
  <div>
    <ul>
    {
      todos.map((todo, index) => (
        <li key={index}>
          {todo.get('text')}
          <button onClick={onDeleteTodo(index)}>X</button>
        </li>
      )).toJS()
    }
    </ul>
  </div>
);

export default TodoList;

若是一切顺利的话就可以在浏览器上看到自己努力的成果囉!(因为我们有使用 redux-logger 所以打开 console 会看到 action 和 state 的变化情形,但记得在 production 环境要拿掉)

image

总结

以上就是 Redux 实战入门,对于第一次自己动手写 Redux 的朋友可能会需要多练习几次,多体会整个架构。在接下来的章节我们将优化我们的 React Redux TodoApp,让它可以有更清晰好维护的架构。

延伸阅读

  1. Redux 官方网站
image
EchoEcho官方
无论前方如何,请不要后悔与我相遇。
1377
发布数
439
关注者
2243269
累计阅读

热门教程文档

Python
76小节
Golang
23小节
爬虫
6小节
React Native
40小节
Javascript
24小节