# react-markdown笔记 **Repository Path**: cai-lunduo/react-markdown-notes ## Basic Information - **Project Name**: react-markdown笔记 - **Description**: react学习的markdown笔记合集 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 3 - **Forks**: 0 - **Created**: 2021-05-30 - **Last Updated**: 2023-11-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## 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内部是异步修改的 例子: ```js export default class CartSample extends Component { this.state = { text: '' } setGoodsText = e => { this.setState({ text: e.target.value }) } render() { return ( ) } } ``` 上面CartSample就是一个组件,里面有state和setState,监听input框的change事件,一旦触发则让setState改变state中的值,这是最简单也是最常用的用法。 ## 2. 条件判断 在jsx中,我们在大括号{}内只能写表达式,不能编写逻辑代码,如if判断、for循环,此时我们可以通过短路逻辑判断,如: 下面例子写了当state的goods长度不为0时则展示对应数据 ```js export default class CartSample extends Component { this.state = { goods: [ { id: 1, text: "web全栈架构师", count: 2 }, ] } render() { return ( ) } } ``` ## 3. 循环展示列表 引用2的例子,循环展示商品列表,因为{}里面只能是表达式,因此我们的思路是遍历数组的每个元素,并返回不同的值,因此数组内置的map函数很适合做这种工作。 ```js export default class CartSample extends Component { this.state = { goods: [ { id: 1, text: "web全栈架构师", count: 2 }, { id: 2, text: "python全栈架构师", count: 0 }, ] } render() { return ( ) } } ``` ## 4. 两种组件写法 ```js // 方式1: 类 import React, { Component } from 'react' export default class index extends Component { render() { return (
) } } // 方式2: 函数 import React from 'react' export default function index() { return (
) } ``` 本来两者是有些差异的,hook出现之前,用函数式写组件一般都是只写傻瓜式组件,也就是没有处理业务逻辑的。但是自从react 16.80版本出现后,新增加了hook,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性,因此可以试着去学学hook,这样子也可以在function里编写逻辑代码,也更简练。 文档:https://react.docschina.org/docs/hooks-intro.html ## 5. 事件绑定 事件监听采用Javascript的驼峰式写法,如onClick,onChange,onBlur等 例子: ```js export default class CartSample extends Component { this.state = { text: '' } setGoodsText = e => { this.setState({ text: e.target.value }) } render() { return ( ) } } ``` 上面是最简单的例子 那么传参事件该怎么绑定呢? 学之前先注意一点: {} 是个表达式 因为是表达式,如果我们要改上面代码为带参事件,我们不能用 ```js onChange={this.setGoodsText(args)} ``` 这样子的话会被立刻执行,因此我们需要让他监听一个匿名函数,写法如下 ``` onChange={() => this.setGoodsText(args)} ``` ## (补充)使用PropTypes进行类型检查 随着你的应用程序不断增长,你可以通过类型检查捕获大量错误。对于某些应用程序来说,你可以使用 [Flow](https://flow.org/) 或 [TypeScript](https://www.typescriptlang.org/) 等 JavaScript 扩展来对整个应用程序做类型检查。但即使你不使用这些扩展,React 也内置了一些类型检查的功能。要在组件的 props 上进行类型检查,你只需配置特定的 `propTypes` 属性 ```react import PropTypes from 'prop-types'; class Greeting extends React.Component { render() { return (

Hello, {this.props.name}

); } } Greeting.propTypes = { name: PropTypes.string }; ``` 同样的,由于在类原型上的属性属于静态属性,所以我们可以写在类里面,只要加个static关键字即可。 ```react export class MyPropType extends React.Component{ static propTypes = { name: PropType.string, age: PropType.number.isRequired, } constructor(props) { super(props); console.log(props) } render() { return (
prop测试
) } } ``` ### 默认 Prop 值 ```react 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 ```react export function FuncPropType({name, age}) { console.log(name, age) return ( <>
{name}
{age}
) } FuncPropType.propTypes = { name: PropTypes.string, age: PropTypes.number } FuncPropType.defaultProps = { name: 'tan', age: 17 } ``` ## 6.类方法中的this指向问题 给JSX绑定事件时由于没有指定上下文,`this指向会指向顶层`。 而指向顶层有两种情况:严格模式与非严格模式,而react是在`严格模式下`的,在定义,所以this为undefined。 为了让我们方法中的this可以指向类本身,我们可以选择四种方式: ```react // 方式一 构造器中给对应函数指定上下文, 如 this.addGood = this.addGood.bind(this) ------> 推荐 // 方式二 在JSX给种绑定this
// 方式三 类里面方法用箭头函数定义,箭头函数是没有上下文的,上下文会指向类 addGood = () => {} ------> 推荐 // 方式四 JSX里用箭头函数 addGood = () => { () => {this.addGood()} } ``` ## 7. setState注意事项 使用setState修改值有两个方法: 一个是传入对象修改,另一个方式是传入函数修改 ```react // 方式一 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需要修改的对象重新设置一个新的的值。 如: ```js this.state = { goods: [ { id: 1, text: "web全栈架构师" }, { id: 2, text: "python全栈架构师" } ], text: '' } textChange = e => { this.setState({ text: e.target.value }); }; ``` 现在当点击某个按钮时会触发addGood事件,addGood用来添加新商品数据到goods中,此时addGood的写法是这样的 ```js 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 ``` 试用按钮组件: ```js import Button from 'antd/lib/button'; import 'antd/dist/antd.css' export default class App extends Component { render() { return (
) } } ``` 上面那种方式引入很麻烦,而且是样式是引用全部的样式表,如何按需引入呢? **安装react-app-rewired取代react-scripts,可以扩展webpack的配置,类似vue.config.js** ```js npm install react-app-rewired@2.0.2-next.0 babel-plugin-import --save ``` 修改package.json的scripts字段: ```js // 所有react-scripts 都替换成 react-app-rewired 如 "scripts": { "start": "react-app-rewired start", ... }, ``` 在根目录下新建config-overrides.js,内容如下 ```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; }; ``` 这样子配置就完成了,接下来我们可以实现按需导入了,在需要引入组件的地方: ```js import {Button} from 'antd' ``` ## 9. 容器组件与展示组件 基本原则:容器组件负责数据获取,展示组件负责根据props显示信息,例子如下 ```react 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 (
{this.state.comments.map((c, i) => { return })}
) } } // 展示组件 function Comment({ data }) { return (

