2 Star 3 Fork 0

蔡伦多/react-markdown笔记

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README

React学习记录

参考文档:https://react.docschina.org/docs/getting-started.html

这里不讲述概念,直接上语法,至于jsx是什么,到上面那个网站上面看

生命周期图

https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

1. state与setState

[TOC]

这是react最主要的两个内置属性与内置方法,state表示当前组件的状态/属性,而要改变state的值则通过setState方法,切不可直接修改state,因为setState内部是异步修改的

例子:

export default class CartSample extends Component {
    this.state = {
      text: ''
    }
    setGoodsText = e => {
        this.setState({
          text: e.target.value
        })
  	}
    render() {
        return (
        	<input type="text" onChange={this.setGoodsText}/>
        )
    }
}

上面CartSample就是一个组件,里面有state和setState,监听input框的change事件,一旦触发则让setState改变state中的值,这是最简单也是最常用的用法。

2. 条件判断

在jsx中,我们在大括号{}内只能写表达式,不能编写逻辑代码,如if判断、for循环,此时我们可以通过短路逻辑判断,如:

下面例子写了当state的goods长度不为0时则展示对应数据

export default class CartSample extends Component {
    this.state = {
      goods: [
      	{ id: 1, text: "web全栈架构师", count: 2 },
      ]
    }
    render() {
        return (
        	<ul>
              {this.state.goods.length && this.state.goods}
            </ul>
        )
    }
}

3. 循环展示列表

引用2的例子,循环展示商品列表,因为{}里面只能是表达式,因此我们的思路是遍历数组的每个元素,并返回不同的值,因此数组内置的map函数很适合做这种工作。

export default class CartSample extends Component {
    this.state = {
      goods: [
      	{ id: 1, text: "web全栈架构师", count: 2 },
      	{ id: 2, text: "python全栈架构师", count: 0 },
      ]
    }
    render() {
        return (
        	<ul>
              {this.state.goods.length && this.state.goods.map( item => {
              	<li>{item.name}</li>
              })}
            </ul>
        )
    }
}

4. 两种组件写法

// 方式1: 类
import React, { Component } from 'react'

export default class index extends Component {
  render() {
    return (
      <div>
        
      </div>
    )
  }
}

// 方式2: 函数
import React from 'react'

export default function index() {
  return (
    <div>
      
    </div>
  )
}

本来两者是有些差异的,hook出现之前,用函数式写组件一般都是只写傻瓜式组件,也就是没有处理业务逻辑的。但是自从react 16.80版本出现后,新增加了hook,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性,因此可以试着去学学hook,这样子也可以在function里编写逻辑代码,也更简练。

文档:https://react.docschina.org/docs/hooks-intro.html

5. 事件绑定

事件监听采用Javascript的驼峰式写法,如onClick,onChange,onBlur等

例子:

export default class CartSample extends Component {
    this.state = {
      text: ''
    }
    setGoodsText = e => {
        this.setState({
          text: e.target.value
        })
  	}
    render() {
        return (
        	<input type="text" onChange={this.setGoodsText}/>
        )
    }
}

上面是最简单的例子

那么传参事件该怎么绑定呢?

学之前先注意一点: {} 是个表达式

因为是表达式,如果我们要改上面代码为带参事件,我们不能用

onChange={this.setGoodsText(args)}

这样子的话会被立刻执行,因此我们需要让他监听一个匿名函数,写法如下

onChange={() => this.setGoodsText(args)}

(补充)使用PropTypes进行类型检查

随着你的应用程序不断增长,你可以通过类型检查捕获大量错误。对于某些应用程序来说,你可以使用 FlowTypeScript 等 JavaScript 扩展来对整个应用程序做类型检查。但即使你不使用这些扩展,React 也内置了一些类型检查的功能。要在组件的 props 上进行类型检查,你只需配置特定的 propTypes 属性

import PropTypes from 'prop-types';

class Greeting extends React.Component {
  render() {
    return (
      <h1>Hello, {this.props.name}</h1>
    );
  }
}

Greeting.propTypes = {
  name: PropTypes.string
};

同样的,由于在类原型上的属性属于静态属性,所以我们可以写在类里面,只要加个static关键字即可。

export class MyPropType extends React.Component{
  static propTypes = {
    name: PropType.string,
    age: PropType.number.isRequired,
  }
  constructor(props) {
    super(props);
    console.log(props)
  }
  render() {
    return (
      <div>prop测试</div>
    )
  }
}

默认 Prop 值

export class MyPropType extends React.Component{
  static propTypes = {
    name: PropType.string,
    age: PropType.number.isRequired,
  }
  static defaultProps = {
    name: 'chen',
    age: 18
  }
  constructor(props) {
    ...
  }
  render() {
	...
  }
}

函数组件的PropType

export function FuncPropType({name, age}) {
  console.log(name, age)
  return (
    <>
      <div>{name}</div>
      <div>{age}</div>
    </>
  )
}
FuncPropType.propTypes = {
  name: PropTypes.string,
  age: PropTypes.number
}
FuncPropType.defaultProps = {
  name: 'tan',
  age: 17
}

6.类方法中的this指向问题

给JSX绑定事件时由于没有指定上下文,this指向会指向顶层

而指向顶层有两种情况:严格模式与非严格模式,而react是在严格模式下的,在<React.StrictMode>定义,所以this为undefined。

为了让我们方法中的this可以指向类本身,我们可以选择四种方式:

// 方式一 构造器中给对应函数指定上下文, 如
this.addGood = this.addGood.bind(this)       ------>  推荐

// 方式二 在JSX给种绑定this
<div @click="{ this.addGood.bind(this) }"></div>

// 方式三 类里面方法用箭头函数定义,箭头函数是没有上下文的,上下文会指向类
addGood = () => {}                           ------>  推荐

// 方式四 JSX里用箭头函数
addGood = () => { () => {this.addGood()} }

7. setState注意事项

使用setState修改值有两个方法:

一个是传入对象修改,另一个方式是传入函数修改

// 方式一
state = {
	text: ''
}
this.setState({
	text: '新值'
})

// 方式二
state = {
	text: ''
}
this.setState(prevState => {
	let text = '123'
	prevState.text = text
})

7.1 方式一注意事项

在使用方式一时要注意,若多次使用方式一进行setState,react最终会将多个setState操作给合并,这时候,若是多次setState了通过值,则只取最后一次setState的时候的值,如:

state = {
	text: ''
}
this.setState({
	text: '新值'
})
this.setState({
	text: '新新值'
})

最后的值为 : 新新值

7.2 方式二注意事项

使用方式二时,我们最好按照react给我们定的规范,每次都给state需要修改的对象重新设置一个新的的值。

如:

this.state = {
      goods: [
        { id: 1, text: "web全栈架构师" },
        { id: 2, text: "python全栈架构师" }
      ],
      text: ''
}
textChange = e => {
    this.setState({ text: e.target.value });
};
<input type="text" value={this.state.text} onChange={this.textChange} />
<button onClick={this.addGood}>添加商品</button>

现在当点击某个按钮时会触发addGood事件,addGood用来添加新商品数据到goods中,此时addGood的写法是这样的

addGood = () => {
    this.setState(prevState => {
      return {
        goods: [
          ...prevState.goods,
          {
            id: prevState.goods.length + 1,
            text: prevState.text
          }
        ]
      };
    });
  }

会将以前state.goods里的数据展开,拷贝进一个新的数组,再将新数据加进去,这就是react的规范。

8. antd框架引入

安装:

npm install antd --save

试用按钮组件:

import Button from 'antd/lib/button';
import 'antd/dist/antd.css'

export default class App extends Component {
  render() {
    return (
      <div>
        <Button type='primary'>Button</Button>
      </div>
    )
  }
}

上面那种方式引入很麻烦,而且是样式是引用全部的样式表,如何按需引入呢?

安装react-app-rewired取代react-scripts,可以扩展webpack的配置,类似vue.config.js

npm install react-app-rewired@2.0.2-next.0 babel-plugin-import --save

修改package.json的scripts字段:

// 所有react-scripts 都替换成 react-app-rewired 如
"scripts": {
    "start": "react-app-rewired start",
	...
},

在根目录下新建config-overrides.js,内容如下

const { injectBabelPlugin } = require("react-app-rewired");
module.exports = function override(config, env) {
  config = injectBabelPlugin(
    // 在默认配置基础上注入
    // 插件名,插件配置
    ["import", { libraryName: "antd", libraryDirectory: "es", style: "css" }],
    config
  );
  return config;
};

这样子配置就完成了,接下来我们可以实现按需导入了,在需要引入组件的地方:

import {Button} from 'antd'

<Button type='primary'>Button</Button>

9. 容器组件与展示组件

基本原则:容器组件负责数据获取,展示组件负责根据props显示信息,例子如下

import React, { Component } from 'react';

// 容器组件
export default class CommentList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      comments: [],
    }
  }
  componentDidMount() {
    setTimeout(() => {
      this.setState({
        comments: [
          { name: 'cai', age: 20, education: 'university' },
          { name: 'chen', age: 22, education: 'university' },
        ]
      })
    }, 3000)
  }
  render() {
    return (
      <div>
        {this.state.comments.map((c, i) => {
          return <Comment key={i} data={c}></Comment>
        })}
      </div>
    )
  }
}

// 展示组件
function Comment({ data }) {
  return (
    <div>
        <p>{data.name}</p>
        <p>---   {data.age}</p>
        <p>---   {data.education}</p>
    </div>
  )
}

10. 性能优化:浅比较

参考文档:https://react.docschina.org/docs/optimizing-performance.html

UI 更新需要昂贵的 DOM 操作,而 React 内部使用几种巧妙的技术以便最小化 DOM 操作次数。对于大部分应用而言,使用 React 时无需专门优化就已拥有高性能的用户界面。尽管如此,你仍然有办法来加速你的 React 应用。

还是引用9的例子,只不过我们会给CommentList加一个定时器,每隔1.5s就对comments列表进行setState操作,但是我们不修改它的值:

componentDidMount() {
    this.timmer = setInterval(() => {
      this.setState({
        comments: [
          { name: 'cai', age: 20, education: 'university' },
          { name: 'chen', age: 22, education: 'university' },
        ]
      })
    }, 1500)
}

然后再在Comment组件打印日志。我们会发现打印日志会每隔1.5s打印,这时候问题就来了,明明我们的数据没发生实质上的变化,但页面却一直在渲染,这是一个非常值得优化的点。

解决方案:

10.1 使用shouldComponentUpdate

因为使用到生命周期函数,所以我们的Comment组件应该转换为类形式,shouldComponentUpdate用来检查旧值与新值是否相等,返回一个bool值,如为true则重新渲染,否则不渲染

class Comment extends React.Component{

  constructor(props) {
    super(props)
  }

  shouldComponentUpdate(nextProps){
      if (nextProps.data.name === this.props.data.name &&
        nextProps.data.age === this.props.data.age &&
        nextProps.data.education === this.props.data.education
        ) {
          return false;
      }
      return true;
  }

