如何使用webpack+react+redux从头搭建Todolist应用

来源:互联网 发布:网络设计方案案例 编辑:程序博客网 时间:2024/06/08 02:38

  • webpack环境配置
  • 应用整体框架设计
  • 代码实现
    • Container
    • Components
    • Actions
    • Reducers
    • indexjs
    • 测试
  • 总结

Todolist UI

一言不和先上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 initnpm install react react-dom redux react-redux css-loader style-loader sass-loader node-sass file-loader url-loader autoprefixer postcss-loader --savenpm install webpack -g npm install webpack --save-dev npm install extract-text-webpack-plugin html-webpack-plugin --save-devnpm 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的规划,这个应用可以有两种设计方案:

  1. 前文提到了应用的三个部分,正好可以对应三个component,上层弄一个container作为 component和store 的桥梁。
  2. 直接在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”。最终的框架如下所示,

Todolist 前端框架

目录结构如下,
外层目录:

外层目录

请不要漏掉.babelrc, server.js文件,前者配置了babel,后者配置了测试用的server。
src下的代码目录:

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需要注意下面几点:

  1. 每个reducer的名字需要和对应的部分state名字相同,否则新的state各部分名字会和旧的不一致,从上面的reducer默认state参数可以看出这点。
  2. 需要default返回state本身,因为每次都会重新生成新的state,若不返回则会丢失该部分的state。
  3. 更新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,非常方便。

DevTools

测试

运行
npm start
然后在浏览器中输入localhost:3000,回车,就可以看到效果啦。
运行
webpack
即可打包文件到dist目录下。

总结

react和redux的概念不算少,需要一定的时间去适应,但优点也很明显,单向的数据流,全局统一的状态树,view和model的分离,对于程序的维护扩展帮助较大。只要理解了其工作原理,不管多么复杂的应用,都能在代码中清晰的展现。

原创粉丝点击