{data.name}

--- {data.age}

--- {data.education}

) } ``` ## 10. 性能优化:浅比较 参考文档:https://react.docschina.org/docs/optimizing-performance.html UI 更新需要昂贵的 DOM 操作,而 React 内部使用几种巧妙的技术以便最小化 DOM 操作次数。对于大部分应用而言,使用 React 时无需专门优化就已拥有高性能的用户界面。尽管如此,你仍然有办法来加速你的 React 应用。 还是引用9的例子,只不过我们会给CommentList加一个定时器,每隔1.5s就对comments列表进行setState操作,但是我们不修改它的值: ```react 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则重新渲染,否则不渲染 ```react 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 (

{this.props.data.name}

--- {this.props.data.age}

--- {this.props.data.education}

); } } ``` ### 10.2 使用PureComponent 首先我们先研究一下PureComponent的实现原理: PureComponent首先会判断新值与旧值**是否在同一片内存地址**,如果是,则返回true,也就是相同。 否则会再对旧值的内部做一层循环比较(注意:浅比较只会进行一层for循环,有**深层次的结构时浅比较是没有效果的**) 由此可以看出PureComponent的两个缺点: 一、如果是一个对象,或者数组,我们对其内部进行修改,而**没有改变内存地址**时,页面不会重新渲染 二、如果包含**二层及以上的层次结**构时,浅比较无法生效 如果我们有二层的层次结构时还想进行钱比较该怎么办呢? 方法: 传给展示组件的值用第二层的值传递,而非将整个对象或数组传递给展示组件。 继续引用9的例子 此时我们传给Comment组件的值不传入每个comments列表项,而是用展开运算符将每个对象展开,传入最里边的值,意思就是,我有下面这个结构的数组: ```js // 需要传给展示组件的数据 comments: [ { name: 'cai', age: 20, education: 'university' }, { name: 'chen', age: 22, education: 'university' }, ] // 传入的值:{...c} {this.state.comments.map((c, i) => { return })} // 此时...c == 每个对象的name、age、education值 // 展示组件继承PureComponent class Comment extends React.PureComponent{ constructor(props) { super(props) } render() { console.log("render comment"); return (

{this.props.name}

--- {this.props.age}

