65.9K
CodeProject 正在变化。 阅读更多。
Home

关于 Redux 的说明

starIconstarIconstarIconemptyStarIconemptyStarIcon

3.00/5 (1投票)

2018年9月22日

CPOL

6分钟阅读

viewsIcon

7344

downloadIcon

80

这是关于 Redux 的说明。

引言

这是一篇关于 Redux 的说明。

背景

Angular、React 和 Vue 都鼓励基于组件的编程。但它们都面临一个普遍的问题,即如何处理共享数据以及组件之间的通信。为了解决这个问题,Redux 应运而生。在这篇文章中,我将用一个简单的例子来展示如何在 React 中使用 Redux。

Node 项目和 Webpack

附件是一个 Node 项目,用于服务“client”目录中的内容。虽然它大量参考了“create-react-app”脚本创建的项目,但该项目要简单得多、小得多,因此可以将 Webpack 和 React 程序轻松添加到其他环境中,例如 Visual Studio、Maven 或 Eclipse。以下是“package.json”文件。

{
  "name": "redux-example",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "pack-d": "cross-env NODE_ENV=development webpack",
    "pack-dw": "cross-env NODE_ENV=development webpack --watch",
    "pack-p": "cross-env NODE_ENV=production webpack"
  },
  "dependencies": {
    "express": "4.16.2",
    "errorhandler": "1.5.0"
  },
  "devDependencies": {
    "autoprefixer": "7.1.6",
    "babel-core": "6.26.0",
    "babel-eslint": "7.2.3",
    "babel-jest": "20.0.3",
    "babel-loader": "7.1.2",
    "babel-preset-react-app": "^3.1.2",
    "babel-runtime": "6.26.0",
    "case-sensitive-paths-webpack-plugin": "2.1.1",
    "chalk": "1.1.3",
    "css-loader": "0.28.7",
    "dotenv": "4.0.0",
    "dotenv-expand": "4.2.0",
    "eslint": "4.10.0",
    "eslint-config-react-app": "^2.1.0",
    "eslint-loader": "1.9.0",
    "eslint-plugin-flowtype": "2.39.1",
    "eslint-plugin-import": "2.8.0",
    "eslint-plugin-jsx-a11y": "5.1.1",
    "eslint-plugin-react": "7.4.0",
    "extract-text-webpack-plugin": "3.0.2",
    "file-loader": "1.1.5",
    "fs-extra": "3.0.1",
    "html-webpack-plugin": "2.29.0",
    "object-assign": "4.1.1",
    "postcss-flexbugs-fixes": "3.2.0",
    "postcss-loader": "2.0.8",
    "promise": "8.0.1",
    "raf": "3.4.0",
    "react": "^16.5.0",
    "react-dev-utils": "^5.0.2",
    "react-dom": "^16.5.0",
    "react-redux": "5.0.7",
    "redux": "3.5.2",
    "resolve": "1.6.0",
    "style-loader": "0.19.0",
    "sw-precache-webpack-plugin": "0.11.4",
    "url-loader": "0.6.2",
    "webpack": "3.8.1",
    "webpack-manifest-plugin": "1.3.2",
    "whatwg-fetch": "2.0.3",
    "cross-env": "5.2.0"
  },
  "babel": {
    "presets": [
      "react-app"
    ]
  },
  "eslintConfig": {
    "extends": "react-app"
  }
}

该项目使用 Webpack 来打包 ES6 React 代码,以下是“webpack.config.js”文件。

'use strict';
    
const autoprefixer = require('autoprefixer');
const path = require('path');
const webpack = require('webpack');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
const eslintFormatter = require('react-dev-utils/eslintFormatter');
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
    
const paths = {
        publicPath: '/build/',
        outputPath: path.resolve('./client/build'),
        polyfills: path.resolve('./client/src/polyfills'),
        index: path.resolve('./client/src/index.js'),
        appNodeModules: path.resolve('./node_modules'),
        appSrc: path.resolve('./client/src'),
        appPackageJson: path.resolve('./package.json'),
};
    