  render() {
    console.log("render comment");

    return (
      <div>
          <p>{this.props.data.name}</p>
          <p>---   {this.props.data.age}</p>
          <p>---   {this.props.data.education}</p>
      </div>
    );
  }
}

10.2 使用PureComponent

首先我们先研究一下PureComponent的实现原理:

PureComponent首先会判断新值与旧值是否在同一片内存地址,如果是,则返回true,也就是相同。

否则会再对旧值的内部做一层循环比较(注意:浅比较只会进行一层for循环,有深层次的结构时浅比较是没有效果的

由此可以看出PureComponent的两个缺点:

一、如果是一个对象,或者数组,我们对其内部进行修改,而没有改变内存地址时,页面不会重新渲染

二、如果包含二层及以上的层次结构时,浅比较无法生效

如果我们有二层的层次结构时还想进行钱比较该怎么办呢?

方法: 传给展示组件的值用第二层的值传递,而非将整个对象或数组传递给展示组件。

继续引用9的例子

此时我们传给Comment组件的值不传入每个comments列表项,而是用展开运算符将每个对象展开,传入最里边的值,意思就是,我有下面这个结构的数组:

// 需要传给展示组件的数据
comments: [
          { name: 'cai', age: 20, education: 'university' },
          { name: 'chen', age: 22, education: 'university' },
 ]

// 传入的值:{...c}
{this.state.comments.map((c, i) => {
          return <Comment key={i} {...c}></Comment>
})}

// 此时...c == 每个对象的name、age、education值

// 展示组件继承PureComponent
class Comment extends React.PureComponent{
    constructor(props) {
        super(props)
    }
    render() {
    console.log("render comment");

    return (
      <div>
          <p>{this.props.name}</p>
          <p>---   {this.props.age}</p>
          <p>---   {this.props.education}</p>
      </div>
    );
}

10.3 使用memo

类形式的组件可以进行浅比较,现在在React v16.6.0之后,函数式也添加了浅比较,也就是memo, 使用方式如下:

constJoke=React.memo(() => (
  <div>
    {this.props.value||'loading...'}
  </div>
));

只需将组件用React.memo包裹即可。

11. 高阶组件

11.1 核心概念

高阶组件是参数为组件,返回值为新组件的函数

高阶组件其实就是装饰器的原型,装饰器内部就是对高阶组件封装了而已,也就是语法糖。

高阶组件的一个最明显的好处就是你可以自行扩展组件行为。想到行为扩展想必你已经联想到mixins了,那为什么不用mixins而非要弄个高阶组件出来呢? 参考文档:https://react.docschina.org/blog/2016/07/13/mixins-considered-harmful.html

接下来我们就讲讲高阶组件的使用:

假设有一个 CommentList 组件,它订阅外部数据源,用以渲染评论列表:

class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // 假设 "DataSource" 是个全局范围内的数据源变量
      comments: DataSource.getComments()
    };
  }

  componentDidMount() {
    // 订阅更改
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    // 清除订阅
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    // 当数据源更新时,更新组件状态
    this.setState({
      comments: DataSource.getComments()
    });
  }

  render() {
    return (
      <div>
        {this.state.comments.map((comment) => (
          <Comment comment={comment} key={comment.id} />
        ))}
      </div>
    );
  }
}

稍后,编写了一个用于订阅单个博客帖子的组件,该帖子遵循类似的模式:

class BlogPost extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      blogPost: DataSource.getBlogPost(props.id)
    };
  }

  componentDidMount() {
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    this.setState({
      blogPost: DataSource.getBlogPost(this.props.id)
    });
  }

  render() {
    return <TextBlock text={this.state.blogPost} />;
  }
}

CommentListBlogPost 不同 - 它们在 DataSource 上调用不同的方法,且渲染不同的结果。但它们的大部分实现都是一样的:

  • 在挂载时,向 DataSource 添加一个更改侦听器。
  • 在侦听器内部,当数据源发生变化时,调用 setState
  • 在卸载时,删除侦听器。

你可以想象,在一个大型应用程序中,这种订阅 DataSource 和调用 setState 的模式将一次又一次地发生。我们需要一个抽象,允许我们在一个地方定义这个逻辑,并在许多组件之间共享它。这正是高阶组件擅长的地方。

对于订阅了 DataSource 的组件,比如 CommentListBlogPost,我们可以编写一个创建组件函数。该函数将接受一个子组件作为它的其中一个参数,该子组件将订阅数据作为 prop。让我们调用函数 withSubscription

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);

第一个参数是被包装组件。第二个参数通过 DataSource 和当前的 props 返回我们需要的数据。

当渲染 CommentListWithSubscriptionBlogPostWithSubscription 时, CommentListBlogPost 将传递一个 data prop,其中包含从 DataSource 检索到的最新数据:

// 此函数接收一个组件...
function withSubscription(WrappedComponent, selectData) {
  // ...并返回另一个组件...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ...负责订阅相关的操作...
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props)
      });
    }

    render() {
      // ... 并使用新数据渲染被包装的组件!
      // 请注意,我们可能还会传递其他属性
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}

请注意,HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用。

被包装组件接收来自容器组件的所有 prop,同时也接收一个新的用于 render 的 data prop。HOC 不需要关心数据的使用方式或原因,而被包装组件也不需要关心数据是怎么来的。

因为 withSubscription 是一个普通函数,你可以根据需要对参数进行增添或者删除。例如,您可能希望使 data prop 的名称可配置,以进一步将 HOC 与包装组件隔离开来。或者你可以接受一个配置 shouldComponentUpdate 的参数,或者一个配置数据源的参数。因为 HOC 可以控制组件的定义方式,这一切都变得有可能。

与组件一样,withSubscription 和包装组件之间的契约完全基于之间传递的 props。这种依赖方式使得替换 HOC 变得容易,只要它们为包装的组件提供相同的 prop 即可。例如你需要改用其他库来获取数据的时候,这一点就很有用。

11.2 高阶组件的链式调用

比如我们有两个高阶组件,分别定义不同的行为:

// 高阶组件1
const withLog = Comp => {
  console.log("日志打印...");
  return props => <Comp {...props} />;
};
// 高阶组件2
const withSubscribtion = Comp => {
  // 获取name,name可能来自于接口或其他手段
  const name = "其他属性";
  console.log("do something");
  return class extends React.Component {
    render() {
      return <Comp {...this.props} name={name} />;
    }
  };
};
// 被扩展组件
class NeedToExtend extends Component {
  render() {
    return (
      <div></div>
    );
  }
}
// 链式调用过程
const newComp = withLog(withSubscribtion(NeedToExtend))

11.3 装饰器语法糖使用

ES7装饰器可用于简化高阶组件写法

npm i -D babel-plugin-transform-decorators-legacy

在config-overrides.js添加配置

config = injectBabelPlugin(
    ["@babel/plugin-proposal-decorators", { legacy: true }],
    config
);

使用装饰器,以11.2为例子

// 链式调用过程
const newComp = withLog(withSubscribtion(NeedToExtend))

// 装饰器使用
@withSubscribtion
@withLog
class NeedToExtend extends Component {
  render() {
    return (
      <div></div>
    );
  }
}

12. 复合组件

12.1 复合内容为jsx

复合组件其实就是展示组件预留个插口位置待容器组件插入相应的值,等同于Vue中的插槽。

对应Vue中的匿名插槽就是props.children,如:

import React, { Component } from 'react';

// 展示组件
function Dialog(props) {
  return (
    <div>
      {props.children}
    </div>
  );
}

// WelcomeDialog通过复合提供内容
function WelcomeDialog(props) {
  return (
    <Dialog {...props}>
      <h1>欢迎光临</h1>
      <p>感谢使用react</p>
    </Dialog>
  );
}

export default function() {
  return (
    <div>
      <WelcomeDialog></WelcomeDialog>
    </div>
  )
}

此时{props.children}就是插进来的内容

<h1>欢迎光临</h1>
<p>感谢使用react</p>

那么对应Vue的有名插槽是怎么实现的呢?

其实很简单,就是通过props传值,如我在展示组件里在定义一个页脚,页脚内容通过容器组件传入:

function Dialog(props) {
  return (
    <div>
      {props.children}
      <div className="footer">{props.footer}</div>
    </div>
  );
}
const footer = <button onClick={() => alert("确定!")}>确定</button>;
<WelcomeDialog color="green" footer={footer} />

以上例子复合内容形式均为jsx。

12.2 复合内容为函数

复合内容为函数时就相当于Vue中的作用于插槽,例子如下

假设我现在定义一个Fetcher组件,组件内的复合内容是一个带参函数,我想通过这个函数的参数渲染对应的内容:

<Fetcher name="getUser">
	{user => (
		<p>
			{user.name}-{user.age}
		</p>
	)}
</Fetcher>

定义组件:

// api接口
const Api = {
  getUser() {
    return { name: "jerry", age: 20 };
  }
};
// 组件
function Fetcher(props) {
  const user = Api[props.name](); //获取name并执行函数获取数据
  return props.children(user);  //通过props.children返回数据
}

12.3 复合内容为数组

复合内容为数组时其实实质上也是jsx

假如我们想要对复合内容(类型为数组的jsx)进行过滤,值过滤出类型为type的元素进行展示:

我们可以用到React.Children.map方法,遍历复合内容,并过滤出我们想要的

function Filter({ children, type }) {
  return (
    <div>
      {React.Children.map(children, child => {
        if (child.type !== type) {
          return;
        }
        return child;
      })}
    </div>
  );
}
<Filter type="p">
    <h1>react</h1>
    <p>react很不错</p>
    <h1>vue</h1>
    <p>vue很不错</p>
</Filter>

若我们想对传进来的复合内容进行修改,则必须谨记:

复合内容为jsx,是虚拟节点,内容不能修改,因此如果我们要修改它的内容,那我们只能克隆对应的元素在做自己想要的修改即可,如:

现在我有一个按钮组,结构如下

<RadioGroup name="mvvm">
    <Radio value="vue">vue</Radio>
    <Radio value="react">react</Radio>
    <Radio value="react">angular</Radio>
</RadioGroup>

我不想给每个按钮都加一个name,而是给Radio的父组件RadioGroup加一个name,然后让RadioGroup循环遍历每个Radio,对每个Radio加上同一个name该怎么操作呢?

如下

function RadioGroup(props) {
  return (
    <div>
      {React.Children.map(props.children, child => {
        //   vdom不可更改,克隆一个新的去改才行
        return React.cloneElement(child, { name: props.name });
      })}
    </div>
  );
}

function Radio({children, ...rest}) {
  return (
    <label>
      <input type="radio" {...rest} />
      {children}
    </label>
  );
}

这里需要注意的一点就是,再给Radio赋值的时候要注意props包含children父级组件传进来的值

因此Radio接收参数时需要做分割,分为children复合内容部分剩余部分剩余部分传给input作为属性值。

13 Refs and the DOM

Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。

在典型的 React 数据流中,props 是父组件与子组件交互的唯一方式。要修改一个子组件,你需要使用新的 props 来重新渲染它。但是,在某些情况下,你需要在典型数据流之外强制修改子组件。被修改的子组件可能是一个 React 组件的实例,也可能是一个 DOM 元素。对于这两种情况,React 都提供了解决办法。

