基于react-app搭建react-router+redux项目

前言

总括: 本文采用react+redux+react-router+less+es6+webpack,以实现一个简易备忘录(todolist)为例尽量全面的讲述使用react全家桶实现一个完整应用的过程。javascript

人生不失意,焉能暴己知。html

技术说明

技术架构:本备忘录使用react+react-router+redux+less+ES6+webpack实现;前端

页面UI参照:TodoList官网实现;java

在线演示地址:Damonare的备忘录;react

功能说明

  • 支持回车添加新事项;
  • 支持删除事项(点击X符号);
  • 支持状态转换具体包括:
    • 新建事项->正在进行(点击checkbox选项)
    • 正在进行->已完成(点击文字内容自己)
    • 正在进行->新建事项(点击checkbox选项)
    • 已完成->正在进行(点击文字自己)
  • 支持判断输入空字符,过长字符(20个汉字之内);
  • 支持搜索;
  • 支持本地化存储;
  • 支持状态的展开隐藏(点击标题)
  • 兼容手机端(iPhone6及以上)
  • 支持路由切换

1. React浅谈

1.1 组件化

当谈到React的时候不能避免的会提到组件化思想。React刚开始想解决的问题是UI层面的问题,view层面的问题。后来越滚越大,从最先的UI引擎变成了一整套先后端通吃的 Web App 解决方案。一个完整的页面是由大大小小的组件堆叠而成,组件套组件组成了用户所能看到的完整的页面。webpack

1.2 JSX语法糖

​ 使用React,不必定非要使用JSX语法,可使用原生的JS进行开发。可是React做者强烈建议咱们使用JSX,以下:git

 

简单明了!!!具体的JSX语法很少说了,学习更多戳这:JSX es6

1.3 Virtual DOM

虚拟DOM的概念并非FB独创却在FB的手上大火了起来。github

页面对应了一个DOM树,在传统页面的开发模式中,每次须要更新页面时,都须要对DOM进行更新,DOM操做十分昂贵,为减小对于真实DOM的操做,诞生了Virtual DOM的概念,也就是用javascript把真实的DOM树描述了一遍,使用的也就是咱们刚刚说过的JSX语法。web

对好比下:

 

每次数据更新以后,从新计算Virtual DOM,并和上一次的Virtual DOM对比,对发生的变化进行批量更新。React也提供了shouldComponentUpdate生命周期回调,来减小数据变化后没必要要的Virtual DOM对比过程,提高了性能。

Virtual DOM虽然渲染方式比传统的DOM操做要好一些,但并不明显,由于对比DOM节点也是须要计算的,最大的好处在于能够很方便的和其它平台集成,好比react-native就是基于Virtual DOM渲染出原生控件。具体渲染出的是Web DOM仍是Android控件或是iOS控件就由平台决定了。这一点上让咱们多平台的编码至关方便。

1.4 函数式编程

​ 过去编程方式主要是以命令式编程为主。电脑生硬的执行指令,给电脑下达命令,电脑去执行,如今主要的编程语言(好比:Java,C,C++等)都是由命令式编程构建起来的。

​ 而函数式编程就不同了,这是模仿咱们人类的思惟方式发明出来的。例如:操做某个数组的每个元素而后返回一个新数组,若是是计算机的思考方式,会这样想:建立一个新数组=>遍历旧数组=>给新数组赋值。若是是人类的思考方式,会这样想:建立一个数组方法,做用在旧数组上,返回新数组。这样此方法能够被重复利用。而这就是函数式编程了。

1.5 数据流