module.exports = {
  devtool: 'cheap-module-source-map',
  entry: { index: [ paths.polyfills, paths.index ]},
  output: {
    pathinfo: true,
    path: paths.outputPath,
    filename: '[name].js',
    chunkFilename: '[name].chunk.js',
    publicPath: paths.publicPath
  },
  resolve: {
    modules: ['node_modules'],
    extensions: ['.web.js', '.mjs', '.js', '.json', '.web.jsx', '.jsx'],
    alias: { 'react-native': 'react-native-web' },
    plugins: [ new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]) ],
  },
  module: {
    strictExportPresence: true,
    rules: [
      {
        test: /\.(js|jsx|mjs)$/, enforce: 'pre',
        use: [
          {
            options: {
              formatter: eslintFormatter,
              eslintPath: require.resolve('eslint'),
            },
            loader: require.resolve('eslint-loader'),
          },
        ], include: paths.appSrc,
      },
      {
        oneOf: [
          {
            test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
            loader: require.resolve('url-loader'),
            options: {
              limit: 10000,
              name: 'static/media/[name].[hash:8].[ext]',
            },
          },
          {
            test: /\.(js|jsx|mjs)$/,
            include: paths.appSrc,
            loader: require.resolve('babel-loader'),
            options: { cacheDirectory: true, },
          },
          {
            test: /\.css$/,
            use: [
              require.resolve('style-loader'),
              {
                loader: require.resolve('css-loader'),
                options: {
                  importLoaders: 1,
                },
              },
              {
                loader: require.resolve('postcss-loader'),
                options: {
                  ident: 'postcss',
                  plugins: () => [
                    require('postcss-flexbugs-fixes'),
                    autoprefixer({
                      browsers: [
                        '>1%',
                        'last 4 versions',
                        'Firefox ESR',
                        'not ie < 9',
                      ],
                      flexbox: 'no-2009',
                    }),
                  ],
                },
              },
            ],
          },
          {
            exclude: [/\.(js|jsx|mjs)$/, /\.html$/, /\.json$/],
            loader: require.resolve('file-loader'),
            options: { name: 'static/media/[name].[hash:8].[ext]', },
          },
        ],
      },
    ],
  },
  plugins: [
    new webpack.NamedModulesPlugin(),
    new CaseSensitivePathsPlugin(),
    new WatchMissingNodeModulesPlugin(paths.appNodeModules),
    new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
    new webpack.DefinePlugin({
        'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
    }),
  ],
  node: { dgram: 'empty', fs: 'empty', 
  net: 'empty', tls: 'empty', child_process: 'empty', },
  performance: { hints: false, },
};
    
if (process.env.NODE_ENV === 'production') {
    module.exports.plugins = (module.exports.plugins || []).concat([
        new webpack.optimize.UglifyJsPlugin({})])}

如果您收到有关“production”捆绑包的警告,您可以将“webpack.DefinePlugin”添加到“webpack.config.js”文件中。

if (process.env.NODE_ENV === 'production') {
      module.exports.plugins = (module.exports.plugins || []).concat([
        new webpack.optimize.UglifyJsPlugin({}),
        new webpack.DefinePlugin({
          'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
        })]);
}

Node 应用程序的入口点是“app.js”文件。

var express = require('express'),
    http = require('http'),
    path = require('path'),
    errorhandler = require('errorhandler');
    
var app = express();
    
app.set('port', 3000);
    
app.use(function (req, res, next) {
    res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate');
    res.header('Expires', '-1');
    res.header('Pragma', 'no-cache');
    next();
});
    
app.use(express.static(path.join(__dirname, 'client')));
    
app.use(errorhandler());
    
http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

要运行该应用程序,您需要首先转换和打包 React 程序。您可以使用以下命令创建开发捆绑包。

npm run pack-d

如果您想设置一个监视器,以便在保存相关文件时重新生成捆绑包,可以使用以下命令

npm run pack-dw

如果由于某种原因监视器不起作用或未显示任何消息就退出了,您可以将以下条目添加到“webpack.config.js”文件中。