--- {this.props.education}

); } ``` ### 10.3 使用memo 类形式的组件可以进行浅比较,现在在React v16.6.0之后,函数式也添加了浅比较,也就是memo, 使用方式如下: ```react constJoke=React.memo(() => (
{this.props.value||'loading...'}
)); ``` 只需将组件用React.memo包裹即可。 ## 11. 高阶组件 ### 11.1 核心概念 **高阶组件是参数为组件,返回值为新组件的函数** 高阶组件其实就是装饰器的原型,装饰器内部就是对高阶组件封装了而已,也就是语法糖。 高阶组件的一个最明显的好处就是你可以自行扩展组件行为。想到行为扩展想必你已经联想到mixins了,那为什么不用mixins而非要弄个高阶组件出来呢? 参考文档:https://react.docschina.org/blog/2016/07/13/mixins-considered-harmful.html 接下来我们就讲讲高阶组件的使用: 假设有一个 `CommentList` 组件,它订阅外部数据源,用以渲染评论列表: ```react 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 (
{this.state.comments.map((comment) => ( ))}
); } } ``` 稍后,编写了一个用于订阅单个博客帖子的组件,该帖子遵循类似的模式: ```react 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 ; } } ``` `CommentList` 和 `BlogPost` 不同 - 它们在 `DataSource` 上调用不同的方法,且渲染不同的结果。但它们的大部分实现都是一样的: - 在挂载时,向 `DataSource` 添加一个更改侦听器。 - 在侦听器内部,当数据源发生变化时,调用 `setState`。 - 在卸载时,删除侦听器。 你可以想象,在一个大型应用程序中,这种订阅 `DataSource` 和调用 `setState` 的模式将一次又一次地发生。我们需要一个抽象,允许我们在一个地方定义这个逻辑,并在许多组件之间共享它。这正是高阶组件擅长的地方。 对于订阅了 `DataSource` 的组件,比如 `CommentList` 和 `BlogPost`,我们可以编写一个创建组件函数。该函数将接受一个子组件作为它的其中一个参数,该子组件将订阅数据作为 prop。让我们调用函数 `withSubscription`: ```react const CommentListWithSubscription = withSubscription( CommentList, (DataSource) => DataSource.getComments() ); const BlogPostWithSubscription = withSubscription( BlogPost, (DataSource, props) => DataSource.getBlogPost(props.id) ); ``` 第一个参数是被包装组件。第二个参数通过 `DataSource` 和当前的 props 返回我们需要的数据。 当渲染 `CommentListWithSubscription` 和 `BlogPostWithSubscription` 时, `CommentList` 和 `BlogPost` 将传递一个 `data` prop,其中包含从 `DataSource` 检索到的最新数据: ```react // 此函数接收一个组件... 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 ; } }; } ``` 请注意,HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件*包装*在容器组件中来*组成*新组件。HOC 是纯函数,没有副作用。 被包装组件接收来自容器组件的所有 prop,同时也接收一个新的用于 render 的 `data` prop。HOC 不需要关心数据的使用方式或原因,而被包装组件也不需要关心数据是怎么来的。 因为 `withSubscription` 是一个普通函数,你可以根据需要对参数进行增添或者删除。例如,您可能希望使 `data` prop 的名称可配置,以进一步将 HOC 与包装组件隔离开来。或者你可以接受一个配置 `shouldComponentUpdate` 的参数,或者一个配置数据源的参数。因为 HOC 可以控制组件的定义方式,这一切都变得有可能。 与组件一样,`withSubscription` 和包装组件之间的契约完全基于之间传递的 props。这种依赖方式使得替换 HOC 变得容易,只要它们为包装的组件提供相同的 prop 即可。例如你需要改用其他库来获取数据的时候,这一点就很有用。 ### 11.2 **高阶组件的链式调用** 比如我们有两个高阶组件,分别定义不同的行为: ```react // 高阶组件1 const withLog = Comp => { console.log("日志打印..."); return props => ; }; // 高阶组件2 const withSubscribtion = Comp => { // 获取name,name可能来自于接口或其他手段 const name = "其他属性"; console.log("do something"); return class extends React.Component { render() { return ; } }; }; // 被扩展组件 class NeedToExtend extends Component { render() { return (
); } } // 链式调用过程 const newComp = withLog(withSubscribtion(NeedToExtend)) ``` ### 11.3 装饰器语法糖使用 ES7装饰器可用于简化高阶组件写法 ``` npm i -D babel-plugin-transform-decorators-legacy ``` 在config-overrides.js添加配置 ```js config = injectBabelPlugin( ["@babel/plugin-proposal-decorators", { legacy: true }], config ); ``` 使用装饰器,以11.2为例子 ```react // 链式调用过程 const newComp = withLog(withSubscribtion(NeedToExtend)) // 装饰器使用 @withSubscribtion @withLog class NeedToExtend extends Component { render() { return (
); } } ``` ## 12. 复合组件 ### 12.1 复合内容为jsx 复合组件其实就是展示组件预留个插口位置待容器组件插入相应的值,等同于Vue中的插槽。 对应Vue中的匿名插槽就是`props.children`,如: ```react import React, { Component } from 'react'; // 展示组件 function Dialog(props) { return (
{props.children}
); } // WelcomeDialog通过复合提供内容 function WelcomeDialog(props) { return (

欢迎光临

感谢使用react

); } export default function() { return (
) } ``` 此时`{props.children}`就是插进来的内容 ```html

欢迎光临

感谢使用react

``` 那么对应Vue的有名插槽是怎么实现的呢? 其实很简单,就是通过props传值,如我在展示组件里在定义一个页脚,页脚内容通过容器组件传入: ```react function Dialog(props) { return (
{props.children}
{props.footer}
); } const footer = ; ``` 以上例子复合内容形式均为jsx。 ### 12.2 复合内容为函数 复合内容为函数时就相当于Vue中的作用于插槽,例子如下 假设我现在定义一个Fetcher组件,组件内的复合内容是一个带参函数,我想通过这个函数的参数渲染对应的内容: ```react {user => (

{user.name}-{user.age}

)}
``` 定义组件: ```js // 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`方法,遍历复合内容,并过滤出我们想要的 ```react function Filter({ children, type }) { return (
{React.Children.map(children, child => { if (child.type !== type) { return; } return child; })}
); }

react

react很不错

vue

vue很不错

