如何使用webpack+react+redux从头搭建Todolist应用
2017-06-04 17:19
1291 查看
webpack环境配置
应用整体框架设计
代码实现
Container
Components
Actions
Reducers
indexjs
测试
总结
一言不和先上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语法。
首先是初始化,之后分别安装了react,redux,一些常用loaders,webpack,plugins(抽离css文件以及自动生成html文件),babel(用于支持ES6,ES7语法)以及调试工具。
在webpack.config.js中配置webpack:
entry是应用的入口,其中的index定义了入口文件,vendor用于单独打包react等框架,提升打包速度。output指定了输出文件路径和文件名,源代码中的dist文件夹就是打包后的代码所在地。module中定义了一些常用的loader,plugins中的功能包括了自动生成html,打包vendor的输出路径和文件名,单独打包css,自动编译工具等。server.js中定义了测试用的webpack-dev-server的相关配置,.babelrc配置了react使用ES6以及ES7的decorator功能。
前文提到了应用的三个部分,正好可以对应三个component,上层弄一个container作为 component和store 的桥梁。
直接在container里实现全部代码,因为功能单一,代码简单,作为一个整体也不会混乱, react+redux的设计宗旨就是少而清晰的层级结构,否则state和actions的层层传递会多费很多工夫。
这里还是选用option 1, 可以帮助我们更好的理解react的层级结构,体会state和actions的传递过程。state的设计也非常直接,一个样例state是如下形式:
可以看到todoitems存储了整个事件列表,每个事件有两个属性,content就是事件本身内容,isDone是标记该事件是否已经完成。shownMode存储了当前的显示模式,AppConstants.ShownModesString 中包含了三种模式:”Show All”, “Show Todo”, “Show Done”。最终的框架如下所示,
目录结构如下,
外层目录:
请不要漏掉.babelrc, server.js文件,前者配置了babel,后者配置了测试用的server。
src下的代码目录:
我曾在这里踩过一个坑,from后面的字符串要是一个路径,假设AddTodo和Container在同一个目录,那么需要写成import AddTodo from ‘./AddTodo’,而不是import AddTodo from ‘AddTodo’。
container class:
Container和Component都是继承自React.Component,constructor如果没有额外逻辑的话也可以不写,render函数是一定要有的,这里的逻辑就是从props中拿出state和actions,render的结果是三个components,并把子组件所需的state和actions以props的形式传下去。
类型检查:
连接Container和Store:
bindActionCreators的作用是简化代码,如果没有它,在component中需要显式的dispatch(someAction),使用它之后,调用actionFactory中的function即可,它会自动dispatch产生的action。
connect是react-redux封装的函数,它会根据RootContainer重新生成一个新的container,绑定了store state和actions到props中,所以在RootContainer中可以从props里拿到这两个object并传递给子组件。
AddTodo的显示不需要state,所以只传进来了actions,在click Add按钮时需要dispatch action,为事件列表增加一个Todo事件,AddItem 是定义在actionFactory中的action产生函数,后面会介绍它的实现。从这里的实现不难看出,react+redux的框架使得component只需要关注state的render以及指定合适的用户交互回调函数,不需要关心真正修改state的逻辑实现,结构清晰,模块独立。
同理可以实现另外两个components:
ShownModes根据state中的shownMode来决定显示当前是哪种显示模式,对应按钮的文字显示成红色。
实现TodoList时偷了个小懒,常量的字符串(如”Show Todo”)最好是从constants类中读取,便于统一管理,而不是在这里hard code,挖个小坑。
传入的参数会被放到产生的action中,在reducer里修改state时会被用到。
一般而言type对应的string最好在一个type.js中统一定义,方便管理。不同的Container对应的actionFactory可以放到不同的文件,置于Actions文件夹之下。
最后通过combineReducers合在一起,组成新的store state。
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。
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目录下。
应用整体框架设计
代码实现
Container
Components
Actions
Reducers
indexjs
测试
总结
一言不和先上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 dependenciesimport 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的分离,对于程序的维护扩展帮助较大。只要理解了其工作原理,不管多么复杂的应用,都能在代码中清晰的展现。相关文章推荐
- 使用 Babel + React + Webpack 搭建 Web 应用
- 基于ES6,使用React、Webpack、Babel构建模块化JavaScript应用
- 使用Webpack和Babel来搭建React应用程序
- 使用yeoman搭建react-webpack框架的学习
- 使用webpack和babel搭建react开发环境
- webpack搭建的vue-cli项目如何使用sass?
- React-redux-webpack项目总结之reduce、action、store、components 是如何开车的
- 使用webpack和react搭建项目
- react-redux-webpack搭建一
- webpack构建react应用三:使用webpack Loaders 模块加载器(一)
- webpack构建React应用五:使用webpack Loaders 模块加载器(三)
- React+Redux+Webpack构建开发单页应用环境
- Electron+React+Webpack+Vscode应用桌面开发平台搭建
- 使用webpack和babel搭建react开发环境
- react-redux-webpack-express开发环境搭建
- 使用webpack、babel、react、antdesign配置单页面应用开发环境
- webpack构建React应用四:使用webpack Loaders 模块加载器(二)
- Electron+React+Webpack+Vscode应用桌面开发平台搭建
- 使用webpack搭建一个react项目
- webpack构建React应用二:webpack的安装及基础使用