watchOptions: {
    poll: true
}

您可以使用以下命令创建生产捆绑包,这需要更长的时间来生成,但捆绑包的体积要小得多。

npm run pack-p

生成捆绑包后,您可以启动 Node 服务器来服务应用程序。

node app.js

Redux、Actions、Reducers、Connect、Provider 和 React

这是一个 Redux 在 React 中的示例,其中 Redux 是一个用于共享数据和在 React 组件之间通信的框架。Redux 有三个原则:

  • 单一数据源 - 数据存储在单个 store 中的一个对象树中
  • 状态是只读的 - 改变状态的唯一方法是发出一个 action,一个描述发生了什么的对象
  • 状态改变由纯函数创建 - 这些函数称为 reducers

这篇文章是关于 Redux 的,而不是 React。如果您不熟悉 React,可以参考我之前的 笔记。要将 Redux 与 React 一起使用,您需要“redux”和“react-redux”npm 包。典型的 Redux React 应用程序将包含以下构建块:

  • Actions (动作) - 用于生成 Redux 事件的辅助函数
  • Reducers (归约器) - 用于处理事件并根据事件类型返回适当状态条目的函数
  • Components (组件) - 与 Redux 连接的 React 组件,以便它们可以分发事件并接收状态变化的通知
  • Store (仓库) 和 Provider (提供者) - 需要通过 reducers 创建一个 store,并将其与 provider 关联,以便它可以与组件通信

牢记 Redux 概念,让我们看看使用 React 进行具体的示例实现。

Actions (动作)

export const addNumberEvent = number => ({
    type: 'ADD_NUMBER',
    payload: number
});
    
export const clearNumbersEvent = () => ({
    type: 'CLEAR_NUMBERS'
});

actions 是生成 action 对象的辅助函数。典型的 action 对象有一个 type 和一个 payload 属性。“addNumberEvent”创建一个 action 对象来指示 reducer 将一个数字添加到数据存储列表。“clearNumbersEvent”创建一个 action 对象来指示 reducer 清空数字列表。action 对象由 reducers 处理。

Reducers (归约器) 和 Combined Reducer (组合归约器)

import { combineReducers } from 'redux';
    
// numbers reducer
const numbers = (state = [], action) => {
    switch (action.type) {
        case 'ADD_NUMBER':
            return [
                    ...state,
                    action.payload
            ];
        case 'CLEAR_NUMBERS':
            return state.length === 0? state: [];
        default:
            return state;
    }
};
    
// Combine all the reducers and export
export default combineReducers({ numbers });
  • reducer 函数接受两个参数:state 条目和 action 对象。
  • state 条目具有默认值非常重要,这是 Redux store 对象中 state 条目的初始值。
  • reducer 函数根据 action 对象中的信息来改变 state 条目。
  • 如果没有采取 action,reducer 函数不应改变 state 条目,而应将其直接返回给调用者。
  • 在典型的 Redux 应用程序中,我们可能有多个 reducers。导出“combineReducers”很重要。
  • Redux 框架使用 reducers 的组合信息来创建 store 的初始状态。Redux store 中每个数据条目的属性名对应于每个 reducer 的函数名。

React Components (React 组件) 和 Connected Components (已连接组件)

import React from 'react';
import PropTypes from 'prop-types';
import { addNumberEvent, clearNumbersEvent } from '../actions';
    
import { connect } from 'react-redux';
    
// Commander NG component
const Commander = ({ onClick, ownProps, children }) => (
    <button onClick={onClick} className="commander">{children}</button>
);
    
Commander.propTypes = {
  onClick: PropTypes.func.isRequired
};
    
// Wrap the component to a container
const mapDispatchToProps = (dispatch, ownProps) => ({
      onClick: () => {
          switch (ownProps.command) {
            case 'ADD_NUMBER':
                // Create a random number & dispatch it
                let number = Math.floor((Math.random() * 100) + 1);
                dispatch(addNumberEvent(number));
                break;
            case 'CLEAR_NUMBERS':
                dispatch(clearNumbersEvent());
                break;
            default:
                return;
          }
      }
});
    
