关于 Redux 的说明
这是关于 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日:首次修订