``` 若我们想对传进来的复合内容进行修改,则必须谨记: **复合内容为jsx,是虚拟节点,内容不能修改**,因此如果我们要修改它的内容,那我们只能克隆对应的元素在做自己想要的修改即可,如: 现在我有一个按钮组,结构如下 ```html vue react angular ``` 我不想给每个按钮都加一个`name`,而是给`Radio`的父组件`RadioGroup`加一个`name`,然后让`RadioGroup`循环遍历每个`Radio`,对每个`Radio`加上同一个`name`该怎么操作呢? 如下 ```react function RadioGroup(props) { return (
{React.Children.map(props.children, child => { // vdom不可更改,克隆一个新的去改才行 return React.cloneElement(child, { name: props.name }); })}
); } function Radio({children, ...rest}) { return ( ); } ``` 这里需要注意的一点就是,再给`Radio`赋值的时候要注意`props`包含`children`和`父级组件传进来的值`。 因此Radio接收参数时需要做分割,分为`children复合内容部分`和`剩余部分`,`剩余部分`传给input作为属性值。 ## 13 Refs and the DOM Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。 在典型的 React 数据流中,[props](https://zh-hans.reactjs.org/docs/components-and-props.html) 是父组件与子组件交互的唯一方式。要修改一个子组件,你需要使用新的 props 来重新渲染它。但是,在某些情况下,你需要在典型数据流之外强制修改子组件。被修改的子组件可能是一个 React 组件的实例,也可能是一个 DOM 元素。对于这两种情况,React 都提供了解决办法。 ### ## 13. Hook *Hook* 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性 你之前可能把它们叫做“无状态组件”。但现在我们为它们引入了使用 React state 的能力,所以我们更喜欢叫它”函数组件”。 ### 自定义Hook(补充) 自定义hook可以让我们将组件逻辑提取到一个可重复利用的组件内。 这里会有两个案例讲解自定义hook的使用 **第一个简单的例子**: ```react import {useState, useEffect} from 'react' function Comp() { let [num, setNum] = useState(0) useEffect(() => { setTimeout(() => { setNum(++num) }, 2000) }, []) return (

Comp -- {num}

) } function App() { return (
); } export default App; ``` 如上所示,我们可以将`Comp`组件的 ```react let [num, setNum] = useState(0) useEffect(() => { setTimeout(() => { setNum(++num) }, 2000) }, []) ``` 给提取到一个单独的组件中,这时候便是一个自定义hook,使得我们调用的代码更简洁,且逻辑抽离到了一起。 我们将上述代码抽离到`./hooks/numHook.js`中。 ```react import {useState, useEffect} from 'react' export default function useNum() { let [num, setNum] = useState(0) useEffect(() => { setTimeout(() => { setNum(++num) }, 2000) }, []) return [num, setNum] } ``` 这时候我们在需要引用该hook的地方进行导入引用即可,例子将可以改写为: ```react import useNum from './hooks/numHook' function Comp() { let [num] = useNum() return (

Comp -- {num}

) } function App() { return (
); } export default App; ``` 这就是最简单的自定义hook。 ### 自定义请求数据hook 这里我们要做的自定义Hook是发起数据请求的hook 实现效果如下: ![image-20210530111434733](https://gitee.com/cai-lunduo/react-selfdefine-hook/raw/master/public/effects.jpg) 安装bootstrap: ```shell npm i -S bootstrap ``` **server端**: ```js 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 node server ``` 在`./hooks/useRequest.js`中: ```react 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)。 ```js onClick={() => { setOptions({ ...options, currentPage: index + 1 }) }} ``` 组件一旦挂载我们就发起数据请求,然后将通过`setData`设置数据,并将`data`返回。可以看到最后返回的格式是[data, options, setOptions],意味着我们在组件内可以通过类似`useState`的方式获取自定义hook的值。 下面组件的取法: ```js let [data, options, setOptions] = useRequest() ``` 在`./pages/Tabel.js`中: ```react 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 ( <> {list.map((item) => { return ( ) })}
id name
{item.id} {item.name}
{/* 下拉框 */}
) } ``` ### 13.1 useState useState的用法很简单也很方便,比起class的方式简直要简洁得不少 ```react import React, { useState } from 'react'; export default function Example() { const [count, setCount] = useState(0) const [fruit, setFruit] = useState('banana') return (
你点击了{count}次

{fruit}

) } ``` 如上例子所示,useState(0),0表示初始值,而将useState解构成两个成员,一个是状态属性,一个是更改状态的函数,其中useState有多个,这样子就比class的形式要简洁多了。 ### 13.2 useEffect 你之前可能已经在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。 `useEffect` 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 `componentDidMount`、`componentDidUpdate` 和 `componentWillUnmount` 具有相同的用途,只不过被合并成了一个 API。 例如,下面这个组件在 React 更新 DOM 后会设置一个页面标题: ```react 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 (
你点击了{count}次
) } ``` 当你调用 `useEffect` 时,就是在告诉 React 在完成对 DOM 的更改后运行你的“副作用”函数。由于副作用函数是在组件内声明的,所以它们可以访问到组件的 props 和 state。默认情况下,React 会在每次渲染后调用副作用函数 —— **包括**第一次渲染的时候 副作用函数还可以通过返回一个函数来指定如何“清除”副作用。例如,在下面的组件中使用副作用函数来订阅好友的在线状态,并通过取消订阅来进行清除操作 ```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会在组件更新时调用,所以我们可以维护一个状态,当这个状态改变,我们调用函数重新发送请求。 ```react 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 (
你点击了{count}次
) } ``` #### 13.2.1 useEffect的优化点 当然,`useEffect`和`useState`一样可以同时存在多个,还可以指定每次副作用函数只对那些起作用,和指定那些副作用函数只执行一次,因为比如我们要在`useEffect`中异步获取数据,我们总不能每次渲染操作都获取一次,因此有下面的解决方案: ```react 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 (
你点击了{count}次
) } ``` 如上所示,我们可以在useEffect传入一个数组参数,里面为空则表示只执行一次,如果里面有状态则表示只对里面的状态起作用。 ![image-20210514205707734](https://gitee.com/cai-lunduo/react-markdown-notes/raw/master/public/clearEffect.png) **为什么要在 effect 中返回一个函数?** 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。 **React 何时清除 effect?** React 会在组件卸载的时候执行清除操作,相当于类组件的`componentWillUnmount`,也会在每次组件更新之前调用该回调,相当于`componentWillUpdate`。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React *会*在执行当前 effect 之前对上一个 effect 进行清除。 #### 13.2.2 小结 了解了 `useEffect` 可以在组件渲染后实现各种不同的副作用。有些副作用可能需要清除,所以需要返回一个函数: ```react useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }); ``` 其他的 effect 可能不必清除,所以不需要返回。 ```react useEffect(() => { document.title = `You clicked ${count} times`; }); ``` ### useContext(补充) 我们通过`createContext`创建一个Context之后,在子组件可以通过`useContext`拿到我们在祖先组件创建的`Context`,但是前提是我们需要对子组件用`Context.Provider`标签进行包裹,然后通过`value`属性传值。 这里需要注意` const states = useContext(Context)`只能在子组件内部调用,不可在顶级元素使用。 例子如下: ```react import {createContext, useContext} from 'react' const Context = createContext() function Child() { const states = useContext(Context) return (
{states.map(state => { return (

{state}

) })}
) } function App() { return (
); } export default App; ``` 在这里我们通过`Context.Provider`传输了一个数字数组到子代组件,子代组件通过`const states = useContext(Context)`获取到父组件传过来的数据并进行展示。 ### 优化 假如我们要给多个子组件提供同一份数据,那么我可以将提供数据的父组件给抽离成一个单独的负责**提供数据**的组件: ```react 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 ( {children} ) } ``` 子组件: ```react import {useContext} from 'react' import {Context, CountContextProvider} from './CountContextProvider' function CountChild() { const {count, add, minus} = useContext(Context) return (

{count}

) } let r = () => { return ( ) } export default r ``` ## 14. Context Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。 因为典型的React应用数据是通过Props属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。 总体步骤 ```shell 1.creareContext 2.MyContext.Provider包裹通过value传值 3.在需要该值的组件static contextType = ThemeContext;接收context ``` ### 14.1 使用Context之前 **何时使用 Context** Context的目的是共享那些应该在全局共享的数据,例如当前用户的Token数据,或者首选语言。举个例子,在下面的代码中,我们通过一个 “theme” 属性手动调整一个按钮组件的样式: ```react class App extends React.Component { render() { return ; } } function Toolbar(props) { // Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。 // 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事, // 因为必须将这个值层层传递所有组件。 return (
); } class ThemedButton extends React.Component { render() { return ) } } export default KForm ``` ## 16. Redux的使用 ![img](https://gitee.com/cai-lunduo/react-markdown-notes/raw/master/public/redux.png) ```node cnpm install redux --save ``` ### 16.1 简单使用 在文件store.js内: ```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](https://gitee.com/cai-lunduo/react-markdown-notes/raw/master/public/store.png) 如上所示,我们创建的`Store`对象包括了这么些个属性,我们可以通过`getState`方法获取state的值,也可以通过`dispatch`方法改变`state`的值,还有最后我们还要在页面渲染那里用`subscribe`进行订阅。 ```react import React from 'react' import store from '../store' // 原始Store组件 function StoreTest() { return ( <>
{store.getState()}
) } export default StoreTest ``` 如上所示,我们可以通过`getState`获取到前面`store.js`的`state`值,再给 ) } } export default connect(mapStateToProps, mapDispatchToProps)(StoreTest2) ``` 在这里,我们使用了`connect`高阶组件,传入两个配置项,再对需要扩展的组件进行包装,注意这里的写法已经变了,逻辑已经被我们提到了外面,此时`ShowTest2`只是一个傻瓜式组件,不负责`dispatch`也不用调用`getState`方法,`connect`内部的两个配置项分别帮我们把`state`的值映射到被包装组件`StoreTest2`的内部,并放在`props`里面,这样子我们在组件内就可以直接通过`props`拿到`state`和`dispatch`了。 ### **16.4 装饰器简化** 只是个语法糖 ```react @connect(mapStateToProps, mapDispatchToProps) class StoreTest3 extends React.Component { render() { return ( <>
{this.props.num}
) } } export default StoreTest3 ``` ### **16.5 处理异步** ![image-20210401102441651](https://gitee.com/cai-lunduo/react-markdown-notes/raw/master/public/redux-thunk.png) 如图所示,在处理`action`的时候会先经过中间件层的处理,中间件会识别传进来的`action`到底是个对象还是函数,如果是函数则表明是异步请求,要先执行完才能继续往下一步进行。 **安装两个中间件** ``` npm install redux-thunk redux-logger -S ``` `redux-thunk`用来处理异步请求,`redux-logger`是一个日志打印中间件。 **使用** ```js // 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 ``` 需要进行异步操作的地方 ```react 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 ( <>
{this.props.num}
) } } export default StoreTest4 ``` 这里新增了一个`asyncAdd`方法,注意这里的写法是返回一个带`dispatch`参数的函数,而不像其他同步操作直接返回对象。 ### **16.6 代码抽离** 由于我们全局共享状态是有多种类型的,如用户的登录态、全局的主题、用户信息等,因此我们将所有全局共享状态都放置再store.js将会导致后期变得可维护差,为了使代码更具有可用性、易用性、可维护性。我们应该将每一部分的状态都放置在一个文件。 **步骤** 创建`store`文件夹,创建`index.js`为入口,将来通过`combineReducers`引入所有的状态,我们将上面的`counterReducer`和其相对于的`action creator`给分离到一个文件。 ```js // 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) } ``` 在入口文件引入 ```js // 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`的路径 ```js // index.js import store from './store/index' ``` 在需要用到`counter`状态的地方 ```react ... 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 ( <>
{this.props.num}
) } } export default StoreTest5 ``` 这里我们只需将对应的`action creator`导入,使得管理起来更加方便。实现了统一在store文件夹下的文件对所有共享状态进行管理,可维护性就瞬间提升了。 ## react-router ### 介绍 ### 安装 ```shell npm install --save react-router-dom ``` ### 起步 `index.js` ```react // ... 其他包导入 import {BrowserRouter} from 'react-router-dom' ReactDOM.render( , document.getElementById('root') ); ``` 如上所示,我们需要给根组件包裹一个`BrowserRouter`标签,这样子我们就可以在浏览器使用路由了。 在`react`中,路由即组件,简单用法如下: ```react import { Link, Route } from 'react-router-dom' function About() { return (
关于页
) } function Home() { return (
首页
) } export default function RouteTest() { return (
    Home About
) } ``` `Link`作用类似于`a`标签,会在页面展示让用户点击,而`Route`就是一个真正的路由映射组件,如上所示,`path`指定访问路径,`component`指定要展示的组件。其中因为`react-router`的路由包容性(只要path匹配到,就会渲染),我们还需对`path`为`/`的路由添加一个`exact`字段,这样子就只会精准匹配了。 ### Router对象 被`Route`标签包裹的组件,传入的值就不是`props`了,而是一个路由器对象,我们可以打印看看路由器对象到底长啥样 ![image-20210401165044914](https://gitee.com/cai-lunduo/react-markdown-notes/raw/master/public/router.png) 如上所示,有`history`、`location`、`match`三个主要属性 ```js // 1.history: 导航指令 // 2.match: 获取参数信息 // 3.location: 当前url信息 ``` ### Switch `react`路由是包容性的,也就是匹配到的路由都会被展示出来。但是我们可以通过`Switch`标签让`react`路由不再具有包容性,只展示匹配到的第一个路由: ```react import { Link, Route, Switch, NavLink } from 'react-router-dom' import { BrowserRouter } from 'react-router-dom' function About(router) { return
about
} function Home() { return
home
} function NotFound() { return (

not found

) } function App() { return (
{/* 内联样式 */}
Home about
{/* 一个switch为一组,只会展示第一个被匹配的组件 */}
) } ``` 如上所示,我们让`Switch`包裹的路由为一组,在这一组中只匹配第一个匹配到的路由则停止继续匹配,可以注意到我们在最后放了一个NotFound路由,这也是路由找不到时可以写的写法,因为没有`path`,前面的匹配不到则`NotFound`一定会被匹配到。 ### Redirect redirect就是重定向组件,有一个from和to属性 常用基础写法如下 ```react {/* 将from 定向到 to */} { return login ? : }}> ``` ### 子路由与动态路由 #### 子路由 子路由部分包括动态路由,那么在`react`中如何编写子路由呢? 举个例子,比如我们想要在上面的`About`页嵌套子路由,写法可以参考下面 ```react function About(params) { return (

个人中心

个人信息 订单查询
Me
} />
order
} />
); } ``` 很简单,只要在`About`组件嵌套路由即可。 但是需要注意一点: 写子路由时父亲路由不能开启路由严格模式,即不能在父组件加个exact属性,否则一旦开启严格模式,子路由将不会被路由系统找到,从而匹配不到,如: ```react import { Link, Route, Switch, NavLink, Redirect } from 'react-router-dom' import { BrowserRouter } from 'react-router-dom' function AAA() { return (

aaa

) } function BBB() { return (

bbb

) } function About(router) { return (
a b
) } function Home() { return
home
} function App() { return (
{/* 内联样式 */}
Home about
{/* 一个switch为一组,只会展示第一个被匹配的组件 */}
) } export default App ``` 在这里我们对`/about`路由开启了严格模式,而`About`组件下有子组件,但是我们会发现访问子组件的时候,子组件并没有展示出来,这是因为`/about`路由开启了严格模式从而导致子路由匹配不到。 #### 动态路由 动态路由对于在`url`传值的很简单,使用`:参数`即可,如果要通过`query`传值可以在`Route`加上相应的属性值,然后在对应组件的参数`路由对象`中的`match`字段便可拿到 ```react import {Link, Route, Switch, Redirect} from 'react-router-dom' function Detail(props) { return (
详情页:{props.match.params.id}
) } Detail ``` ### 传参 react传参可以通过三种方式,可以通过params、query或者state对象进行传参 ```react 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 (
    {/* params传参 /home/:id/:title */} {/* { data.map(item => {item.title}) } */} {/* query传参 /home?id=1&title=weibo */} {/* { data.map(item => {item.title}) } */} {/* state参数 */} { data.map(item => {item.title}) }
) } // 子组件 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 (
total
) } function App() { return (
{/* 内联样式 */}
Home
{/* 一个switch为一组,只会展示第一个被匹配的组件 */}
) } export default App ``` ### Redux ![image-20210401165044914](https://gitee.com/cai-lunduo/react-markdown-notes/raw/master/public/redux.png) #### redux三大核心: ​ reducers:数据控制器,数据修改者 ​ store: 数据仓库 ​ action: 描述发生了什么的一个对象 #### redux的三大原则: ​ 单一数据源(只创建一个store) ​ state是只读的(不允许被直接修改,具体修改在reducer集中化处理) ​ reducer使用纯函数执行修改(有个固定的值,而不是在reducer里有而外的操作,如在reducer执行一些不确定的操作,例如发起ajax异步请求,这就违犯了redux的原则) #### 步骤学习: 首先引入redux的createStore ```react import {createStore} from 'redux' ``` 然后我们需要创建一个store,createStore的第一个参数是`reducers`,`reducers`结构如下 ```react // 默认状态 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的第一个参数 ```react //创建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订阅,更新视图。 ```react 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 (