export default connect(null, mapDispatchToProps)(Commander);
  • Commander”是一个标准的 React 组件,它通过“onClick”React“prop”触发一个点击事件;
  • mapDispatchToProps”函数接受一个“dispatch”参数,并使用它将适当的 action 对象发送到 reducer 函数;
  • 调用“connect”函数并导出已连接的组件非常重要,这样 Redux 框架才能与该组件通信。

Commander”组件分发 actions 来改变 Redux store 中的数据,而“ItemAggregator”和“ItemList”组件显示数据。

import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
    
// ItemAggregator NG component
const ItemAggregator = ({ sum }) => (
    <div className="ItemAggregator">Total SUM - {sum}</div>
);
    
ItemAggregator.propTypes = {
        sum: PropTypes.number.isRequired
};
    
// Wrap the component into a container
const mapStateToProps = (state) => {
    return {
        sum: state.numbers.reduce((a, b) => a + b, 0)
    };
};
    
export default connect(mapStateToProps)(ItemAggregator);
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
    
// The ItemList component
const ItemList = ({ items }) => (
    <div>
        <div className="ItemList">
        {items.map((item, i) =>
            <div key={i}>No.{i + 1} - {item}</div>
        )}
        </div>
    </div>
);
    
ItemList.propTypes = {
        items: PropTypes.arrayOf(PropTypes.number).isRequired
};
    
// Wrap the component into a container
const mapStateToProps = (state) => {
    return {
        items: state.numbers
    };
};
    
export default connect(mapStateToProps)(ItemList);
  • mapStateToProps”函数接收 store 中的 state 对象,并将其转换为 React 组件所需的格式;
  • mapDispatchToProps”和“mapStateToProps”函数都需要返回一个对象,该对象中的条目名称与 React 组件期望的“props”名称匹配;
  • 这两个 React 组件都已连接,以便 Redux 框架可以将状态更改传递给它们。

UI 显示和 Store (仓库) 和 Provider (提供者)

import React from 'react';
import Commander from './Commander';
import ItemAggregator from './ItemAggregator';
import ItemList from './ItemList';
    
const App = () => (
  <div className="container">
      <Commander command='ADD_NUMBER'>Add a random number</Commander>
      <Commander command='CLEAR_NUMBERS'>Clear the numbers</Commander>
      <ItemList />
      <ItemAggregator />
  </div>
);
    
export default App;

App”组件在“index.js”文件中绑定到 UI。

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import reducers from './reducers'
    
import App from './components/App';
import './styles/app-style.css';
    
    
const store = createStore(reducers);
    
    
ReactDOM.render(<Provider store={store}><App /></Provider>,
        document.getElementById('root'));
  • 要使用 Redux,我们需要基于组合的 reducers 创建一个 store。
  • store 需要被分配给一个“Provider”组件,而共享 store 的其他组件需要成为“Provider”组件的子级。

现在我们完成了一个简单的带 Redux 的 React 应用程序。对于我们要做的这项工作,它可能显得有点过于复杂,但当您的应用程序变得更大时,您将看到 Redux 的好处。如果您回顾一下我们所做的一切,您应该会注意到组件和 Redux reducers 可以独立开发,但通过 Redux 框架简单地连接起来,以便很好地共享数据和相互通信。

运行应用程序

如果您转换并打包您的 React 包并启动您的 node 服务器,您可以通过“https://:3000/”运行该应用程序。

您可以点击“Add...”按钮添加一个随机数到列表中,您可以通过“Clear...”按钮清除数字。尽管没有一个 React 组件实际拥有数据,但它们在 store 中共享数据,并通过 Redux 框架很好地进行通信。

关注点

  • 这是一篇关于 Redux 的说明。
  • 虽然 Redux 是一个不错的框架,但它在简单性、性能和可伸缩性方面肯定还有改进的空间。
  • 希望您喜欢我的博文,并希望这篇笔记能以某种方式帮助您。

历史

  • 2018年9月21日:首次修订
© . All rights reserved.