React,Redux,ReactRouter 基础

284 阅读11分钟

react

安装

$ cnpm install -g create-react-app
$ create-react-app my-demo
$ cd my-demo/
$ npm start

react官方文档很好理解,并且在下边的学习中也会应用到react的知识,在此不再过多的赘述。

redux

redux普通对象来描述state

想要更改state中的数据,需要发起一个 action 。action也是一个普通对象,用于描述发生了什么。强制使用action来描述所有变化可以让我们清晰知道到底发生了什么变化。

为了把 action 和 state 串起来,开发 reducer 函数,接收 state 和 action ,并返回新的 state。对于大的应用,我们不可能只写一个reducer 函数,可以些很多个小函数分别管理state的一部分,然后用一个reducer调用,从而管理整个 state。

function todos1(state, action) {}
function todos2(state, action) {}

function reducerAll(state, action) {
  return {
    todos1: todos1(state.todos1, action),
    todos2: todos2(state.todos2, action),
  };
}

三大原则

单一数据源

整个应用的 state 存储在一棵 object tree 中,并且这个object tree 只存在于唯一一个store 中。

State是只读的

改变state的唯一办法是触发action。任何视图和网络请求都不能直接修改state,只能表达修改的意图。

用纯函数来执行修改

reducers 用来描述action 如何改变 state tree

reducer 是纯函数,接收旧的stateaction 返回新的state

开始可以只写一个reducer,随着应用的变大,可以把他拆成多个小的 reducers,分别操作state tree的不同部分。

基本概念

Action

  • 通过store.dispatchaction (含数据)送到store。是store 数据的唯一来源。
  • 本质上是JavaScript 普通对象,必须使用一个字符串类型的type 字段表示要执行的动作(并没有更新state)。一般将type定义成字符串常量。项目规模大时,使用单独的模块或者文件来存放action。(使用单独的模块或文件来定义 action type 常量并不是必须的,甚至根本不需要定义。对于小应用来说,使用字符串做 action type 更方便些。不过,在大型应用中把它们显式地定义成常量还是利大于弊的。)
  • 尽量少在Action 中传递数据。
  • Action 创建函数:返回action 的函数。

Reducer

  • 接收旧的 state action ,返回新的 state

  • 保持 reducer 纯净非常重要。永远不要reducer里做这些操作(只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。):

    • 修改传入参数;
    • 执行有副作用的操作,如API请求和路由跳转;
    • 调用非纯函数,如 Date.now()Math.random()
  • 注意:

    • 不修改 state:

      • Object.assign({},state,{data}) ,因为第一个参数的值会改变,必须把第一个参数设置为空对象。
      • {...state,...newState} 来更新数据。
    • **在default情况下返回旧的state:**遇到未知的action时一定要返回旧的state

  • 只写一个reducer会让代码看起来冗长,可以进行拆分。每个reducer只负责全局state中他负责的一部分,每个reducer对应的state参数都不同,分别对应他管理的那部分state数据。

  • combineReducers()工具类合并reducer

    • combineReducers 接收一个对象,可以把所有顶级的 reducer 放到一个独立的文件中,通过 export 暴露出每个 reducer 函数,然后使用 import * as reducers 得到一个以它们名字作为 key object

      import { combineReducers } from 'redux'
      import * as reducers from './reducers'
      
      const todoApp = combineReducers(reducers)
      

Store

action 来描述发生什么,reducers根据action 更新stateStore是联系actionreducers的对象。有以下职责:

  • 维持应用的state
  • 提供getState() 方法获取 state
  • 提供dispatch(action)方法更新state
  • 通过subscribe(listener)注册监听器
  • 通过subscribe(listener)返回的函数注销监听器

Redux应用只有单一的store。当需要拆分数时,应该用reducer组合,而不是创建多个store

通过reducer创建store非常容易:

import { createStore } from 'redux'
import todoApp from './reducers' // combineReducers 将多个 reducer 合并成一个

let store = createStore(todoApp)

createStore的第二个参数是可选的, 用于设置 state 初始状态。也可以用 reducer传入的state初始化相应的state,不初始化默认为 undefined

数据流

严格的单向数据流Redux 架构的设计核心。

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

  1. 调用 store.dispatch(action):可以在认个地方调用
  2. Redux store 调用传入的 reducer 函数。
  3. reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
  4. Redux store 保存了根 reducer 返回的完整 state 树。

代码理解

借助代码理解redux。用create-react-app创建一个react应用。

场景

store中存数组数据cart,我们利用redux 进行管理,实现增加、删除和更新的操作。

Actions

// src/actions/cartActions.js

// 定义action.type 字符常量
export const ADD_TO_CART = "ADD_TO_CART";
export const DELETE_FROM_CART = "DELETE_FROM_CART";
export const UPDATE_CART = "UPDATE_CART";