在React中,数据是单向流动的,是从上向下的方向,即从父组件到子组件的方向。
state和props是其中重要的概念,若是顶层组件初始化props,那么React会向下遍历整颗组件树,从新渲染相关的子组件。其中state表示的是每一个组件中内部的的状态,这些状态只在组件内部改变。
把组件当作是一个函数,那么他接受props做为参数,内部由state做为函数的内部参数,返回一个虚拟dom的实现。
state
state是组件内部的状态,当组件内部使用内置的setState方法,该组件会从新进行渲染。
注意一下,setState方法是一个异步的方法,在一个生命周期中的全部setState方法会合并操做。
props
props是React中用来让组件之间相互联系的一种机制,props的传递过程对React是很是直观的,React的单向数据流主要的流动管道就是props,props自己是不可变的,当咱们试图改变props的原始值,React会报类型错误的警告。组件的props必定是来源于默认指定的属性或者是从父组件传入的。
React为props提供了默认配置,经过defaultProps静态变量的方式来定义,当组件被调用的时候,默认值保证渲染后始终有值。
在React中有一个内置的props--children,表明的是子组件的集合,根据子组件的数量,this.props.children的数据类型并且不一致,当没有子组件的时候为undefined,当有一个的时候为object,当有多个的时候为array。
propTypes
propTypes是用来规范props的类型和必须的状态,若是组件定义了propTypes,那么在开发环境下会对props进行检查,在生产环境下是不会进行检查的。

 

1.6 React主要就下图之中的这些内容,掌握了这个图就算入门了。

 

2. React-router

官方例子:react-router-tutorial。

完事之后能够再看一下阮一峰老师的教程,主要是对一些API的讲解:React Router 使用教程

3. Redux 

3.1 简介

随着 JavaScript 单页应用开发日趋复杂,JavaScript 须要管理比任什么时候候都要多的 state (状态)。 这些 state 可能包括服务器响应、缓存数据、本地生成还没有持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。若是一个 model 的变化会引发另外一个 model 变化,那么当 view 变化时,就可能引发对应 model 以及另外一个 model 的变化,依次地,可能会引发另外一个 view 的变化。乱!

这时候Redux就强势登场了,如今你能够把React的model看做是一个个的子民,每个子民都有本身的一个状态,纷纷扰扰,各自维护着本身状态,太乱了,咱们须要一个King来领导你们,咱们就能够把Redux看做是这个King。网罗全部的组件组成一个国家,掌控着一切子民的状态!防止有人叛乱生事!

这个时候就把组件分红了两种:容器组件(King或是路由)和展现组件(子民)。

  • 容器组件:即redux或是router,起到了维护状态,出发action的做用,其实就是King高高在上下达指令。
  • 展现组件:不维护状态,全部的状态由容器组件经过props传给他,全部操做经过回调完成
      展现组件 容器组件
    做用 描述如何展示(骨架、样式) 描述如何运行(数据获取、状态更新)
    直接使用 Redux
    数据来源 props 监听 Redux state
    数据修改 从 props 调用回调函数 向 Redux 派发 actions
    调用方式 手动 一般由 React Redux 生成

Redux三大部分:store,action,reducer。至关于King的直系下属。

那么也能够看出Redux只是一个状态管理方案,彻底能够单独拿出来使用,这个King不只仅能够是React的,去Angular,Ember那里也是能够作King的。在React中维系King和组件关系的库叫作 react-redux

, 它主要有提供两个东西:Provider 和connect,具体使用文后说明。

提供几个Redux的学习地址:官方教程-中文版Redux 入门教程(一):基本用法

3.2 Store

Store 就是保存数据的地方,它其实是一个Object tree。整个应用只能有一个 Store。这个Store能够看作是King的首相,掌控一切子民(组件)的活动(state)。

Redux 提供createStore这个函数,用来生成 Store。

import { createStore } from 'redux'; const store = createStore(func);

createStore接受一个函数做为参数,返回一个Store对象(首相诞生记)

咱们来看一下Store(首相)的职责:

3.3 action

State 的变化,会致使 View 的变化。可是,用户接触不到 State,只能接触到 View。因此,State 的变化必须是 View 致使的。Action 就是 View 发出的通知,表示 State 应该要发生变化了。即store的数据变化来自于用户操做。action就是一个通知,它能够看做是首相下面的邮递员,通知子民(组件)改变状态。它是 store 数据的惟一来源。通常来讲会经过 store.dispatch() 将 action 传到 store。