13. Hook

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性

你之前可能把它们叫做“无状态组件”。但现在我们为它们引入了使用 React state 的能力,所以我们更喜欢叫它”函数组件”。

自定义Hook(补充)

自定义hook可以让我们将组件逻辑提取到一个可重复利用的组件内。

这里会有两个案例讲解自定义hook的使用

第一个简单的例子

import {useState, useEffect} from 'react'

function Comp() {
  let [num, setNum] = useState(0)

  useEffect(() => {
    setTimeout(() => {
      setNum(++num)
    }, 2000)
  }, [])

  return (
    <h2>Comp -- {num}</h2>
  )
}

function App() {
  return (
    <div className="App">
        <Comp1></Comp1>
    </div>
  );
}

export default App;

如上所示,我们可以将Comp组件的

let [num, setNum] = useState(0)

useEffect(() => {
  setTimeout(() => {
    setNum(++num)
  }, 2000)
}, [])

给提取到一个单独的组件中,这时候便是一个自定义hook,使得我们调用的代码更简洁,且逻辑抽离到了一起。

我们将上述代码抽离到./hooks/numHook.js中。

import {useState, useEffect} from 'react'

export default function useNum() {
  let [num, setNum] = useState(0)

  useEffect(() => {
    setTimeout(() => {
      setNum(++num)
    }, 2000)
  }, [])
  return [num, setNum]
}

这时候我们在需要引用该hook的地方进行导入引用即可,例子将可以改写为:

import useNum from './hooks/numHook'

function Comp() {
  
  let [num] = useNum()
  return (
  <h2>Comp -- {num}</h2>
  )
}

function App() {
  return (
    <div className="App">
        <Comp></Comp>
    </div>
  );
}

export default App;

这就是最简单的自定义hook。

自定义请求数据hook

这里我们要做的自定义Hook是发起数据请求的hook

实现效果如下:

image-20210530111434733

安装bootstrap:

npm i -S bootstrap

server端

const express = require('express');
const cors = require('cors');

const app = express();
app.use(cors())

app.get('/list', (req, res) => {

  // 当前页和每页多少条
  let {currentPage, perSize} = req.query
  perSize = parseInt(perSize, 10)
  // 数据 页数和条数对应得数据
  let list = []

  // 总数据条数
  let total = 66

  // 总页数
  let pageCount = Math.ceil(total / perSize)

  // 起始索引
  let offset = (currentPage - 1) * perSize

  if(currentPage >= pageCount) {
    perSize = total % perSize
  }

  for(let i = offset; i < offset + perSize; i++) {
    list.push({id: i+1, name: 'cai-'+(i+1)})
  }

  res.json({
    currentPage,
    perSize,
    total,
    pageCount,
    list
  })
})

app.listen(8080, () => {
  console.log('listening on 8080...')
})

记得先启动服务端:

node server

./hooks/useRequest.js中:

import {useState, useEffect} from 'react'

export default function useRequest() {
  /* 用户可更改的请求数据 */
  let [options, setOptions] = useState({currentPage: 1, pageSize: 10})
  let [data, setData] = useState({
    total: 0,
    pageCount: 0,
    list: []
  })

  // 请求数据
  function reqeust() {
    let {currentPage, pageSize} = options
    fetch(`http://localhost:8080/list?currentPage=${currentPage}&perSize=${pageSize}`)
      .then(res => res.json())
      .then(res => setData({...res}))
  }

  useEffect(reqeust, [options])

  return [data, options, setOptions]
}

这里的options state是单独抽离出来的,因为我们要返回给组件setOptions让他自定义请求参数(如:用户点击下一页时我们要重新改变请求参数的currentPage为当前页+1)。

onClick={() => {
	setOptions({ ...options, currentPage: index + 1 })
}}

组件一旦挂载我们就发起数据请求,然后将通过setData设置数据,并将data返回。可以看到最后返回的格式是[data, options, setOptions],意味着我们在组件内可以通过类似useState的方式获取自定义hook的值。

下面组件的取法:

 let [data, options, setOptions] = useRequest()

./pages/Tabel.js中:

import {useState} from 'react'
import useRequest from '../hooks/useRequest'

export default function Table() {
  let [data, options, setOptions] = useRequest()

  let { list, pageCount } = data

  let [size, setSize] = useState(10)

  return (
    <>
      <table className="table table-striped">
        <thead>
          <tr>
            <th>id</th>
            <th>name</th>
          </tr>
        </thead>
        <tbody>
          {list.map((item) => {
            return (
              <tr key={item.id}>
                <td>{item.id}</td>
                <td>{item.name}</td>
              </tr>
            )
          })}
        </tbody>
      </table>
      <div>
        <nav>
          {/* 分页组件 */}
          <ul className="pagination">
            {new Array(pageCount).fill(0).map((item, index) => {
              return (
                <li
                  className="page-item"
                  key={index}
                  onClick={() => {
                    setOptions({ ...options, currentPage: index + 1 })
                  }}
                >
                  <a className="page-link" href="#">
                    {index + 1}
                  </a>
                </li>
              )
            })}
          </ul>
        </nav>

        {/* 下拉框 */}
        <select 
          className="form-select"
          value={size}
          onChange={(e) => {setSize(e.target.value); setOptions({currentPage: 1, pageSize: e.target.value})}}
        >
          <option value={5}>5</option>
          <option value={10}>10</option>
          <option value={20}>20</option>
        </select>
      </div>
    </>
  )
}

13.1 useState

useState的用法很简单也很方便,比起class的方式简直要简洁得不少

import React, { useState } from 'react';

export default function Example() {
  const [count, setCount] = useState(0)
  const [fruit, setFruit] = useState('banana')
  return (
    <div>
      你点击了{count}次
      <button onClick={() => setCount(count+1)}>点击</button>
      <p>{fruit}</p>
    </div>
  )
}

如上例子所示,useState(0),0表示初始值,而将useState解构成两个成员,一个是状态属性,一个是更改状态的函数,其中useState有多个,这样子就比class的形式要简洁多了。

13.2 useEffect

你之前可能已经在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。

useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 具有相同的用途,只不过被合并成了一个 API。

例如,下面这个组件在 React 更新 DOM 后会设置一个页面标题:

import React, { useState, useEffect } from 'react';

export default function Example() {
  const [count, setCount] = useState(0)
  const [fruit, setFruit] = useState('banana')
  //相当于componentDidMount 和 componentDidUpdate
  useEffect(() => {
    // 使用浏览器的API更新页面标题
    document.title = `您点击了 ${count} 次`
  })

  return (
    <div>
      你点击了{count}次
      <button onClick={() => setCount(count+1)}>点击</button>
    </div>
  )
}

当你调用 useEffect 时,就是在告诉 React 在完成对 DOM 的更改后运行你的“副作用”函数。由于副作用函数是在组件内声明的,所以它们可以访问到组件的 props 和 state。默认情况下,React 会在每次渲染后调用副作用函数 —— 包括第一次渲染的时候

副作用函数还可以通过返回一个函数来指定如何“清除”副作用。例如,在下面的组件中使用副作用函数来订阅好友的在线状态,并通过取消订阅来进行清除操作

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);    return () => {      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);    };  });
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

useEffect模拟重新发送请求

因为useEffect会在组件更新时调用,所以我们可以维护一个状态,当这个状态改变,我们调用函数重新发送请求。

import React, { useState, useEffect } from 'react';

export default function Example() {
  const [count, setCount] = useState(0)
  const [flag, setFlag] = useState(false)
    
  function request() {
      setCount(++count)
  }
    
  useEffect(() => {
      request()
  },[flag])
  

  return (
    <div>
      你点击了{count}次
      <button onClick={() => setFlag(!flag)}>点击</button>
    </div>
  )
}

13.2.1 useEffect的优化点

当然,useEffectuseState一样可以同时存在多个,还可以指定每次副作用函数只对那些起作用,和指定那些副作用函数只执行一次,因为比如我们要在useEffect中异步获取数据,我们总不能每次渲染操作都获取一次,因此有下面的解决方案:

import React, { useState, useEffect } from 'react';

export default function Example() {
  const [count, setCount] = useState(0)
  const [fruit, setFruit] = useState('banana')
  //相当于componentDidMount 和 componentDidUpdate
  useEffect(() => {
    // 使用浏览器的API更新页面标题
    // 该副作用函数只对count和fruit起作用
    document.title = `您点击了 ${count} 次`
    console.log(count)
  }, [fruit, count])
  
  // 只会调用一次
  useEffect(() => {
    console.log('异步获取数据操作')
  },[])

  return (
    <div>
      你点击了{count}次
      <button onClick={() => setCount(count+1)}>点击</button>
    </div>
  )
}

如上所示,我们可以在useEffect传入一个数组参数,里面为空则表示只执行一次,如果里面有状态则表示只对里面的状态起作用。

image-20210514205707734

为什么要在 effect 中返回一个函数? 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。

React 何时清除 effect? React 会在组件卸载的时候执行清除操作,相当于类组件的componentWillUnmount,也会在每次组件更新之前调用该回调,相当于componentWillUpdate。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React 在执行当前 effect 之前对上一个 effect 进行清除。

13.2.2 小结

了解了 useEffect 可以在组件渲染后实现各种不同的副作用。有些副作用可能需要清除,所以需要返回一个函数:

 useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

其他的 effect 可能不必清除,所以不需要返回。

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

useContext(补充)

我们通过createContext创建一个Context之后,在子组件可以通过useContext拿到我们在祖先组件创建的Context,但是前提是我们需要对子组件用Context.Provider标签进行包裹,然后通过value属性传值。

这里需要注意 const states = useContext(Context)只能在子组件内部调用,不可在顶级元素使用。

例子如下:

import {createContext, useContext} from 'react'

const Context = createContext()

function Child() {
  const states = useContext(Context)
  return (
    <div>
      {states.map(state => {
        return (
        <h2>{state}</h2>
        )
      })}
    </div>
  )
}

function App() {
  return (
    <div className="App">
      <Context.Provider value={[1,2,3]}>
        <Child></Child>
      </Context.Provider>
    </div>
  );
}

export default App;

在这里我们通过Context.Provider传输了一个数字数组到子代组件,子代组件通过const states = useContext(Context)获取到父组件传过来的数据并进行展示。

优化

假如我们要给多个子组件提供同一份数据,那么我可以将提供数据的父组件给抽离成一个单独的负责提供数据的组件:

import {createContext, useState} from 'react'

export const Context = createContext()

export function CountContextProvider({children}) {
  let [count, setCount] = useState(10)

  const countObj = {
    count,
    add() {
      setCount(++count)
    },
    minus() {
      setCount(--count)
    }
  }

  return (
    <Context.Provider value={countObj}>
      {children}
    </Context.Provider>
  )
}

子组件:

import {useContext} from 'react'
import {Context, CountContextProvider} from './CountContextProvider'

function CountChild() {
  const {count, add, minus} = useContext(Context)
  return (
    <div>
      <h2>{count}</h2>
      <button onClick={() => {add()}}>增加</button>
      <button onClick={() => {minus()}}>减少</button>
    </div>
  )
}


