一言不和先上demo: https://mschuan.github.io/Todo-list-react-redux/dist/index.html,代码托管在github: https://github.com/MSChuan/Todo-list-react-redux。
想必大家都听说过这个简单应用-Todolist。
它有如下三个部分:
- 文本框和Add按钮。在文本框中输入todo的事件,点击Add将其添加到事件列表中
- 事件列表。除了显示作用之外,还可以通过点击将其标记为todo或者done(显示出删除线)
- 事件过滤。三种模式:显示全部;显示Todo的事件;显示已经完成的事件
-
本文将用webpack+react+redux一步步的完成这个demo,代码使用了javascript ES6语法。
webpack环境配置
请自行google or baidu安装npm,然后新建一个文件夹,运行如下命令:
npm init
npm install react react-dom redux react-redux css-loader style-loader sass-loader node-sass file-loader url-loader autoprefixer postcss-loader --save
npm install webpack -g
npm install webpack --save-dev
npm install extract-text-webpack-plugin html-webpack-plugin --save-dev
npm install babel-loader babel-core babel-preset-es2015 babel-preset-react babel-preset-stage-2 babel-plugin-transform-decorators-legacy babel-plugin-import babel-cli --save-dev
npm install path webpack-dev-server redux-devtools redux-devtools-log-monitor redux-devtools-dock-monitor --save-dev
首先是初始化,之后分别安装了react,redux,一些常用loaders,webpack,plugins(抽离css文件以及自动生成html文件),babel(用于支持ES6,ES7语法)以及调试工具。
在webpack.config.js中配置webpack:
var webpack = require('webpack'),
path = require('path'),
ExtractTextPlugin = require('extract-text-webpack-plugin'),
HtmlWebpackPlugin = require('html-webpack-plugin');
var config = {
entry: {
index: [
'webpack-dev-server/client?http://localhost:3000',
'webpack/hot/only-dev-server',
'./src/index.js'
],
vendor: [ // pack react and react-dom independently
"react",
"react-dom"
]
},
output: {
path: __dirname + "/dist/",
filename: "js/[name].js"
},
module: {
loaders: [{ // babel loader
test: /\.js?$/,
exclude: /node_modules/,
loader: "babel-loader"
}, {
test: /\.(scss|sass|css)$/, // pack sass and css files
loader: ExtractTextPlugin.extract({fallback: "style-loader", use: "css-loader!sass-loader"})
}, {
test: /\.(png|jpg|jpng|eot|ttf)$/, // pack images and fonts
loader: 'url-loader?limit=8192&name=images/[name].[ext]'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.tpl.html',
inject: 'body',
filename: 'index.html'
}),
new webpack.optimize.CommonsChunkPlugin("bundle/vendor.bundle.js"), //packed independently such as react and react-dom
new ExtractTextPlugin("css/index.css"), // pack all the sass and css files into index.csss
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development')
})
]
};
module.exports = config;
entry是应用的入口,其中的index定义了入口文件,vendor用于单独打包react等框架,提升打包速度。output指定了输出文件路径和文件名,源代码中的dist文件夹就是打包后的代码所在地。module中定义了一些常用的loader,plugins中的功能包括了自动生成html,打包vendor的输出路径和文件名,单独打包css,自动编译工具等。server.js中定义了测试用的webpack-dev-server的相关配置,.babelrc配置了react使用ES6以及ES7的decorator功能。
应用整体框架设计
首先应该考虑的就是container和component的规划,这个应用可以有两种设计方案:
- 前文提到了应用的三个部分,正好可以对应三个component,上层弄一个container作为 component和store 的桥梁。
- 直接在container里实现全部代码,因为功能单一,代码简单,作为一个整体也不会混乱, react+redux的设计宗旨就是少而清晰的层级结构,否则state和actions的层层传递会多费很多工夫。
这里还是选用option 1, 可以帮助我们更好的理解react的层级结构,体会state和actions的传递过程。state的设计也非常直接,一个样例state是如下形式:
const AppConstants = {
ShownModesString: ["Show All", "Show Todo", "Show Done"]
};
const initialState = {
todoItems: [
{
content: 'first item',
isDone: false
},
{
content: 'second item',
isDone: true
}
],
shownMode: AppConstants.ShownModesString[0]
};
export {AppConstants, initialState};
可以看到todoitems存储了整个事件列表,每个事件有两个属性,content就是事件本身内容,isDone是标记该事件是否已经完成。shownMode存储了当前的显示模式,AppConstants.ShownModesString 中包含了三种模式:”Show All”, “Show Todo”, “Show Done”。最终的框架如下所示,
目录结构如下,
外层目录:
请不要漏掉.babelrc, server.js文件,前者配置了babel,后者配置了测试用的server。
src下的代码目录:
代码实现
Container
Container负责连接store并拿到所需的state和actions,首先import dependencies
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import actionFactory from '../Actions/ActionFactory';
import { bindActionCreators } from 'redux';
import TodoList from '../Components/TodoList';
import ShownModes from '../Components/ShownModes';
import AddTodo from '../Components/AddTodo';
我曾在这里踩过一个坑,from后面的字符串要是一个路径,假设AddTodo和Container在同一个目录,那么需要写成import AddTodo from ‘./AddTodo’,而不是import AddTodo from ‘AddTodo’。
container class:
class RootContainer extends React.Component {
constructor(props) {
super(props);
}
render() {
const { state, actions } = this.props;
return (<div>
<AddTodo actions={actions} />
<TodoList state={state} actions={actions} />
<ShownModes shownMode={state.shownMode} actions={actions} />
</div>);
}
}
Container和Component都是继承自React.Component,constructor如果没有额外逻辑的话也可以不写,render函数是一定要有的,这里的逻辑就是从props中拿出state和actions,render的结果是三个components,并把子组件所需的state和actions以props的形式传下去。
类型检查:
RootContainer.propTypes = {
state: PropTypes.object,
actions: PropTypes.object
};
连接Container和Store:
const buildActionDispatcher = (dispatch) => ({
actions: bindActionCreators(actionFactory, dispatch)
});
export default connect(
(state) => {
return ({ state: state });
}, buildActionDispatcher)(RootContainer);
bindActionCreators的作用是简化代码,如果没有它,在component中需要显式的dispatch(someAction),使用它之后,调用actionFactory中的function即可,它会自动dispatch产生的action。
connect是react-redux封装的函数,它会根据RootContainer重新生成一个新的container,绑定了store state和actions到props中,所以在RootContainer中可以从props里拿到这两个object并传递给子组件。
Components
AddTodo component:
class AddTodo extends React.Component {
render() {
const { actions } = this.props;
let input = '';
return (<div>
<input type="text" ref={(text) => {input = text;}} placeholder={"Todo"} />
<input type="button" onClick={() => actions.AddItem(input.value)} value="Add" />
</div>);
}
}
AddTodo的显示不需要state,所以只传进来了actions,在click Add按钮时需要dispatch action,为事件列表增加一个Todo事件,AddItem 是定义在actionFactory中的action产生函数,后面会介绍它的实现。从这里的实现不难看出,react+redux的框架使得component只需要关注state的render以及指定合适的用户交互回调函数,不需要关心真正修改state的逻辑实现,结构清晰,模块独立。
同理可以实现另外两个components:
class ShownModes extends React.Component {
render() {
const { shownMode, actions } = this.props;
const shownModes = AppConstants.ShownModesString.map((item, index) => {
return (<input type="button" value={item} style={{color: item === shownMode ? "red" : "black"}}
onClick={() => actions.SetMode(item)} />);
});
return <div>{shownModes}</div>;
}
}
ShownModes根据state中的shownMode来决定显示当前是哪种显示模式,对应按钮的文字显示成红色。
class TodoList extends React.Component {
render() {
const { state, actions } = this.props;
const todoList = state.todoItems.map((item, index) => {
if((state.shownMode === "Show Todo" && item.isDone) || (state.shownMode === "Show Done" && !item.isDone)) {
return;
}
return (<li style={{textDecoration: item.isDone ? 'line-through' : 'none'}}
onClick={() => actions.Done(index)}>
<a href="#" style={{textDecoration: "none", color: "black"}}>{item.content}</a>
</li>);
});
return <ul>{todoList}</ul>;
}
}
实现TodoList时偷了个小懒,常量的字符串(如”Show Todo”)最好是从constants类中读取,便于统一管理,而不是在这里hard code,挖个小坑。
Actions
在上述Container和Components中,我们总共用到了3 actions。
const actionFactory = {
AddItem: (content) => ({
type: "AddItem",
content: content
}),
Done: (index) => ({
type: "Done",
index: index
}),
SetMode: (shownMode) => ({
type: "SetMode",
shownMode: shownMode
}),
};
传入的参数会被放到产生的action中,在reducer里修改state时会被用到。
一般而言type对应的string最好在一个type.js中统一定义,方便管理。不同的Container对应的actionFactory可以放到不同的文件,置于Actions文件夹之下。
Reducers
上述三个actions会被dispatch给reducers进行处理,所有的reducers都是function,输入是store里的state以及传入的action,返回值是修改过的state。这里根据state设计了两个reducer:
const todoItems = (state = initialState.todoItems, action) => {
switch(action.type) {
case "AddItem":
return [...state, {
content: action.content,
isDone: false
}];
case "Done":
return [...state.slice(0, action.index),
Object.assign({}, state[action.index], {isDone: !state[action.index].isDone}),
...state.slice(action.index + 1)];
default:
return state;
}
};
const shownMode = (state = initialState.shownMode, action) => {
switch(action.type) {
case "SetMode":
return action.shownMode;
default:
return state;
}
};
最后通过combineReducers合在一起,组成新的store state。
const rootReducer = combineReducers({
todoItems,
shownMode
});
Reducer需要注意下面几点:
- 每个reducer的名字需要和对应的部分state名字相同,否则新的state各部分名字会和旧的不一致,从上面的reducer默认state参数可以看出这点。
- 需要default返回state本身,因为每次都会重新生成新的state,若不返回则会丢失该部分的state。
- 更新state时需要返回一个新的object,不能在原有state object上修改,否则新的state === 旧的state将会是true,component不会重新render,可以使用Object.assign({}, {old state], [changed Items])来产生新的state。
index.js
有了上述功能代码,我们还需要一个入口文件。
const store = createStore(rootReducer, initialState, DevTools.instrument());
render(
<Provider store={store}>
<div>
<RootContainer />
<DevTools />
</div>
</Provider>,
document.getElementById('root')
);
createStore会产生整个应用的store,Provider是react-redux封装的component,它只干了一件事情,就是把store通过context传递给下面的Container,刚才提到的connect函数所产生的container会从context中拿到这里的store,从而绑定其state,需要注意的是我们的代码中不要从context中去拿这个store,会破坏代码结构的清晰度,context也是react的一个测试功能,未来很可能会有大的变化,放到代码中不易于未来维护扩展。
我们还使用了DevTools,这是一个调试工具,可以显示每一次dispatch的action以及reducer之后的新state,非常方便。
测试
运行
npm start
然后在浏览器中输入localhost:3000,回车,就可以看到效果啦。
运行
webpack
即可打包文件到dist目录下。
总结
react和redux的概念不算少,需要一定的时间去适应,但优点也很明显,单向的数据流,全局统一的状态树,view和model的分离,对于程序的维护扩展帮助较大。只要理解了其工作原理,不管多么复杂的应用,都能在代码中清晰的展现。