Action 是一个对象。其中的type属性是必须的,表示 Action 的名称。

const action = {
  type: 'ADD_TODO',
  payload: 'Learn Redux'
};

 

Action建立函数

Action 建立函数 就是生成 action 的方法。“action” 和 “action 建立函数” 这两个概念很容易混在一块儿,使用时最好注意区分。

在 Redux 中的 action 建立函数只是简单的返回一个 action:

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

这样作将使 action 建立函数更容易被移植和测试。

 

3.4 reducer

Action 只是描述了有事情发生了这一事实,并无指明应用如何更新 state。而这正是 reducer 要作的事情。也就是邮递员(action)只负责通知,具体你(组件)如何去作,他不负责,这事情只能是大家村长(reducer)告诉你如何去作。

专业解释: Store 收到 Action 之后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫作 Reducer。

Reducer 是一个函数,它接受 Action 和当前 State 做为参数,返回一个新的 State。

const reducer = function (state, action) {
  // ...
  return new_state;
};

3.5 数据流

下图展现了Redux带来的变化。

 

Redux 应用中数据的生命周期遵循下面 4 个步骤:

  • 调用 store.dispatch(action)
  • Redux store 调用传入的 reducer 函数。
  • 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
  • Redux store 保存了根 reducer 返回的完整 state 树

工做流程图以下:

 component -> action -> store -> reducer -> state 的单向数据流,归纳的说就是:React组件里面获取了数据以后(好比ajax请求),而后建立一个action通知store我有这个想改变state的意图,而后reducers(一个action可能对应多个reducer,能够理解为action为订阅的主题,可能有多个订阅者)来处理这个意图而且返回新的state,接下来store会收集到全部的reducer的state,最后更新state。

3.6 Connect

这里须要再强调一下:Redux 和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。

尽管如此,Redux 仍是和 React 和 Deku 这类框架搭配起来用最好,由于这类框架容许你以 state 函数的形式来描述界面,Redux 经过 action 的形式来发起 state 变化。

Redux 默认并不包含 React 绑定库,须要单独安装。 

npm install --save react-redux

 

 React-Redux 提供connect方法,用于从 UI 组件生成容器组件。connect的意思,就是将这两种组件连起来。 

import { connect } from 'react-redux';
const TodoList = connect()(Memos);

 

 上面代码中Memos是个UI组件,TodoList就是由 React-Redux 经过connect方法自动生成的容器组件。

而只是纯粹的这样把Memos包裹起来毫无心义,完整的connect方法这样使用: 

import { connect } from 'react-redux'
const TodoList = connect(
  mapStateToProps
)(Memos)

 

 上面代码中,connect方法接受两个参数:mapStateToPropsmapDispatchToProps。它们定义了 UI 组件的业务逻辑。前者负责输入逻辑,即将state映射到 UI 组件的参数(props),后者负责输出逻辑,即将用户对 UI 组件的操做映射成 Action。

 

3.7 Provider

 这个Provider 实际上是一个中间件,它是为了解决让容器组件拿到King的指令(state对象)而存在的。

import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp);
render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

 

4.实战备忘录

讲解以前能够先看一下github上的代码,你能够clone下来学习,也能够在线给我提issue,欢迎戳这:React全家桶实现简易备忘录

 4.1目录结构

 

接下来,咱们只关注app目录就行了。

4.2入口文件  

app/main.jsx 

import React from 'react';
import ReactDOM from 'react-dom';
import { Route, IndexRoute, hashHistory, Router } from 'react-router';
import { Provider } from 'react-redux';
import App from './container/App';
import AllMemosRoute from './routes/AllMemosRoute';
import TodoRoute from './routes/TodoRoute';
import DoingRoute from './routes/DoingRoute';
import DoneRoute from './routes/DoneRoute';
import configureStore from './stores';
import './main.less';