let r = () => {
  return (
    <CountContextProvider>
      <CountChild/>
    </CountContextProvider>
  )
}

export default r

14. Context

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

因为典型的React应用数据是通过Props属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

总体步骤

1.creareContext 

2.MyContext.Provider包裹通过value传值 

3.在需要该值的组件static contextType = ThemeContext;接收context

14.1 使用Context之前

何时使用 Context

Context的目的是共享那些应该在全局共享的数据,例如当前用户的Token数据,或者首选语言。举个例子,在下面的代码中,我们通过一个 “theme” 属性手动调整一个按钮组件的样式:

class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />;
  }
}

function Toolbar(props) {
  // Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。
  // 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
  // 因为必须将这个值层层传递所有组件。
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

class ThemedButton extends React.Component {
  render() {
    return <Button theme={this.props.theme} />;
  }
}

使用 context, 我们可以避免通过中间元素传递 props:

// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');
class App extends React.Component {
  render() {
    // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。    
    // 无论多深,任何组件都能读取这个值。    
      // 在这个例子中,我们将 “dark” 作为当前的值传递下去。    
      return (
      <ThemeContext.Provider value="dark">        
              <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {  
    return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // 指定 contextType 读取当前的 theme context。  
  // React 会往上找到最近的 theme Provider,然后使用它的值。  
  // 在这个例子中,当前的 theme 值为 “dark”。  
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;  
  }
}

使用Context之前应该有什么考虑?

使用Context之前我们必须清楚,Context会使得组件的复用性变差,它主要应用场景在于很多不同层级的组件需要访问同样一些的数据,请谨慎使用。

如果只是为了避免层层传递一些属性,组件组合有时候是比Context更好的解决方案:

如:

<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<Link href={user.permalink}>
  <Avatar user={user} size={avatarSize} />
</Link>

当我们发现user只在Avatar使用到但其他上层组件都要层层传递的时候,代码会变得非常冗余。解决方案除了用Context之外,我们还可以将该Avatar组件自身传递下去,这样中间组件无需知道 user 或者 avatarSize 等 props:

function Page(props) {
  const user = props.user;
  const userLink = (
    <Link href={user.permalink}>
      <Avatar user={user} size={props.avatarSize} />
    </Link>
  );
  return <PageLayout userLink={userLink} />;
}

// 现在,我们有这样的组件:
<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<PageLayout userLink={...} />
// ... 渲染出 ...
<NavigationBar userLink={...} />
// ... 渲染出 ...
{props.userLink}

这种对组件的控制反转减少了在你的应用中要传递的 props 数量,这在很多场景下会使得你的代码更加干净,使你对根组件有更多的把控。但是,这并不适用于每一个场景:这种将逻辑提升到组件树的更高层次来处理,会使得这些高层组件变得更复杂,并且会强行将低层组件适应这样的形式,这可能不会是你想要的。

而且你的组件并不限制于接收单个子组件。你可能会传递多个子组件,甚至会为这些子组件(children)封装多个单独的“接口(slots)

14.2 Context用法

Context可以有三种用法

14.2.1 Consumer(函数组件)

就是通过Consumer包裹需要拿到值的元素,这样子那个元素就能拿到上下文的值了

import React from 'react'

function Child(props) {
  return (
  <div>Child: {props.foo}</div>
  )
}

const MyContext = React.createContext()
const { Provider, Consumer } = MyContext;

export default function ContextTest() {
  return (
    <div>
      <Provider value={{foo: 'bar'}}>
        <Consumer>
          { value => <Child {...value}></Child> }
        </Consumer>
      </Provider>
    </div>
  )
}

Provider设置值value,然后Consumer就可以拿到值,Consumer内部组件可以通过Props.children格式为带参函数的形式拿到value,然后让对应组件渲染结果。

14.2.2 Hook取值

Hook是一种很方便的存在,它让我们不用通过Consumer就可以很简便的取到上级传给我们的值,但是有版本限制,必须

import React, {useContext} from 'react'
const MyContext = React.createContext()
const { Provider, Consumer } = MyContext;

function Child2() {
  const value = useContext(MyContext)
  return (
  <div>Child2: {value.foo}</div>
  )
}

export default function ContextTest() {
  return (
    <div>
      <Provider value={{foo: 'bar'}}>
        <Child2></Child2>
      </Provider>
    </div>
  )
}

14.2.3 Class取值(类组件)

import React from 'react'
const MyContext = React.createContext()
const { Provider, Consumer } = MyContext;

class Child3 extends React.Component {
  static contextType = MyContext
  render() {
    return (
      <div>Child3: {this.context.foo}</div>
    )
  }
}

export default function ContextTest() {
  return (
    <div>
      <Provider value={{foo: 'bar'}}>
        <Child3></Child3>
      </Provider>
    </div>
  )
}

14.3 总结

总共有三种方法取到Context的值,如下所示:

import React, {useContext} from 'react'
const MyContext = React.createContext()
const { Provider, Consumer } = MyContext;

// Consumer取值
function Child(props) {
  return (
  <div>Child: {props.foo}</div>
  )
}

//  Hook取值
function Child2() {
  const value = useContext(MyContext)
  return (
  <div>Child2: {value.foo}</div>
  )
}

// 类的方式指定静态属性contextType,这样子上下文就会自动有个context
class Child3 extends React.Component {
  static contextType = MyContext
  render() {
    return (
      <div>Child3: {this.context.foo}</div>
    )
  }
}

export default function ContextTest() {
  return (
    <div>
      <Provider value={{foo: 'bar'}}>
        {/* Consumer包裹元素 */}
        <Consumer>
          { value => <Child {...value}></Child> }
        </Consumer>
        <Child2></Child2>
        <Child3></Child3>
      </Provider>
    </div>
  )
}

15. Form表单组件设计

高阶组件设计功能目的:

1、给input框进行包装,从而对input事件进行管理

2、把触发的事件收集的数据放在一起,按照参数传进来的规则进行校验

3、可以提供给当前表单一个校验方法,用来判断当前的校验是否全都通过,从而决定是否提交表单信息,或者不提交

1的思路:

对input框进行包装,也就是用高阶组件进行包装,在此我们需要谨记不能对vdom进行扩展,而是通过React.cloneElement克隆组件,再进行扩展,扩展内容包括:

  • 设置name属性

  • 设置value值(默认为空串)

  • 绑定onChange事件并让包装器进行管理,也就是状态提升(当子组件发生某件事的时候,让父组件处理),这样子我们就可以再父组件中管理所有input的值

  • 在父组件的handleChange中,我们通过解构方式获取name和value,然后进行setState改变值,这样子render函数就会重新执行

当然,在setState之后我们需要进行验证,这里我们需要注意setState可能是异步的,我们的校验函数不能直接放在setState之后,而是放置在setState执行后的回调函数内。

2的思路

image-20210330150832948

数据收集我们可以放在input包装器内,因为包装器传入了两个参数,第一个是校验字段,第二个是参数选项,在包装器进行克隆元素的时候我们可以缓存这些数据。

缓存步骤:

  • 将第二个参数值缓存到包装器上下文中,校验时可用到
  • 设置name属性为传进来的第一个参数field,将value初始化为空并放到state中,等待状态变化而改变

3的思路

在最外面的包装组件内,定义一个校验字段函数,函数首先获取2所缓存的校验规则,然后用数组的some方法遍历每一项rule,如果有一项不符合就设置错误信息并返回false,否则将错误信息置空。

接着再定义一个校验函数,作用就是对每个input项进行校验,可以通过Object.keys拿出缓存的options再进行map对每个项都进行校验字段。

最后拿到的是一个bool数组,通过数组的every方法判断是否校验成功,如果成功则调用回调函数,并传入成功标识符和相应数据。

最后要展示错误信息,通过判断当前字段的错误信息是否存在,如果存在则显示错误信息,否则不展示。

import React from 'react'
import {Button, Input} from 'antd'

// 创建一个高阶组件:扩展现有a表单,事件处理、数据收集、校验功能
function kFormCreate(Comp) {
  return class extends React.Component {

    constructor(props) {
      super(props);
      this.state = {}
      this.options = {}
    }

    // 当值变化,改变state值
    handleChange = e => {
      const {name, value} = e.target
      console.log(name, value)

      this.setState({
        [name]: value
      },// 设置完值在做验证
      () => this.validateField(name))
    }

    validate = cb => {
      // 对所有Input框都进行验证
      const rets = Object.keys(this.options).map(field => this.validateField(field))
      const ret = rets.every(v => v === true)
      cb(ret, this.state)
    }
    
    // 对单项input进行规则验证
    validateField = field => {
      const rules = this.options[field].rules
      
      // some找到一个验证不通过的规则就不再继续遍历
      const ret = rules && !rules.some(rule => {
        if(rule.required && !this.state[field]) {
          // 验证失败,我们要设置错误信息
          this.setState({
            [field+'Message']:'该字段不能为空'
          })
          return true
        }
        return false
      })
      
      // 如果验证成功,错误信息为空串, 
      if(ret){
        this.setState({
          [field+'Message']:''
        })
      }
      return ret
    }

    getFieldDec = (field, options) => {
      // 这里做了数据收集与状态提升到包装器内部处理
      // 保存当前输入配置项
      this.options[field] = options
      return InputComp => (
        <div>
          {React.cloneElement(InputComp, {
            name: field,
            value: this.state[field] || '',
            // 状态提升
            onChange: this.handleChange
          })}
          {this.state[field+'Message'] && <p style={{color: 'red'}}>{this.state[field+'Message']}</p>}
        </div>
      )
    }

    render() {
      return <Comp getFieldDec={this.getFieldDec} validate={this.validate}></Comp>
    }
  }
}

@kFormCreate
class KForm extends React.Component {

  onSubmit = e => {
    this.props.validate((isValid, data) => {
      if(isValid) {
        console.log("验证通过!")
        // 做登录相关的逻辑操作
      }
    })
  }

  render() {
    const { getFieldDec } = this.props
    return (
      <div>
        {getFieldDec("userName", {
          rules: [{ required: true, message: "Please input your username!" }]
        })(
          <Input/>
        )}
        <Button onClick={this.onSubmit}>提交</Button>
      </div>
    )
  }
}

export default KForm

16. Redux的使用

img

cnpm install redux --save

16.1 简单使用

在文件store.js内:

import {createStore} from 'redux'

const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case 'add': 
      return state + 1
    case 'minus':
      return state - 1
    default:
      return state
  }
}

const store = createStore(counterReducer)

export default store

我们通过createStore创建Store对象,参数为Reducer对象,state是当前store的属性值,可以是一个对象,在通过对action参数的type属性进行判断,修改state的值。

接下来我们在需要引用store里面的全局状态的组件引入store,在此之前我们先通过在控制台打印一下store到底是什么:

image-20210331210840761

如上所示,我们创建的Store对象包括了这么些个属性,我们可以通过getState方法获取state的值,也可以通过dispatch方法改变state的值,还有最后我们还要在页面渲染那里用subscribe进行订阅。

import React from 'react'
import store from '../store'

// 原始Store组件
function StoreTest() {
  return (
    <>
    <div>{store.getState()}</div>
    <div>
      <button onClick={() => store.dispatch({type: 'add'})}>+</button>
      <button onClick={() => store.dispatch({type: 'minus'})}>-</button>
    </div>
    </>
  )
}
export default StoreTest

如上所示,我们可以通过getState获取到前面store.jsstate值,再给绑定点击事件,执行dispatch改变state的值。

订阅

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import store from './store';

const render = () => {
  ReactDOM.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
    document.getElementById('root')
  );
}
render()
store.subscribe(render)

