「React」万字保姆级基础教程>2

204 阅读23分钟

书接上文:juejin.cn/post/721252…

五、react-router

路由理解

SPA: 单页Web应用(Single Page Application),整个应用只有一个完整的页面。点击页面中的链接不会刷新页面,只会做页面的局部更新。数据都需要通过ajax请求获取,并在前端异步展现。

路由: 一个路由就是一个映射关系(key-value)。key是路径,value可能是function或者component

路由的分类

  • 后端路由

    • 理解:value是function,用来处理客户端提交的请求
    • 注册路由:router.get(path, function(req, res))
    • 工作过程:当node接收到一个请求时,根据请求路径找到匹配的路由,调用路由中的函数来处理请求,返回响应数据
  • 前端路由

    • 浏览器端路由,value是component,用于展示页面内容
    • 注册路由: <Router path="/text" component={Test}>(6版本component变为了element)
    • 工作过程:当浏览器的path变为/test时,当前路由组件就会变为Test组件

react-router API

react的一个插件库,专门用来实现一个SPA应用。

react-router-dom 安装(我这里是6.x版本):npm i -S react-router-dom

相关API

  • 内置组件
    • <BrowserRouter>
    • <HashRouter>
    • <Redirect>(6.x移除。使用<Navigate>)
    • <Link>
    • <NavLink>
    • <Switch>(6.x移除,新增<Routes />)
    • <Router>
  • 其他
    • history对象
    • match对象
    • withRouter函数

路由基本使用

  • 明确好界面中的导航区、展示区
  • 导航区的<a>标签修改为<Link>标签
    • <Link to="/xxx">XXX
  • 展示区写<Route>标签进行路径的匹配
    • <Route path="/home" element={} />
  • <App>的最外侧包一个<BrowserRouter>或<HashRouter>

index.js:

ReactDOM.render(<BrowserRouter><App /></BrowserRouter>, document.getElementById('root'))

App.js:

export default function App() {
  return (
    <div>
      <div>
        <NavLink className={({ isActive }) => (isActive ? "demostyle" : "")} to="/about">About</NavLink>&nbsp;&nbsp;
        <NavLink className={({ isActive }) => (isActive ? "demostyle" : "")} to="/home">Home</NavLink>
      </div>
      <div style={{ border: "1px dashed orange", width: "400px", height: "100px" }}>
        {/* 注册路由 */}
        <Routes>
          <Route path="/about" element={<About />} />
          <Route path="/home" element={<Home />} />
        </Routes>
      </div>
    </div>
  )
}

模糊匹配与精准匹配

在v6版本中,默认为精准匹配。如果想要变为模糊匹配,则需要加 /*

<Routes> 
  <Route path="/about" element={<About />} />
  <Route path="/home/*" element={<Home />} />
</Routes>

Navigate重定向

在react-router-dom v6版本中,<Redirect>标签被删除了。使用v6版本中提供的<Navigate>标签一样可以实现重定向功能

<Routes>
  <Route path="/about" element={<About />} />
  <Route path="/home" element={<Home />} />
  <Route path="/" element={<Navigate to="/about" />} />
</Routes>
export default function Home() {
  const [sum, setSum] = useState(1);
  return (
    <div>
      Home
      {sum === 2 ? <Navigate to="/about" /> : <h5>当前Sum的值:{sum}</h5>}
      <button onClick={() => {setSum(2);}}>点击将sum变为2</button>
    </div>
  );
}

路由表

import { Navigate } from "react-router-dom";
import About from '../pages/About';
import Home from '../pages/Home'

export default [
    { path: '/', element: <Navigate to="/about" /> },
    { path: '/about', element: <About /> },
    { path: '/home', element: <Home /> }
]
import React from 'react'
import { NavLink, useRoutes } from "react-router-dom";
import routes from './routes'
import './App.css'

export default function App() {
  const main = useRoutes(routes)  // 引入使用路由表
  return (
    <div>
      <div>
        <div>
          <div>
            <NavLink className={({ isActive }) => (isActive ? "demostyle" : "")} to="/about">About</NavLink>&nbsp;&nbsp;
            <NavLink className={({ isActive }) => (isActive ? "demostyle" : "")} to="/home">Home</NavLink>
          </div>
        </div>
        <div style={{ border: "1px dashed orange", width: "400px", height: "200px" }}>
          {/* 注册路由 */}
          {main}
        </div>
      </div>
    </div>
  )
}

嵌套路由

5.x版本:

一级:

<Routes>
  <Route path="/about" element={<About />} />
  <Route path="/home/*" element={<Home />} />
</Routes>

二级:

export default class Home extends Component {
  render() {
    return (
      <div>
        <div>
          <div>
            <NavLink
              className={({ isActive }) => (isActive ? "demostyle" : "")}
              to="/home/news">News</NavLink>
            <NavLink
              className={({ isActive }) => (isActive ? "demostyle" : "")}
              to="/home/message">Message</NavLink>
          </div>
        </div>
        <div>
          {/* 同样需要注册路由 */}
          <Routes>
            <Route path="/news" element={<News />} />
            <Route path="/message" element={<Message />} />
          </Routes>
        </div>
      </div>
    );
  }
}

6.x版本:

路由表

import { Navigate } from "react-router-dom";
import About from '../pages/About';
import Home from '../pages/Home'
import News from "../pages/News";
import Message from "../pages/Message";

export default [
    { path: '/', element: <Navigate to="/about" /> },
    { path: '/about', element: <About /> },
    {
        path: '/home', element: <Home />,
        children: [
            { path: 'message', element: <Message /> }, //  注意这里这里不要加/
            { path: 'news', element: <News /> },  //  注意这里这里不要加/
        ]
    }
]

组件使用:

import React from "react";
import { NavLink,Outlet  } from "react-router-dom";

export default function Home() {
  return (
    <div>
      Home
      <div>
        <NavLink className={({ isActive }) => (isActive ? "demostyle" : "")} to="news">
          News
        </NavLink>&nbsp;&nbsp;
        <NavLink className={({ isActive }) => (isActive ? "demostyle" : "")} to="message">
          Message
        </NavLink>
      </div>
      <div style={{ border: "1px dashed orange", width: "200px", height: "100px" }}>
        <Outlet />
      </div>
    </div>
  );
}