const store = configureStore();
ReactDOM.render(
    <Provider store={store}>
        <Router history={hashHistory}>
            <Route path="/" component={App}>
                <IndexRoute component={AllMemosRoute} />
                <Route path="/todo" component={TodoRoute} />
                <Route path="/doing" component={DoingRoute} />
                <Route path="/done" component={DoneRoute} />
            </Route>
        </Router>
    </Provider>,
    document.getElementById('root')
);

 

这里咱们从react-redux中获取到 Provider 组件,咱们把它渲染到应用的最外层。
他须要一个属性 store ,他把这个 store 放在context里,给Router(connect)用。

4.3 Store

app/store/index.jsx 

import { createStore } from 'redux';
import reducer from '../reducers';
export default function configureStore(initialState) {
  const store = createStore(reducer, initialState);
  if (module.hot) {
    // Enable Webpack hot module replacement for reducers
    module.hot.accept('../reducers', () => {
      const nextReducer = require('../reducers');
      store.replaceReducer(nextReducer);
    });
  }
  return store;
}

 

4.4 Action 建立函数和常量

app/action/index.jsx

'use strict';
/*
 * @author Damonare 2016-12-10
 * @version 1.0.0
 * action 类型
 */
export const Add_Todo = 'Add_Todo';
export const Change_Todo_To_Doing = 'Change_Todo_To_Doing';
export const Change_Doing_To_Done = 'Change_Doing_To_Done';
export const Change_Done_To_Doing = 'Change_Done_To_Doing';
export const Change_Doing_To_Todo = 'Change_Doing_To_Todo';
export const Search='Search';
export const Delete_Todo='Delete_Todo';
/*
 * action 建立函数
 * @method  addTodo添加新事项
 * @param  {String} text 添加事项的内容
 */
export function addTodo(text) {
  return {
      type: Add_Todo,
      text
  }
}
/*
 * @method  search 查找事项
 * @param  {String} text 查找事项的内容
 */
export function search(text) {
  return {
      type: Search,
      text
  }
}
/*
 * @method  changeTodoToDoing 状态由todo转为doing
 * @param  {Number} index 须要改变状态的事项的下标
 */
export function changeTodoToDoing(index) {
  return {
      type: Change_Todo_To_Doing,
      index
  }
}
/*
 * @method  changeDoneToDoing 状态由done转为doing
 * @param  {Number} index 须要改变状态的事项的下标
 */
export function changeDoneToDoing(index) {
  return {
      type: Change_Done_To_Doing,
      index
  }
}
/*
 * @method  changeDoingToTodo 状态由doing转为todo
 * @param  {Number} index 须要改变状态的事项的下标
 */
export function changeDoingToTodo(index) {
  return {
      type: Change_Doing_To_Todo,
      index
  }
}
/*
 * @method  changeDoingToDone 状态由doing转为done
 * @param  {Number} index 须要改变状态的事项的下标
 */
export function changeDoingToDone(index) {
  return {
      type: Change_Doing_To_Done,
      index
  }
}
/*
 * @method  deleteTodo 删除事项
 * @param  {Number} index 须要删除的事项的下标
 */
export function deleteTodo(index) {
  return {
      type: Delete_Todo,
      index
  }
}

 

 在声明每个返回 action 函数的时候,咱们须要在头部声明这个 action 的 type,以便好组织管理。
每一个函数都会返回一个 action 对象,因此在 组件里面调用 

text =>
  dispatch(addTodo(text))

 

 就是调用dispatch(action) 。

 

4.5 Reducers 

app/reducers/index.jsx  

import { combineReducers } from 'redux';
import todolist from './todos';
// import visibilityFilter from './visibilityFilter';

const reducer = combineReducers({
    todolist,
});

export default reducer; 

app/reducers/todos.jsx

 