{count}

) } } ``` ### 模块抽取 随着项目的逐渐扩大,为防止逻辑之间冗余,我们需要对`actions、reducers`和`store`进行单独的抽离。 以上面的例子为例: 新建一个store文件夹,里面包含index.js、reducers.js、actions.js ```react // 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的第二个参数内。 ```react // 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`。 ```react // 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}) }) } } ``` 组件引用 ```react 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 (

{count}

{1}

) } } ``` #### 常量提取 在编写`actions`的type时,`reducers`也会用到type,如果`actions`的type与`reducers`的type不一致,或者说拼写错了,那么`reducers`将不会触发,而且不会报错提醒你,这时候我们就需要用到常量提取,将type提取为常量,再引入使用。 如: constants/index.js ```react export const ADD = 'add' export const REQUEST = 'request' ``` actions.js ```react 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 ```react 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组件就是只负责展示数据的组件。 #### 用法示例 ```react 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 (

{count}

{name}

) } } /** * 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`映射到`props`;`connect`第二个参数是`mapDispatchToProps`,是一个函数,负责将`dispatch`映射到`props`,这样子我们就能在UI组件内通过`props`拿到`store`的数据了,而且`connect`内部还帮我们订阅了`store`数据的变化,一但数据更新,`props`也会跟着更新,所以就会重新渲染。 **简单总结一下**:React 只负责页面渲染, 而不负责页面逻辑, 页面逻辑可以从中单独抽取出来, 变成 store,目的就是让组件尽量变成PureComponent,然后用connect让这个组件跟store联系,connect还会帮我们订阅store的变化。 #### 进一步的模块分离 由于我们的`actions`和`reducers`是多个的,且不同组件会用到F:\学习\前端作品\JS学习\react\redux-pro\01-review\src\store\actions\request.js不同的`actions`和`reducers`,所以我们可以按照`一个组件一个action | reducer`或者`一个功能一个action | reducer`,这里采用的是后者。 将`action`的抽取到不同文件很简单,只需将对应部分给抽取到对应文件,改变一下引入路径即可;而`reducer`的抽取,我们要用的时候还需要通过`combineReducers`将各个`reducer`给合并为一个`reducers`。 新建**store/reducers**文件夹: ```react // 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`的时候分别给`counrReducer`和`requestReducer`给取了个别名`count`和`request`,我们在取值的时候需要加上命名空间: 之前的`mapStateToProps`写法: ```js function mapStateToProps(state) { return { count: state.count, name: state.name } } ``` 现在: ```js function mapStateToProps(state) { return { count: state.count.count, name: state.request.name, } } ``` 新建**store/actions**文件夹: ```react // 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`的组件。 **方式一**:直接传入 ```react
``` **方式二**:通过Provider统一传入(推荐方式) ```react import {Provider} from 'react-redux'
``` **gitee链接**:https://gitee.com/cai-lunduo/react-redux ### 路由导航守卫(重点) 路由导航守卫是我们编写路由不可避免的一个部分,那么我们在`react`如何编写路由导航呢? **答案:通过高阶组件扩展实现** 现在我想实现一个常见的功能:访问`About`页的时候判断用户是否处于登录态,如果是登录态则让他访问`About`页,如果没登陆则重定向到`Login`页。 我们一步步来,既然我们要判断用户是否处于登录态,由于登录态需要全局共享,我们需要用到`redux`,则我们可以在`store`下创建一个`user.redux.js`,管理用户的登录状态,注意,我们`login`动作也放在了这里,将来使用需要导入。 ```react 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` ```react // 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 ``` **第一步完成**,我们已经将用户的登录态放在全局进行管理。 接下来,我们先要创建一个自定义的组件,内部其实也是返回,但是我们将要对其功能进行增强。 ```react function PrivateRoute({ component:Comp, isLogin, ...rest }) { return ( isLogin ? () : } /> ) } ``` 在这里我们做了这些工作: ```js 1. 解构props为component, isLogin, ...rest 单独解构出component的目的: 1. 避免component属性传入Route,不然render将不生效!因为component优先级比render高 2. 给component重命名,因为react里面的组件是大写字母开头的 2. isLogin怎么来的? 我们等下会通过connect工厂函数链接react和redux,映射用户的登录状态到PrivateRoute的props中 3. 为什么要...rest? 因为我们原本的要接收path等相关参数,我们在这里直接把这些参数搬过来放在我们将要返回的组件中 4. 返回全新组件,在这个组件中,render是一个接收props的函数,我们在这判断用户的登录态,并返回不同的组件 1.如果用户已登录,则返回用户访问的About页面对应的组件 2.否则redirect到login页面,并给to对象里面设置一个state.redirect属性,将来登陆成功则自动定向到这个redirect去 ``` `Login`组件 ```react function Login({location, isLogin, login}) { const redirect = location.state && location.state.redirect || "/"; if(isLogin) { return } return (
) } ``` 这里我们从`location`拿到重定向的地址,为将来登陆成功时指定回跳地址。 至此我们**第二步完成**了。 现在**最后一步**就是将全局的用户登录态给注入`Login`和`PrivateRoute`中,通过 `connect(配置项)(被加工组件)`的方式注入 ```react // ...其他包导入 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 } return (
) }) // 路由导航守卫 const PrivateRoute = connect(state => ({isLogin: state.user.isLogin}))(({ component:Comp, isLogin, ...rest }) => { return ( isLogin ? () : } /> ) }) ``` 用法: ```react ``` ## 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设置三个状态,以此来判断用户的登录状态和展示错误信息。 ```js 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 ```react 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 ``` 如上所示,我们使用了`put`、`call`、`takeEvery`工具函数,按我个人理解: > 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`事件进行监听并派发相应动作。 ```react // 其他包导入 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 ``` 在登录组件 ```js 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 } return (
{/* 错误信息展示 */} {error &&

{error}

} setUname(e.target.value)}/> {/* redux-saga */}
) }) ``` ## umi版本2 参考文档:https://umijs.org/docs ### 介绍 Umi,中文可发音为**乌米**,是可扩展的企业级前端应用框架。Umi 以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。 Umi 是蚂蚁金服的底层前端框架,已直接或间接地服务了 3000+ 应用,包括 java、node、H5 无线、离线(Hybrid)应用、纯前端 assets 应用、CMS 应用等。他已经很好地服务了我们的内部用户,同时希望他也能服务好外部用户。 ### 特性 - **开箱即用**:内置 SSR,一键开启,`umi dev` 即 SSR 预览,开发调试方便。 - **服务端框架无关**:Umi 不耦合服务端框架(例如 [Egg.js](https://eggjs.org/)、[Express](https://expressjs.com/)、[Koa](https://koajs.com/)),无论是哪种框架或者 Serverless 模式,都可以非常简单进行集成。 - **支持应用和页面级数据预获取**:Umi 3 中延续了 Umi 2 中的页面数据预获取(getInitialProps),来解决之前全局数据的获取问题。 - **支持按需加载**:按需加载 `dynamicImport` 开启后,Umi 3 中会根据不同路由加载对应的资源文件(css/js)。 - **内置预渲染功能**:Umi 3 中内置了预渲染功能,不再通过安装额外插件使用,同时开启 `ssr` 和 `exportStatic`,在 `umi build` 构建时会编译出渲染后的 HTML。 - **支持渲染降级**:优先使用 SSR,如果服务端渲染失败,自动降级为客户端渲染(CSR),不影响正常业务流程。 - **支持流式渲染**:`ssr: { mode: 'stream' }` 即可开启流式渲染,流式 SSR 较正常 SSR 有更少的 [TTFB](https://baike.baidu.com/item/TTFB)(发出页面请求到接收到应答数据第一个字节所花费的毫秒数) 时间。 - **兼容客户端动态加载**:在 Umi 2 中同时使用 SSR 和 dynamicImport(动态加载)会有一些问题,在 Umi 3 中可同时开启使用。 - **SSR 功能插件化**:Umi 3 内置的 SSR 功能基本够用,若不满足需求或者想自定义渲染方法,可通过提供的 API 来自定义。 ![image-20210401165044914](https://gitee.com/cai-lunduo/react-markdown-notes/raw/master/public/umi.png) ![image-20210401165044914](https://gitee.com/cai-lunduo/react-markdown-notes/raw/master/public/umi-flow.png) 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的约定 ```elm . ├── 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基本使用 ```elm npm init npm install umi -D ``` **新建index页** ```elm umi g page index ``` **启动服务器** ```elm umi dev ``` #### 路由 ##### **基础路由** `umi`是支持约定式路由与配置式路由的,默认为约定式路由,约定式就是会根据`pages`目录自动生成路由配置。 假设`pages`目录结构如下: ```elm + pages/ + users/ - index.js - list.js - index.js ``` 则umi会自动生成路由配置如下: ```js [ { 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里规定以`$`开头的文件或目录为动态路由 如以下目录结构: ```elm + pages/ + $post/ - index.js - comments.js + users/ $id.js - index.js ``` 会生成的路由配置如下: ```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 里约定动态路由如果带 `$` 后缀,则为可选动态路由。 比如以下结构: ```elm + pages/ + users/ - $id$.js - index.js ``` 生成的路由配置如下: ```js [ { path: '/': component: './pages/index.js' }, { path: '/users/:id?': component: './pages/users/$id$.js' }, ] ``` ##### **嵌套路由** umi 里约定目录下有 `_layout.js` 时会生成嵌套路由,以 `_layout.js` 为该目录的 layout 。 比如以下目录结构: ```elm + pages/ + users/ - _layout.js - $id.js - index.js ``` 生成的路由配置如下: ```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 组件。 比如 ```react export default () => { return (
I am a customized 404 page
); }; ``` > 注意:开发模式下,umi 会添加一个默认的 404 页面来辅助开发,但你仍然可通过精确地访问 `/404` 来验证 404 页面。 ##### **通过注释扩展路由** 约定路由文件的首个注释如果包含 **yaml** 格式的配置,则会被用于扩展路由。 比如: ```elm + pages/ - index.js ``` 如果 `pages/index.js` 里包含: ```js /** * title: Index Page * Routes: * - ./src/routes/a.js * - ./src/routes/b.js */ ``` 则会生成路由配置 ```js [ { path: '/', component: './index.js', title: 'Index Page', Routes: [ './src/routes/a.js', './src/routes/b.js' ], }, ] ``` ##### **配置式路由** 如果你倾向于使用配置式的路由,可以配置 `.umirc.(ts|js)` 或者 `config/config.(ts|js)` [配置文件](https://umijs.org/zh/guide/config.html)中的 `routes` 属性,**此配置项存在时则不会对 `src/pages` 目录做约定式的解析**。 比如: ```js 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 注释添加,配置式的直接配上即可。 比如有以下配置: ```js [ { path: '/', component: './pages/index.js' }, { path: '/list', component: './pages/list.js', Routes: ['./routes/PrivateRoute.js'] }, ] ``` 然后 umi 会用 `./routes/PrivateRoute.js` 来渲染 `/list`。 `./routes/PrivateRoute.js` 文件示例: ```react export default (props) => { return (
PrivateRoute (routes/PrivateRoute.js)
{ props.children }
); } ``` #### antd引用 ```elm 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 } } ``` ```react import React from 'react'; import styles from './index.css'; import {Button} from 'antd' export default function Page() { return (

Page index

); } ``` #### 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