// action 创建函数
export function addToCart(product, quantity, unitCost) {
  return {
    type: ADD_TO_CART,
    payload: { product, quantity, unitCost },
  };
}

export function deleteFromCart(product) {
  return {
    type: DELETE_FROM_CART,
    payload: {
      product,
    },
  };
}

export function updateCart(product, quantity, unitCost) {
  return {
    type: UPDATE_CART,
    payload: {
      product,
      quantity,
      unitCost,
    },
  };
}

Reducer

// src/reducers/cartReducers.js
import {
  ADD_TO_CART,
  UPDATE_CART,
  DELETE_FROM_CART,
} from "../actions/cartActions";

// state 初始化数据
const initialState = {
  cart: [
    {
      product: "bread 700g",
      quantity: 2,
      unitCost: 90,
    },
    {
      product: "milk 500ml",
      quantity: 1,
      unitCost: 47,
    },
  ],
};

// 根据action更新state,reducer传入的第一个参数可以用于初始化state,否则对应state为undefined
export default function (state = initialState, action) { 
  switch (action.type) {
    case ADD_TO_CART: {
      return {
        ...state,
        cart: [...state.cart, action.payload],
      };
    }
    case UPDATE_CART: {
      return {
        ...state,
        cart: state.cart.map((item) =>
          item.product === action.payload.product ? action.payload : item
        ),
      };
    }
    case DELETE_FROM_CART: {
      return {
        ...state,
        cart: state.cart.filter(
          (item) => item.product !== action.payload.product
        ),
      };
    }
    default:
      return state;
  }
}

假设存在别的reducer:

// src/reducers/productsReducer.js

export default function(state = [], action) {
  return state;
};

需要用combineReducers()整合:

// src/reducers/index.js

import {combineReducers} from 'redux'
import productsReducer from './productsReducer';
import cartReducer from './cartReducers'

const allReducers = {
  products: productsReducer,
  shoppingCart: cartReducer,
};

const rootReducer = combineReducers(allReducers);

export default rootReducer

store

// src/store.js

import {createStore} from 'redux'
import rootReducer from './reducers/index'

let store = createStore(rootReducer);

export default store

dispatch

import store from "./store";
import { addToCart,updateCart,deleteFromCart } from "./actions/cartActions";

console.log("initialState", store.getState());

// 订阅
let unsubscribe = store.subscribe(() => console.log(store.getState()));

store.dispatch(addToCart("coffee 500gm", 1, 250));
store.dispatch(addToCart("flour 100g", 1, 250));
store.dispatch(addToCart("juice 2L", 1, 250));
store.dispatch(updateCart("flour 100g", 100, 250));
store.dispatch(deleteFromCart("coffee 500gm", 1, 250))
//解除订阅
unsubscribe();

代码完成后,执行npm start运行,可以看到打印的日志。

React-Redux

  • 安装 react-redux:

    npm install --save react-redux

  • react 代码,并用Provider 类将react应用程序包装在redux容器中:

    import React from 'react';
    import ReactDom from 'react-dom';
    import {Provider} from 'react-redux';
    
    const App = <h1>Redux Shopping Cart</h1>
    
    ReactDom.render(
      <Provider store={store}>
        {App}
      </Provider>,
      document.getElementById('root')
    )
    

以上完成了简单的集成。接下来详细的学习react-redux

安装

react-redux并不是redux内置包,需要单独安装:

npm install --save react-redux

API

<provider store>组件。

根组件嵌套在<Provider> 中才可以使用 connect()方法。唯一的作用是传递store

  • 普通 React

    ReactDOM.render(
      <Provider store={store}>
        <MyRootComponent />
      </Provider>,
      rootEl
    )
    
  • React Router

    ReactDOM.render(
      <Provider store={store}>
        <Router history={history}>
          <Route path="/" component={App}>
            <Route path="foo" component={Foo}/>
            <Route path="bar" component={Bar}/>
          </Route>
        </Router>
      </Provider>,
      document.getElementById('root')
    )
    

参数:

store:应用程序全局唯一的 Redux-Store

children:组件层级的根组件

mapStateToProps(state, [ownProps])

  • store到组件props的映射:监听Redux store的变化,Redux store发生改变,mapStateToProps 函数会被调用,
  • 返回一个纯对象,这个对象会与组件的props合并。
  • 如果指定了该回调函数中的第二个参数 ownProps,则该参数的值为传递到组件的 props,而且只要组件接收到新的 propsmapStateToProps 也会被调用

mapDispatchToProps()

  • 分发action,即组件到store的映射。

connect()

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

实例:计数器

实现一个计数器,计数值由store映射,点击increase按钮通过dispatchstate递增。

