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

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 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的规划,这个应用可以有两种设计方案:

  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的分离,对于程序的维护扩展帮助较大。只要理解了其工作原理,不管多么复杂的应用,都能在代码中清晰的展现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值