如果不想让父级高亮只有子级高亮,可以添加 end属性

<NavLink className={({ isActive }) => (isActive ? "demostyle" : "")} to="/home" end>
  Home
</NavLink>

向路由传递参数

1)param传参

接收参数时使用useParams函数

父组件:

import React, { Component } from "react";
import { Link, Routes, Route } from "react-router-dom";
import Detail from "./Detail";

export default class Message extends Component {
  state = {
    messageArr: [
      { id: "1001", title: "我是消息1" },
      { id: "1002", title: "我是消息2" },
      { id: "1003", title: "我是消息3" },
    ],
  };
  render() {
    const { messageArr } = this.state;
    return (
      <div>
        <ul>
          {messageArr.map((msgObj) => {
            return (
              <li key={msgObj.id}>
                <Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link>
              </li>
            );
          })}
        </ul>
        {/* 同样需要注册路由 */}
        <Routes>
          <Route path="/detail/:id/:title" element={<Detail />} />
        </Routes>
      </div>
    );
  }
}

子组件:

import React from "react";
import { useParams } from "react-router-dom";

export default function Detail() {
  // 注意!!!! 这里不能使用类式组件,需要用函数式组件
  const params = useParams();
  return (
    <div>
      <ul>
        <li>ID:{params.id}</li>
        <li>Title:{params.title}</li>
        <li>Content:我有故人抱剑去,斩尽春风未曾归</li>
      </ul>
    </div>
  );
}

2)search传参

需要用useSearchParams()接收参数

父组件:

<li key={msgObj.id}>
  <Link to={`/home/message/detail?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link>
</li>
....
<Routes>
	<Route path="/detail" element={<Detail />} />
</Routes>

子组件:

import React from "react";
import { useSearchParams } from "react-router-dom";

export default function Detail() {
  // 注意这里不能使用类式组件
  const [search] = useSearchParams();
  const id = search.get("id");
  const title = search.get("title");
  return (
    <div>
      <ul>
        <li>ID:{id}</li>
        <li>Title:{title}</li>
        <li>Content:我有故人抱剑去,斩尽春风未曾归</li>
      </ul>
    </div>
  );
}

3)state传参

这种方式不会把参数在地址栏中展示出来。需要用useLocation()接收参数

父组件:

<li key={msgObj.id}>
  <Link to="/home/message/detail" state={{ id:msgObj.id, title:msgObj.title }}>{msgObj.title}</Link>
</li>

子组件:

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

export default function Detail() {
  // 注意这里不能使用类式组件
  const location  = useLocation()
  console.log("location", location);
  const { state:{id, title} } = location; // 连续解构
  return (
    <div>
      <ul>
        <li>ID:{id}</li>
        <li>Title:{title}</li>
        <li>Content:我有故人抱剑去,斩尽春风未曾归</li>
      </ul>
    </div>
  );
}

路由跳转方式

1)push

false代表使用push模式,可以回退(用的比较多)

<Link replace={false} to="/home/message/detail">{msgObj.title}</Link>

2)replace

true代表使用replace模式,可以替换

<Link replace={true} to="/home/message/detail">{msgObj.title}</Link>

BrowserRouter与HashRouter

两者之间的区别:

  • 底层原理不一样
    • BrowserRouter使用的是H5的history API,不兼容IE9及以下版本
    • HashRouter使用的是URL的哈希值
  • url表现形式不一样
    • BrowserRouter的路径中没有# ,例如:localhost:3000/demo/test (使用较多)
    • HashRouter的路径中包含# ,例如:localhost:3000/#/demo/test
  • 刷新后对路由state参数的影响
    • BrowserRouter没有任何影响,因为state保存在history对象中
    • HashRouter刷新后会导致路由state参数的丢失
  • 备注
    • HashRouter可以用于解决一些路径错误相关的问题
    • 一般还是使用BrowserRouter多一些

编程式路由导航

const navigate = useNavigate()

function showDetail(){
    // navigate("/about");
    navigate("detail", {
        replace: false, // 默认就是false
        state:{id:m.id,title:m.title}
    });
}

useInRouterContext()

作用:如果组件在<Router>的上下文中呈现,则useInRouterContext钩子返回true,否则返回false

(这里App组件被<BrowserRouter>组件包裹了,那么App和他的子组件就都处在路由的上下文环境中了)

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

export default function App() {
  console.log('!!!', useInRouterContext())
  ....
}

useNavigationType()

作用:返回当前的导航类型(即:用户是如何来到当前页面的)

返回值:POP、PUSH、REPLACE

注:POP是指在浏览器中直接打开了这个路由组件(刷新页面)

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

export default function Home() {
  console.log(useNavigationType());
  // 点击进来是PUSH,直接刷新页面是POP,进来之前的链接有replace参数那么就是REPLACE
}

useOutlet()

作用:用来呈现当前组件中要渲染的嵌套路由

  • 如果嵌套路由还没有挂载,则result为null
  • 如果嵌套路由已经挂载,则展示嵌套的路由对象
const result = useOutlet()

useResolvedPath()

作用:给定一个URL值,解析其中的:path、search、hash

console.log("useResolvedPath", useResolvedPath('/user?id=1001&name=Tom'));

输出结果:
useResolvedPath  {pathname: '/user', search: '?id=1001&name=Tom', hash: ''}

六、UI组件库

ant-design

安装:npm add antd 适用于后台管理系统的开发

官网地址:ant.design/components/…

element-ui

官网地址:element.eleme.cn/#/zh-CN

七、redux

redux理解

中文官网:cn.redux.js.org/

redux是一个专门用于做状态管理的JS库。它可以用在react/angular/vue等项目中,但基本与react配合使用。它的作用就是集中式管理react应用中多个组件共享的状态。 【注意:实际开发过程中,更多是使用dva的数据流,我们在另一篇文章中学习】

什么情况下需要使用:

  1. 某个组件的状态,需要让其他组件可以随时拿到(共享)
  2. 一个组件需要改变另一个组件的状态(通信)

redux原理图

React Components:组件

Action Creators:创建action(动作对象,包含两个属性:type data

Store:调度

Reducers:初始化状态、加工状态(根据旧的state和action,产生新的state的纯函数)

redux精简版

⚠️⚠️注意:这里的代码实际开发过程并不推荐这么写,主要用于理解上面的redux原理

Component:

import React, { Component } from "react";
import store from "../../redux/store";

export default class Count extends Component {
  // 组件挂载,监测redux中状态的变化
  //   componentDidMount() {
  //     // redux中任意状态发生变化,都会触发此回调函数
  //     store.subscribe(() => {
  //       this.setState({}); // 假更新
  //     });
  //   }

  increment = () => {
    const { value } = this.selectNumber;
    store.dispatch({ type: "increment", data: value * 1 });
  };
  decrement = () => {
    const { value } = this.selectNumber;
    store.dispatch({ type: "decrement", data: value * 1 });
  };
  incrementIfOdd = () => {
    const { value } = this.selectNumber;
    if (store.getState() % 2 === 1) {
      store.dispatch({ type: "increment", data: value * 1 });
    }
  };
  incrementAsyc = () => {
    const { value } = this.selectNumber;
    setTimeout(() => {
      store.dispatch({ type: "increment", data: value * 1 });
    }, 2000);
  };

  render() {
    return (
      <div>
        <h2>当前求和结果为:{store.getState()}</h2>
        <select ref={(c) => (this.selectNumber = c)}>
          <option value={1}>1</option>
          <option value={2}>2</option>
          <option value={3}>3</option>
        </select>
        &nbsp;<button onClick={this.increment}>+</button>
        &nbsp;<button onClick={this.decrement}>-</button>
        &nbsp;<button onClick={this.incrementIfOdd}>当前求和为奇数,+</button>
        &nbsp;<button onClick={this.incrementAsyc}>异步+</button>
      </div>
    );
  }
}

Store:

// 引入createStore
import {createStore} from 'redux'
// 引入为Count组件服务的reducer
import countReducer from './count_reducer'

// 对外暴露store
export default createStore(countReducer)

Reducers

const initState = 0
// Reducer函数接收两个参数:之前的状态、动作对象
// preState=initState   形参默认值
export default function countReducer(preState = initState, action) {
    const { type, data } = action
    switch (type) {
        case 'increment':
            return preState + data
        case 'decrement':
            return preState - data
        default:
            // 初始化
            return preState
    }
}

但是这样的话,state变化并不会引起页面刷新,所以需要在index页面:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from "./redux/store";

ReactDOM.render(<App />, document.getElementById('root'))

// redux中任意状态发生变化,都会触发此回调函数
store.subscribe(() => {
    ReactDOM.render(<App />, document.getElementById('root'))
});

redux完整版

创建Action Creator,用于给Count组件创建action对象:

import { INCREMENT, DECREMENT } from "./constant";
// 为Count组件生成action对象
export const createIncrementAction = value => ({ type: INCREMENT, data: value })
export const createDecrementAction = value => ({ type: DECREMENT, data: value })

然后组件使用时:

import React, { Component } from "react";
import store from "../../redux/store";
// 引入Action Creator
import {
  createIncrementAction,
  createDecrementAction,
} from "../../redux/count_action";

export default class Count extends Component {
  increment = () => {
    const { value } = this.selectNumber;
    store.dispatch(createIncrementAction(value * 1));
  };
  decrement = () => {
    const { value } = this.selectNumber;
    store.dispatch(createDecrementAction(value * 1));
  };
  incrementIfOdd = () => {
    const { value } = this.selectNumber;
    if (store.getState() % 2 === 1) {
      store.dispatch(createIncrementAction(value * 1));
    }
  };
  incrementAsyc = () => {
    const { value } = this.selectNumber;
    setTimeout(() => {
      store.dispatch(createIncrementAction(value * 1));
    }, 2000);
  };

  render() { return (<div>...</div>); }
}

异步Action

  • Object[]类型的action叫同步action
  • function类型的action叫异步action

首先需要在store里开启对异步action的支持:

// 引入createStore
import { createStore, applyMiddleware } from 'redux'
// 引入为Count组件服务的reducer
import countReducer from './count_reducer'
// 引入redux-thunk用于支持异步action
import thunk from 'redux-thunk'

// 对外暴露store
export default createStore(countReducer, applyMiddleware(thunk))

在action里增加一个异步action:

import { INCREMENT, DECREMENT } from "./constant";

// 为Count组件生成action对象
export const createIncrementAction = value => ({ type: INCREMENT, data: value })
export const createDecrementAction = value => ({ type: DECREMENT, data: value })
// 异步action。里面会调用同步action
export const createIncrementAsyncAction = (value, time) => {
    // action的值为函数
    return (dispatch) => {
        setTimeout(() => {
            dispatch(createIncrementAction(value));
        }, time);
    }
}

在index里使用:

incrementAsyc = () => {
  const { value } = this.selectNumber;
  store.dispatch(createIncrementAsyncAction(value * 1, 2000));
};

react-redux基本使用

react-redux是官方出品的一个插件库

  • 所有的UI组件都应该包裹一个容器组件,他们是父子关系
  • 容器组件是真正和redux打交道的,里面可以随意使用redux的api
  • UI组件不能使用任何redux的api
  • 容器组件会传递给UI组件:redux中保存的状态+用于操作状态的方法
  • 注意,容器组件给UI组件传递状态和操作状态的方法,均是通过props传递

基本使用代码流程如下:

  1. index.js是整个程序入口
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from "./redux/store";

ReactDOM.render(<App />, document.getElementById('root'))

// 监测redux中状态的改变。若redux的状态发生了改变,则重新渲染App组件
store.subscribe(() => {
    ReactDOM.render(<App />, document.getElementById('root'))
});
  1. App.js为所有组件的外壳组件。在这里会引入容器组件,并将store传递给容器组件
import React, { Component } from "react";
import Count from './containers/Count'
import store from './redux/store'

// 所有组件的外壳组件App
export default class App extends Component {
  render() {
    return (
      <div>
        {/* 渲染容器组件,并传递store用于在容器组件里连接UI组件和redux */}
        <Count store={store}/>
      </div>
    );
  }
}
  1. container/Count。是Count的容器组件。在这里最主要的就是创建并暴露一个Count的容器组件,并通过connect连接 : export default connect(mapStateToProps, mapDispatchToProps)(UI组件);

    其中mapStateToProps是容器组件给UI组件传递的状态,mapDispatchToProps是容器组件给UI组件传递的操作状态的方法。容器中的store是靠props传进去的,而不是在容器组件中直接引入的。

    其中操作状态的方法,引入了创建好的Action。

// 这里是容器组件

// 引入action
import {
  createIncrementAction,
  createDecrementAction,
  createIncrementAsyncAction,
} from "../../redux/count_action";
// 引入UI组件
import countUI from "../../components/Count";
// // 引入redux(其实是store)
// import store from '../../redux/store'
// 引入connect用于连接UI组件和redux
import { connect } from "react-redux";

function mapStateToProps(state) {
  // 容器组件给UI组件传递状态
  return { count: state };
}

function mapDispatchToProps(dispatch) {
  // 容器组件给UI组件传递操作状态的方法
  return {
    increment: (value) => {
      dispatch(createIncrementAction(value));
    },
    decrement: (value) => {
      dispatch(createDecrementAction(value));
    },
    syncIncrement: (value, time) => {
      dispatch(createIncrementAsyncAction(value, time));
    },
  };
}

// 创建并暴露一个Count的容器组件
export default connect(mapStateToProps, mapDispatchToProps)(countUI);
  1. count_action.js,Count组件的ActionCreator,用于给Count组件创建action对象:
import { INCREMENT, DECREMENT } from "./constant";

// 为Count组件生成action对象
export const createIncrementAction = value => ({ type: INCREMENT, data: value })
export const createDecrementAction = value => ({ type: DECREMENT, data: value })
// 异步action。里面会调用同步action
export const createIncrementAsyncAction = (value, time) => {
    // action的值为函数
    return (dispatch) => {
        setTimeout(() => {
            dispatch(createIncrementAction(value));
        }, time);
    }
}
  1. Store,redux的调度中心
// 引入createStore
import { createStore, applyMiddleware } from 'redux'
// 引入为Count组件服务的reducer
import countReducer from './count_reducer'
// 引入redux-thunk用于支持异步action
import thunk from 'redux-thunk'

// 对外暴露store
export default createStore(countReducer, applyMiddleware(thunk))
  1. reducer里面做真正的操作:
import { INCREMENT, DECREMENT } from "./constant";
const initState = 0
// Reducer函数接收两个参数:之前的状态、动作对象
// preState=initState   形参默认值
export default function countReducer(preState = initState, action) {
    const { type, data } = action
    switch (type) {
        case INCREMENT:
            return preState + data
        case DECREMENT:
            return preState - data
        default:
            // 初始化
            return preState
    }
}
  1. 最后在真正的UI组件里,只需要使用props传递过来的状态和操作状态的方法即可:
// 这里是UI组件

import React, { Component } from "react";

export default class Count extends Component {
  increment = () => {
    const { value } = this.selectNumber;
    this.props.increment(value * 1);
  };
  decrement = () => {
    const { value } = this.selectNumber;
    this.props.decrement(value * 1);
  };
  incrementIfOdd = () => {
    const { value } = this.selectNumber;
    if (this.props.count % 2 === 1) {
      this.props.increment(value * 1);
    }
  };
  incrementAsyc = () => {
    const { value } = this.selectNumber;
    this.props.syncIncrement(value * 1, 2000);
  };

  render() {
    return (
      <div>
        <h2>当前求和结果为:{this.props.count}</h2>
        <select ref={(c) => (this.selectNumber = c)}>
          <option value={1}>1</option>
          <option value={2}>2</option>
          <option value={3}>3</option>
        </select>
        &nbsp;<button onClick={this.increment}>+</button>
        &nbsp;<button onClick={this.decrement}>-</button>
        &nbsp;<button onClick={this.incrementIfOdd}>当前求和为奇数,+</button>
        &nbsp;<button onClick={this.incrementAsyc}>异步+</button>
      </div>
    );
  }
}

📎 手工绘图理解:

react-redux使用优化

1、mapDispatchToProps 精简写法:

import {
  createIncrementAction,
  createDecrementAction,
  createIncrementAsyncAction,
} from "../../redux/count_action";
import countUI from "../../components/Count";
import { connect } from "react-redux";

// 创建并暴露一个Count的容器组件
export default connect(
  (state) => ({ count: state }),

  //   // mapDispatchToProps的一般写法
  //   (dispatch) => ({
  //     increment: (value) => dispatch(createIncrementAction(value)),
  //     decrement: (value) => dispatch(createDecrementAction(value)),
  //     syncIncrement: (value, time) =>
  //       dispatch(createIncrementAsyncAction(value, time)),
  //   })

  // mapDispatchToProps的精简写法:redux会帮忙自动分发
  {
    increment: createIncrementAction,
    decrement: createDecrementAction,
    syncIncrement: createIncrementAsyncAction,
  }
)(countUI);

2、index中无需再实现监测:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
// import store from "./redux/store";

ReactDOM.render(<App />, document.getElementById('root'))

// 当使用了react-redux后,就不用监测了。因为容器组件已经有了监测的能力 

// 监测redux中状态的改变。若redux的状态发生了改变,则重新渲染App组件
// store.subscribe(() => {
//     ReactDOM.render(<App />, document.getElementById('root'))
// });

3、store不再直接传递给容器组件

import React, { Component } from "react";
import Count from './containers/Count'
// import store from './redux/store'

// 所有组件的外壳组件App
export default class App extends Component {
  render() {
    return (
      <div>
        <Count/>
        {/* <Count store={store}/> */}
      </div>
    );
  } 
}
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from "./redux/store";
import { Provider } from 'react-redux';

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

4、整合UI组件和容器组件为一个文件:

import React, { Component } from "react";
import {
  createIncrementAction,
  createDecrementAction,
  createIncrementAsyncAction,
} from "../../redux/count_action";
import { connect } from "react-redux";

// UI组件
class Count extends Component {
  increment = () => {
    const { value } = this.selectNumber;
    this.props.increment(value * 1);
  };
  decrement = () => {
    const { value } = this.selectNumber;
    this.props.decrement(value * 1);
  };
  incrementIfOdd = () => {
    const { value } = this.selectNumber;
    if (this.props.count % 2 === 1) {
      this.props.increment(value * 1);
    }
  };
  incrementAsyc = () => {
    const { value } = this.selectNumber;
    this.props.syncIncrement(value * 1, 2000);
  };

  render() {
    return (
      <div>
        <h2>当前求和结果为:{this.props.count}</h2>
        <select ref={(c) => (this.selectNumber = c)}>
          <option value={1}>1</option>
          <option value={2}>2</option>
          <option value={3}>3</option>
        </select>
        &nbsp;<button onClick={this.increment}>+</button>
        &nbsp;<button onClick={this.decrement}>-</button>
        &nbsp;<button onClick={this.incrementIfOdd}>当前求和为奇数,+</button>
        &nbsp;<button onClick={this.incrementAsyc}>异步+</button>
      </div>
    );
  }
}

// 创建并暴露一个Count的容器组件
export default connect(
  (state) => ({ count: state }),
  {
    increment: createIncrementAction,
    decrement: createDecrementAction,
    syncIncrement: createIncrementAsyncAction,
  }
)(Count);

总结,一个最简单的案例理解:

import React, { Component } from "react";
import { connect } from "react-redux";
import { createIncrementAction } from "../../redux/count_action";

class Count extends Component {
  add = () => {
    // 通知redux进行加1
    this.props.jiafa(1);
  };
  render() {
    return (
      <div>
        <h2>当前求和结果为:{this.props.sum}</h2>
        <button onClick={this.add}>点我加一</button>
      </div>
    );
  }
}

export default connect((state) => ({ sum: state }), {
  jiafa: createIncrementAction,
})(Count);

数据共享

person的reducer:

import { ADD_PERSON } from "../constant";
const initState = [{ id: '1001', name: 'tom', age: 18 }]
// Reducer函数接收两个参数:之前的状态、动作对象
export default function personReducer(preState = initState, action) {
    const { type, data } = action
    switch (type) {
        case ADD_PERSON:
            return [data, ...preState] // ⚠️注意这里
        default:
            return preState
    }
}

首先需要再store里面将所有的reducer合并到一个对象里:

// 引入createStore
import { createStore, applyMiddleware, combineReducers } from 'redux'
// 引入reducer
import countReducer from './reducers/count'
import personReducer from './reducers/person'
// 引入redux-thunk用于支持异步action
import thunk from 'redux-thunk'

// 合并reducer到一个对象里
const allReducers = combineReducers({
    he: countReducer,
    rens: personReducer
})

// 对外暴露store
export default createStore(allReducers, applyMiddleware(thunk))

组件里就可以使用所有状态对象了:

import React, { Component } from "react";
import { nanoid } from "nanoid";
import { connect } from "react-redux";
import { createAddPersonAction } from "../../redux/actions/person";

class Person extends Component {
  addPerson = () => {
    const name = this.nameNode.value;
    const age = this.ageNode.value;
    const personObj = { id: nanoid(), name, age };
    this.props.addPerson(personObj);
    this.nameNode.value = ""; // 添加后清空数据
    this.ageNode.value = ""; // 添加后清空数据
  };
  render() {
    return (
      <div>
        <h2>总人数为:{this.props.renshu.length}</h2>
        <h3>上方组件求和结果为:{this.props.count}</h3>
        <input
          ref={(c) => (this.nameNode = c)}
          type="text"
          placeholder="请输入姓名"
        />
        <input
          ref={(c) => (this.ageNode = c)}
          type="text"
          placeholder="请输入年龄"
        />
        <button onClick={this.addPerson}>添加</button>
        <ul>
          {this.props.renshu.map((item) => {
            return (
              <li key={item.id}>
                姓名:{item.name}---年龄:{item.age}
              </li>
            );
          })}
        </ul>
      </div>
    );
  }
}

// 创建并暴露一个Count的容器组件
export default connect((state) => ({ renshu: state.rens, count: state.he }), {
  addPerson: createAddPersonAction,
})(Person);

redux开发者工具

1、下载插件:Redux DevTools

2、下载安装 npm install redux-devtools-extension

3、修改store:

// 引入createStore
import { createStore, applyMiddleware, combineReducers } from 'redux'
// 引入reducer
import countReducer from './reducers/count'
import personReducer from './reducers/person'
// 引入redux-thunk用于支持异步action
import thunk from 'redux-thunk'
// 引入redux-devtools-extension 用于支持开发者工具
import {composeWithDevTools} from 'redux-devtools-extension'

// 合并reducer到一个对象里
const allReducers = combineReducers({
    he: countReducer,
    rens: personReducer
})

// 对外暴露store
// export default createStore(allReducers, composeWithDevTools())
// export default createStore(allReducers, applyMiddleware(thunk)) //异步action
export default createStore(allReducers, composeWithDevTools(applyMiddleware(thunk)))

4、浏览器里使用:

代码总结

1)整体文件目录

2)入口文件 index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from "./redux/store";
import { Provider } from 'react-redux';

ReactDOM.render(
    // 使用Redux的Provider包裹APP,是为了让APP所有的后代容器组件都能接收到store
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root'))

3)外壳组件 App.js

import React, { Component } from "react";
// 引入容器组件
import Count from './containers/Count'
import Person from './containers/Person'

// 所有组件的外壳组件App
export default class App extends Component {
  render() {
    return (
      <div>
        <Count /><hr /><Person />
      </div>
    );
  }
}

4)Count组件 Count.jsx

import React, { Component } from "react";
import {
  increment,
  decrement,
  incrementAsync,
} from "../../redux/actions/count";
import { connect } from "react-redux";

// UI组件
class Count extends Component {
  increment = () => {
    const { value } = this.selectNumber;
    this.props.increment(value * 1);
  };
  decrement = () => {
    const { value } = this.selectNumber;
    this.props.decrement(value * 1);
  };
  incrementIfOdd = () => {
    const { value } = this.selectNumber;
    if (this.props.count % 2 === 1) {
      this.props.increment(value * 1);
    }
  };
  incrementAsyc = () => {
    const { value } = this.selectNumber;
    this.props.syncIncrement(value * 1, 2000);
  };

  render() {
    return (
      <div>
        <h2>当前求和结果为:{this.props.count}</h2>
        <h3>下方组件的人数为:{this.props.personArr.length}</h3>
        <select ref={(c) => (this.selectNumber = c)}>
          <option value={1}>1</option>
          <option value={2}>2</option>
          <option value={3}>3</option>
        </select>
        &nbsp;<button onClick={this.increment}>+</button>
        &nbsp;<button onClick={this.decrement}>-</button>
        &nbsp;<button onClick={this.incrementIfOdd}>当前求和为奇数,+</button>
        &nbsp;<button onClick={this.incrementAsyc}>异步+</button>
      </div>
    );
  }
}

// 创建并暴露一个Count的容器组件
export default connect(
  (state) => ({ personArr: state.person, count: state.count }),
  {
    increment: increment,
    decrement: decrement,
    syncIncrement: incrementAsync,
  }
)(Count);

5)Person组件 Person.jsx

import React, { Component } from "react";
import { nanoid } from "nanoid";
import { connect } from "react-redux";
import { addPerson } from "../../redux/actions/person";

// UI组件
class Person extends Component {
  addPerson = () => {
    const name = this.nameNode.value;
    const age = this.ageNode.value;
    const personObj = { id: nanoid(), name, age };
    this.props.addPerson(personObj);
    this.nameNode.value = ""; // 添加后清空数据
    this.ageNode.value = ""; // 添加后清空数据
  };
  render() {
    return (
      <div>
        <h2>总人数为:{this.props.personArr.length}</h2>
        <h3>上方组件求和结果为:{this.props.count}</h3>
        <input
          ref={(c) => (this.nameNode = c)}
          type="text"
          placeholder="请输入姓名"
        />
        <input
          ref={(c) => (this.ageNode = c)}
          type="text"
          placeholder="请输入年龄"
        />
        <button onClick={this.addPerson}>添加</button>
        <ul>
          {this.props.personArr.map((item) => {
            return (
              <li key={item.id}>
                姓名:{item.name}---年龄:{item.age}
              </li>
            );
          })}
        </ul>
      </div>
    );
  }
}

// 创建并暴露一个Person的容器组件
export default connect(
  (state) => ({ personArr: state.person, count: state.count }),
  {
    addPerson: addPerson,
  }
)(Person);

6)store文件 store.js

// 引入createStore用于创建store对象、applyMiddleware用于支持异步action
import { createStore, applyMiddleware } from 'redux'
// 引入redux-thunk用于支持异步action
import thunk from 'redux-thunk'
// 引入redux-devtools-extension 用于支持开发者工具
import { composeWithDevTools } from 'redux-devtools-extension'
// 引入合并后的reducer
import allReducers from './reducers'

// 对外暴露store
// export default createStore(allReducers, composeWithDevTools())
// export default createStore(allReducers, applyMiddleware(thunk)) //异步action
export default createStore(allReducers, composeWithDevTools(applyMiddleware(thunk)))

7)reducer汇总文件

// 该文件用于汇总所有的reducer

// 引入combineReducers用于合并所有reducer
import { combineReducers } from 'redux'
// 引入reducer
import count from './count'
import person from './person'

// 合并reducer到一个对象里
export default combineReducers({ count: count, person: person })

8)countReducer

import { INCREMENT, DECREMENT } from "../constant";
const initState = 0
// Reducer函数接收两个参数:之前的状态、动作对象
// preState=initState   形参默认值
export default function countReducer(preState = initState, action) {
    const { type, data } = action
    switch (type) {
        case INCREMENT:
            return preState + data
        case DECREMENT:
            return preState - data
        default:
            // 初始化
            return preState
    }
}

9)personReducer

import { ADD_PERSON } from "../constant";
const initState = [{ id: '1001', name: 'tom', age: 18 }]
// Reducer函数接收两个参数:之前的状态、动作对象
export default function personReducer(preState = initState, action) {
    const { type, data } = action
    switch (type) {
        case ADD_PERSON:
            return [data, ...preState]
        default:
            return preState
    }
}

10)count Action

import { INCREMENT, DECREMENT } from "../constant";

// 为Count组件生成action对象
export const increment = value => ({ type: INCREMENT, data: value })
export const decrement = value => ({ type: DECREMENT, data: value })
// 异步action。里面会调用同步action
export const incrementAsync = (value, time) => {
    // action的值为函数
    return (dispatch) => {
        setTimeout(() => {
            dispatch(increment(value));
        }, time);
    }
}

11)person Action

import { ADD_PERSON } from '../constant'

// 为Person组件生成action对象
export const addPerson = personObj => ({ type: ADD_PERSON, data: personObj })

12)常量文件

export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
export const ADD_PERSON = 'add_person'

八、项目打包运行

1、构建项目

npm run build 执行完毕后生成一个文件夹build

2、安装serve(可以快速生成服务器)

全局安装serve npm i serve -g

3、进入build目录下,启动serve serve 或者使用 serve 目录/

4、如果后续代码有修改,重新执行一遍(相当于重新发布上线):npm run build serve 目录/

九、React扩展

setState

setState更新状态的两种写法:

  • setState(stateChange, [callback]) -- 对象式的setState
    • stateChange 为状态改变对象(该对象可以体现出状态的更改)
    • callback 是可选的回调函数,它在状态更新完毕、界面也更新后(render调用后)才被调用
  • setState(updater, [callback]) -- 函数式的setState
    • updater 为返回stateChange对象的函数
    • updater 可以接收到state和props
    • callback 是可选的回调函数,它在状态更新完毕、界面也更新后(render调用后)才被调用
import React, { Component } from "react";

export default class StateDemo extends Component {
  state = { count: 0 };
  addNumber = () => {
    // setState(stateChange, [callback]) -- 对象式的setState
    // const { count } = this.state;
    // // this.setState({ count: count + 1 });
    // this.setState({ count: count + 1 }, () => {
    //   console.log(this.state.count);
    // });

    // setState(updater, [callback]) -- 函数式的setState
    this.setState(
      (state, props) => {
        console.log("传递过来的props是:", props);
        return { count: state.count + 1 };
      },
      () => {
        console.log(this.state.count);
      }
    );
  };
  render() {
    return (
      <div>
        <h2>当前求和结果为:{this.state.count}</h2>
        <button onClick={this.addNumber}>点击加一</button>
      </div>
    );
  }
}

总结:

  • 对象式的setState是函数式的setState的简写方式(语法糖)
  • 使用原则:
    • 如果新状态不依赖于原状态(比如设置成一个固定值) ===> 使用对象方式
    • 如果新状态依赖于原状态(比如在原状态基础上设置新值) ===> 使用函数方式
    • 如果需要在setState()执行后获取最新的状态数据,需要在第二个callback函数中读取

路由组件的lazyLoad

import React, { Component, lazy, Suspense } from "react";
import { NavLink, Route, Routes } from "react-router-dom";
// import About from "./About";
// import Home from "./Home";
import "./load.css";

// 懒加载
const About = lazy( () => import('./About') )
const Home = lazy( () => import('./Home') )

export default class Demo extends Component {
  render() {
    return (
      <div>
        <div>
          <div>
            <div>
              <NavLink className={({ isActive }) => (isActive ? "demostyle" : "")} to="/about">About</NavLink>
              &nbsp;
              <NavLink className={({ isActive }) => (isActive ? "demostyle" : "")} to="/home">Home</NavLink>
            </div>
          </div>
          <div
            style={{
              border: "1px dashed orange",
              width: "400px",
              height: "100px",
            }}
          >
            {/* 注册路由。<Routes>相当于5.x版本的<Switch> */}
            <Suspense fallback={<h1>loading...</h1>}>
              <Routes>
                <Route path="/about" element={<About />} />
                <Route path="/home" element={<Home />} />
              </Routes>
            </Suspense>
          </div>
        </div>
      </div>
    );
  }
}

注意:Loading组件不使用懒加载

Hooks

Hook是React 16.8.0版本中新增的特性,可以在函数组件中使用state以及其他的React特性。三个常用的Hook:

  1. State Hook:React.useState()
  2. Effect Hook: React.useEffect()
  3. Ref Hook:React.useRef()

1)State Hook

State Hook让函数组件也可以有state状态,并进行状态数据的读写操作。useState第一次初始化指定的值在内部做缓存。

语法:const [xxx, setXxx] = React.useState(initValue)

setXxx的两种写法:

  1. setXxx(newValue):参数为非函数值,直接指定新的状态值,内部用它覆盖原来的状态值
  2. setXxx(value => newValue):参数为函数,接收原来的状态值,返回新的状态值,内部用它覆盖原来的状态值
export default function Demo() {
  const [count, setCount] = React.useState(0);
  function add() {
    // setCount(count + 1) // 第一种写法
    setCount((count) => count + 1); // 第二种写法
  }
  return (
    <div>
      <h2>当前求和结果为:{count}</h2>
      <button onClick={add}>点击+1</button>
    </div>
  );
}

2)Effect Hook

Effect Hook可以在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)。可以理解为:

EffectHook = componentDidMount() + componentDidUpdate() + componentWillUnmount()

React中的副作用操作:

  • 发ajax请求数据获取
  • 设置订阅/启动定时器
  • 手动更改真实DOM

Effect Hook语法:

useEffect(()=>{
  // 在此可以执行任何带副作用的操作
  ...
  return ()={ // 将会在组件卸载前执行
    // 在此做一些收尾工作,比如清除定时器、取消订阅等
    ... 
  }
},[stateValue]) // 如果指定的是[],回调函数只会在第一次render()后执行
export default function Demo() {
  const [count, setCount] = React.useState(0);
  // 这里相当于挂载组件时调用
  //   React.useEffect(() => {
  //     setInterval(() => {
  //       setCount((count) => count + 1);
  //     }, 1000);
  //   },[]);

  React.useEffect(() => {
    let timer = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);
    return () => {
      clearInterval(timer);
    };
  }, []);
  function add() {
    setCount((count) => count + 1);
  }
  function unmount() {
    ReactDOM.unmountComponentAtNode(document.getElementById("root"));
  }
  return (
    <div>
      <h2>当前求和结果为:{count}</h2>
      <button onClick={add}>点击+1</button>
      <button onClick={unmount}>点击卸载组件</button>
    </div>
  );
}

3)Ref Hook

Ref Hook可以在函数组件中存储/查找组件内的标签或任意其他数据

语法:const refContainer = React.useRef();

作用:保存标签对象,功能与React.createRef()一样

export default function Demo() {
  const myRef = React.useRef();
  function show() {
    alert(myRef.current.value);
  }
  return (
    <div>
      <input type="text" ref={myRef}></input>
      <button onClick={show}>点击提示输入信息</button>
    </div>
  );
}

Fragment

作用:可以不必有一个真实的DOM根标签了

使用方式:<Fragment></Fragment> 或者 <></>

export default class Demo extends Component {
  render() {
    return (
      //   <Fragment key={1}>
      //     <input type="text" />
      //     <input type="text" />
      //   </Fragment>
      <>
        <input type="text" />
        <input type="text" />
      </>
    );
  }
}

Context

一种组件间通信方式,常用于祖组件与后代组件间的通信。Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。

使用方式:

1)创建Context容器对象:
	const XxxContext = React.createContext()

2)渲染子组件时,外面包裹xxxContext.Provider,通过value属性给后代组件传递数据
  <XxxContext.Provider value={数据}>
  	子组件
  </XxxContext.Provider>

3)后代组件读取数据:
	// 第一种方式:仅适用于类组件
	static contextType = xxxContext // 声明接收context
	this.context // 读取context中的value数据

	// 第二种方式:函数组件与类组件都可以使用
	<xxxContext.Consumer>
  	{
    	value=>( // value就是context中的value数据
      	要显示的内容
      )
    }
	</xxxContext.Consumer>
import React, { Component } from "react";

const UserNameContext = React.createContext();

export default class A extends Component {
  state = { username: "Tom", age: 18 };
  render() {
    return (
      <div style={{ border: "1px solid orange", padding: "8px" }}>
        <h2>A组件</h2>
        <h5>A组件里面的年龄是:{this.state.username}</h5>
        <UserNameContext.Provider value={{ username: this.state.username, age: this.state.age }}>
          <B />
        </UserNameContext.Provider>
      </div>
    );
  }
}

class B extends Component {
  render() {
    return (
      <div style={{ border: "1px solid blue", padding: "8px" }}>
        <h2>B组件</h2>
        <C />
      </div>
    );
  }
}

class C extends Component {
  // 声明接收context
  static contextType = UserNameContext;
  render() {
    return (
      <div style={{ border: "1px solid red", padding: "8px" }}>
        <h2>C组件</h2>
        <h5>C组件里面接收到A的名字是:{this.context.username},年龄:{this.context.age}</h5>
        <D />
      </div>
    );
  }
}

function D(){
    return (
      <div style={{ border: "1px solid black", padding: "8px" }}>
        <h2>D组件</h2>
        <h5>D组件里面接收到A的名字是:
            <UserNameContext.Consumer>
                {
                    value => {return `${value.username},年龄:${value.age}`}
                }
            </UserNameContext.Consumer>
        </h5>
      </div>
    )
}

组件优化

Component的两个问题:

1)只要执行setState(),即使不改变状态数据,组件也会重新render()

2)只当前组件重新render(),就会自动重新render子组件(包括没有用父组件任何数据时),这样效率比较低

解决办法:

只在当组件的state或者props数据发生改变时才重新render()

原因:

Component中的shouldComponentUpdate()总是返回true

解决办法:

  1. 办法1:重写shouldComponentUpdate()方法
    1. 比较新旧state或props数据,如果有变化才返回true,如果没有返回false
  2. 办法2:使用 PureComponent
    1. PureComponent重写了shouldComponentUpdate(),只有state或者props数据发生改变时才返回true
    2. 注意:只是进行state和props数据的浅比较,如果只是数据对象内部数据变了,返回false
    3. 不要直接修改state数据,而是要产生新数据(this.setState({ carName: "xxx" })就会产生新数据)
  3. 项目中一般使用 PureComponent 来优化
import React, { Component, PureComponent } from "react";
import "./index.css";

export default class Parent extends PureComponent {
  state = { carName: "奔驰" };
  changeCar = () => {
    this.setState({ carName: "保时捷911" })
    // this.setState({});
  };
  render() {
    console.log("Parent-render()调用");
    const { carName } = this.state
    return (
      <div className="parent">
        <h3>Parent组件</h3>
        <span>父组件:{carName}</span>
        <button onClick={this.changeCar}>点击换车</button>
        <Child carName={carName} />
      </div>
    );
  }
}

class Child extends PureComponent {
  render() {
    console.log("Child-render()调用");
    return (
      <div className="child">
        <h3>Child组件</h3>
        <span>子组件接收:{this.props.carName}</span>
      </div>
    );
  }
}

RenderProps

如何向组件内部动态传入带内容的结构(标签)?

  • vue中:使用slot插槽技术,也就是通过组件标签体传入结构
  • React中:
    • 使用children props:通过组件标签体传入结构
    • 使用render props:通过组件标签属性传入结构,一般用render函数属性

children props:

<A>
  <B>xxxx</B>  
</A>
问题:如果B组件需要A组件中的数据,是做不到的
import React, { Component } from "react";
import "./index.css";

export default class Parent extends Component {
  render() {
    return (
      <div className="parent">
        <h3>Parent组件</h3>
        {/* <A>Parent组件说:hello A</A> */}
        <A>
          <B />
        </A>
      </div>
    );
  }
}

class A extends Component {
  state = { name: "Tom" };
  render() {
    // console.log(this.props); // {children: 'hello A'}
    return (
      <div className="a">
        <h3>A组件</h3>
        {/* {this.props.children} */}
        {/* <B /> */}
        {this.props.children}
      </div>
    );
  }
}

class B extends Component {
  render() {
    return (
      <div className="b">
        <h3>B组件</h3>
      </div>
    );
  }
}

render props:

<A render = {(data) => <C data={data}></C>}> </A>

A组件:{this.props.render(内部state数据)}
C组件:读取A组件传入的数据并展示 {this.props.data}
import React, { Component } from "react";
import "./index.css";

export default class Parent extends Component {
  render() {
    return (
      <div className="parent">
        <h3>Parent组件</h3>
        {/* <A>Parent组件说:hello A</A> */}
        <A render={(name) => <B name={name}/>} />
      </div>
    );
  }
}

class A extends Component {
  state = { name: "Tom" };
  render() {
    // console.log(this.props); // {children: 'hello A'}
    return (
      <div className="a">
        <h3>A组件</h3>
        {/* {this.props.children} */}
        {/* <B /> */}
        {this.props.render(this.state.name)}
      </div>
    );
  }
}

class B extends Component {
  render() {
    return (
      <div className="b">
        <h3>B组件</h3>
        <span>A组件传递过来的状态:{this.props.name}</span>
      </div>
    );
  }
}

ErrorBoundary错误边界

错误边界(ErrorBoundary):是用来捕获后代组件错误,渲染出备用页面

特点:只能捕获后代组件生命周期产生的错误(一般也是用来捕获render中出现的错误) ,不能捕获自己组件产生的错误和其他组件在合成事件、定时器中产生的错误

使用方式:getDerivedStateFromError 配合 componentDidCatch

export default class Parent extends Component {
  state = {
    errorMessage: "", // 用于标识子组件的错误
  };
  // 当Parent的子组件出现报错时,会触发此方法的调用,并携带过来错误信息
  static getDerivedStateFromError(error) {
    console.log(error);
    // 返回新的state,在render之前触发
    return { errorMessage: error };
  }
  // 出现错误时,调用此钩子
  componentDidCatch(error, info) {
    console("渲染组件出错次数,这里可以发送给服务端", error, info);
  }
  render() {
    return (
      <div>
        <h3>Parent组件</h3>
        {this.state.errorMessage ? <h2>子组件错误啦- - !</h2> : <Child />}
      </div>
    );
  }
}

组件间通信方式总结

组件间的关系有:父子组件、兄弟组件(非嵌套组件)、祖孙组件(跨级组件)

几种通信方式:

  • props
    • children props
    • render props
  • 消息订阅发布
    • pub-sub 、event等
  • 集中式管理
    • redux dva
  • context
    • 生产者-消费者模式

比较好的搭配方式:

  • 父子组件:props
  • 兄弟组件: 消息订阅发布、集中式管理
  • 祖孙组件(跨级组件):消息订阅发布、集中式管理、context(一般开发用的少,封装插件用的多)