UI组件

  • 只负责 UI 的呈现,不带有任何业务逻辑
  • 没有状态(即不使用this.state这个变量)
  • 所有数据都由参数(this.props)提供
  • 不使用任何 Redux API
const { Component } = require("react");

export default class Counter extends Component{
  render(){
    const {value,onIncreaseClick} = this.props
    return(
      <div>
        <span>{value}</span>
        <button onClick = {onIncreaseClick}>Increase</button>
      </div>
    )
  }
}

此组件中显示的 valuestate计算得到,onIncreaseClick 向外发出Action

容器组件

  • 负责管理数据和业务逻辑,不负责 UI 的呈现
  • 带有内部状态
  • 使用 Redux API
  • connect方法生成容器组件
import { connect } from 'react-redux';
import {increaseAction} from '../actions'
import Counter from '../components/count';

function mapStateToProps(state) {
  return {
    value: state.count,
  };
}

function mapDispatchToProps(dispatch){
  return {
    onIncreaseClick:()=>dispatch(increaseAction)
  }
}

// App 是一个组件,Counter的容器组件
const App =connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter)

export default App

Action

export  const increaseAction = {
  type:'increase'
}

Reducer

export default function counter(state = { count: 0 }, action) {
  const count = state.count;
  switch (action.type) {
    case "increase":
      return { count: count + 1 };
    default:
      return state;
  }
}

Store

import {createStore} from 'redux'
import counter from './reat-rdx/reducers/index'

const store = createStore(counter);
export default store

组件中使用并用Provider包含

import App from './reat-rdx/containerComponents/index'
ReactDom.render(
  <Provider store={store}>
    <App/>
  </Provider>,
  document.getElementById("root")
);

总结:

在我理解,react-redux是在 UI 组件外层包含一层容器组件来对state进行处理,并进行action分发。

React Router

React Router 4.x 和之前是两个完全不同的体系,本文章主要学习4.x,想学2.x的可以看这篇介绍

基本路由

三个基本页面:主页,关于,用户。如下

// Home.js
import { Component } from "react";

export default class Home extends Component {
  render() {
    return <h2>Home</h2>;
  }
}

// User.js
import { Component } from "react";

export default class User extends Component {
  render() {
    return <h2>User</h2>;
  }
}

// About.js
import { Component } from "react";

export default class About extends Component {
  render() {
    return <h2>About</h2>;
  }
}

控制渲染:

// App.js
import Home from "./Home";
import Users from "./Users";
import About from "./About";

import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
export default function App() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/about">About</Link> //点击这个Link时,路由变为:/about
            </li>
            <li>
              <Link to="/users">Users</Link>
            </li>
          </ul>
        </nav>
        {/* switch 通过Route 渲染匹配当前URL的组件 */}
        <Switch>
          <Route path="/about">           // 路由为/about时,加载About组件
            <About />
          </Route>
          <Route path="/users">
            <Users />
          </Route>
          <Route path="/">
            <Home />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

// index.js
import React from "react";
import ReactDom from "react-dom";
import App from './reat-rout/modules/App'
ReactDom.render(
  <App/>,
  document.getElementById('root')
)

总结:

  • <Link to="/about">About</Link>  //点击这个Link时,路由变为:/about
    <Route path="/about">           // 路由为/about时,加载About组件
    

路由嵌套

添加 Topics 组件:

import {
  Route,
  Link,
  Switch,
  useParams,
  useRouteMatch,
} from "react-router-dom";

export default function Topics() {
  let match = useRouteMatch(); // 只在函数中可以使用
  return (
    <div>
      <h2>Topics</h2>

      <ul>
        <li>
          <Link to={`${match.url}/components`}>Components</Link>
        </li>
        <li>
          <Link to={`${(match.url)}/props-v-state`}>Props v.State</Link>
        </li>
      </ul>
      {/* Topics有自己的switch */}
      <Switch>
        <Route path={`${match.path}/:topicId`}>
          <Topic />
        </Route>
        <Route path={match.path}>
          <h3>please select a topic</h3>
        </Route>
      </Switch>
    </div>
  );
}

function Topic() {
  let { topicId } = useParams();
  return <h3>requested topic ID :{topicId}</h3>;
}

显示:

import Home from "./Home";
import Users from "./Users";
import About from "./About";
import Topics from "./Topics";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
export default function App() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/about">About</Link>
            </li>
            <li>
              <Link to="/users">Users</Link>
            </li>
            <li>
              <Link to="/topics">Topics</Link>
            </li>
          </ul>
        </nav>
        {/* switch 通过Route 渲染匹配当前URL的组件 */}
        <Switch>
          <Route path="/about">
            <About />
          </Route>
          <Route path="/users">
            <Users />
          </Route>
          <Route path="/topics">
            <Topics />
          </Route>
          <Route path="/">
            <Home />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