只有订阅了,store状态才能在随着页面渲染时发生改变,不然你将看不到效果。

16.2 思考

上面的原始方法存在一个问题:在需要共享全局状态的组件,我们都需要引入store.js,这样子显得很繁杂,那么要如何改变这个问题呢?

还记得我们之前可以通过上下文进行隔代传值吗?是的,只要我们把store通过Provider传给根组件,其他组件自然也就可以不用引入store.js了。为此,react已经帮我们做好了,我们可以通过react-redux依赖来进行相应的操作。

cnpm install react-redux --save

16.3 react-redux

Provider传入store

// index.js
import store from './store';
import {Provider} from 'react-redux'

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

在需要用到store里面状态的组件里

import React from 'react'
import store from '../store'
import {connect} from 'react-redux'


const mapStateToProps = state => ({num: state})

const mapDispatchToProps = {
    add: () => ({type: 'add'}),
    minus: () => ({type: 'minus'})
}

// 经过react-redux上下文处理的store组件
class StoreTest2 extends React.Component {
  render() {
    return (
      <>
        <div>{this.props.num}</div>
        <button onClick={() => this.props.add()}>+</button>
        <button onClick={() => this.props.minus()}>-</button>
      </>
    )
  }
}
export default connect(mapStateToProps, mapDispatchToProps)(StoreTest2)

在这里,我们使用了connect高阶组件,传入两个配置项,再对需要扩展的组件进行包装,注意这里的写法已经变了,逻辑已经被我们提到了外面,此时ShowTest2只是一个傻瓜式组件,不负责dispatch也不用调用getState方法,connect内部的两个配置项分别帮我们把state的值映射到被包装组件StoreTest2的内部,并放在props里面,这样子我们在组件内就可以直接通过props拿到statedispatch了。

16.4 装饰器简化

只是个语法糖

@connect(mapStateToProps, mapDispatchToProps)
class StoreTest3 extends React.Component {
  render() {
    return (
      <>
        <div>{this.props.num}</div>
        <button onClick={() => this.props.add()}>+</button>
        <button onClick={() => this.props.minus()}>-</button>
      </>
    )
  }
}

export default StoreTest3

16.5 处理异步

image-20210401102441651

如图所示,在处理action的时候会先经过中间件层的处理,中间件会识别传进来的action到底是个对象还是函数,如果是函数则表明是异步请求,要先执行完才能继续往下一步进行。

安装两个中间件

npm install redux-thunk redux-logger -S

redux-thunk用来处理异步请求,redux-logger是一个日志打印中间件。

使用

// store.js
import {createStore, applyMiddleware} from 'redux'
import thunk from 'redux-thunk'
import logger from 'redux-logger'


// reducer定义
...

// 中间件执行步骤:先logger再thunk
const store = createStore(counterReducer, applyMiddleware(logger, thunk))
export default store

需要进行异步操作的地方

import React from 'react'
import store from '../store'
import {connect} from 'react-redux'

const mapStateToProps2 = state => ({num: state})
const mapDispatchToProps2 =  {
  add: () => ({type: 'add'}),
  minus: () => ({type: 'minus'}),
  asyncAdd: () => dispatch => {
    setTimeout(() => dispatch({type: 'add'}), 1000)
  }
}

@connect(mapStateToProps2, mapDispatchToProps2)
class StoreTest4 extends React.Component {
  render() {
    return (
      <>
        <div>{this.props.num}</div>
        <button onClick={() => this.props.add()}>+</button>
        <button onClick={() => this.props.minus()}>-</button>
        <button onClick={() => this.props.asyncAdd()}>+</button>
      </>
    )
  }
}

export default StoreTest4

这里新增了一个asyncAdd方法,注意这里的写法是返回一个带dispatch参数的函数,而不像其他同步操作直接返回对象。

16.6 代码抽离

由于我们全局共享状态是有多种类型的,如用户的登录态、全局的主题、用户信息等,因此我们将所有全局共享状态都放置再store.js将会导致后期变得可维护差,为了使代码更具有可用性、易用性、可维护性。我们应该将每一部分的状态都放置在一个文件。

步骤

创建store文件夹,创建index.js为入口,将来通过combineReducers引入所有的状态,我们将上面的counterReducer和其相对于的action creator给分离到一个文件。

// store/counter.redux.js
export const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case 'add': 
      return state + 1
    case 'minus':
      return state - 1
    default:
      return state
  }
}

// action creator
export const add = () => ({type: 'add'})
export const minus = () => ({type: 'minus'})
export const asyncAdd = () => dispatch => {
  setTimeout(() => dispatch({type: 'add'}), 1000)
}

在入口文件引入

//  store/index.js
import {createStore, applyMiddleware} from 'redux'
import thunk from 'redux-thunk'
import logger from 'redux-logger'
import {counterReducer} from './counter.redux'

const store = createStore(
  counterReducer,
  applyMiddleware(logger, thunk)
)

export default store

这样就完成了简单的分离,接下来我们还要修改一下程序入口导入store的路径

// index.js
import store from './store/index'

在需要用到counter状态的地方

...
import { add, minus, asyncAdd } from '../store/counter.redux'

const mapStateToProps3 = state => ({num: state})
const mapDispatchToProps3 =  { add, minus, asyncAdd }

@connect(mapStateToProps3, mapDispatchToProps3)
class StoreTest5 extends React.Component {
  render() {
    return (
      <>
        <div>{this.props.num}</div>
        <button onClick={() => this.props.add()}>+</button>
        <button onClick={() => this.props.minus()}>-</button>
        <button onClick={() => this.props.asyncAdd()}>+</button>
      </>
    )
  }
}

export default StoreTest5

这里我们只需将对应的action creator导入,使得管理起来更加方便。实现了统一在store文件夹下的文件对所有共享状态进行管理,可维护性就瞬间提升了。

react-router

介绍

安装

npm install --save react-router-dom

起步

index.js

// ... 其他包导入
import {BrowserRouter} from 'react-router-dom'

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

如上所示,我们需要给根组件包裹一个BrowserRouter标签,这样子我们就可以在浏览器使用路由了。

react中,路由即组件,简单用法如下:

import { Link, Route } from 'react-router-dom'
function About() {
  return (<div>关于页</div>)
}

function Home() {
  return (<div>首页</div>)
}

export default function RouteTest() {
  return (
    <div>
      <ul>
        <Link to='/'>Home</Link>
        <Link to='/about'>About</Link>
      </ul>
      <div>
        <Route exact path='/' component={Home}></Route>
        <Route path='/about' component={About}></Route>
      </div>
    </div>
  )
}

Link作用类似于a标签,会在页面展示让用户点击,而Route就是一个真正的路由映射组件,如上所示,path指定访问路径,component指定要展示的组件。其中因为react-router的路由包容性(只要path匹配到,就会渲染),我们还需对path/的路由添加一个exact字段,这样子就只会精准匹配了。

Router对象

Route标签包裹的组件,传入的值就不是props了,而是一个路由器对象,我们可以打印看看路由器对象到底长啥样

image-20210401165044914

如上所示,有historylocationmatch三个主要属性

  // 1.history: 导航指令
  // 2.match: 获取参数信息
  // 3.location: 当前url信息

Switch

react路由是包容性的,也就是匹配到的路由都会被展示出来。但是我们可以通过Switch标签让react路由不再具有包容性,只展示匹配到的第一个路由:

import { Link, Route, Switch, NavLink } from 'react-router-dom'
import { BrowserRouter } from 'react-router-dom'

function About(router) {
  return <div>about</div>
}

function Home() {
  return <div>home</div>
}

function NotFound() {
  return (
    <div>
      <h2>not found</h2>
    </div>
  )
}

function App() {
  return (
    <div className="App">
      <BrowserRouter>
        {/* 内联样式 */}
        <div>
          <NavLink activeStyle={{color: 'blue'}} to="/home">Home</NavLink>
          <NavLink activeStyle={{color: 'red'}} to="/about">about</NavLink>
        </div>
        {/* 一个switch为一组,只会展示第一个被匹配的组件 */}
        <Switch>
          <Route component={Home} path="/home"></Route>
          <Route component={About} path="/about"></Route>
          <Route component={NotFound}></Route>
        </Switch>
        
      </BrowserRouter>
    </div>
  )
}

如上所示,我们让Switch包裹的路由为一组,在这一组中只匹配第一个匹配到的路由则停止继续匹配,可以注意到我们在最后放了一个NotFound路由,这也是路由找不到时可以写的写法,因为没有path,前面的匹配不到则NotFound一定会被匹配到。

Redirect

redirect就是重定向组件,有一个from和to属性

常用基础写法如下

{/* 将from 定向到 to */}
<Redirect from="/abc" to='/about'></Redirect>

<Redirect to='/home'></Redirect>

<Route path="/info" render={() => {
	return login ? <Info/> : <Redirect to="/home"></Redirect>
}}></Route>

子路由与动态路由

子路由

子路由部分包括动态路由,那么在react中如何编写子路由呢?

举个例子,比如我们想要在上面的About页嵌套子路由,写法可以参考下面

function About(params) {
  return (
    <div>
      <h3>个人中心</h3>
      <div>
        <Link to="/about/me">个人信息</Link>
        <Link to="/about/order">订单查询</Link>
      </div>
      <Switch>
        <Route path="/about/me" component={() => <div>Me</div>} />
        <Route path="/about/order" component={() => <div>order</div>} />
        <Redirect to="/about/me" />
      </Switch>
    </div>
  );
}

很简单,只要在About组件嵌套路由即可。

但是需要注意一点:

写子路由时父亲路由不能开启路由严格模式,即不能在父组件加个exact属性,否则一旦开启严格模式,子路由将不会被路由系统找到,从而匹配不到,如:

import { Link, Route, Switch, NavLink, Redirect } from 'react-router-dom'
import { BrowserRouter } from 'react-router-dom'

function AAA() {
  return (<h2>aaa</h2>)
}
function BBB() {
  return (<h2>bbb</h2>)
}

function About(router) {
  return (<div>
    <Link to="/about/aaa">a</Link>
    <Link to="/about/bbb">b</Link>

    <Route path="/about/aaa" component={AAA}></Route>
    <Route path="/about/bbb" component={BBB}></Route>
  </div>)
}

function Home() {
  return <div>home</div>
}


