资讯专栏INFORMATION COLUMN

实例讲解react+react-router+redux

RiverLi / 3446人阅读

摘要:而函数式编程就不一样了,这是模仿我们人类的思维方式发明出来的。数据流在中,数据的流动是单向的,即从父节点传递到子节点。数据流严格的单向数据流是架构的设计核心。

前言

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

代码地址:React全家桶实现一个简易备忘录

原文博客地址:React全家桶实现一个简易备忘录

知乎专栏&&简书专题:前端进击者(知乎)&&前端进击者(简书)

博主博客地址:Damonare的个人博客

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

技术说明

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

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

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

功能说明

支持回车添加新事项;

支持删除事项(点击X符号);

支持状态转换具体包括:

新建事项->正在进行(点击checkbox选项)

正在进行->已完成(点击文字内容本身)

正在进行->新建事项(点击checkbox选项)

已完成->正在进行(点击文字本身)

支持判断输入空字符,过长字符(20个汉字以内);

支持搜索;

支持本地化存储;

支持状态的展开隐藏(点击标题)

兼容手机端(iPhone6及以上)

支持路由切换

正文 1. React浅谈 1.1 组件化

​ 毫无疑问,当谈到React的时候不能避免的会提到组件化思想。React刚开始想解决的问题只是UI这一层面的问题,也就是MVC中view层面的问题,不成想如今越滚越大,从最早的UI引擎变成了一整套前后端通吃的 Web App 解决方案。对于React组件的理解同样要站在view层面的角度出发,一个完整的页面是由大大小小的组件堆叠而成,就好像搭积木,每一块积木都是一个组件,组件套组件组成了用户所能看到的完整的页面。

1.2 JSX语法糖

​ 使用React,不一定非要使用JSX语法,可以使用原生的JS进行开发。但是React作者强烈建议我们使用JSX,因为JSX在定义类似HTML这种树形结构时,十分的简单明了。这里简单的讲下JSX的由来。

​ 比如,下面一个div元素,我们用HTML语法描述为:

Test

如果换做使用javascript描述这个元素呢?最好的方式可以简单的转化为json对象,如下:

{
  type:"div",
  props:{
    className:"test",
    children:{
      type:"span",
      props:{
        children:"Test"
      }
    }
  }
}

这样我们就可以在javascript中创建一个Virtual DOM(虚拟DOM)了。当然,这样是没法复用的,我们再把它封装一下:

const Div=>({text}){
  return {
    type:"div",
    props:{
      className:"test",
      children:{
        type:"span",
        props:{
          children: text,
        },
      },
    },
  }
}

接下来再实现这个div就可以直接调用Div("Test")来创建。但上述结构看起来实在让人不爽,写起来也很容易写混,一旦结构复杂了,很容易让人找不着北,于是JSX语法应运而生。我们用写HTML的方式写这段代码,再经过翻译器转换成javascript后交给浏览器执行。上述代码用JSX重写:

const Div =()=>(
Test
);

多么简单明了!!!具体的JSX语法不多说了,学习更多戳这:JSX in Depth

1.3 Virtual DOM

其实上面已经提到了Virtual DOM,它的存在也是React长久不衰的原因之一,虚拟DOM的概念并不是FB首创却在FB的手上大火了起来(后台是多么重要)。

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

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

Virtual DOM虽然渲染方式比传统的DOM操作要好一些,但并不明显,因为对比DOM节点也是需要计算的,最大的好处在于可以很方便的和其它平台集成,比如react-native就是基于Virtual DOM渲染出原生控件。具体渲染出的是Web DOM还是Android控件或是iOS控件就由平台决定了。所以我们说react的出现是一场革命,一次对于native app的宣战,就像react-native那句口号——Learn Once,Write Anywhere.

1.4 函数式编程

​ 过去编程方式主要是以命令式编程为主,什么意思呢?简单说电脑的思维方式和我们人类的思考方式是不一样的。我们人类的大脑擅长的是分析问题,提出一个解决问题的方案,电脑则是生硬的执行指令,命令式编程就像是给电脑下达命令,让电脑去执行一样,现在主要的编程语言(比如:Java,C,C++等)都是由命令式编程构建起来的。

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

1.5 数据流

在React中,数据的流动是单向的,即从父节点传递到子节点。也因此组件是简单的,他们只需要从父组件获取props渲染即可。如果顶层的props改变了,React会递归的向下遍历整个组件树,重新渲染所有使用这个属性的组件。那么父组件如何获取子组件数据呢?很简单,通过回调就可以了,父组件定义某个方法供给子组件调用,子组件调用方法传递给父组件数据,Over。

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(首相)的职责:

维持应用的 state;

提供 getState() 方法获取 state;

提供 dispatch(action) 方法更新 state;

通过 subscribe(listener) 注册监听器;

通过 subscribe(listener) 返回的函数注销监听器。

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 树

工作流程图如下:

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

当然,我们这个实例里是不需要的,所有需要的依赖已经在package.json里配置好了。

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(
  
    
  ,
  document.getElementById("root")
)

上面代码中,Provider在根组件外面包了一层,这样一来,App的所有子组件就默认都可以拿到state了。