总结:

我们在这一部分看到了以下生面孔,不要着急先继续往下看,后便会有详细解答

  • let match = useRouteMatch();
    
  • <Link to={`${match.url}/components`}>Components:{match.url}</Link>
    
  • <Route path={`${match.path}/:topicId`}>
    
  • let { topicId } = useParams();
    

主要成分

React Router中的组件主要分为三类:

  • 路由器,像<BrowserRouter><HashRouter>
  • 路线匹配器,例如<Route><Switch>
  • 导航,例如<Link><NavLink><Redirect>

路由器

  • <BrowserRouter>使用常规的URL路径。这些通常是外观最好的URL,但是它们要求正确配置服务器。

  • <HashRouter>将当前位置存储在URL的hash一部分中,因此URL看起来像http://example.com/#/your/page。由于哈希从不发送到服务器,因此这意味着不需要特殊的服务器配置。

  • 要使用路由器,只需确保将其呈现在元素层次结构的根目录下。

路线匹配器

  • 有两个路由匹配组件:SwitchRoute。当<Switch>被渲染,它会搜索其children <Route>内容找到一个其path当前的URL匹配。当找到一个对象时,它将渲染该对象<Route>而忽略所有其他对象。

  • 所以要将<Route>的特定性更高的放在比较靠前位置。

  • 如果没有<Route>匹配项,则<Switch>不会呈现任何内容。

      <Switch>
        //  如果当前URL是/about,则呈现此路由其他的都被忽略了
        <Route path="/about">
          <About />
        </Route>

        // 注意这两条路线是如何排序的。更具体的path=“/contact/:id”位于path=“/contact”之前,因此当查			看单个联系人时,将呈现路由/contact/:id
        <Route path="/contact/:id">
          <Contact />
        </Route>
        <Route path="/contact">
          <AllContacts />
        </Route>

       // 如果之前的路径都没有渲染任何内容,
			这条路线起到了后备的作用。重要提示:路径为“/”的路线将始终匹配URL,因为所有URL都以/开头。这就				是为什么我们把这个放在最后
        <Route path="/">
          <Home />
        </Route>
      </Switch>

需要注意的重要一件事是a<Route path>匹配URL的开头,而不是整个开头。因此,<Route path="/">始终与网址匹配。因此,我们通常将这放在<Route>最后<Switch>

导航(路线更改器)

  • 所谓的导航就是Link组件,类似于<a>标签。

    <Link to="/">Home</Link>
    // <a href="/">Home</a>
    
  • <NavLink>是一种特殊类型的<Link>,当其to prop与当前位置匹配时,可以将自己设置为“active”。

    <NavLink to="/react" activeClassName="hurray">
      React
    </NavLink>
    
    // When the URL is /react, this renders:
    // <a href="/react" className="hurray">React</a>
    
    // When it's something else:
    // <a href="/react">React</a>
    
  • 任何时候要强制导航,都可以渲染<Redirect>。当<Redirect>渲染时,它将使用其to prop进行导航。

    <Redirect to="/login" />
    

API

钩子

React Router附带了一些钩子,可让您访问路由器的状态并从组件内部执行导航。

  • useHistory
  • useLocation
  • useParams
  • useRouteMatch

useHistory

访问history,可用于导航。

import { useHistory } from "react-router-dom";

export default function HomeBtn() {
  let history = useHistory();
  function handleClick() {
    history.push("/");
  }
  return <button onClick={handleClick}>go Home</button>;
}

useLocation

返回代表当前URL的对象,当URL改变的时候返回新的location对象。

import { useLocation } from "react-router-dom";

export default function UseLocation() {
  let location = useLocation();
  return (
    <h3>
      location:{location.pathname}
      {console.log(location)}
    </h3>
  );
}

useParams

返回URL参数的键/值对的对象。可用于匹配路由的参数。

<Route path="topics/:topicId">
  <Topic />
</Route>


function Topic() {
  let { topicId } = useParams();
  return <h3>requested topic ID :{topicId}</h3>;
}

这是前文【路由嵌套】部分用到的,当路由为:http://localhost:3000/topics/props-v-state时,topicIdprops-v-state

useRouteMatch

useRouteMatch钩子尝试以与<Route>相同的方式匹配当前URL。它对于在不实际渲染<Route>的情况下访问匹配数据非常有用。

除了钩子函数之外还有很多API,想了解的可以去官网查看。

github

参考:

Redux入门教程(快速上手)

Redux 中文文档

React-Redux中文文档

Redux 入门教程(三):React-Redux 的用法

React Router教程

React Router 4.x官网