function App() {
  return (
    <div className="App">
      <BrowserRouter>
        {/* 内联样式 */}
        <div>
          <NavLink activeStyle={{color: 'blue'}} to="/home">Home</NavLink>
          <NavLink activeStyle={{color: 'red'}} to="/about">about</NavLink>
        </div>
        {/* 一个switch为一组,只会展示第一个被匹配的组件 */}
        <Switch>
          <Route exact component={Home} path="/home"></Route>
          <Route exact component={About} path="/about"></Route>
        </Switch>
        
      </BrowserRouter>
    </div>
  )
}

export default App

在这里我们对/about路由开启了严格模式,而About组件下有子组件,但是我们会发现访问子组件的时候,子组件并没有展示出来,这是因为/about路由开启了严格模式从而导致子路由匹配不到。

动态路由

动态路由对于在url传值的很简单,使用:参数即可,如果要通过query传值可以在Route加上相应的属性值,然后在对应组件的参数路由对象中的match字段便可拿到

import {Link, Route, Switch, Redirect} from 'react-router-dom'

function Detail(props) {
  return (<div>
    <ul>
      <li></li>
    </ul>
    详情页:{props.match.params.id}
  </div>)
}

<Link to='/detail'>Detail</Link>
<Route path='/detail/:id' component={Detail}></Route>

传参

react传参可以通过三种方式,可以通过params、query或者state对象进行传参

import { Link, Route, Switch, NavLink, Redirect } from 'react-router-dom'
import { BrowserRouter } from 'react-router-dom'
import { useState } from 'react'
import qs from 'querystring'


/**
 * 路由传参
 */


function Home(props) {
  let [data] = useState([
    {id: 1, title: '新闻'},
    {id: 2, title: '财经'},
    {id: 3, title: '体育'},
    {id: 4, title: '文化'},
  ])
  return (<div>
    <ul>
      {/* params传参 /home/:id/:title */}
      {/* {
        data.map(item => <Link key={item.id} to={`${props.match.path}/total/${item.id}/${item.title}`} >{item.title}</Link>)
      } */}
      {/* query传参 /home?id=1&title=weibo */}
      {/* {
        data.map(item => <Link key={item.id} to={`${props.match.path}/total?id=${item.id}&title=${item.title}`} >{item.title}</Link>)
      } */}
      {/* state参数 */}
      {
        data.map(item => <Link key={item.id} to={{pathname: `${props.match.path}/total`, state: {id: item.id, title: item.title} }} >{item.title}</Link>)
      }
      <Route path={`${props.match.path}/total`} component={Total}></Route>
    </ul>
  </div>)
}

// 子组件
function Total(props) {
  console.log(props)
  // params取值
  // let {id, title} = props.match.params
  // query取值
  // let queryStr = props.location.search
  // let queryObj = qs.parse(queryStr.slice(1))
  // console.log(queryObj)
  // state参数取值
  let stateObj = props.location.state
  console.log(stateObj)

  return (
    <div>total</div>
  )
}


function App() {
  return (
    <div className="App">
      <BrowserRouter>
        {/* 内联样式 */}
        <div>
          <NavLink activeStyle={{color: 'blue'}} to="/home">Home</NavLink>
        </div>
        {/* 一个switch为一组,只会展示第一个被匹配的组件 */}
        <Switch>
          <Route component={Home} path="/home"></Route>
        </Switch>
        
      </BrowserRouter>
    </div>
  )
}

export default App

Redux

image-20210401165044914

redux三大核心:

​ reducers:数据控制器,数据修改者

​ store: 数据仓库

​ action: 描述发生了什么的一个对象

redux的三大原则:

​ 单一数据源(只创建一个store)

​ state是只读的(不允许被直接修改,具体修改在reducer集中化处理)

​ reducer使用纯函数执行修改(有个固定的值,而不是在reducer里有而外的操作,如在reducer执行一些不确定的操作,例如发起ajax异步请求,这就违犯了redux的原则)

步骤学习:

首先引入redux的createStore

import {createStore} from 'redux'

然后我们需要创建一个store,createStore的第一个参数是reducersreducers结构如下

// 默认状态
let defaultState = {
  count: 0,
  name: 'cai'
}
// reducers
function reducers(state = defaultState, action) {
  console.log(action)
  switch(action.type) {
    case 'add':
      return {
        ...state,
        count: state.count+action.val,
        
      }
    default:
      return state
  }
}

需要注意的是,我们还需要给reducers的第一个参数state指定一个默认值。当我们通过判断action的type进行改变state的值时,我们需要通过对象展开运算符展开state的其他属性值,因为改变state的值是覆盖操作,所以我们必须要保持其他不需要改的属性不变,而...state不能写在后面,不然被修改的值会被覆盖。

接着传入createStore的第一个参数

//创建store
let store = createStore(reducers);

在视图里我们可以通过以下三个API来操作store

  • store.dispatch({type:"", ...}) --- 触发动作
  • store.subscribe(() => { this.setState() }) --- 订阅store(触发更新)
  • store.getState() --- 获取state

简单用法如下:

在这里需要注意的是,在react中,component要重新渲染的话,需要通过setState改变state的值或者改变props的值导致render函数重新执行,所以我们可以利用这点,结合store.subscribe订阅,更新视图。

export default class MyComp extends Component {
  componentDidMount() {
    store.subscribe(() => {
      this.setState({})
    })
  }
  // 同步请求
  addHandler = () => {
    store.dispatch({
      type: 'add',
      val: 2
    })
  }
  
  // 异步请求
  requsetHandler = () => {
    fetch("http://localhost:4000/api/citylist")
      .then(res => res.json())
      .then(res => {
        store.dispatch({type: 'request', data: res})
      })
  }
  
  render() {
    let {count} = store.getState()
    return (
      <div>
        <h2>{count}</h2>
        <button onClick={this.addHandler}>add</button>
      </div>
    )
  }
}

模块抽取

随着项目的逐渐扩大,为防止逻辑之间冗余,我们需要对actions、reducersstore进行单独的抽离。

以上面的例子为例:

新建一个store文件夹,里面包含index.js、reducers.js、actions.js

// index.js

import {createStore, applyMiddleware} from 'redux'
import reducers from './reducers'

// 中间件 redux-thunk
import thunk from 'redux-thunk'

//创建store
let store = createStore(reducers, applyMiddleware(thunk));

export default store

由于需要操作异步数据,我们需要引入redux-thunk中间件,放在createStore的第二个参数内。

// reducers.js
// 默认状态
let defaultState = {
  count: 0
}
// reducers
export default function reducers(state = defaultState, action) {
  console.log(action)
  switch(action.type) {
    case 'add':
      return {
        ...state,
        count: state.count+action.val,
        
      }
    default:
      return state
  }
}

在这里我们还把fetch整个操作给抽到了actions里面,意味着我们我们在调用的时候要用到store.dispatch(),在redux中,当我们dispatch的是一个函数的时候,则表明为异步操作,当dispatch的是一个对象则表明为同步操作,所以我们在fetch外面包裹了一层函数,且redux知道我们需要用到store.dispatch,所以他给我们封装了一个带dispatch的函数,我们可以直接使用,而不需要引入store

// actions.js
export const add = () => {
  return {
    type: 'add',
    val: 10
  }
}

// 异步的action需要返回一个函数
// 不需要用到store,redux知道我们要用到dispatch,作为参数传进来了
export const request = () => {
  return dispatch => {
    fetch("http://localhost:4000/api/citylist")
      .then(res => res.json())
      .then(res => {
        dispatch({type: 'request', data: res})
      })
  }
}

组件引用

import React, { Component } from 'react'
import store from '../store'
import {add, request} from '../store/actions'

export default class MyComp extends Component {
  componentDidMount() {
    store.subscribe(() => {
      this.setState({})
    })
  }

  addHandler = () => {
    store.dispatch(add())
  }

  requsetHandler = () => {
    store.dispatch(request())
  }
  render() {
    let {count} = store.getState()
    return (
      <div>
        <h2>{count}</h2>
        <button onClick={this.addHandler}>add</button>
        <h2>{1}</h2>
        <button onClick={this.requsetHandler}>request</button>
      </div>
    )
  }
}

常量提取

在编写actions的type时,reducers也会用到type,如果actions的type与reducers的type不一致,或者说拼写错了,那么reducers将不会触发,而且不会报错提醒你,这时候我们就需要用到常量提取,将type提取为常量,再引入使用。

如:

constants/index.js

export const ADD = 'add'
export const REQUEST = 'request'

actions.js

import {ADD, REQUEST} from './constants'

export const add = () => {
  return {type: ADD, val: 2}
}

export const request = () => {
  return dispatch => {
    fetch("http://localhost:4000/api/foodtype")
      .then(res => res.json())
      .then(res => dispatch({type: REQUEST, data: res}))
  }
}

reducers.js

import {REQUEST, ADD} from './constants'

let defaultState = {
  count: 0,
  name: 'cai'
}
export default function reducers(state = defaultState, action) {
  switch (action.type) {
    case ADD:
      ...
    case REQUEST:
      ...
    default:
      return state
  }
  
}

react-redux使用

在上面直接使用redux的时候我们会发现在组件内我们用到很多对store的逻辑操作,而React的组件应该尽量是Pure Component,我们应该把处理逻辑都给抽取出去,这时候react-redux的作用就来了。

思想:把组件变成Pure Component,我们再通过connect(mapStateToProps, mapDispatchToProps)(Comp)去连接store,让这个组件可以拿到store的数据进行展示。

这个时候就产生了两种组件区别:UI组件容器组件

容器组件在这里就是connect之后返回的组件,而UI组件就是只负责展示数据的组件。

用法示例

import React, { Component } from 'react'
import {add, request} from '../store/actions'
import {connect} from 'react-redux'


/**
 * UI组件
 */
class Comp2 extends Component {

  render() {
    console.log('this.props =========>', this.props);

    let {count, name, add, request} = this.props
    return (
      <div>
        <h2>{count}</h2>
        <button onClick={add}>add</button>
        <h2>{name}</h2>
        <button onClick={request}>request</button>
      </div>
    )
  }
}

/**
 * react-redux 容器组件
 */

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

let mapDispatchToProps = {
  add,
  request
}

export default connect(mapStateToProps, mapDispatchToProps)(Comp2)

由于我们没有再在UI组件内使用store.subscribe订阅,此时数据不会更新,所以我们可以通过改变props让组件重新渲染,connect第一个参数是mapStateToProps,是一个函数,负责将state映射到propsconnect第二个参数是mapDispatchToProps,是一个函数,负责将dispatch映射到props,这样子我们就能在UI组件内通过props拿到store的数据了,而且connect内部还帮我们订阅了store数据的变化,一但数据更新,props也会跟着更新,所以就会重新渲染。

简单总结一下:React 只负责页面渲染, 而不负责页面逻辑, 页面逻辑可以从中单独抽取出来, 变成 store,目的就是让组件尽量变成PureComponent,然后用connect让这个组件跟store联系,connect还会帮我们订阅store的变化。

进一步的模块分离

由于我们的actionsreducers是多个的,且不同组件会用到F:\学习\前端作品\JS学习\react\redux-pro\01-review\src\store\actions\request.js不同的actionsreducers,所以我们可以按照一个组件一个action | reducer或者一个功能一个action | reducer,这里采用的是后者。