4.实战备忘录

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

4.1目录结构
.
├── app                 #开发目录
|   |   
|   ├──actions          #action的文件
|   |   
|   ├──components       #展示组件
|   |   
|   ├──containers       #容器组件,主页
|   |   
|   ├──reducers         #reducer文件
|   |
|   |——routes           #路由文件,容器组件
|   |
|   |——static           #静态文件
|   |
|   ├──stores           #store配置文件
|   |
|   |——main.less        #路由样式
|   |
|   └──main.js          #入口文件
|      
├── build                #发布目录
├── node_modules        #包文件夹
├── .gitignore     
├── .jshintrc      
├── webpack.production.config.js  #生产环境配置      
├── webpack.config.js   #webpack配置文件
├── package.json        #环境配置
└── README.md           #使用说明

接下来,我们只关注app目录就好了。

4.2入口文件
import React from "react";
import ReactDOM from "react-dom";
import {Route, IndexRoute, browserHistory, Router} from "react-router";
import {createStore} from "redux";
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(
    
        
            
                
                
                
                
            
        
   ,
 document.body.appendChild(document.createElement("div")))

这里我们从 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,
    Change_Done_To_Doing,
    Search
} from "../actions";
let todos;
(function() {
    if (localStorage.todos) {
        todos = JSON.parse(localStorage.todos)
    } else {
        todos = []
    }
})();
function todolist(state = todos, action) {
    switch (action.type) {
            /*
        *  添加新的事项
        *  并进行本地化存储
        *  使用ES6展开运算符链接新事项和旧事项
        *  JSON.stringify进行对象深拷贝
        */
        case Add_Todo:
            localStorage.setItem("todos", JSON.stringify([
                ...state, {
                    todo: action.text,
                    istodo: true,
                    doing: false,
                    done: false
                }
            ]));
            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 Change_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;

具体的展示组件这里就不罗列代码了,感兴趣的可以戳这:备忘录展示组件地址

后记

严格来说,这个备忘录并不是使用的react全家桶,毕竟还有一部分less代码,不过这一个应用也算是比较全面的使用了react+react-router+redux,作为react全家桶技术学习的练手的小项目再适合不过了。如果您对这个小东西感兴趣,欢迎戳这:React全家桶实现简易备忘录给个star。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/86577.html

相关文章

  • 基于react+react-router+redux+socket.io+koa开发一个聊天室

    摘要:最近练手开发了一个项目,是一个聊天室应用。由于我们的项目是一个单页面应用,因此只需要统一打包出一个和一个。而就是基于实现的一套基于事件订阅与发布的通信库。比如说,某一个端口了,而如果端口订阅了,那么在端,对应的回调函数就会被执行。 最近练手开发了一个项目,是一个聊天室应用。项目虽不大,但是使用到了react, react-router, redux, socket.io,后端开发使用了...

    NusterCache 评论0 收藏0
  • react+react-router4+redux最新版构建分享

    摘要:相关配置请参考中文文档。关于的更多使用方法及理解需要详细具体讲解,涉及篇幅较大,本文暂不涉及,有兴趣可以看文档中文文档,我会整理后再单独章节分享接下来我们将编写路由组件这与有一些差别,原来的方法已经不再使用,在中或组件从中引入。       相信很多刚入坑React的小伙伴们有一个同样的疑惑,由于React相关库不断的再进行版本迭代,网上很多以前的技术分享变得不再适用。比如react-...

    weapon 评论0 收藏0
  • 从项目中由浅入深的学习react (2)

    摘要:序列文章从项目中由浅入深的学习微信小程序和快应用前言从和原生两个项目来介绍的使用搞懂这两个项目上手撸代码篇效果图项目欢迎技术栈路由版本状态管理组件字体适配方案适配技能点分析技能点对应的种定义组件方法函数式定义的无状态组 showImg(https://segmentfault.com/img/bVbqPvN?w=820&h=512); 序列文章 从项目中由浅入深的学习vue,微信小程序...

    leap_frog 评论0 收藏0
  • React 入门实践

    摘要:更多相关介绍请看这特点仅仅只是虚拟最大限度减少与的交互类似于使用操作单向数据流很大程度减少了重复代码的使用组件化可组合一个组件易于和其它组件一起使用,或者嵌套在另一个组件内部。在使用后,就变得很容易维护,而且数据流非常清晰,容易解决遇到的。 欢迎移步我的博客阅读:《React 入门实践》 在写这篇文章之前,我已经接触 React 有大半年了。在初步学习 React 之后就正式应用到项...

    shenhualong 评论0 收藏0
  • React 328道最全面试题(持续更新)

    摘要:希望大家在这浮夸的前端圈里,保持冷静,坚持每天花分钟来学习与思考。 今天的React题没有太多的故事…… 半个月前出了248个Vue的知识点,受到很多朋友的关注,都强烈要求再出多些React相前的面试题,受到大家的邀请,我又找了20多个React的使用者,他们给出了328道React的面试题,由我整理好发给大家,同时发布在了前端面试每日3+1的React专题,希望对大家有所帮助,同时大...

    kumfo 评论0 收藏0

发表评论

0条评论

RiverLi

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<