import {
    ADD_TODO,
    DELETE_TODO,
    CHANGE_TODO_TO_DOING,
    CHANGE_DOING_TO_DONE,
    CHANGE_DOING_TO_TODO,
    CAHNGE_DONE_TO_DOING,
    SEARCH,
} from '../actions';

let todos;
(() => {
    if (localStorage.todos) {
        todos = JSON.parse(localStorage.todos);
    } else {
        todos = [];
    }
})();
const todolist = (state = todos, action) => {
    switch (action.type) {
       /*
        *  添加新的事项
        *  并进行本地化存储
        *  使用ES6展开运算符连接新事项和旧事项
        *  JSON.stringify进行对象深拷贝
        */
        case ADD_TODO:
            return [
                ...state, {
                    todo: action.text,
                    istodo: true,
                    doing: false,
                    done: false
                }
            ];
        /*
         * 将todo转为doing状态,注意action.index的类型转换
         */
        case CHANGE_TODO_TO_DOING:
            localStorage.setItem('todos', JSON.stringify([
                ...state.slice(0, action.index),
                {
                    todo:state[action.index].todo,
                    istodo: false,
                    doing: true,
                    done: false
                },
                ...state.slice(parseInt(action.index) + 1)
            ]));
            return [
                ...state.slice(0, action.index),
                {
                    todo:state[action.index].todo,
                    istodo: false,
                    doing: true,
                    done: false
                },
                ...state.slice(parseInt(action.index) + 1)
            ];
        /*
         * 将doing转为done状态
         */
        case CHANGE_DOING_TO_DONE:
            localStorage.setItem('todos', JSON.stringify([
                ...state.slice(0, action.index),
                {
                    todo:state[action.index].todo,
                    istodo: false,
                    doing: false,
                    done: true
                },
                ...state.slice(parseInt(action.index) + 1)
            ]));
            return [
                ...state.slice(0, action.index),
                {
                    todo:state[action.index].todo,
                    istodo: false,
                    doing: false,
                    done: true
                },
                ...state.slice(parseInt(action.index) + 1)
            ];
        /*
         * 将done转为doing状态
         */
        case CAHNGE_DONE_TO_DOING:
            localStorage.setItem('todos', JSON.stringify([
                ...state.slice(0, action.index),
                {
                    todo:state[action.index].todo,
                    istodo: false,
                    doing: true,
                    done: false
                },
                ...state.slice(parseInt(action.index) + 1)
            ]));
            return [
                ...state.slice(0, action.index),
                {
                    todo:state[action.index].todo,
                    istodo: false,
                    doing: true,
                    done: false
                },
                ...state.slice(parseInt(action.index) + 1)
            ];
        /*
         * 将doing转为todo状态
         */
        case CHANGE_DOING_TO_TODO:
            localStorage.setItem('todos', JSON.stringify([
                ...state.slice(0, action.index),
                {
                    todo:state[action.index].todo,
                    istodo: true,
                    doing: false,
                    done: false
                },
                ...state.slice(parseInt(action.index) + 1)
            ]));
            return [
                ...state.slice(0, action.index),
                {
                    todo:state[action.index].todo,
                    istodo: true,
                    doing: false,
                    done: false
                },
                ...state.slice(parseInt(action.index) + 1)
            ];
        /*
         * 删除某个事项
         */
        case DELETE_TODO:
            localStorage.setItem('todos', JSON.stringify([
                ...state.slice(0, action.index),
                ...state.slice(parseInt(action.index) + 1)
            ]));
            return [
                ...state.slice(0, action.index),
                ...state.slice(parseInt(action.index) + 1)
            ];
        /*
         * 搜索
         */
        case SEARCH:
            let text = action.text;
            let reg = eval("/"+text+"/gi");
            return state.filter(item=> item.todo.match(reg));
        default:
            return state;
    }
}
export default todolist;
相关文章
相关标签/搜索