action的抽取到不同文件很简单,只需将对应部分给抽取到对应文件,改变一下引入路径即可;而reducer的抽取,我们要用的时候还需要通过combineReducers将各个reducer给合并为一个reducers

新建store/reducers文件夹:

// count.js
import {ADD} from '../constants'

let defaultState = {
  count: 0,
}
export default function reducers(state = defaultState, action) {
  console.log("state ===> ", state)
  console.log("action ===> ", action)
  switch (action.type) {
    case ADD:
      return {
        ...state,
        count: state.count + action.val
      }
    default:
      return state
  }
  
}

// request.js
import {REQUEST_BEGIN, REQUEST_SUCCESS, REQUEST_FAIL} from '../constants'

let defaultState = {
  name: 'cai',
  isLoading: false,
  err: {errCode: 0, errMsg: undefined }
}
export default function reducers(state = defaultState, action) {
  console.log("state ===> ", state)
  console.log("action ===> ", action)
  switch (action.type) {
    case REQUEST_BEGIN:
      return {
        ...state,
        isLoading: true
      }
    case REQUEST_SUCCESS:
      return {
        ...state,
        data: action.data,
        isLoading: false,
        err: {errCode: 0, errMsg: ''}
      }
    case REQUEST_FAIL:
      return {
        ...state,
        err: {errCode: action.err[0], errMsg: action.err[1]},
        isLoading: false
      }
    default:
      return state
  }
  
}

// index.js
import countReducer from './count'
import requestReducer from './request'
import {combineReducers} from 'redux'


export default combineReducers({
  count: countReducer,
  request: requestReducer
})

这里需要注意:由于我们将reducers给分离了,我们combineReducers的时候分别给counrReducerrequestReducer给取了个别名countrequest,我们在取值的时候需要加上命名空间:

之前的mapStateToProps写法:

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

现在:

function mapStateToProps(state) {

  return {
    count: state.count.count,
    name: state.request.name,
  }
}

新建store/actions文件夹:

// count.js
import {ADD} from '../constants'

export const add = () => {
  return {type: ADD, val: 2}
}

// request.js
import { REQUEST_BEGIN, REQUEST_SUCCESS, REQUEST_FAIL } from '../constants'

export const request = () => {

  return dispatch => {
    // 请求前, isLoading --> true
    dispatch({type: REQUEST_BEGIN, isLoading: true})

    fetch("http://localhost:4000/api/foodtype")
      .then(res => res.json())
      .then(res => {
        // 请求成功
        return dispatch({type: REQUEST_SUCCESS, data: res})
      })
      .catch(err => {
        // 请求失败
        return dispatch({type: REQUEST_FAIL, err: [1, err.message]})
      })
  }
}

另外的:

在 根组件APP中我们需要提供store作为props传入需要用到react-redux的组件。

方式一:直接传入

<div className="App">
	<Comp1 store={store}></Comp1>
	<Count store={store}></Count>
</div>

方式二:通过Provider统一传入(推荐方式)

import {Provider} from 'react-redux'

<div className="App">
	<Provider store={store}>
		<Comp1></Comp1>
		<Count></Count>
	</Provider>
</div>

gitee链接https://gitee.com/cai-lunduo/react-redux

路由导航守卫(重点)

路由导航守卫是我们编写路由不可避免的一个部分,那么我们在react如何编写路由导航呢?

答案:通过高阶组件扩展实现

现在我想实现一个常见的功能:访问About页的时候判断用户是否处于登录态,如果是登录态则让他访问About页,如果没登陆则重定向到Login页。

我们一步步来,既然我们要判断用户是否处于登录态,由于登录态需要全局共享,我们需要用到redux,则我们可以在store下创建一个user.redux.js,管理用户的登录状态,注意,我们login动作也放在了这里,将来使用需要导入。

const initial = {
  isLogin: false,
  loading: false,
};

export default (state = initial, action) => {
  switch (action.type) {
    case 'requestLogin':
      return {
        isLogin: false,
        loading: true,
      }
    case 'login': 
      return {
        isLogin: true,
        loading: false,
      }
    default:
      return state
  }
}

// action creator
export const login = () => dispatch => {
  dispatch({type: 'requestLogin'})
  setTimeout(() => {
    dispatch({type: 'login'})
  }, 3000)
}

store/index.js下引入这个Reducer,并通过combineReducers来合并所有的reducer

// store/index.js
import {createStore, applyMiddleware, combineReducers} from 'redux'
import thunk from 'redux-thunk'
import logger from 'redux-logger'
import {counterReducer} from './counter.redux'
import user from './user.redux'

const store = createStore(
  combineReducers({counterReducer, user}),
  applyMiddleware(logger, thunk)
)

export default store

第一步完成,我们已经将用户的登录态放在全局进行管理。

接下来,我们先要创建一个自定义的组件,内部其实也是返回,但是我们将要对其功能进行增强。

function PrivateRoute({ component:Comp, isLogin, ...rest }) {
  return (
    <Route 
    {...rest} 
    render={props => 
        isLogin ? (<Comp/>) : <Redirect to={{ 
          pathname: '/login', 
          state: { redirect: props.location.pathname} 
        }}/> 
      }
    />
  )
}

在这里我们做了这些工作:

1. 解构props为component, isLogin, ...rest
单独解构出component的目的:
	1. 避免component属性传入Route,不然render将不生效!因为component优先级比render高
	2. 给component重命名,因为react里面的组件是大写字母开头的
2. isLogin怎么来的?
	我们等下会通过connect工厂函数链接react和redux,映射用户的登录状态到PrivateRoute的props中
3. 为什么要...rest?
	因为我们原本的<Route/>要接收path等相关参数,我们在这里直接把这些参数搬过来放在我们将要返回的组件中
4. 返回全新组件<Route/>,在这个组件中,render是一个接收props的函数,我们在这判断用户的登录态,并返回不同的组件
	1.如果用户已登录,则返回用户访问的About页面对应的组件
	2.否则redirect到login页面,并给to对象里面设置一个state.redirect属性,将来登陆成功则自动定向到这个redirect去

Login组件

function Login({location, isLogin, login}) {

  const redirect = location.state && location.state.redirect || "/";

  if(isLogin) {
    return <Redirect path={redirect}/>
  }
  return (<div><button onClick={() => login()}>登录</button></div>)
}

这里我们从location拿到重定向的地址,为将来登陆成功时指定回跳地址。

至此我们第二步完成了。

现在最后一步就是将全局的用户登录态给注入LoginPrivateRoute中,通过

connect(配置项)(被加工组件)的方式注入

// ...其他包导入
import {login} from '../store/user.redux'
import {connect} from 'react-redux'

const Login = connect(
  state => ({isLogin:state.user.isLogin}),
  {login}
 )(function({location, isLogin, login}) {

  const redirect = location.state && location.state.redirect || "/";

  if(isLogin) {
    return <Redirect path={redirect}/>
  }
  return (<div><button onClick={() => login()}>登录</button></div>)
})

// 路由导航守卫
const PrivateRoute = connect(state => ({isLogin: state.user.isLogin}))(({ component:Comp, isLogin, ...rest }) => {
  return (
    <Route 
    {...rest} 
    render={props => 
        isLogin ? (<Comp/>) : <Redirect to={{ 
          pathname: '/login', 
          state: { redirect: props.location.pathname} 
        }}/> 
      }
    />
  )
})

用法:

<PrivateRoute path='/about' component={About}></PrivateRoute>

redux-saga

redux-saga 是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的 library,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易。

redux-saga内部用到了ES6的Generator功能,使得我们异步流程可以像简单的操作同步代码一样(步骤看起来像async/await),但是Generator的功能比async/await更加强大,同时可测试性还增强了。

初级教程:https://redux-saga-in-chinese.js.org/docs/introduction/BeginnerTutorial.html

如果对redux-saga没有过了解,请点击上述链接到官方文档去看入门教程,写的非常详细也很容易明白。

你可能已经用了 redux-thunk 来处理数据的读取。不同于 redux thunk,你不会再遇到回调地狱了,你可以很容易地测试异步流程并保持你的 action 是干净的。

接下来我会以一个用户登录验证的功能来讲述redux-saga

store/user.redux.js,redux中用户的登录态,我们给state设置三个状态,以此来判断用户的登录状态和展示错误信息。

const initial = {
  isLogin: false,
  loading: false,
  error: ''
};

export default (state = initial, action) => {
  switch (action.type) {
    case 'requestLogin':
      return {
        isLogin: false,
        loading: true,
        error: ''
      }
    case 'login': 
      return {
        isLogin: true,
        loading: false,
        error: ''
      }
    case 'requestError':
      return {
        isLogin: false,
        loading: false,
        error: action.message
      }
    default:
      return state
  }
}

// action creator for redux-thunk
// export const login = () => dispatch => {
//   dispatch({type: 'requestLogin'})
//   setTimeout(() => {
//     dispatch({type: 'login'})
//   }, 3000)
// }

export function login(uname) {
  return { type: 'login', uname}
}

上面代码中, 被注释的代码是以前写redux-thunk的时候所遗留的,可以观察到redux-thunk他在执行异步函数时需要返回一个函数,而在redux-saga中,只需返回一个plain Object对象即可,(plain Object:纯粹的对象(通过 "{}" 或者 "new Object" 创建的))

接下来我们编写sagas.js

import { put, call, takeEvery } from 'redux-saga/effects'

const UserService = {
  login(uname) {
    return new Promise((resolve, reject) => {
      // 模拟异步
      setTimeout(() => {
        if(uname === 'cai') {
          resolve({id:1, name:'cai', mobile:'13226752873'})
        }else {
          reject('用户名或密码错误')
        }
      },2000)
    })
  }
}

function* login(action) {
  try {
    // 标识开始请求
    yield put({type: 'requestLogin'})
    const result = yield call(UserService.login, action.uname)
    // 登录成功
    yield put({type: 'login', result})
  } catch(message) {
      // 登录失败,将错误信息传入action,让user的reducer能够接收,为后续map到页面的props做准备
    yield put({type: 'requestError', message})
  }
}

function* mySaga() {
  yield takeEvery('login', login)
}


export default mySaga

如上所示,我们使用了putcalltakeEvery工具函数,按我个人理解:

call : 用来通知saga中间件执行异步函数的工具方法,最后会拿到异步函数执行后返回的数据

put : 用来通知saga中间件向redux进行dispatch操作,是同步的

takeEvery : 用来监听action,相当于进行了一层拦截, 然后再进行自己定义的操作,如上例所示,我们监听了login的调用,一旦login函数被触发,则会被takeEvery拦截到,然后进行我们自己定义的Generator,也就是function* login(action) 。

需要注意一点的是,dispatch并不由call和put派发,而是通知saga中间件让他进行dispatch

接下来在store的入口文件对中间件进行注册和run一下我们自定义的mySaga,因为这样子takeEvery才可以在全局对login事件进行监听并派发相应动作。

// 其他包导入
import user from './user.redux'
import createSagaMiddleware from 'redux-saga'
import MySaga from './sagas'

// 1. 创建saga中间件
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
  combineReducers({..., user}),
  // 2. 注册saga中间件
  applyMiddleware(sagaMiddleware)
)
// 3. 让我们的saga在全局监听
sagaMiddleware.run(MySaga)

export default store

在登录组件

const LoginForSaga = connect(
  state => ({
    // 将user的redux给map到当前组件的props
    isLogin: state.user.isLogin,
    error: state.user.error
  }),
  {login}
  )(function({location, isLogin, login, error}) {

  const redirect = location.state && location.state.redirect || "/";
  const [uname, setUname] = useState('')

  if(isLogin) {
    return <Redirect path={redirect}/>
  }
  return (
    <div>
      {/* 错误信息展示 */}
      {error && <p>{error}</p>}
      <input type="text" onChange={e => setUname(e.target.value)}/>
      {/* redux-saga */}
      <button onClick={() => login(uname)}>登录</button>
    </div>)
})

umi版本2

参考文档:https://umijs.org/docs

介绍

Umi,中文可发音为乌米,是可扩展的企业级前端应用框架。Umi 以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。

Umi 是蚂蚁金服的底层前端框架,已直接或间接地服务了 3000+ 应用,包括 java、node、H5 无线、离线(Hybrid)应用、纯前端 assets 应用、CMS 应用等。他已经很好地服务了我们的内部用户,同时希望他也能服务好外部用户。

特性

  • 开箱即用:内置 SSR,一键开启,umi dev 即 SSR 预览,开发调试方便。

  • 服务端框架无关:Umi 不耦合服务端框架(例如 Egg.jsExpressKoa),无论是哪种框架或者 Serverless 模式,都可以非常简单进行集成。

  • 支持应用和页面级数据预获取:Umi 3 中延续了 Umi 2 中的页面数据预获取(getInitialProps),来解决之前全局数据的获取问题。

  • 支持按需加载:按需加载 dynamicImport 开启后,Umi 3 中会根据不同路由加载对应的资源文件(css/js)。

  • 内置预渲染功能:Umi 3 中内置了预渲染功能,不再通过安装额外插件使用,同时开启 ssrexportStatic,在 umi build 构建时会编译出渲染后的 HTML。

  • 支持渲染降级:优先使用 SSR,如果服务端渲染失败,自动降级为客户端渲染(CSR),不影响正常业务流程。

  • 支持流式渲染ssr: { mode: 'stream' } 即可开启流式渲染,流式 SSR 较正常 SSR 有更少的 TTFB(发出页面请求到接收到应答数据第一个字节所花费的毫秒数) 时间。

  • 兼容客户端动态加载:在 Umi 2 中同时使用 SSR 和 dynamicImport(动态加载)会有一些问题,在 Umi 3 中可同时开启使用。

  • SSR 功能插件化:Umi 3 内置的 SSR 功能基本够用,若不满足需求或者想自定义渲染方法,可通过提供的 API 来自定义。

    image-20210401165044914

    image-20210401165044914

Dva 是基于 React + Redux + Saga 的最佳实践沉淀, 做了 3 件很重要的事情, 大大提升了编码体验:

  1. 把 store 及 saga 统一为一个 model 的概念, 写在一个 js 文件里面

  2. 增加了一个 Subscriptions, 用于收集其他来源的 action, eg: 键盘操作

  3. model 写法很简约, 类似于 DSL 或者 RoR, coding 快得飞起✈️

与其他框架对比

create-react-app 是基于 webpack 的打包层方案,包含 build、dev、lint 等,他在打包层把体验做到了极致,但是不包含路由,不是框架,也不支持配置。所以,如果大家想基于他修改部分配置,或者希望在打包层之外也做技术收敛时,就会遇到困难。

next.js 是个很好的选择,Umi 很多功能是参考 next.js 做的。要说有哪些地方不如 Umi,我觉得可能是不够贴近业务,不够接地气。比如 antd、dva 的深度整合,比如国际化、权限、数据流、配置式路由、补丁方案、自动化 external 方面等等一线开发者才会遇到的问题。

dva与umi的约定

.
├── dist/                          // 默认的 build 输出目录
├── mock/                          // mock 文件所在目录,基于 express
├── config/
    ├── config.js                  // umi 配置,同 .umirc.js,二选一
└── src/                           // 源码目录,可选
    ├── layouts/index.js           // 全局布局
    ├── pages/                     // 页面目录,里面的文件即路由
        ├── .umi/                  // dev 临时目录,需添加到 .gitignore
        ├── .umi-production/       // build 临时目录,会自动删除
        ├── document.ejs           // HTML 模板
        ├── 404.js                 // 404 页面
        ├── page1.js               // 页面 1,任意命名,导出 react 组件
        ├── page1.test.js          // 用例文件,umi test 会匹配所有 .test.js 和 .e2e.js 结尾的文件
        └── page2.js               // 页面 2,任意命名
    ├── global.css                 // 约定的全局样式文件,自动引入,也可以用 global.less
    ├── global.js                  // 可以在这里加入 polyfill
    ├── app.js                     // 运行时配置文件
├── .umirc.js                      // umi 配置,同 config/config.js,二选一
├── .env                           // 环境变量
└── package.json

umi基本使用

npm init
npm install umi -D

新建index页

umi g page index

启动服务器

umi dev

路由

基础路由

umi是支持约定式路由与配置式路由的,默认为约定式路由,约定式就是会根据pages目录自动生成路由配置。

假设pages目录结构如下:

+ pages/
  + users/
    - index.js
    - list.js
  - index.js

则umi会自动生成路由配置如下:

[
  { path: '/', component: './pages/index.js' },
  { path: '/users/', component: './pages/users/index.js' },
  { path: '/users/list', component: './pages/users/list.js' },
]

若.umirc.(ts|js)或者config/config.(ts|js)文件对router进行了配置,约定式路由将改为配置式路由,新的页面将不会自动被umi编译

动态路由

umi里规定以$开头的文件或目录为动态路由

如以下目录结构:

+ pages/
  + $post/
    - index.js
    - comments.js
  + users/
    $id.js
  - index.js

会生成的路由配置如下:

[
  { path: '/', component: './pages/index.js' },
  { path: '/users/:id', component: './pages/users/$id.js' },
  { path: '/:post/', component: './pages/$post/index.js' },
  { path: '/:post/comments', component: './pages/$post/comments.js' },
]
可选的动态路由

umi 里约定动态路由如果带 $ 后缀,则为可选动态路由。

比如以下结构:

+ pages/
  + users/
    - $id$.js
  - index.js

生成的路由配置如下:

[
  { path: '/': component: './pages/index.js' },
  { path: '/users/:id?': component: './pages/users/$id$.js' },
]
嵌套路由

umi 里约定目录下有 _layout.js 时会生成嵌套路由,以 _layout.js 为该目录的 layout 。

比如以下目录结构:

+ pages/
  + users/
    - _layout.js
    - $id.js
    - index.js

生成的路由配置如下:

[
  { path: '/users', component: './pages/users/_layout.js',
    routes: [
     { path: '/users/', component: './pages/users/index.js' },
     { path: '/users/:id', component: './pages/users/$id.js' },
   ],
  },
]
404 路由

约定 pages/404.js 为 404 页面,需返回 React 组件。

比如

export default () => {
  return (
    <div>I am a customized 404 page</div>
  );
};

注意:开发模式下,umi 会添加一个默认的 404 页面来辅助开发,但你仍然可通过精确地访问 /404 来验证 404 页面。

通过注释扩展路由

约定路由文件的首个注释如果包含 yaml 格式的配置,则会被用于扩展路由。

比如:

+ pages/
  - index.js

如果 pages/index.js 里包含:

/**
 * title: Index Page
 * Routes:
 *   - ./src/routes/a.js
 *   - ./src/routes/b.js
 */

则会生成路由配置

[
  { path: '/', component: './index.js',
    title: 'Index Page',
    Routes: [ './src/routes/a.js', './src/routes/b.js' ],
  },
]
配置式路由

如果你倾向于使用配置式的路由,可以配置 .umirc.(ts|js) 或者 config/config.(ts|js) 配置文件中的 routes 属性,此配置项存在时则不会对 src/pages 目录做约定式的解析

比如:

export default {
  routes: [
    { path: '/', component: './a' },
    { path: '/list', component: './b', Routes: ['./routes/PrivateRoute.js'] },
    { path: '/users', component: './users/_layout',
      routes: [
        { path: '/users/detail', component: './users/detail' },
        { path: '/users/:id', component: './users/id' }
      ]
    },
  ],
};

注意:

  1. component 是相对于 src/pages 目录的
权限路由

umi 的权限路由是通过配置路由的 Routes 属性来实现。约定式的通过 yaml 注释添加,配置式的直接配上即可。

比如有以下配置:

[
  { path: '/', component: './pages/index.js' },
  { path: '/list', component: './pages/list.js', Routes: ['./routes/PrivateRoute.js'] },
]

然后 umi 会用 ./routes/PrivateRoute.js 来渲染 /list

./routes/PrivateRoute.js 文件示例:

export default (props) => {
  return (
    <div>
      <div>PrivateRoute (routes/PrivateRoute.js)</div>
      { props.children }
    </div>
  );
}

antd引用

cnpm i antd -S
cnpm install umi-plugin-react -D

修改 config/config.js

// umi2的配置方式
export default {
  plugins: [
    // 有参数
    [
      'umi-plugin-react',
      {
        dva: {},
        antd: {},
      },
    ],
    './plugin',
  ],
};

// umi3的配置方式
export default {
  dva: {},
  antd: {}
};

// package.json
{
  "devDependencies": {
-   "umi": "^2"       // for umi2
+   "umi": "^3"       // for umi3
-   "umi-plugin-react": "^1"   // for umi2
+   "@umijs/preset-react": "^1"  // for umi3
  }
}
import React from 'react';
import styles from './index.css';
import {Button} from 'antd'

export default function Page() {
  return (
    <div>
      <h1 className={styles.title}>Page index</h1>
      <Button>登录</Button>
    </div>
  );
}

umi3使用dva

https://blog.csdn.net/weixin_43787651/article/details/110224586

dva教程:
https://blog.csdn.net/qq_39523111/article/details/88050125?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-0&spm=1001.2101.3001.4242

react整合electron

https://zhuanlan.zhihu.com/p/29164782

一、打包报 Application entry file “build\electron.js” in the “D:\workspace\electronDemo\demo-test\demo\dist\win-unpacked\resources\app.asar” does not exist

https://blog.csdn.net/qq_40593656/article/details/100101911

二、在react打包后静态资源在electron展示不了

https://segmentfault.com/a/1190000020229885?utm_source=tag-newest

空文件

简介

react学习的markdown笔记合集 展开 收起
取消

发行版

暂无发行版

贡献者

全部

近期动态

不能加载更多了
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/cai-lunduo/react-markdown-notes.git
git@gitee.com:cai-lunduo/react-markdown-notes.git
cai-lunduo
react-markdown-notes
react-markdown笔记
master

搜索帮助