39. B站 学习react全家桶笔记 尚硅谷【React简介】 - yiqunkeke/react-jianshu-shangguigu-zty GitHub Wiki

1.React 是什么?

React-用于构建用户界面的JavaScript库

  • 发送请求获取数据
  • 处理数据(过滤、整理格式等)
  • 操作DOM呈现页面

React 只做第3步。

React 是一个将 数据 渲染为 HTML视图 的开源JavaScript库。(数据----> html视图)

2.谁开发的?

由FaceBook开发,且开源。

3.为什么要学?

  • 之前的方式:原生JavaScript操作DOM繁琐、效率低(原来的方式:是在使用DOM-API操作UI)
   /*用DOM的API操作UI*/
   document.getElementById('app')
   document.querySelector('#app')
   document.getElementsByTagName('span')
  • 每一次使用JavaScript操作DOM,都会使浏览器拼命地进行绘制和排列(重绘和重排

    即便使用jquery(不使用原生js),它也只是简化了写法,归根到底,第一次操作dom,浏览器都是要干活的(绘制和排列)。

  • 原生JavaScript没有组件化编码方案,代码复用率低。

    模块化:简言之,就是把一个非常庞大的js,按照功能点拆分成一个一个的js

    组件化:构成界面的3驾马车是 html/css/js。如果你只会模块化,那代表你只会拆js。那它的结构(html)和样式(css)该怎么拆? 比如界面上有些固定的结构html,几个人都要用怎么弄?---组件化

    组件化其实就是一拆到底的感觉。不仅js要拆,结构和样式都要拆,甚至是构成局部功能的图片,字体,音频视频等。

以上是原生js开发的痛点

4.React的特点

  • 采用 组件化 模式、声明式编码 ,提高了组件复用率和开发效率。

    命令式编码:向左走,拿起水杯,接水,送到我这里,让我喝

    声明式编码:接好水,喝吧

  • 在React Native中可以使用React语法进行 移动端开发

  • 使用 虚拟DOM + 优秀的 Diffing算法 ,尽量减少与真实DOM的交互。 image image image

关键点来了:React 会进行虚拟DOM的比较

React发现,之前001对应的虚拟DOM与后来001对应的虚拟DOM一样(002同理),则React不会再生成新的真实DOM。

就是说经过虚拟DOM的对比发现,新的虚拟DOM中有两个与原来的虚拟DOM是一样的,React就直接使用之前的真实DOM

真正新生成的真实DOM,其实只有003肖战-20这条数据。

所以当数据量大的时候,优势就体现出来了。所以React操作数据的优势就是:原来有100条数据,现在有101条数据,那React为你渲染的只有第101条中的最后一条。之前的100条数据直接使用之前在界面上渲染出来的真实DOM直接用。

这就是React高效的原因

5.React高效的原因

使用虚拟DOM,不总是直接操作页面真实DOM。,而且在操作真实DOM之前,会把两个不同的虚拟DOM进行比较,比较出有差异的那一部分,再去更新页面,所以说人家高效。

DOM Diffing算法,最小化页面重绘。

6.学习React之前要掌握的JavaScript基础知识

  • 判断this 的指向
  • class(类)
  • ES6语法规范
  • npm 包管理器
  • 原型、原型链
  • 数组常用方法
  • 模块化

7. hello_react案例

image

学习react,我们从最原始的方式开始学习,回想下一开始是如何学习jquery的?

就是在html页面中直接引入jquery.js文件,然后去查阅相关文档,学习怎么使用$符号操作dom。

学习react也是一样,我们从最简单的开始,只需要在html页面中引入react就可以了。那都需要引入哪些文件?

相关js库

  • babel.min.js文件

    作用1: ES6 ==> ES5

    比如,学习模块化时,写 import 语句,浏览器不能直接解析,需要借助babel

    作用2:把 jsx ==> js

    我们学习 react 时,写的不是原生的js,而是另外一个东西: jsx

    我们写的是jsx,浏览器却只认js,这就得请出babel了。所以babel还有另外一个作用,就是把 jsx ==> js

  • react.development.js文件

    这个就是react 核心库

  • react-dom.development.js文件

    react的扩展库,是React用来帮你操作DOM的。

完整源码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 1.type="text/javascript" -->
    <!-- 
        <script type="text/javascript">
            // 注意,这里如果不写 type="text/javascript",那么默认script标签里面写的是js,
            // 但是我们今天写的不是js,而是jsx,所以这里就不能写 text/javascript了,而应该写成 text/babel
        </script> 
    -->

    <!-- 2. type="text/babel"
        此处一定要写成type="text/babel"表示这个script标签里面写的不再是js,而是jsx,而且靠babel来翻译 -->
    <script type="text/babel">
        // 1. 创建虚拟DOM
        const VDOM = <h1>Hello, React</h1> /*此处一定等号后面一定不要写引号,因为VDOM不是字符串*/

        // 2. 渲染虚拟DOM到页面
        // ReactDOM.render(虚拟DOM,容器)
        ReactDOM.render(VDOM, document.getElementById('test'))
    </script>

</body>
</html>

运行结果如下图:

image

8. 为什么不用原生的js,非要用jsx?

const VDOM = <h1>Hello, React</h1> /*此处一定等号后面一定不要写引号,因为VDOM不是字符串*/

思考这样一个问题: 为什么不用原生的js,非要用jsx?

React官方要求我们使用jsx。

那React官方为什么要出jsx语法?

接下来用一个案例来解释这个问题。

image

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 3.为什么不用原生的js,非要用jsx? 
        React官方要求我们使用jsx。

        那React官方为什么要出jsx语法?
        接下来用一个案例来解释这个问题。
    -->

    <!-- 4. 虚拟DOM的两种创建方式:jsx 和 js
                1)使用jsx实现给<h1>加上id="title"属性
    -->
    <!-- 
        <script type="text/babel">
            // 使用jsx语法,则可以直接在h1标签上添加 id="title"实现
            const VDOM = <h1 id="title">Hello, React</h1>

            ReactDOM.render(VDOM, document.getElementById('test'))
        </script>  
    -->

    <!-- 4. 虚拟DOM的两种创建方式:jsx 和 js
                2)使用js创建虚拟DOM
                document.createElement 是帮你创建真实DOM的
                React.createElement是创建虚拟DOM的
    -->
    <!-- 
        <script type="text/javascript">
            // 1. 创建虚拟DOM
            // const VDOM = React.createElement(标签名,标签属性,标签内容)
            const VDOM = React.createElement('h1',{id: 'title'},'Hello, React')

            // 渲染虚拟DOM到页面
            ReactDOM.render(VDOM, document.getElementById('test'))
        </script>
    -->

    <!-- 由此可见,不用jsx,也可以创建虚拟DOM。若又来一个需求,h1中不直接写 Hello,React。而是需要包一个span标签,在span标签中写 Hello,React -->

    <!-- 5. 在h1中包裹span,在span中写Hello,React
           1)使用jsx实现 
    -->
    <!-- 
        <script type="text/babel">
            // 使用jsx语法
            const VDOM = <h1 id="title"><span>Hello, React</span></h1>

            ReactDOM.render(VDOM, document.getElementById('test'))
        </script> 
    -->

    <!-- 5. 在h1中包裹span,在span中写Hello,React
           2)使用js实现 
    -->
    <script type="text/javascript">

        const VDOM = React.createElement('h1',{id: 'title'}, React.createElement('span',{},'Hello, React'))

        ReactDOM.render(VDOM, document.getElementById('test'))
    </script>

    <!-- 总结:如果多层嵌套,你就会发现js原生创建虚拟DOM的写法非常的繁琐,而jsx写法,就如同当年你写html一样方便。

        这就体现出jsx的优势了,这也就是为什么react要打造出jsx。

        jsx来到这个世界上就是因为用js创建虚拟DOM太繁琐了。有了jsx可以让编码人员更加简单的创建虚拟DOM,写法更加流畅。

        而且我们还可以换行写虚拟DOM

        const VDOM = (
            <h1 id="title">
                <span>Hello, React</span>
            </h1>
            )
        
        等号后面用()包裹起来,代表()里面是一个整体。
    -->

    <!-- JSX方式创建虚拟DOM是原始js创建虚拟DOM的语法糖 

        即:
        const VDOM = (
            <h1 id="title">
                <span>Hello, React</span>
            </h1>
            )

        最终被babel翻译成了浏览器可以解析的:
        const VDOM = React.createElement('h1',{id: 'title'},'Hello, React')
    -->
</body>
</html>

9. 打印查看虚拟DOM与真实DOM的区别

我们来看下虚拟DOM与真实DOM的区别:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 6. 虚拟DOM与真实DOM -->
    <script type="text/babel">
        const VDOM = (
            <h1 id="title">
                <span>Hello, React</span>
            </h1>
            )

        ReactDOM.render(VDOM, document.getElementById('test'))
        
        // 打印下虚拟DOM
        console.log('虚拟DOM',VDOM)
        console.log(typeof VDOM)
        console.log(VDOM instanceof Object)

        
        // 打印下真实DOM
        let TDOM = document.getElementById('test')
        console.log('真实DOM', TDOM)
         
        debugger;
        /*
            关于虚拟DOM本质:
                1.本质是Object类型的对象(一般对象)
                2. 虚拟DOM比较轻,真实DOM比较重,因为虚拟DOM是React内部在用,无需真实DOM上那么多的属性。
                3. 虚拟DOM最终会被React转化为真实DOM,呈现在页面上。
        */
    </script> 
</body>
</html>

10. jsx 语法规则

  1. 全称 Javascript XML

    XML早期用于存储和传输数据: 比如要存储一个学生的信息

    <student>
       <name>Tom</name>
       <age>19</age>
    </student>

    特点:结构(<student><name><age>)比内容(tom 和 19)还要多

    后来这种方式被json替代:

    "{"name":"Tom", "age": 19}"

    更方便简单

    而且js中内置的JSON对象上有两个特别好用的方法:JSON.parse() 和 JSON.stringfy()

  2. react定义的一种类似于XML的JS扩展语法: JS + XML

  3. 本质是 React.createElement(component, props, ...children)的语法糖

  4. 作用:用来简化创建虚拟DOM

     a. 写法: `var ele = <h1>Hello JSX!</h1>`
    
     b. 注意1:它不是字符串(等号后面不需加引号),也不是 html/xml 标签
    
     c: 注意2:它最终产生的就是一个JS对象
    
  5. 标签名任意:html标签或其他标签

html源码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 7. jsx语法规则一:定义虚拟DOM时,不要写引号 -->
    <!-- 
        <script type="text/babel">
            // 创建虚拟DOM
            const VDOM = (
                <h2 id="atguigu">
                    <span>Hello, React</span>
                </h2>
                )

            // 渲染虚拟DOM到页面
            ReactDOM.render(VDOM, document.getElementById('test'))

            /*
                jsx语法规则:
                    1. 定义虚拟DOM时,不要写引号
            */
        </script>  
    -->

    <!-- 需求:把 "atguigu" 和 "Hello,React" 通过读取变量的方式读取出来 -->
    <!-- 7. jsx语法规则二:标签中混入js表达式时要用{} -->
    <!-- 
        <script type="text/babel">
            const myId = 'atguigu'
            const myData = 'Hello, React'
            // 创建虚拟DOM

            // 标签中混入js表达式时的错误写法
            // const VDOM = (
            //     <h2 id="myId">
            //         <span>myData</span>
            //     </h2>
            //     )

            // 标签中混入js表达式时,要用 {}
            const VDOM = (
                <h2 id={myId}>
                    <span>{myData}</span>
                </h2>
                )

            // 渲染虚拟DOM到页面
            ReactDOM.render(VDOM, document.getElementById('test'))
        
            /*
                jsx语法规则:
                    2. 标签中混入js表达式时要用 {}
            */
        </script>  
    -->

    <!-- 需求:想让h2有个样式,而且是提前写好的class名
        即:把title样式应用给h2
    -->
    <!-- 7. jsx语法规则三:样式的类名指定不要用class,要用 className-->
    <!-- 
        <style>
            .title {
                background-color: orange;
                width: 200px;
            }
        </style>
        <script type="text/babel">
            const myId = 'atguigu'
            const myData = 'Hello, React'
            
            // 错误写法:
            // 如果直接写class="title" 样式也可以出来,但是控制台会报错:
            // react-dom.development.js:61 Warning: Invalid DOM property `class`. Did you mean `className`?

            // const VDOM = (
            //     <h2 class="title" id={myId}>
            //         <span>{myData}</span>
            //     </h2>
            //     )

            // 正确写法用 className
            const VDOM = (
                <h2 className="title" id={myId}>
                    <span>{myData}</span>
                </h2>
                )

            ReactDOM.render(VDOM, document.getElementById('test'))
            /*
                jsx语法规则:
                    3. 样式的类型指定不要用class,要用 className
            */
        </script>  
    -->

    <!-- 需求:想给span添加一个样式,而且用内联样式实现 -->
    <!-- 7. jsx语法规则四:内联样式,要用 style={{key:value}}的形式去写 -->
    <!-- 
        <script type="text/babel">
            const myId = 'atguigu'
            const myData = 'Hello, React'

            // 错误写法:如果直接在span上添加style="color: white",则直接会报错:
            // The `style` prop expects a mapping from style properties to values, not a string. For example, style={{marginRight: spacing + 'em'}} when using JSX.
            // 意思是 style不能是字符串,得用 {{ }}
            // const VDOM = (
            //     <h2 id={myId}>
            //         <span style="color: orange">{myData}</span>
            //     </h2>
            //     )

            // 正确写法:
            // {{ }} 最外层的{}代表你将在这对{}里面写JS表达式
            // {{ }} 里层的{}代表你写的是js中的对象
            // 且属性名用小驼峰命名
            const VDOM = (
                <h2 id={myId}>
                    <span style={{color: 'orange', fontSize:'30px'}}>{myData}</span>
                </h2>
                )

            ReactDOM.render(VDOM, document.getElementById('test'))
            /*
                jsx语法规则:
                    4. 内联样式,要用 style={{key:value}}的形式去写
            */
        </script>  
    -->

    <!-- 需求:在页面中加一个input输入框 -->
    <!-- 7. jsx语法规则五:只能有一个根标签 -->
    <!-- 7. jsx语法规则六:标签必须闭合 -->
    <!-- 
        <script type="text/babel">
            const myId = 'atguigu'
            const myData = 'Hello, React'

            // 只能有一个根标签,且标签必须闭合
            // 不能只写 <input type="text">
            // 必须写成 <input type="text"></input> 或者 <input type="text"/>
            const VDOM = (
                <div>
                    <h2 id={myId}>
                        <span>{myData}</span>
                    </h2>
                    <input type="text"></input>
                </div>
                )

            ReactDOM.render(VDOM, document.getElementById('test'))
            /*
                jsx语法规则:
                    5. 只能有一个根标签
                    6. 标签必须闭合
            */
        </script>  
    -->

    <!-- 思考:标签里面能写什么东西?
        写的不是html标签,而是JSX里面的标签.
        jsx里面的标签,会被转为 html标签。
    -->
    <!-- 7. jsx语法规则七:关于标签首字母 -->
    <script type="text/babel">
        const myId = 'atguigu'
        const myData = 'Hello, React'

        // 错误写法一:标签以小写字母开头
        // const VDOM = (
        //     <div>
        //         <h2 id={myId}>
        //             <span>{myData}</span>
        //         </h2>
        //         <good>123</good>
        //     </div>
        //     )
        // Warning: The tag <good> is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter.
        // React在拿good标签去找html中的同名元素时,对不上。
        // 你是不是想定义一个组件?如果是,则标签开头首字母应大写

        // React 在把jsx标签转为html标签时 ,如果jsx标签是以小写字母开头,则会转为html中的同名标签:
        // 比如 span  ---> span
        // 比如 h1 ----> h1
        
        // 现在你错误的写了一个不存在的标签,而且还是小写字母开头。而React只要发现小写字母开头的标签,就二话不说直接去html中寻找同名标签。找不到,则报错。   
        

        // 错误写法二:标签以大写字母开头
        // const VDOM = (
        //     <div>
        //         <h2 id={myId}>
        //             <span>{myData}</span>
        //         </h2>
        //         <Good>123</Good>
        //     </div>
        //     )
        // Uncaught ReferenceError: Good is not defined
        // 若标签是以大写字母开头,则React就去渲染对应的组件,若组件没有定义,则报错 

        // 
        const VDOM = (
            <div>
                <h2 id={myId}>
                    <span>{myData}</span>
                </h2>
                123
            </div>
            )

        ReactDOM.render(VDOM, document.getElementById('test'))


        /*
            jsx语法规则:
                7. 关于标签首字母:
                  (1) 若标签小写字母开头,则将该标签中转为html中同名元素,若html中无该标签对应的同名元素,则报错。
                  (2) 若标签是以大写字母开头,则React就去渲染对应的组件,若组件没有定义,则报错
                  
                  所以在react中,标签不能乱写。
        */
    </script> 
</body>
</html>

11. jsx 练习

image

代码实现过程:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>
    
    <!-- 8.jsx 练习 -->
    <!-- 需求:动态展示 angluar、react、vue列表 -->

    <!-- 8.jsx 练习一:写死数据 -->
    <!-- 
        <script type="text/babel">
            // 1. 创建虚拟DOM
            const VDOM = (
                <div>
                        <h1>前端js框架列表</h1>
                        <ul>
                            <li>Angluar</li>
                            <li>React</li>
                            <li>Vue</li>
                        </ul>
                </div>
            )

            // 2. 渲染虚拟DOM到页面
            ReactDOM.render(VDOM, document.getElementById('test'))
        </script>  
    -->

    <!-- 8.jsx 练习二:模拟数据-->
    <!-- 
        <script type="text/babel">
            // 模拟一些数据
            const data = ['Angluar', 'React', 'Vue']
            const data2 = [ <li>Angluar</li>,<li>React</li>,<li>Vue</li> ]
            const data3 = {name: 'Angluar',value: 'React',age: 'Vue'}
            // 1. 创建虚拟DOM
            const VDOM = (
                <div>
                        <h1>前端js框架列表</h1>
                        <ul>
                            {/* 写法一:*/}
                            {/* 虽然这样直接写{data},react也能帮你遍历数组,
                                但是丢失了li的结构 */}
                            {/* data */}

                            {/* 写法二:把纯数据加工成带有标签的数据*/}
                            {data2}

                            {/* React 无法展示对象*/}
                            {/* data3 */}
                            
                            {/* 总结
                                注意1:若给React一个数组,则React会自动遍历并展示数据内容
                                注意2:若给React一个对象,则不会展示任何东西且报错
                            */}    

                        </ul>
                </div>
            )

            // 2. 渲染虚拟DOM到页面
            ReactDOM.render(VDOM, document.getElementById('test'))
        </script> 
    -->

    <!-- 8. 表达式、语句、代码的区别 -->
    <!-- 一定注意区分:【js语句】与【js表达式】
        1. 表达式: 一个表达式会产生一个值,可以放在任何一个需要值的地方
            下面这些都是表达式
            (1). a
            (2). a+b
            (3). demo(1)
            (4). arr.map()
            (5). function test() {} 

        2. 语句:
            下面这些都是语句
            (1). if() {}
            (2). for() {}
            (3). switch() { case: xxx}   
            这些都是控制代码走向的,没有值,所以不是表达式

        而 { } 当中只能写js表达式。不能写语句。   
    -->

    <!-- 8.jsx 练习四:【思路】把纯数据数组,加工成带有<li>标签的数组 -->
    <script type="text/babel">

        const data = ['Angluar', 'React', 'Vue']
        // 需要把上面的数据数组,加工成下面这种带有<li>标签的数据数组
        // const data = [ <li>Angluar</li>,<li>React</li>,<li>Vue</li> ]

        const VDOM = (
            <div>
                    <h1>前端js框架列表</h1>
                    <ul> 
                        {
                            data.map((item,index) => {
                                return <li key={index}>{item}</li>
                            })
                        }
                    </ul>
            </div>
        )

        // 2. 渲染虚拟DOM到页面
        ReactDOM.render(VDOM, document.getElementById('test'))
    </script>
</body>
</html>

总结:

注意1:若给React一个数组,则React会自动遍历并展示数据内容

注意2:若给React一个对象,则不会展示任何东西且报错

注意3:{ } 当中只能写js表达式。不能写语句。

12. 模块与组件、模块化与组件化的理解

    (1) 模块:向外提供特定功能的js程序,一般就是一个js文件

    (2) 为什么要拆成模块?
    随着业务逻辑的增加,代码越来越我且复杂

    (3) 作用:复用js,简化js的编写,提高js运行效率

    总结模块,就是js模块,只是拆js


    (1) 组件:用来实现局部功能代码和资源的组合

    (2) 为什么:一个界面的功能可能更复杂 

    (3) 作用:复用代码,简化项目编码,提高运行效率


    模块化: 当应用的js都以模块来编写的,这个应用就是一个模块化的应用

    组件化: 当应用是以多组件的方式实现,这个应用就是一个组件化的应用

    一拆到底

13. 开发者工具的安装

这个工具 React Developer Tools 是基于chrome 浏览器的

    React小图标:

    红色+小虫子:代表网站没有打包上线,是在开发环境

    蓝色:代表线上环境

image

components这个tab, 可以用来观察网页由什么组件组成,每个组件中有哪些属性

image

profiler这个tab,用于记录网站的性能,比如渲染组件用了多久?哪个组件加载最慢?

14. 函数式组件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 11. 函数式组件 -->
    <!-- 
        <script type="text/babel">
            // 1. 创建函数式组件
            // function demo() {
            //     /*
            //         思考:组件的定义是什么?
            //         包含页面局部功能、全部代码和资源的集合,叫组件。

            //         组件中可能包含: html/css/js/image/icon/font/video...

            //         也就是说一个组件,至少得有html结构->所以说函数式组件必须得有return返回值
            //     */
            //     return <h2>我是用函数定义的组件</h2>
            // }

            // 2.渲染组件到页面

            // 错误写法一: 函数是不能做为react的节点的
            // ReactDOM.render(demo, document.getElementById('test'))
            /*
                这样的话,页面不会展示任何东西,且控制台报错了:
                Warning: Functions are not valid as a React child. This may happen if you return a Component instead of <Component /> from render. Or maybe you meant to call this function rather than return it.
                函数类型是不能做为react的节点的
            */

            // 错误写法二:写<demo/>标签    
            // ReactDOM.render(<demo/>, document.getElementById('test'))
            /*
                Warning: The tag <demo> is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter.
                <demo>标签在浏览器中不存在。  
                jsx语法规则中:
                    (1)若jsx标签以小写字母开头,则将转为html中的同名元素,若html中无该标签对应的同名元素,则报错
                    (2)若jsx标签以大写字母开头,react就去渲染对应的组件,若组件没有定义,则报错      
            */

            // 所以既然写的是组件,则标签的首字母应该大写。
            
            // 正确写法如下:
            // 1.创建函数式组件
            function Demo() {
                return <h2>我是用函数定义的组件</h2>
            }
            // 2.渲染组件到页面
            ReactDOM.render(<Demo/>, document.getElementById('test'))
        </script> 
    -->

    <!-- 总结函数式组件:
        (1). 在定义函数式组件时,想都不用想,首字母必须大写
        (2). ReactDOM.render()渲染组件时,要写<组件/>标签,且标签自闭合

        如果在以前,你问我function Demo() {} 是什么?我肯定回答是在定义一个函数Demo。
        但现在我们在学习react,那我们一定要知道我们是在定义一个组件(函数式组件)Demo。
    -->

    <!-- 聊一聊函数式组件的几个细节:
        1. Demo是不是函数? 是
        2. Demo这个函数是你调用的吗? 不是
            调用函数的方式: Demo()
        3. Demo函数有没有被调用? 有(不调用的话,页面上不会有内容)
        4. 问题是:谁调用了Demo函数呢? React帮你调用的
                你只需要在第1步创建好组件并在第2步写对组件标签。
                React会自动帮你调用Demo函数。调完了这个函数,就得到了返回值,就帮你把内容呈现在了页面。
        5. 深入聊一下,Demo函数中的 this 是谁?
               如果你直接定义了function demo() {}, 并自己调用了 demo(),那么this指的就是浏览器的window 

               但通过在下面Demo函数中打印this,发现 this 是undefined. 为什么?
               因为下面<script>中的代码会经过babel的翻译,且在翻译时开启了严格模式。
               严格模式下,禁止自定义函数中的this指向window。
               所以说这里的this就指向了undefined。

    -->

    <script type="text/babel">
        // 1.创建函数式组件
        function Demo() {
            console.log(this)  // undefined
            return <h2>我是用函数定义的组件</h2>
        }
        // 2.渲染组件到页面
        ReactDOM.render(<Demo/>, document.getElementById('test'))
    </script>
    
    <!-- 执行了第2步(ReactDOM.render(<Demo/>, document.getElementById('test')))后发生了什么?
               (1).React解析组件标签,找到了Demo组件
               (2).发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,呈现在页面中   
     -->
</body>
</html>

15. 复习类相关知识

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 12. 复习类相关知识  -->

    <!-- (1).创建类和实例对象 -->
    <!-- 
        <script type="text/javascript">
            // 创建一个Person类
            // 用class关键字定义一个类
            class Person {

            }

            // 创建一个Person的实例对象
            // 类的作用:用来创建实例对象
            const p1 = new Person()

            // 打印下实例对象
            console.log(p1)  // Person {}
            // 后面的 {} 就代表 p1 是一个实例对象
            // 前面的 Person 代表 p1是由Person类new出来的
        
        </script>  
    -->

    <!-- (2).类中的构造器方法 -->
    <!-- 给类里面传递参数,并在构造器方法中接收参数,使实例对象上有了属性 -->
    <!--
        <script type="text/javascript">
            class Person {
                // 类里面有什么?属性和方法
                // 借助构造器方法接收传递进来的参数

                // 构造器方法(类中的构造器方法理论上可以不写)
                constructor(name, age) {
                    // 把传递进来的name和age放到实例自身
                    this.name = name
                    this.age = age

                    /*  思考:
                        构造器中的this是谁?-- 类的实例对象
                        const p1 = new Person('tom', 18)
                        这行代码,this指的就是 p1
                        const p2 = new Person('jerry', 19)
                        这行代码,this指的就是 p2
                    */
                }
            }

            // 给类中传递两个参数
            const p1 = new Person('tom', 18)
            const p2 = new Person('jerry', 19)


            // 这样,实例对象上就有了name和age属性
            console.log(p1) // Person {name: 'tom', age: 18}
            console.log(p2) // Person {name: 'jerry', age: 19}   

        </script>
    -->

    <!-- (3).类中的一般方法 -->
    <!-- 
        <script type="text/javascript">
            class Person {
                // 构造器方法
                constructor(name, age) {
                    this.name = name
                    this.age = age
                }

                // 一般方法
                // 除了构造器方法,程序员根据业务场景写的方法叫一般方法
                // 注意:前面不能加 function 关键字
                speak() {
                    console.log(`我叫${this.name},我年龄是${this.age}`)
                }

            }

            const p1 = new Person('tom', 18)
            const p2 = new Person('jerry', 19)

            console.log(p1) // Person {name: 'tom', age: 18}
            console.log(p2) // Person {name: 'jerry', age: 19}
            
            // 调用一般方法
            p1.speak() // 我叫tom,我年龄是18
            p2.speak() // 我叫jerry,我年龄是19
        </script> 
    -->

    <!-- 重点 
         思考问题一:speak方法放在了哪里?为什么没有在p1和p2上看到speak方法?-Person类的原型对象上
         思考问题二:speak方法是给谁用的?-供实例使用
         思考问题三:speak中的this是谁?-通过Person实例调用speak时,speak中的this就是Person实例
            p1.speak()
            p2.speak()
            就是通过Person实例调用speak,所以speak中的this就是指向的 p1、p2 (p1、p2都是Person实例)

            但是如果 p1.speak.call({a:1,b:2}) 调用时,speak指向的就是{a:1,b:2}对象
    -->
    
    <!-- (4).类的继承 -->
    <!-- 子类中写构造器方法,用于接收子类中独有的属性(父类中没有) -->
    <!-- 
            <script type="text/javascript">
            // 创建一个Person类
            class Person {
                constructor(name, age) {
                    this.name = name
                    this.age = age
                }
                speak() {
                    console.log(`我叫${this.name},我年龄是${this.age}`)
                }
            }

            // 创建一个Student类,继承于Person类
            // 类的继承用extends关键字
            class Student extends Person {
                // 构造器可以不写。但是此处因为Student类的实例对象上拥有了Person类没有的"年级"属性,所以此处才写了构造器方法,用于接收"年级"属性
                constructor(name, age, grade) {
                    this.name = name
                    this.age = age
                    this.grade = grade
                }
                /*
                Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
                控制台报错:当子类继承了父类,并且在子类中写了构造器方法时,必须调用下super()
                */
            }

            const s1 = new Student('小张', 15, '高一')
            console.log(s1) // Student {name: '小张', age: 15}
            s1.speak() // 我叫小张,我年龄是15
            /*
                可以总结出:
                (1). Student类继承了Person类中的构造器方法,
                    s1这个实例对象上也有name和age属性
                (2). s1实例对象也可以调用speak方法     
            */
        </script> 
    -->
    <!-- 上面的代码报错,是由于在子类中写了构造器方法,但是在子类的构造器方法中未调用super(),所以才会的报错 -->

    <!-- (5).子类构造器方法中的super() -->
    <!-- 
        <script type="text/javascript">
            // 创建一个Person类
            class Person {
                constructor(name, age) {
                    this.name = name
                    this.age = age
                }
                speak() {
                    console.log(`我叫${this.name},我年龄是${this.age}`)
                }
            }

            // 创建一个Student类,继承于Person类
            class Student extends Person {
                constructor(name, age, grade) {
                    // this.name = name
                    // this.age = age
                    // 在父类和子类中重复,当父类中的属性很多时,就会在子类中重复很多。
                    // 只需要调用super()方法,并把Student和Person共有的属性,通过super(共有属性1,共有属性2)给父类的构造器递过去即可
                    super(name,age)
                    this.grade = grade

                    /*
                        细节:super()必须在构造器中的最开始处调用
                    */
                }
                
            }

            const s1 = new Student('小张', 15, '高一')
            console.log(s1) // Student {name: '小张', age: 15, grade: '高一'}
            s1.speak() // 我叫小张,我年龄是15

            /*
                子类也可以使用父类原型上的方法
            */
        </script> 
    -->

    <!-- (6).重写从父类继承过来的方法 -->
    <script type="text/javascript">
        // 创建一个Person类
        class Person {
            constructor(name, age) {
                this.name = name
                this.age = age
            }
            speak() {
                console.log(`我叫${this.name},我年龄是${this.age}`)
            }
        }

        // 创建一个Student类,继承于Person类
        class Student extends Person {
            constructor(name, age, grade) {
                super(name,age)
                this.grade = grade
            }
            // 重写从父类继承过来的方法
            speak(){
                console.log(`我叫${this.name},我年龄是${this.age},我读的是${this.grade}`)
            }
            // 子类独有的方法
            study() {
                console.log('我很努力的学习')
            }
            /*
                重点:
                    (1). study 方法放在了哪里?--类的原型对象上,供实例使用
                    (2). 通过Student实例调用study时,study中的this就是Student实例
            */
        }

        const s1 = new Student('小张', 15, '高一')
        console.log(s1) // Student {name: '小张', age: 15, grade: '高一'}
        s1.speak() // 我叫小张,我年龄是15,我读的是高一
        s1.study() // 我很努力的学习
    </script>

    <!-- (7).类的总结:
        (1). 类中的构造器不是必须写的。要对实例进行一些初始化的操作,如添加指定属性时才写
        (2). 如果子类继承了父类,且子类中写了构造器,则子类中的super是必须要调用的。
        (3). 类中的定义的方法,都是放在了类的原型对象上,供实例调用
    -->
</body>
</html>

16. 类式组件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 13. 类式组件 -->
    <!-- 函数式组件:函数名就是组件名
         类式组件:类名就是组件名
    -->
    <script type="text/babel">
        // 1. 创建类式组件

        // 错误写法:下面的写法只是创建了一个类,叫MyComponent
        // 并不是创建了一个类式组件
        // class MyComponent {

        // }

        // 在react中,创建类式组件,该类必须继承于React中的内置类(React.Component)
        class MyComponent extends React.Component {
            /*
                用类来定义一个组件(即定义类式组件),有几个必要条件:
                    (1). 该类必须继承 React.Component 这个父类
                    (2). 该类中必须要写 render()方法
                    (3). 而且render()中必须有返回值 
            */
            render() {
                console.log('render中的this:', this) 
                return <h2>我是用类定义的组件</h2>
            }
        }

        // 2. 渲染组件到页面
        ReactDOM.render(<MyComponent/>, document.getElementById('test'))

        /*
            聊一聊类式组件的几个详细的问题:
                (1). render()是放在哪里的?
                    -MyComponent类的原型对象上,供实例使用
                (2). 问题是:实例在哪呢?
                    之前我们写好类,然后通过new Person()来创建实例。
                    而现在, 我们并没有写 new MyComponent()
                    
                    反向推理,页面上有内容 -> 证明已经拿到了return 的返回值 -> 证明已经调用到了render(),再加上 render()是供实例使用的 ->
                    所以说,在我们看不到的位置,肯定存在一个MyComponent类的实例

                    验证推理:直接在网页的控制台上输入: MyComponent.prototype 
                    可以查看一下    
        */

       /*
            细节:    
            
            (1). 只要你在第2步中写了组件标签<MyComponent/>,React就会自动帮你new MyComponent() 一个实例
            
            (2). 执行了第2步(ReactDOM.render(<MyComponent/>,document.getElementById('test')))之后,发生了什么?
                1).React解析组件标签,找到了MyComponent组件。
                2).发现组件是使用类定义的,随后new出来该类的实例,并通过该实例调用到原型上的render()方法
                3).将render返回的虚拟DOM转为真实DOM,随后呈现在页面中

            (3). render() 中的 this是谁? 
                MyComponent的实例对象 

            (4).查看MyComponent的实例对象
                console.log('render中的this:', this) 
                // MyComponent {props: {…}, context: {…}, refs: {…}, updater: {…}, _reactInternals: FiberNode, …}
        */
        
    </script>
</body>
</html>

image

上图结果可以看出,在类的原型对象上,确实有render()方法

image

上图结果可以看到MyComponent类的实例对象

17. 对state的理解

  1. 复杂组件:有state

  2. 简单组件:无state

  3. state:

    人: 状态 影响 行为

    组件: 状态 驱动 页面

组件的状态里边存着数据,

数据的改变驱动着页面的展示。

  1. state是谁身上的?

    -不是类本身上的

    -是由这个类缔造的实例身上的。(看16节中的结果截图,是render中的this身上的。也就是实例对象上的。)

所以说是“组件实例的三大核心属性之一state

18. 借助构造器初始化state

(实现案例:今天天气很炎热/凉爽 点击进行切换)

weather

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>


    <script type="text/babel">
        // 1. 创建组件
        // class Weather extends React.Component {
        //     render() {
        //         console.log('组件实例对象', this) 
        //         // 可以看出 Weather类(组件)的实例对象身上有state属性,值为 null。
        //         // 思路:给state上放一个属性,isHot
        //         // 借助构造器方法:给类的实例对象身上进行一些初始化的操作(比如增加一个属性、或者修改一个属性的值)
        //         return <h1>今天天气很炎热</h1>
        //     }
        // } 


        // 1. 创建组件
        class Weather extends React.Component {
            // 问题:constructor能接到什么? - 取决于通过new创建实例对象上,传递进来的是什么
            // 问题来了: new Weather() 是你写的吗?不是,而是react帮你new的
            constructor(props) {
                super(props)

                // 【1】写constructor的目的是:给Weather类的实例身上做些初始化的事
                this.state = {
                    isHot: true
                }
            }

            render() {
                // 【2】render中的this,就是组件实例对象
                console.log('组件实例对象this', this)

                // 【3】读取 state 
                return <h1>今天天气很{ this.state.isHot == true ? '炎热' : '凉爽' }</h1>
            }
        } 

        // 2. 渲染组件到页面
        ReactDOM.render(<Weather/>, document.getElementById('test'))
    </script>


</body>
</html>

运行结果:

image

19. 复习原生事件绑定

html源码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 16. 复习原生事件绑定 -->

    <button id="btn1">按钮1</button>
    <button id="btn2">按钮2</button>
    <!-- 当点击按钮3时,直接把""里边的js语句拉出来执行 -->
    <button onclick="demo()">按钮3</button>

    <script type="text/javascript">
        const btn1 = document.getElementById('btn1')
        btn1.addEventListener('click', () => {
            alert('按钮1被点击了')
        })

        const btn2 = document.getElementById('btn2')
        btn2.onclick = () => {
            alert('按钮2被点击了')
        }
        
        // 【重点】 React 推荐使用第3种方式
        function demo() {
            alert('按钮3被点击了')
        }
    </script>

</body>
</html>

20. React中的事件绑定

(实现案例:今天天气很炎热/凉爽 点击进行切换)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 17. React中的事件绑定 -->
    <!-- 思路:给h1添加一个点击事件,在点击事件的回调里边,想办法把状态中的isHot值取反 -->
    
        <script type="text/babel">
            class Weather extends React.Component {
                constructor(props) {
                    super(props)
                    
                    // 初始化状态
                    this.state = {
                        isHot: true
                    }
                }

                render() {
                    // 读取状态
                    const { isHot } = this.state

                    // 【1】使用前2种方式实现点击事件
                    // return <h1 id="title">今天天气很{ isHot == true ? '炎热' : '凉爽' }</h1>

                    // 【2】使用第3种方式

                    // 错误写法一
                    // return <h1 onclick="demo()">今天天气很{ isHot == true ? '炎热' : '凉爽' }</h1>

                    // Warning: Invalid event handler property `onclick`. Did you mean `onClick`?
                    // 报错:一个不被允许的事件监听属性onclick,应该写 onClick

                    // 【React的设计】
                    //  react把所有的onxxx 都改成了 onXxx
                    //          比如 onclick -> onClick
                    //               onblur -> onBlur

                    // 错误写法二
                    // return <h1 onClick="demo()">今天天气很{ isHot == true ? '炎热' : '凉爽' }</h1>

                    // Warning: Expected `onClick` listener to be a function, instead got a value of `string` type.
                    // onClick的监听者必须是一个函数,而不是字符串
                    // 想到用{}放表达式

                    // 错误写法三
                    // return <h1 onClick={demo()}>今天天气很{ isHot == true ? '炎热' : '凉爽' }</h1>

                    // 发现还没点击,就已经执行了demo函数,控制台上打印了内容

                    /*
                        说一个细节上的问题:
                            <h1 onClick={demo()}> 
                                demo() 表示函数运行
                                {demo()} 是一个函数调用表达式 (函数运行的返回值是什么? undefined)
                                onClick={demo()} 表示把函数运行的返回值(undefied)赋值给onClick作为回调 
                    */

                    // 正确写法
                    // 直接把demo函数作为回调,交给onClick
                    return <h1 onClick={demo}>今天天气很{ isHot == true ? '炎热' : '凉爽' }</h1>
                    

                }
            } 

            function demo() {
                console.log('标题被点击了')
            }

            ReactDOM.render(<Weather/>, document.getElementById('test'))

            // 方式1
            // const title = document.getElementById('title')
            // title.addEventListener('click', () => {
            //     console.log('标题被点击了')
            //  })

            // 方式2
            // const title = document.getElementById('title')
            // title.onclick = () => {
            //     console.log('标题被点击了')
            // }

            // 虽然上面2种方式都可以,但是尽量不要写,原因是这2种方式,都使用了document.getElementById()


            /*
                总结在React中写事件绑定注意事项
                 onClick={demo}
                (1). onClick的要小驼峰命名 (C要大写)
                (2). =后面的{}里面,不要写demo()形式(不要直接去调函数)
                      只需要指定好函数名,由React自动帮你去调用
            */
        </script> 
   


</body>
</html>

21. 类外部的方法访问不到 this

(实现案例:今天天气很炎热/凉爽 点击进行切换)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>
     
    <!-- 18. 类外部的方法访问不到 this -->
    <script type="text/babel">
        class Weather extends React.Component {
            constructor(props) {
                super(props)
                
                // 初始化状态
                this.state = {
                    isHot: true
                }
            }

            render() {
                // 读取状态
                const { isHot } = this.state

                return <h1 onClick={demo}>今天天气很{ isHot == true ? '炎热' : '凉爽' }</h1>
                

            }
        } 

        function demo() {
            // 思路:接下来,只需要在这里拿到 isHot的值,并修改
            console.log('此处修改isHot的值')

            // const {isHot} = this.state
            // console.log(isHot)

            // Uncaught TypeError: Cannot read properties of undefined (reading 'state')
            // 不能在 this上读取到 state, 所以说是this出问题了
            
            // 分析:就在这个位置【function demo() {}的函数内部】你根本碰不到Weather组件的实例对象this.
            // 为什么?
            // 因为Weather类的实例对象根本不是你自己new出来的,
            // 而是由React自动帮你new的
        }

        ReactDOM.render(<Weather/>, document.getElementById('test'))

    </script>

</body>
</html>

1659333066310

如上图,在上面两个红色框内,都能够访问到this。

构造器中的this,就是Weather类的组件实例对象

render中的this,也是Weather类的组件实例对象

而类外部的方法根本访问不到类的实例对象this

通过缓存 this来解决【不推荐】

如下图

image

分析:

红色框内的在定义组件

蓝色框内在渲染组件到页面

而粉色框内的代码却有些繁琐。

22. 类内部的方法this为undefined

(实现案例:今天天气很炎热/凉爽 点击进行切换)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 19.  类内部的方法this为undefined -->
    <script type="text/babel">
        class Weather extends React.Component {
            constructor(props) {
                super(props)
                
                // 初始化状态
                this.state = {
                    isHot: true
                }
            }

            render() {
                // 读取状态
                const { isHot } = this.state

                return <h1 onClick={changeWeather}>今天天气很{ isHot == true ? '炎热' : '凉爽' }</h1>
                // ReferenceError: changeWeather is not defined
                // 问题:onClick={changeWeather} 这里直接这么写,就能找到Weather类中的changeWeather方法吗?

            }

            // (1) 注意类中定义方法,函数名前面不要写function关键字
            changeWeather() {
                // 【灵魂拷问】:
                // changeWeather放在了哪里?--Weather类的原型对象上,供实例使用
                // 通过Weather实例调用changeWeather时,changeWeather中的this就是Weather实例
                console.log(this.state.isHot)
            }

        } 

        ReactDOM.render(<Weather/>, document.getElementById('test'))

    </script>
</body>
</html> 

原因如下:

1659334809387

直接报错:

image

正确写法如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 19.  类内部的方法this为undefined -->
    <script type="text/babel">
        class Weather extends React.Component {
            constructor(props) {
                super(props)
                
                // 初始化状态
                this.state = {
                    isHot: true
                }
            }

            render() {
                // 读取状态
                const { isHot } = this.state

                // 错误写法一
                // return <h1 onClick={changeWeather}>今天天气很{ isHot == true ? '炎热' : '凉爽' }</h1>

                // ReferenceError: changeWeather is not defined
                // 问题:onClick={changeWeather} 这里直接这么写,就能找到Weather类中的changeWeather方法吗?  
                // 不能,【必须使用 this.changeWeather】

                // 正确写法
                return <h1 onClick={this.changeWeather}>今天天气很{ isHot == true ? '炎热' : '凉爽' }</h1>

                // 改成 this.changeWeather后依然报错:
                // TypeError: Cannot read properties of undefined (reading 'state')
                // changeWeather里面的this又出问题了

            }

            // (1) 注意类中定义方法,函数名前面不要写function关键字
            changeWeather() {
                // 【灵魂拷问】:
                // changeWeather放在了哪里?--Weather类的原型对象上,供实例使用
                // 通过Weather实例调用changeWeather时,changeWeather中的this就是Weather实例
                console.log(this) // undefined
                console.log(this.state.isHot)

                /*
                   难点:
                    为什么changeWeather里的this是undefined?
                   原理:
                    只有通过Weather的实例对象调用changeWeather时,changeWeather中的this才是Weather实例对象

                    原因是:changeWeather不是通过实例调用的,所以changeWeather中的this不是指向实例自身
                */
               
               /*
                    回顾:render是怎么被调用的?
                    在第2步“ReactDOM.render(<Weather/>, document.getElementById('test'))”中写了Weather标签之后,

                    (1).React先去解析组件标签,找到Weather组件
                    (2).发现组件标签是使用类定义的,随后new出来该类的实例,并【通过该实例调用】到原型上的render方法
                    (3).将render返回的虚拟DOM转为真实DOM,随后呈现在页面中
                    
                    所以说render中的this指向Weather实例对象没问题。(因为是通过实例.render()调用的)
               */

               /*
                    回顾:constructor中的 this 指向实例对象自身。
               */

               /*   总结:
                    changeWeather中的this不是实例的原因是:changeWeather【不是通过实例调用】的,
                    所以changeWeather中的this不是指向实例自身
               */


            }

        } 

        ReactDOM.render(<Weather/>, document.getElementById('test'))

    </script>
</body>
</html> 

23. 复习类内部的方法的this指向

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 20. 复习类内部的方法的this指向 -->
    <script type="text/javascript">
        class Person {
            constructor(name, age) {
                this.name = name
                this.age = age
            }

            study() {
                // 【灵魂拷问】:
                // study方法放在了哪里?-类的原型对象上,供实例使用
                // 通过Person实例调用study时,study中的this就是Person实例
                console.log(this)
            }

        } 

        const p1 = new Person('张三', 18)
        p1.study() // Person {name: '张三', age: 18}
        // 上面是【通过实例调用】study()

        // 接下来我做一个动作
        const x = p1.study
        x() // undefined

        /*
            理性的分析问题:

            p1实例自身有study吗? 没有,但是p1.study()也不报错,那是因为顺着原型链可以找到study方法

            const x = p1.study 是在干嘛?
            这是一个赋值语句,这行代码根本没有调用study方法
            而是【把p1身上的study属性交给x】
            【而且study属性还是一个特殊的属性--一个可以调用的函数】
            所以我们管study也叫方法。

            注意:
               【p1.study()】属于实例.调用
               【x()】属于直接调用

               【类内部的方法,在局部都开启了严格模式】   
        */

    </script>
</body>
</html>

1659339161172

24. 为什么类内部方法中的this为undefined?

(实现案例:今天天气很炎热/凉爽 点击进行切换)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 21. 为什么类内部方法中的this为undefined?-->
    <script type="text/babel">
        class Weather extends React.Component {
            constructor(props) {
                super(props)
                
                // 初始化状态
                this.state = {
                    isHot: true
                }
            }

            render() {
                // 读取状态
                const { isHot } = this.state

                return <h1 onClick={this.changeWeather}>今天天气很{ isHot == true ? '炎热' : '凉爽' }</h1>
                
                /*
                    分析:
                    onClick={this.changeWeather} 这行代码写完之后,有没有调用changeWeather?
                    根本没调。
                    你只是通过类的实例对象this沿着原型链查找,找到了changeWeather函数,然后把这个changeWeather函数直接交给onClick作为回调了。
                    等你点击<h1>时,React直接从堆里面把changeWeather函数拉出来,直接执行。
                    那我想问:changeWeather还算是通过实例调用吗?根本不是。
                
                */

                /*
                    总结:
                    由于changeWeather是作为onClick的回调,所以【不是通过实例调用】的,是【直接调用】,
                    再加上【类内部的方法默认开启了局部的严格模式】,
                    所以changeWeather中的this为undefined
                    
                */
            }

            changeWeather() {
                console.log(this) // undefined
                console.log(this.state.isHot)
            }

        } 

        ReactDOM.render(<Weather/>, document.getElementById('test'))
    </script>
</body>
</html>

一句话总结:

onClick={this.changeWeather} 不是通过实例.调用的,而是属于直接调用。所以changeWeather内部的this,不可能指向实例对象自身

25. 解决类内部方法中this指向的问题

(实现案例:今天天气很炎热/凉爽 点击进行切换)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 22. 解决类内部方法中this指向的问题 -->
    <script type="text/babel">
        class Weather extends React.Component {
            constructor(props) {
                super(props)
                
                // 初始化状态
                this.state = {
                    isHot: true
                }

                // 解决方法一
                this.changeWeather = this.changeWeather.bind(this)
                /*
                 拿到原型上的changeWeather函数,并生成一个新的函数,这个新的函数中的this,指向Weather类的实例对象this。

                 然后再挂到Weather类的实例对象自身this上。
                
                */
            }

            render() {
                // 读取状态
                const { isHot } = this.state
                
                return <h1 onClick={this.changeWeather}>今天天气很{ isHot == true ? '炎热' : '凉爽' }</h1>


                // 解决办法二
                // return <h1 onClick={() => { this.changeWeather() }}>今天天气很{ isHot == true ? '炎热' : '凉爽' }</h1>
                // 外层 () => {} 是一个匿名的箭头函数,这个匿名的箭头函数是作为了onClick的回调。
                // 在匿名箭头函数的内部,通过 this.changeWeather() 【实例.】调用,
                // 所以changeWeather内部的this,指向实例对象自身。
                
            }

            changeWeather() {
                console.log(this) // Weather {props: {…}, context: {…}, refs: {…}, updater: {…}, state: {…}, …}
                console.log(this.state.isHot) // true
            }

        } 

        ReactDOM.render(<Weather/>, document.getElementById('test'))
    </script>
</body>
</html>

1f02ab183440c8c94b6deb3a7702987

26. setState()的使用

(实现案例:今天天气很炎热/凉爽 点击进行切换)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 23. setState()的使用 -->
    <script type="text/babel">
        class Weather extends React.Component {
            constructor(props) {
                console.log('constructor')
                super(props)
                // 初始化状态
                this.state = {
                    isHot: true,
                    wind: '微风'
                }
                // 解决changeWeather中this指向问题
                this.changeWeather = this.changeWeather.bind(this)
            }

            render() {
                console.log('render')
                // 读取状态
                const { isHot, wind } = this.state
                return <h1 onClick={this.changeWeather}>今天天气很{ isHot == true ? '炎热' : '凉爽' }{wind}</h1>
                
            }

            changeWeather() {
                console.log(this)

                const isHot = this.state.isHot

                // 错误写法:直接更改state中的值,React不认可这种更改
                // this.state.isHot = !isHot
                // console.log(this.state.isHot)

                // 【问题】:
                // isHot值已经改变,但是页面展示不跟着变化
                // 【严重注意】:
                // 状态(state)不可直接更改,要借助一个内置API--setState()去更改。

                // 正确写法:
                this.setState({
                    isHot: !isHot
                })

                /*
                   setState() 是合并,不是替换
                */

            }

        } 

        ReactDOM.render(<Weather/>, document.getElementById('test'))

        /*
            构造器调用几次?
             ----1次。
             因为你写了1次<Weather/>标签,React就帮你new出来1个Weather实例,所以构造器也就只执行了1次。

        */

        /*
            render调用几次?
             ----1 + n次。
             1 是初始化的那次
             n 是你每一次setState()修改状态,React都会重新调用一次render

        */

        /*
            changeWeather调用几次?
             ----点几次,调几次
        */

        /*
            小总结:
                (1). 构造器为啥要写?
                        为了初始化状态
                        还可以解决this指向问题
                (2). render里面的this就是组件实例对象
                        render中做的最多的事情就是:
                        从状态(state)中读东西并展示。
                (3). changeWeather就是读state中的值并修改        
        */
    </script>
</body>
</html>

27. 总结与分析类式组件

1. constructor()中,this指向该类的实例对象

2. render()中的this也是实例对象

因为你写了组件标签,然后react中自动帮你new Weather()了一个实例对象,比如叫w1,并且通过【w1.render()调用】的render函数,

所以render中的this指向实例对象,也没问题。

3. changeWeather()中的this指向为什么指向undefined?

因为changeWeather根本就【不是w1.调用】的。

而是【作为事件的回调在使用】。(等触发了点击事件时,直接把changeWeather函数拉出来,直接调用)

而且由于类内部的方法开启了局部严格模式,

所以changeWeather()中的this指向了undefined

4. 【在类内部写的自定义方法(一般方法),基本上都是作为事件回调在使用】。

也就是说类的所有内部方法中的this,都会指向undefined

如下图

image

5. 为什么要写构造器?

(1)因为我们要对Weather类的实例对象,进行一些初始化的操作(初始化状态)

(2)为事件处理函数绑定实例

感觉我们的构造器写的很被动。

28. 复习在类中直接写赋值语句,给类的实例对象添加属性

<script type="text/javascript">
        class Car {
            constructor(name, price) {
                this.name = name
                this.price = price

                /* 
                    如果要给每个实例对象自身增加一个属性,且属性值不需要传递进来,而是一个固定的值

                    那么这个属性根本不需要往构造器中写 
                */
                // this.wheel = 4
            }

            /*
                类中可以直接写【赋值语句】
                含义就是:
                往实例自身上追加一个属性,名为wheel,值为4

                也就是说,不一定非要在构造器中做初始化这件事,
                
                但如果属性值是new时传递进来的,则必须在构造器中接并初始化
            */
            wheel = 4
        }

        const c1 = new Car('奔驰C63', 199)
        console.log(c1)

    </script>

如下图:

image

29. 总结state

(实现案例:今天天气很炎热/凉爽 点击进行切换)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 26. 总结state -->
    <script type="text/babel">
        class Weather extends React.Component {

            // 【类中写赋值语句】,表示给实例自身添加一个属性
            // (属性名叫state,值是一个对象)
            state = {
                isHot: true,
                wind: '微风'
            }

            render() {
                const { isHot, wind } = this.state
                return <h1 onClick={this.changeWeather}>今天天气很{ isHot == true ? '炎热' : '凉爽' }, {wind}</h1>
                
            }

            //  解决类内部方法中this指向undefined问题

            // 【原理1:类中写赋值语句】 + 【原理2:箭头函数中的this,指向其外部的this】
            // 【原理1:就是把changWeather换了一个地方】
            //  原来是放在类的原型对象上,现在是放在类所缔造的实例对象自身
            // 【原理2:箭头函数中的this,指向其外部的this】

            // 下面这种写法是:
            // 给Weather类的实例对象自身添加了一个changeWeather属性,而Weather类的原型上就不再有changeWeather方法
            // changeWeather属性值是箭头函数,则箭头函数中的this就指向Weather类中的this。
            // 【Weather类中的this也指向实例对象】
            changeWeather = () => {
                const isHot = this.state.isHot
                this.setState({
                    isHot: !isHot
                })
            }

            // 原来的写法是:
            // 把changeWeather方法放在了Weather类的原型上
            // changeWeather () {
            //     const isHot = this.state.isHot
            //     this.setState({
            //         isHot: !isHot
            //     })
            // }

        } 

        ReactDOM.render(<Weather/>, document.getElementById('test'))
    </script>

</body>
</html>

原理如下图:

1659423006022

image

30. 给组件标签添加属性,从组件外部,向组件内部传递数据并使用

实现以下案例:

image

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>
    
    <!-- 27. 从组件外部,向组件内部传递数据并使用 (给 <组件标签/> 添加属性的方式)-->
    <script type="text/babel">
        // 创建组件
        class Person extends React.Component {
            render() {
                console.log(this)
                return (
                    <ul>
                        <li>姓名:{this.props.name}</li>
                        <li>性别:{this.props.sex}</li>
                        <li>年龄:{this.props.age}</li>
                    </ul>
                )
            }
        }

        // 渲染组件到页面
        ReactDOM.render(<Person name='jerry' age='19' sex='男' />, document.getElementById('test'))

        /*
            在标签上添加了 name='jerry':
            React在帮你new Person()实例的时候,
            就自动的把 {name:'jerry'}这组{key:value}搜集好,
            放在了实例this.props属性上

        */

        /*
            (1). 向组件内部传递数据:

                通过给【组件】标签【添加属性】的方式,
                可以向组件内部传递数据。

                <Person name='jerry' age='19' sex='男' />

            (2). 组件内部接收数据:
            
                传递进来的数据,都会放在组件的实例对象this的props属性上。

                并且是:以多组{key1:value1, key2:value2, ...}的形式
                即 this.props = {name:'jerry',age:'19', sex:'男'}
        */

    </script>
</body>
</html> 

image

31. 复习展开运算符

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 28. 复习展开运算符 -->
    <script type="text/javascript">
        let arr1 = [1,3,5,7,9]
        let arr2 = [2,4,6,8,10]

        // 1. 展开一个数组
        console.log(...arr1)  // 1 3 5 7 9

        // 2. 拼接两个数组
        let arr3 = [...arr1, ...arr2]
        console.log(arr3) // [1, 3, 5, 7, 9, 2, 4, 6, 8, 10]

        // 3. 在函数中使用
        /*
            function sum(a,b) {
                return a+b
            }
            console.log(sum(1,2))
        */

        function sum(...numbers) {
            /*
                numbers是由实参组成的数组
                console.log(numbers) // [1, 2, 3, 4]
            */    
            
            return numbers.reduce((total, currentValue) => {
                /* 
                    (1). reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。
                    (2). total --- 初始值, 或者计算结束后的返回值
                    (3). currentValue --- 当前元素
                */
                return total + currentValue
            })
        }
        console.log(sum(1,2,3,4)) // 10

        // 4. 展开运算符,【不能直接展开对象】
        let person = {name: 'tom', age: 18}
        // console.log(...person) // TypeError: Found non-callable @@iterator

        // 5. 复制对象的引用关系【不生成新对象】(仅仅是引用关系的复制)
        let p = person
        p.name = 'rose'
        console.log('@', person.name)  // rose
        // p 和 person 指向的是同一个对象

        // 6. 复制对象【生成新对象】(person2为新对象)
        let person2 = {...person} 
        person.name = 'jerry'
        console.log(person2.name) // tom

        // 7. 复制对象并修改新对象属性
        let person3 = {...person, name: 'jack'}
        console.log(person3) // {name: 'jack', age: 18}

    </script>
</body>
</html>

image

32. 向组件内部,批量传递数据

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 29. 向组件内部,【批量传递数据】 -->
    <script type="text/babel">
        class Person extends React.Component {
            render() {
                console.log(this)
                return (
                    <ul>
                        <li>姓名:{this.props.name}</li>
                        <li>性别:{this.props.sex}</li>
                        <li>年龄:{this.props.age}</li>
                    </ul>
                )
            }
        }

        // 模拟数据
        let p = {name: 'tom', sex: '男', age: 19}

        // 使用展开运算符{...obj},向组件内部,【批量传递数据】
        ReactDOM.render(<Person {...p}/>, document.getElementById('test'))
    </script> 
</body>
</html>

33. 在组件内部,对传递进来的组件标签属性(props)限制

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 30. 在组件内部,对传递进来的组件标签属性(props)限制 -->
    <!-- 
        需求1:在组件内部,将传递进来的age标签属性,进行+1
        需求2:若在组件外部,没有传递sex标签属性,则在组件内部,将sex标签属性默认展示“男”
        需求3:对name标签属性必传的校验
    -->
    <div id="test2"></div>
    <div id="test3"></div>
    
    <!-- 引入prop-types,用于对组件标签属性进行限制 -->
    <!-- 全局可以直接使用 PropTypes对象 -->
    <script src="../js/prop-types.js"></script>

    <script type="text/babel">
        class Person extends React.Component {
            render() {
                return (
                    <ul>
                        <li>姓名:{this.props.name}</li>
                        <li>性别:{this.props.sex}</li>
                        {/*在组件内部,对传递进来的age标签属性,进行+1*/}
                        <li>年龄:{this.props.age+1}</li>
                    </ul>
                )
            }
        }

        /*
            在组件内部,对传递进来组件标签属性进行限制
            
            【给类自身添加propTypes】属性,注意p小写
        */
       Person.propTypes = {
        // name: React.PropTypes.string 
        
        /* 
            Uncaught TypeError: Cannot read properties of undefined (reading 'string')

            在 React 15.xxx版本时,React自身一直在维护 React.PropTypes

            在 React 16 版本之后,React自身就不再维护 PropTypes 属性了,而是单独抽离出来了prop-types.js 库

            引入prop-types.js之后,全局就可以直接使用PropTypes对象。不再从React身上取这个PropTypes 
            
            【注意P大写】
        */

        // 表示name标签属性必须是string类型
        // name: PropTypes.string 

        // 表示name标签属性必须是string类型,且必传
        name: PropTypes.string.isRequired, 

        // 表示 age标签属性必须是number类型
        age: PropTypes.number
       }

        // let p = {name: 100, sex: '男', age: 19}
        /*  
            Failed prop type: Invalid prop `name` of type `number` supplied to `Person`, expected `string`.
            控制台报错:name标签属性期待是string类型,而非number
        */

        // let p = {sex: '男', age: 19}
        /*
            Failed prop type: The prop `name` is marked as required in `Person`, but its value is `undefined`.
            控制台报错:name标签属性被标记为必传
        */
        

        /*
            在组件内部,给没有传递的组件标签属性,设置默认值
        */
        Person.defaultProps = {
            sex: '不男不女'
        }

        // let p = {name: '老刘', age: 19}
        /*
            姓名:老刘
            性别:不男不女
            年龄:19
        */

        let p = {name: '老刘', sex: '男'}
        /*
            姓名:老刘
            性别:男
            年龄:NaN

            由于没有传递age属性,则组件拿到的age属性就是undefined。
            而undefined参数数学运算+1,就变成了NaN

            解决方法:最好在Person.defaultProps中给age添加一个默认值

            Person.defaultProps = {
                sex: '不男不女', // 给sex标签属性添加默认值 
                age: 18 // 给age标签属性添加默认值 
            }
        */

        ReactDOM.render(<Person {...p}/>, document.getElementById('test'))

        // 传递Number类型的age标签属性 age={19}
        ReactDOM.render(<Person name='jerry' age={19} sex='女'/>, document.getElementById('test2'))
        
        // 传递字符串类型的age标签属性 age="19"
        // ReactDOM.render(<Person name='tom' age='18' sex='男'/>, document.getElementById('test3'))

        ReactDOM.render(<Person name='tom' age={18} sex='男'/>, document.getElementById('test3'))
    </script>  
</body>
</html>

运行结果:

image

34. 不允许修改传递进来的组件标签属性(props)

   render() {
     // 【不允许修改】传递进来的组件标签属性
     this.props.name = 'jack' 
     return (
                    <ul>
                        <li>姓名:{this.props.name}</li>
                        <li>性别:{this.props.sex}</li>
                        {/*下面这行是【运算】,不是修改*/}
                        <li>年龄:{this.props.age+1}</li>
                    </ul>
                )
  }

35. 在类中使用static关键字 + 赋值语句,给类自身添加属性 (简写propTypes和defaultProps)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <div id="test2"></div>
    <div id="test3"></div> 

    <!-- 引入prop-types,用于对组件标签属性进行限制 -->
    <!-- 全局可以直接使用 PropTypes对象 -->
    <script src="../js/prop-types.js"></script>

    <!-- 32. 使用static关键字,给类自身添加属性【简写propTypes和defaultProps】-->

    <script type="text/babel">
        class Person extends React.Component {

            // 使用static关键字,可以给Person类身上添加属性,叫propTypes
            static propTypes = {
                name: PropTypes.string.isRequired, 
                age: PropTypes.number
            }

            // 使用static关键字给类自身添加defaultProps属性
            static defaultProps = {
                sex: '不男不女'
            }

            /*
                原理:只要保证类自身有propTypes或者defaultProps属性,则类就能帮你进行标签属性的限制

                类外部的 Person.propTypes = {} 等同于 
                类内部的 static propTypes = {}

                类外部的 Person.defaultProps = {} 等同于 
                类内部的 static defaultProps = {}
            */

            render() {
                return (
                    <ul>
                        <li>姓名:{this.props.name}</li>
                        <li>性别:{this.props.sex}</li>
                        <li>年龄:{this.props.age+1}</li>
                    </ul>
                )
            }
        }

        let p = {name: '老刘', sex: '男'}
        
        ReactDOM.render(<Person {...p}/>, document.getElementById('test'))

        ReactDOM.render(<Person name='jerry' age={19} sex='女'/>, document.getElementById('test2'))
        
        ReactDOM.render(<Person name='tom' age={18} sex='男'/>, document.getElementById('test3'))
    </script>

36. 构造器中的props参数与this.props的关系

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 引入prop-types,用于对组件标签属性进行限制 -->
    <!-- 全局可以直接使用 PropTypes对象 -->
    <script src="../js/prop-types.js"></script>

    <!-- 33. 构造器中的props参数与this.props的关系 -->
    <script type="text/babel">
        class Person extends React.Component {

            // (1). 构造器中不接props,super()不传props,不使用 this.props
            // ok-控制台不报错
            // constructor() {
            //     super()
            // }

            // (2). 构造器中接收props,super()传递props,不使用 this.props
            // ok-控制台不报错
            // constructor(props) {
            //     super(props)
            // }

            // (3). 构造器中接收props,super()传递props,使用 this.props
            // ok-控制台不报错
            constructor(props) {
                super(props)
                console.log(this.props) // {name: '老刘', sex: '男', age: 18}
                console.log(props) // {name: '老刘', sex: '男', age: 18}
            }

            // (4). 构造器中接收props,super()不传props
            // 则在构造器中的【实例自身的this.props】为undefined
            // constructor(props) {
            //     super()
            //     console.log(this.props) // undefined
            // }

            // (5). 构造器中不接props,super()不传props
            // 则在构造器中的【实例自身的this.props】为undefined
            // constructor() {
            //     super()
            //     console.log(this.props) // undefined
            // }
            

            /*
                一句话总结:
                类中的构造器能不能省略? 完全可以

                如果在构造器中,不接也不传props,那你就无法通过【实例.props】来取值
            */

            /*
                构造器是否接收props,是否传递给super,取决于:是否希望在构造中通过this访问props
            */

            /*
                一般在构造器中,我们可以直接用props,不使用this.props。
                所以构造器能省略就省略。(在开发当中,几乎不写)
            */

            static propTypes = {
                name: PropTypes.string.isRequired, 
                age: PropTypes.number
            }

            static defaultProps = {
                sex: '不男不女'
            }

            render() {
                return (
                    <ul>
                        <li>姓名:{this.props.name}</li>
                        <li>性别:{this.props.sex}</li>
                        <li>年龄:{this.props.age+1}</li>
                    </ul>
                )
            }
        }

        let p = {name: '老刘', sex: '男', age: 18}
        
        ReactDOM.render(<Person {...p}/>, document.getElementById('test'))

    </script> 
</body>
</html>

37. 函数式组件中的props参数以及对组件标签属性(props)进行限制

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 引入prop-types,用于对组件标签属性进行限制 -->
    <!-- 全局可以直接使用 PropTypes对象 -->
    <script src="../js/prop-types.js"></script>

    <!-- 34. 函数式组件中的props参数以及对标签属性(props)进行限制 -->
    <script type="text/babel">
        function Person(props) {
            // 在函数式组件中,组件的标签属性都被自动搜集到了props参数
            return (
                    <ul>
                        <li>姓名:{props.name}</li>
                        <li>性别:{props.sex}</li>
                        <li>年龄:{props.age+1}</li>
                    </ul>
                )
        }

        // 通过【类.propTypes】方式,对组件的标签属性(props)进行类型和必要性的限制
        Person.propTypes = {
            name: PropTypes.string.isRequired, 
            age: PropTypes.number
        }

        // 通过【类.defaultProps】方式,对组件的标签属性(props)进行默认值的限制
        Person.defaultProps = {
            sex: '不男不女'
        }

        let p = {name: '老刘', sex: '男', age: 18}
        
        ReactDOM.render(<Person {...p}/>, document.getElementById('test'))
    </script> 
    
</body>
</html>

38. 总结props

1. 组件标签的属性

<Person name="jerry" age={19} sex="男" />

name,age,sex 都是组件标签的属性

对于类式组件,组件标签的属性自动搜集到组件实例对象的props属性上。

对于函数式组件,组件标签的属性会自动搜集到函数的props参数上

2. 对组件标签的属性进行限制

给组件自身添加 propTypes 和 defaultProps 属性。

注意 p 和 d 小写

另外,可以直接使用大写的 PropTypes,

是因为引入了 <script src="../js/prop-types.js"></script>

3. 组件.propTypes和组件.defaultProps的简写

其实就是把 propTypes 和 defaultProps 属性,从类的外部,移到了类的内部。

利用了类中的 static 关键字。

而函数式组件,无法简写,只能通过 组件.propTypes和组件.defaultProps 方式对组件标签的属性进行限制

4. 函数式组件,也能使用 props参数

但是要对函数式组件的props做限制,就无法在组件内部实现,

只能通过组件.propTypes 或 组件.defaultProps 方式在组件(函数)外部进行。

理解:

1)每个组件对象都会有 props 属性

  1. 组件标签的所有属性都保存在props中

作用:

  1. 给组件标签添加属性,可以从组件外部向组件内部,传递变化的数据

  2. 注意:组件内部不要修改props

5. 从react 15.5开始,prop-types.js库从react库中单独抽离出来

从15.5开始,需要从 prop-types.js库中读取 PropTypes对象,

不再是React.PropTypes获取

6. 可以通过展开运算符批量传递组件标签的属性

<Person {...p} />

7. 设置组件标签属性的默认值

  Person.defaultProps = {
     age: 18,
     sex: '男'
  }

8. 构造器中的props参数与this.props的关系探讨

39. 使用ref属性给JSX标签打标识

需求:

页面中有两个input,一个button

  1. 点击button,提示第一个输入框中的值

  2. 第二个输入框失去焦点时,提示这个输入框中的值

props_event (2)

40. 字符串ref(作用:ref当前所处的DOM会被搜集到实例自身的this.refs属性上)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 字符串ref(作用:ref当前所处的DOM会被搜集到实例自身的this.refs属性上) -->

    <script type="text/babel">

        class Demo extends React.Component {

            showData = () => {

                /* (1)
                   【原生js】获取真实DOM:
                    通过document.getElementById()获取真实DOM
                    (不推荐)
                */
                // let input1 = document.getElementById('input1')
                // console.log(input1.value)

                /* (2)
                   React中,所有被ref属性标识的JSX标签所对应的真实DOM,
                   都自动被搜集到了实例对象的refs属性上
                   通过 this.refs.xx 访问

                   注意,this.refs.xxx 拿到的是真实DOM,而非JSX标签的虚拟DOM
                */
                console.log(this.refs.input1)
                console.log(this.refs.input1.value)

            }

            handleBlur = () => {
                console.log(this.refs.input2.value)
            }

            render() {
                return (
                    <div>
                        {/* (1)【原生js】给JSX标签打标识:给JSX标签添加id属性 */}
                        {/* <input id="input1" type="text" placeholder="点击按钮获取数据"/> &nbsp; */}

                        {/* (2)React给JSX标签打标识:给JSX标签添加 ref属性 */}
                        <input ref="input1" type="text" placeholder="点击按钮获取数据"/> &nbsp;

                        <button onClick={this.showData}>点击按钮</button> &nbsp;

                        <input ref="input2" onBlur={this.handleBlur} type="text" placeholder="鼠标失去焦点获取数据"/>

                        {/*
                            理解:
                                组件内的标签可以定义ref来标识自己。
                                (就类似于原生中的id)
                        */}

                    </div>
                )
            }
        }
        
        ReactDOM.render(<Demo />, document.getElementById('test'))
    </script> 
</body>
</html>

image

41. 回调函数形式的ref(作用:把ref当前所处的DOM挂到实例自身this上)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 38. 回调函数形式的ref(作用:把ref当前所处的DOM挂到实例自身this上)-->
    <script type="text/babel">

        class Demo extends React.Component {

            showData = () => {
                console.log(this.input1)
            }

            handleBlur = () => {
                console.log(this.input2)
            }

            render() {
                return (
                    <div>
                        {/*
                            回调函数:
                                1). 你定义的函数
                                2). 你没有调用
                                3). 函数最终执行了

                                ref={()=>{console.log('@')}} 就是一个回调函数。

                        */}
                        {/* <input ref={()=>{console.log('@')}} type="text" placeholder="点击按钮获取数据"/> &nbsp; */}

                        {/*
                            回调函数能收到什么参数?
                                取决于谁调的它。
                            换句话说:回调函数能收到什么参数,取决于回调函数的调用者

                            ref={(a)=>{console.log(a)}} 查看下参数a
                            a正是当前这个ref所处的DOM节点

                        */}
                        {/* <input ref={(a)=>{console.log(a)}} type="text" placeholder="点击按钮获取数据"/> &nbsp; */}

                        {/*
                            this.input1 = a
                            把当前ref所处的节点,挂在实例对象自身上,并取名为input1
                        */}
                        {/* <input ref={(a)=>{this.input1 = a}} type="text" placeholder="点击按钮获取数据"/> &nbsp; */}
                         
                        {/*
                            精简写法:
                                箭头函数的参数只有一个,则箭头左侧的()可以省略
                                箭头函数的函数体中只有一句,则箭头右侧的 {} 可以省略
                        */}
                        <input ref={c => this.input1 = c} type="text" placeholder="点击按钮获取数据"/> &nbsp;

                        <button onClick={this.showData}>点击按钮</button> &nbsp;

                        <input ref={c => this.input2 = c} onBlur={this.handleBlur} type="text" placeholder="鼠标失去焦点获取数据"/>

                    </div>
                )
            }
        }
        
        ReactDOM.render(<Demo />, document.getElementById('test'))
    </script>

</body>
</html>

42. ref内联回调函数执行次数的问题

如果ref回调函数是以【内联函数】的方式定义的,在【更新】过程中它会被执行两次

内联函数

ref={c => this.input1 = c}

这种形式就是内联函数

换句话说:在JSX标签内直接写函数就是内联函数

更新

在render中不会存在这个问题,只有在更新时才会被执行两次

43. 解决ref内联回调函数执行2次的问题

案例:综合天气炎热/凉爽与点击按钮获取取input值2个案例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 40. 解决ref内联回调函数执行2次的问题 -->
    <!-- 案例:综合天气炎热/凉爽与点击按钮获取取input值2个案例 -->
    <script type="text/babel">
        class Demo extends React.Component {

            state = {
                isHot: true
            }

            // 案例1: ref内联回调函数 ref={c => { this.input1 = c; console.log('@', c)}}
            showData = () => {
                console.log(this.input1.value)
            }

            // 案例1: 把ref回调函数放在实例自身 ref={ this.saveNode }
            saveNode = (c) => {
                this.input1 = c; 
                console.log('@', c)
            }

            // 案例2
            toggleWeather = () => {
                const { isHot } = this.state
                this.setState({
                    isHot: !isHot
                })
            }

            render() {
                const { isHot } = this.state
                return (
                    <div>
                        {/* 案例1:点击按钮获取input值 */}

                        {/* ref内联回调函数 */}
                        {/* <input ref={c => { this.input1 = c; console.log('@', c)}} type="text" /> &nbsp; */}

                        {/* 把ref回调函数放在实例自身 */}
                        <input ref={this.saveNode} type="text" /> &nbsp;
                        <button onClick={this.showData}>点击按钮</button>

                        {/* 案例2:点击h2切换显示天气炎热/凉爽 */}
                        <h2 onClick={this.toggleWeather}>今天天气很{isHot ? '炎热': '凉爽'}</h2>
                    </div>
                )
                /*  
                    案例2中点击h2,更新state时,
                    案例1中的ref={c => { this.input1 = c; console.log('@', c)}} 执行了2次
                    案例1受到了案例2的影响。

                    控制台输出:
                    @ null
                    @ <input type=​"text">​

                    原因:
                    react在更新时,第1次的节点传入null,是为了做清空的动作
                    第2次才把当前ref所处DOM节点传入

                    换句话说,这是因为在每次渲染时会创建一个新的函数实例,所以React清空旧的ref再设置新的。


                    解决方法:
                    把ref的内联回调函数(下面这种)
                    <input ref={c => { this.input1 = c; console.log('@', c)}} type="text" />
                    换成放在实例自身
                    <input ref={this.saveNode} type="text" />

                    结果:
                    首先在render时,ref回调函数会自动执行1次。
                    在state更新时,render函数再次被调用,
                    但由于ref回调函数放在了实例对象自身,则ref回调函数不会再次被调用。
                    也就是不会再输出
                    @ null
                    @ <input type=​"text">​
                    是两次都不会再输出。
                    也就是说案例2就不会再影响案例1

                    但是这一点无关紧要
                    以后还是【建议直接写ref内联回调函数】。
                */
            }
        }

        ReactDOM.render(<Demo/>, document.getElementById('test'))
    </script>
</body>
</html>

总结一句话:这一点无关紧要以后还是【建议直接写ref内联回调函数】

44. React.createRef()(作用:创建ref容器,存放被ref属性所标识的DOM节点)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 41. React.createRef()(作用:创建ref容器,存放被ref属性所标识的DOM节点) -->
    <script type="text/babel">

        class Demo extends React.Component {

            /*
                React.createRef调用后可以返回一个容器,该容器可以存储被ref所标识的节点

                该容器是专人专用的,意思是只能存放一个被ref标识的节点

                若要存放多个被ref标识的节点,需要创建多个这样的容器。
            */
            myRef = React.createRef()

            myRef2 = React.createRef()

            showData = () => {
                console.log(this.myRef) 
                // {current: input}
                console.log(this.myRef.current)
                console.log(this.myRef.current.value)
            }

            handleBlur = () => {
                console.log(this.myRef2.current.value)
            }

            render() {
                return (
                    <div>
                        <input ref={this.myRef} type="text" placeholder="点击按钮获取数据"/> &nbsp;

                        <button onClick={this.showData}>点击按钮</button> &nbsp;

                        <input ref={this.myRef2} onBlur={this.handleBlur} type="text" placeholder="鼠标失去焦点获取数据"/>

                    </div>
                )
            }
        }
        
        ReactDOM.render(<Demo />, document.getElementById('test'))
    </script>
</body>
</html>

45. 不要过度使用ref

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 42. 不要过度使用ref -->
    <script type="text/babel">

        class Demo extends React.Component {

            myRef = React.createRef()

            myRef2 = React.createRef()

            showData = () => {
                console.log(this.myRef.current.value)
            }

            // 不要过度使用ref,可以用event.target
            handleBlur = (event) => {
                console.log(event.target.value)
            }

            render() {
                return (
                    <div>
                        <input ref={this.myRef} type="text" placeholder="点击按钮获取数据"/> &nbsp;

                        <button onClick={this.showData}>点击按钮</button> &nbsp;

                        <input onBlur={this.handleBlur} type="text" placeholder="鼠标失去焦点获取数据"/>

                    </div>
                )
            }
        }
        
        ReactDOM.render(<Demo />, document.getElementById('test'))
    </script>
</body>
</html>

46. 在react中,如何搜集表单数据?

需求:

输入用户名和密码,点击登录,提示输入的表单数据

form

非受控组件(现用现取数据)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 非受控组件:现用现取数据 -->
    <script type="text/babel">

        class Demo extends React.Component {

            handleSubmit = (event) => {
                /*
                    使用event.preventDefault(),阻止form表单的默认跳转
                */
                event.preventDefault()

                const { username, passowrd } = this
                alert(`输入的用户名是:${username.value},输入的密码是:${passowrd.value}`)
            }
            
            /*
                用原生中的form,表单提交之后,默认会进行页面的跳转。

                跳转之前的页面会刷新,表单数据就清空了

                (即便去掉form的action属性,提交之后依然会跳转页面)
            */
            render() {
                return (
                   <form action="http://www.baidu.com" onSubmit={this.handleSubmit}>
                    用户名:<input ref={c=>this.username=c} type="text" name="username"/>
                    密码:<input ref={c=>this.passowrd=c} type="password" name="password"/>
                    <button>登录</button>
                   </form> 
                )
            }

            /*
                上面代码就是“非受控组件”:就是现用现取数据
            */
        }
        
        ReactDOM.render(<Demo />, document.getElementById('test'))
    </script>
</body>
</html>

受控组件(使用onChange事件,维护state)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 45. 受控组件 -->
    <script type="text/babel">

        class Demo extends React.Component {

            // 初始化状态
            state = {
                username: '',
                password: ''
            }

            handleUsername = (event) => {
                this.setState({
                    username: event.target.value
                })
            }

            handlePassword = (event) => {
                this.setState({
                    password: event.target.value
                })
            }

            handleSubmit = (event) => {
                event.preventDefault()
                
                const {username, password} = this.state
                alert(`输入的用户名是:${username},输入的密码是:${password}`)
            }
            
            render() {
                return (
                <form action="http://www.baidu.com" onSubmit={this.handleSubmit}>
                    用户名:<input onChange={this.handleUsername} type="text" name="username"/>
                    密码:<input onChange={this.handlePassword}type="password" name="password"/>
                    <button>登录</button>
                </form> 
                )
            }

            /*
                上面代码就是“受控组件”:

                使用onChange事件监测表单输入项,使用event.target.value来获取到输入的值,存储到state中

                等需要用的时候,直接从state中取。
            */

            /*
                受控组件就是 vue 中的双向数据绑定。

                而react中没有双向数据绑定,需要程序员手动用onChange来向state中写数据
            */

            /*
                总结:
                    (1) 非受控组件就是现用现取
                    (2) 随着你的输入,维护state 就是受控组件
            */

            /*
                【推荐使用受控组件】
                因为在受控组件中,一个ref都没有用到。

                而非受控组件中,有几个输入项,就需要写几个ref。
                而react中明确表示:不要过度使用ref
            */
        }
        
        ReactDOM.render(<Demo />, document.getElementById('test'))
    </script>
</body>
</html>

47. 必须把一个函数交给onXXX做为回调(柯里化写法)

697f520f0a8bc5e28efb5c3488e8b4c

a77274c76bb640c870e420633c7f87f

b73dc1f258b083e6e2e7de0e752077e

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 46.必须把一个函数交给onXXX做为回调 -->
    <script type="text/babel">

        class Demo extends React.Component {

            // 初始化状态
            state = {
                username: '',
                password: ''
            }

            /*
                (1) onChange={this.handleFormData('username')}
                表示把this.handleFormData【函数调用的返回值】交给onChange作为回调

                而this.handleFormData函数调用的返回值恰好又是一个函数。
                所以onChange的真正的回调函数,不是handleFormData,而是handleFormData中的return后面函数。
                
                (2) onChange={this.handleFormData}
                表示把this.handleFormData【函数自身】交给onChange作为回调

                总结:事件的回调,必须是一个函数(函数自身、函数调用后返回值是函数)。
            */
            handleFormData = (type) => {
                /*
                    程序员通过调用this.handleFormData()时,会拿到参数type
                */

                /*
                    React在调用return后面的函数(onChange的回调函数)时,会拿到参数event
                */
                return (event) => {
                    console.log(type, event.target.value)
                    this.setState({
                        [type]: event.target.value
                        // 使用[]读取对象的动态属性
                    })
                }
            }

            handleSubmit = (event) => {
                event.preventDefault()
                
                const {username, password} = this.state
                alert(`输入的用户名是:${username},输入的密码是:${password}`)
            }
            
            render() {
                return (
                <form action="http://www.baidu.com" onSubmit={this.handleSubmit}>
                    用户名:<input onChange={this.handleFormData('username')} type="text" name="username"/>
                    密码:<input onChange={this.handleFormData('password')}type="password" name="password"/>
                    <button>登录</button>
                </form> 
                )
            }

            /*
                高阶函数: 如果一个函数符合下面2个规范中的任何一个,那该函数就是一个高阶函数。
                    1. 若A函数,接收的参数是一个函数,那么A函数就可以称之为高阶函数
                    2. 若A函数,调用的返回值依然是一个函数,那么A函数就可以称之为高阶函数

            */

            /*
                想想:常见的高阶函数有哪些?
                    setTimeout(()=>{})
                    数组身上的函数 arr.map(()=>{})
            */

            /*
                函数的柯里化: 通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式
            */
        }
        
        ReactDOM.render(<Demo />, document.getElementById('test'))
    </script>

</body>
</html>

48. 必须把一个函数交给onXXX做为回调(不使用柯里化)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 47. 必须把一个函数交给onXXX做为回调(不使用柯里化) -->
    <script type="text/babel">

        class Demo extends React.Component {

            // 初始化状态
            state = {
                username: '',
                password: ''
            }
            
            handleFormData = (type,event) => {
                console.log(type, event.target.value)
                this.setState({
                    [type]: event.target.value
                })
            }

            handleSubmit = (event) => {
                event.preventDefault()
                
                const {username, password} = this.state
                alert(`输入的用户名是:${username},输入的密码是:${password}`)
            }
            
            render() {
                return (
                <form action="http://www.baidu.com" onSubmit={this.handleSubmit}>
                    {/*
                        (1).把一个匿名函数交给onChange做为回调,这个匿名函数可以接收到参数event
                            注意:这个匿名函数是react帮你调用的,react在调用这个匿名的回调函数时,会帮你把event传递进去

                        (2).在这个匿名函数中,手动调用handleFormData函数,根据词法作用域的访问规则,
                        handleFormData可以访问event参数,同时可以传递自己的参数(username和password)
                    */}
                    用户名:<input onChange={event => this.handleFormData('username',event)} type="text" name="username"/>
                    密码:<input onChange={event => this.handleFormData('password',event)}type="password" name="password"/>
                    <button>登录</button>
                </form> 
                )
            }

        }
        
        ReactDOM.render(<Demo />, document.getElementById('test'))
    </script>

</body>
</html>

49. 生命周期回调函数componentDidMount(挂载后)、componentWillUnmount(卸载前)

需求:

1.让指定的文本作显示/隐藏的渐变动画

2.从完全可见,到彻底消失,耗时2s

3.点击“不活了”按钮,从界面中卸载组件

lifecycle

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 48. 生命周期回调函数
         componentDidMount(组件挂载后)、componentWillUnmount(组件卸载前)
    -->
    <!-- 需求:
        1.让指定的文本作显示/隐藏的渐变动画
        2.从完全可见,到彻底消失,耗时2s
        3.点击“不活了”按钮,从界面中卸载组件
    -->
    <script type="text/babel">

        class Demo extends React.Component {

            // state中的数据驱动着页面
            state = {
                opacity: 1
            }

            // 挂载后
            componentDidMount() {
                this.timer = setInterval(() => {
                    let { opacity } = this.state
                    opacity -= 0.1
                    if(opacity <= 0) {
                        opacity = 1
                    }
                    this.setState({opacity})
                }, 200)
            }

            // 卸载前
            componentWillUnmount() {
                clearInterval(this.timer)
            }

            handleDeath = () => {
                // 卸载组件
                ReactDOM.unmountComponentAtNode(document.getElementById('test'))
            }

            // 初始化渲染、state更新后
            render() {
                const { opacity } = this.state
                return (
                    <div>
                        <h2 style={{opacity}}>react学不会,怎么办?</h2>
                        <button onClick={this.handleDeath}>不活了</button>
                    </div>
                    
                )
            }
        }
  
        ReactDOM.render(<Demo />, document.getElementById('test'))
    </script>
</body>
</html>

50. 理解生命周期回调函数

理解:

  1. 组件从创建到死亡它会经历一些特定的阶段

  2. React组件中包含一系列钩子函数(生命周期回调函数),会在特定的时刻调用

  3. 我们在定义组件时,会在特定的生命周期回调函数中,做特定的工作

51. 生命周期回调函数有哪些?

image

挂载、更新

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 49. 生命周期回调函数有哪些? -->
    <!-- 需求:
        1. 页面上显示“当前求和为:xx”
        2. 点击按钮进行加1
    -->
    <script type="text/babel">
        class Demo extends React.Component {

            // 构造器
            constructor (props) {
                console.log('construtor')
                super(props)
                this.state = {
                    count: 0
                }
            }

            // 挂载前
            componentWillMount() {
                console.log('componentWillMount')
            }

            // 挂载后
            componentDidMount() {
                console.log('componentDidMount')
            }

            // 卸载前
            componentWillUnmount() {
                console.log('componentWillUnmount')
            }

            // 是否更新?
            shouldComponentUpdate() {
                console.log('shouldComponentUpdate')
                return false
            }

            // 更新前
            componentWillUpdate() {
                console.log('componentWillUpdate')
            }

            // 更新后
            componentDidUpdate() {
                console.log('componentDidUpdate')
            }

            handleAdd = () => {
              let { count } = this.state 
              count += 1
              this.setState({count}) 
            }

            handleDeath = () => {
                ReactDOM.unmountComponentAtNode(document.getElementById('test'))
            }

            handleForce = () => {
                this.forceUpdate()
            }

            // 初始化渲染、更新
            render() {
                console.log('render')
                const { count } = this.state
                return (
                    <div>
                        <h2>当前求和为:{count}</h2>
                        <button onClick={this.handleAdd}>点我+1</button>  
                        <button onClick={this.handleDeath}>卸载</button>
                        <button onClick={this.handleForce}>强制更新</button>
                    </div>
                )
            }
        }

        ReactDOM.render(<Demo/>, document.getElementById('test'))
    </script>
</body>
</html>

父组件render

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello_react</title>
</head>
<body>
    <!-- 准备好一个“容器” -->
    <div id="test">

    </div>

    <!-- 引入react核心库 -->
    <!-- 全局可以直接使用 React -->
    <script src="../js/react.development.js"></script>

    <!-- 引入react-dom,用于支持react操作dom -->
    <!--  全局可以直接使用 ReactDOM -->
    <script src="../js/react-dom.development.js"></script>

    <!-- 引入babel,用于将jsx转为js -->
    <script src="../js/babel.min.js"></script>

    <!-- 50. 生命周期回调函数有哪些? -->
    <!-- 需求:
        1. 父组件A,子组件B
        1. 父组件A展示"我是A组件",点击“换车”按钮,切换B组件中接收到的车名
    -->
    <script type="text/babel">
        class A extends React.Component {
            state = {
                carName: '奔驰'
            }

            handleChange = () => {
                this.setState({
                    carName: '奥托'
                })
            }

            render() {
                const { carName } = this.state

                return (
                    <div>
                        <h2>我是A组件</h2>
                        <button onClick={this.handleChange}>换车</button>
                        <B carName={carName}/>
                    </div>
                )
            }
        }

        class B extends React.Component {

            // 接收【新】props前
            componentWillReceiveProps() {
                console.log('componentWillReceiveProps')
                // 注意:第一次不算
            }

            render() {
                const {carName} = this.props

                return (
                    <div>
                        我是B组件,接收到的车名是 {carName}
                    </div>
                )
            }
        }

        ReactDOM.render(<A/>, document.getElementById('test'))
    </script>
</body>
</html>

52. 总结生命周期回调函数

1.初始化阶段:由ReactDOM.render()触发---初次渲染

  1. constructor()

  2. componentWillMount()

  3. render()

  4. componentDidMount()

    常用

    一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息

2.更新阶段:由组件内部this.setState() 或者父组件render触发

  1. shouldComponentUpdate()

  2. componentWillUpdate()

  3. render()

  4. componentDidUpdate()

  5. componentWillReceiveProps()

3.卸载组件:由ReactDOM.unmountComponentAtNode()触发

  1. componentWillUnmount()

    常用

    一般在这个钩子中做一些收尾的事:例如关闭定时器、取消订阅消息

4. 新版本的react中的生命周期

  在新版本 18.x 中,所有带will的除了componentWillUnmount()之外的钩子,都要加上 UNSAFE_ 前缀:

5. 需要加UNSAFE_前缀的都有哪些?

  UNSAFE_componentWillMount()

  UNSAFE_componentWillUpdate()

  UNSAFE_componentWillReceiveProps()

53. react脚手架

(1) 脚手架的目的:简单、快速的编写一个应用。

(2) 脚手架用什么搭建的?

webpack(中包含很多的loader和plugins)

可以完成一些功能:比如代码压缩、语法检查、图片压缩、兼容性处理...

react脚手架也得借助webpack.

(3) react脚手架是不是需要亲手用webpack搭建?

不用。

facebook团队已经打造好了一套【react脚手架】。

(4) 先过概念性的东西:

(5) xxx脚手架:用来帮助程序员快速创建一个基于xxx库的模板项目。

前端三大框架Angular/React/Vue,如果它们三个不出脚手架的话,那写起代码来就特别麻烦,效率非常低。

脚手架包含了所有需要的配置(语法检查、jsx编译、devServer...)

脚手架下载好了所有相关的依赖

脚手架可以直接运行一个简单的效果

(6) react 提供了一个用于创建react项目的脚手架库: 【create-react-app

完整点说,是【创建一个基于react脚手架的模板项目】。

react 官方出了一个命令,用这个命令就可以创建一个基于react脚手架的模板项目。这个命令就是 【create-react-app】

了解模板项目文件目录,再把自己的业务逻辑加进去,就可以真正的编写一个应用了。

(7) 项目的核心技术有: react、webpack、es6、eslint...

(8) 使用脚手架开发的项目特点:模块化、组件化、工程化

54. 使用create-react-app命令创建一个基于react脚手架的模板项目

第一步:全局安装 npm install -g create-react-app

必须全局安装这个命令,才能使用这个命令创建基于react脚手架的模板项目

第二步:切换到想创建项目的目录,使用命令:create-react-app hello-react

image

第三步:进入项目文件夹:cd hello-react

第四步:启动项目: npm start

image

55. 介绍 public/index.html (主界面)

public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <!-- 1. 整个项目中,只有这一个html页面,所以是SPA(single page application)应用 -->
    <!-- %PUBLIC_URL% 代表public文件夹的路径 -->
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <!-- 2. 开启理想视口,用于做移动端网页的适配 -->
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <!-- 3. 用于配置浏览器地址栏颜色(仅适用于安卓手机浏览器)
      在开发中很少用,兼容性不好
    -->
    <meta name="theme-color" content="#000000" />
    <!-- 4. 描述网站信息 -->
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <!-- 5. 用于指定【把网页添加到手机主屏幕】后的图标(只支持苹果手机)
      兼容性不好
    -->
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <!-- 6. 应用加壳时的配置文件 -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <title>React App</title>
  </head>
  <body>
    <!-- 7. 如果你的浏览器不支持js的运行,下面这行内容就会出现在页面上 -->
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <!-- 8. 以后所有的组件都会放到root节点中 -->
    <div id="root"></div>
    <!-- 9. 只把一个组件放到root节点里面,不会放第二个组件
      ReactDOM.render()是追加的动作吗?不是。
      如果放第二个组件,则之前的组件会被顶掉。

      所以,只往root节点中放一个组件。
      这个一个组件就是App组件。

      也就是说以后用react脚手架进行开发时,ReactDOM.render()方法只执行一次。
      只是把APP组件放到root节点里
    -->
  </body>
</html>

56. 介绍react脚手架自带文件

image

57. 用react脚手架进行开发时,ReactDOM.render()方法只执行一次

image

58. 介绍 src/index.js (入口文件)

src/index.js 入口文件

image

// 引入 react 核心库 (全局可以直接使用 React)
import React from 'react';
// 引入 react-dom/client,用于支持react操作dom(全局可以直接使用 ReactDOM)
import ReactDOM from 'react-dom/client';
// 如果有样式文件,引用全局样式
import './index.css';
// 引入 APP 组件
import App from './App';
// 这个文件用于记录页面上的性能
import reportWebVitals from './reportWebVitals';

// 渲染APP组件到页面
const root = ReactDOM.createRoot(document.getElementById('root'));
// 这个index.js入口文件,怎么就能找到public/index.html里面的root容器呢?
// 这都是react官方脚手架底层写好的。我们不需要任何操作。

root.render(
  // <React.StrictMode>标签的作用
  // 用于检查App及APP内部所有子组件写的东西是否合理。
  // 比如如果使用了字符串形式的ref="xxx",则会在控制台警告。
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

reportWebVitals();

59. 主界面(public/index.html)、入口文件(src/index.js)、App外壳组件(src/App.js)的执行顺序

image

  1. 整个项目在启动后,首先来到src/index.js,在这里面

    • 引入了react核心库
    • 引入了reactDOM
    • 引入了通用样式
    • 引入了App组件
    • 最后触发了ReactDOM.render(<App/>,document.getElementById('root'))

    其实渲染的是App组件,且由于document.getElementById('root')写了root节点,则react脚手架就会自动去public/index.html中找id为root的节点

  2. 所以说 src/index.js 会主动找public/index.html

    因此,src/index.js和public/index.html不要随意更改文件名。

  3. ReactDOM.render(<App/>,document.getElementById('root'))执行完之后,App组件就显示到页面上了

  4. 在App组件内部,由于引入了src/index.css样式文件,所以通用样式也就生效了

60. 正规项目的目录结构

image

60. 介绍 src/App.js (外壳组件)

src/App.js

// 在react内部,一定使用了默认暴露export default 和 分别暴露export 多种暴露形式,
// 才可以写下面这种形式的引入
import React, { Component } from "react";

// 写法一: 先创建App组件,再暴露App组件

// // 创建App组件
// class App extends Component {
//   render() {
//     return (
//       <div>
//         Hello react!
//       </div>
//     );
//   }
// }

// // 暴露App组件
// export default App;

// -----------------------------------------------------------------


// 写法二:创建App组件的同时,暴露App组件

// // 创建并暴露App组件
// export default class App extends Component {
//   render() {
//     return (
//       <div>
//         hello,react
//       </div>
//       )
//   }
// }

// -----------------------------------------------------------------


// 写法三: 最好把App组件当成“外壳”组件

// 将 "hello,react"放在一个组件内,并将这个组件当做App的子组件
// import Hello from './components/Hello'
// export default class App extends Component {
//   render() {
//     return (
//       <div>
//         <Hello/>
//       </div>
//       )
//   }
// }

// -----------------------------------------------------------------

// 写法四:将组件分不同的文件夹统一放在components目录下

// 组件【文件夹名称大写】,代表里面放的是组件
// 【组件用index.jsx】,这样在引入时,可以少写一层

/*
   (在react中,.js和.jsx文件在引入时可以省略后缀名;
   且若文件是index.js或者index.jsx命名,则在引入文件时可以省略文件,只写文件所在的目录)
 */
import Hello from './components/Hello'
import Welcome  from "./components/Welcome";
export default class App extends Component {
  render() {
    return (
      <div>
        <Hello/>
        <Welcome/>
      </div>
      )
  }
}

61. 在组件内部引入样式文件

image

src/components/Hello/index.jsx

import React, { Component } from "react";
// 在组件内部引入样式文件
// 使用 import
import "./index.css";

export default class Hello extends Component {
  render() {
    return <h2 className="title">hello, react</h2>;
  }
}

62.不同组件中样式重名,发生样式覆盖

image

Hello组件

import React, { Component } from "react";
// 在组件内部引入样式文件
// 使用 import
import "./index.css";

export default class Hello extends Component {
  render() {
    return <h2 className="title">hello, react</h2>;
  }
}

Hello组件的样式文件

.title {
    background-color: orange;
}

Welcome组件

import React, { Component } from "react";
import "./index.css";

export default class Welcome extends Component {
  render() {
    return <h2 className="title">welcome</h2>;
  }
}

Welcome组件的样式文件

.title {
    background-color: skyblue;
}

App外壳组件

import React, { Component } from "react";
import Hello from './components/Hello'
import Welcome  from "./components/Welcome";
export default class App extends Component {
  render() {
    return (
      <div>
        <Hello/>
        <Welcome/>
      </div>
      )
  }
}

由于Hello组件和Welcome组件中,用了共同的title样式,且在引入App外壳组件时,后引入了Welcome组件,

所以Welcome组件中的title会覆盖掉Hello组件中的title样式,发生样式冲突

image

63. 解决不同组件中的样式冲突问题

1661395022201

Hello组件

import React, { Component } from "react";
// 在引入样式文件时,将 import './index.css' 写法改成下面这种形式:
import styles from "./index.module.css";

export default class Hello extends Component {
  render() {
    // 在使用样式时,将 className='title' 写法改成 className={styles.title} 这种形式:
    return <h2 className={styles.title}>hello, react</h2>;
  }
}

这样即可解决不同组件的css文件中的样式冲突

image

64. Vscode中安装react代码片段插件

image

用了这个插件,就可以使用 rcc 快速定义一个react class component

用 rfc 快速定义一个 react function component

rcc

image

运行结果

image

rfc

image

运行结果

image

65. 组件化编码的流程(通用)

image

66. todolist案例的需求

demo2_todo list (4)

67. 拆分组件(步骤1)

1661407111854

image

68. 实现静态组件(步骤2)

1661411756169

Header组件

import React, { Component } from "react";
import "./index.css";

export default class Header extends Component {
  render() {
    return (
      <div className="todo-header">
        <input type="text" placeholder="请输入你的任务名称,按回车键确认" />
      </div>
    );
  }
}

Header组件的样式文件

/*header*/
.todo-header input {
    width: 560px;
    height: 28px;
    font-size: 14px;
    border: 1px solid #ccc;
    border-radius: 4px;
    padding: 4px 7px;
  }
  
  .todo-header input:focus {
    outline: none;
    border-color: rgba(82, 168, 236, 0.8);
    box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
  }

List组件

import React, { Component } from "react";
import Item from "../Item";
import "./index.css";

export default class List extends Component {
  render() {
    return (
      <ul className="todo-main">
        <Item />
        <Item />
      </ul>
    );
  }
}

List组件的样式文件

/*main*/
.todo-main {
    margin-left: 0px;
    border: 1px solid #ddd;
    border-radius: 2px;
    padding: 0px;
  }
  
  .todo-empty {
    height: 40px;
    line-height: 40px;
    border: 1px solid #ddd;
    border-radius: 2px;
    padding-left: 5px;
    margin-top: 10px;
  }

Item组件

import React, { Component } from "react";
import "./index.css";

export default class Item extends Component {
  render() {
    return (
      <li>
        <label>
          <input type="checkbox" />
          <span>yyyy</span>
        </label>
        <button className="btn btn-danger" style={{ display: "none" }}>
          删除
        </button>
      </li>
    );
  }
}

Item组件的样式文件

/*item*/
li {
    list-style: none;
    height: 36px;
    line-height: 36px;
    padding: 0 5px;
    border-bottom: 1px solid #ddd;
  }
  
  li label {
    float: left;
    cursor: pointer;
  }
  
  li label li input {
    vertical-align: middle;
    margin-right: 6px;
    position: relative;
    top: -1px;
  }
  
  li button {
    float: right;
    display: none;
    margin-top: 3px;
  }
  
  li:before {
    content: initial;
  }
  
  li:last-child {
    border-bottom: none;
  }

Footer组件

import React, { Component } from "react";
import "./index.css";

export default class Footer extends Component {
  render() {
    return (
      <div className="todo-footer">
        <label>
          <input type="checkbox" />
        </label>
        <span>
          <span>已完成0</span> / 全部2
        </span>
        <button className="btn btn-danger">清除已完成任务</button>
      </div>
    );
  }
}

Footer组件的样式文件

/*footer*/
.todo-footer {
    height: 40px;
    line-height: 40px;
    padding-left: 6px;
    margin-top: 5px;
  }
  
  .todo-footer label {
    display: inline-block;
    margin-right: 20px;
    cursor: pointer;
  }
  
  .todo-footer label input {
    position: relative;
    top: -1px;
    vertical-align: middle;
    margin-right: 5px;
  }
  
  .todo-footer button {
    float: right;
    margin-top: 5px;
  }

App外壳组件

import React, { Component } from 'react'
import Header from './components/Header'
import List from './components/List'
import Footer from './components/Footer'
import './App.css'
export default class App extends Component {
  render() {
    return (
      <div className="todo-container">
        <div className="todo-wrap">
          <Header/>
          <List/>
          <Footer/>
        </div>
      </div>
    )
  }
}

App组件的样式文件(通用样式文件)

/*base*/
body {
    background: #fff;
  }
  
  .btn {
    display: inline-block;
    padding: 4px 12px;
    margin-bottom: 0;
    font-size: 14px;
    line-height: 20px;
    text-align: center;
    vertical-align: middle;
    cursor: pointer;
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
    border-radius: 4px;
  }
  
  .btn-danger {
    color: #fff;
    background-color: #da4f49;
    border: 1px solid #bd362f;
  }
  
  .btn-danger:hover {
    color: #fff;
    background-color: #bd362f;
  }
  
  .btn:focus {
    outline: none;
  }
  
  .todo-container {
    width: 600px;
    margin: 0 auto;
  }
  .todo-container .todo-wrap {
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 5px;
  }

69. 实现动态组件(步骤3)

1.动态显示初始化数据

image

App组件

import React, { Component } from 'react'
import Header from './components/Header'
import List from './components/List'
import Footer from './components/Footer'
import './App.css'
export default class App extends Component {

  // 将初始化数据todos放在App组件的state中
  state = {
    todos: [
      {id: '001', name: '吃饭', done: true},
      {id: '002', name: '睡觉', done: true},
      {id: '003', name: '打代码', done: false},
      {id: '004', name: '逛街', done: true}
    ]
  }

  render() {
    const { todos } = this.state
    return (
      <div className="todo-container">
        <div className="todo-wrap">
          <Header/>
          {/* 将todos传递给子组件List */}
          <List todos={todos}/>
          <Footer/>
        </div>
      </div>
    )
  }
}

List组件

import React, { Component } from "react";
import Item from "../Item";
import "./index.css";

export default class List extends Component {
  render() {
    // 接收从App组件传递进来的todos
    const { todos } = this.props;
    return (
      <ul className="todo-main">
        {todos.map((todo) => {
          // 将todo传递给子组件Item
          return <Item key={todo.id} {...todo} />;
        })}
      </ul>
    );
  }
}

Item组件

import React, { Component } from "react";
import "./index.css";

export default class Item extends Component {
  render() {
    // 接收从List组件传递进来的todo
    const { name, done } = this.props;
    return (
      <li>
        <label>
          <input type="checkbox" defaultChecked={done} />
          <span>{name}</span>
        </label>
        <button className="btn btn-danger" style={{ display: "none" }}>
          删除
        </button>
      </li>
    );
  }
}

2.交互(从绑定事件监听开始)

image

子组件Header

image

父组件App

image

子组件Header

import React, { Component } from "react";
import { nanoid } from "nanoid";
import "./index.css";

export default class Header extends Component {
  // 鼠标抬起事件的回调
  handleKeyUp = (event) => {
    const { keyCode, target } = event;
    // 判断是否是回车键
    if (keyCode !== 13) return;
    // 判断输入的值是否为空
    if (!target.value.trim()) {
      alert("输入不能为空");
      return;
    }
    // 调用从父组件App中传递进来的addTodo
    this.props.addTodo({ id: nanoid(), name: target.value, done: false });
    // 清空input框
    target.value = "";
  };

  render() {
    return (
      <div className="todo-header">
        <input
          type="text"
          onKeyUp={this.handleKeyUp}
          placeholder="请输入你的任务名称,按回车键确认"
        />
      </div>
    );
  }
}

父组件App

import React, { Component } from 'react'
import Header from './components/Header'
import List from './components/List'
import Footer from './components/Footer'
import './App.css'
export default class App extends Component {

  // 将初始化数据todos放在App组件的state中
  state = {
    todos: [
      {id: '001', name: '吃饭', done: true},
      {id: '002', name: '睡觉', done: true},
      {id: '003', name: '打代码', done: false},
      {id: '004', name: '逛街', done: true}
    ]
  }

  // 定义一个函数,供子组件Header调用
  // 子组件在调用addTodo时,会传递todo对象进来
  // 从而实现【子组件向父组件传值】
  addTodo = (todo) => {
    let {todos} = this.state
    let newTodos = [todo, ...todos]
    this.setState({
      todos: newTodos
    })
  }

  render() {
    const { todos } = this.state
    return (
      <div className="todo-container">
        <div className="todo-wrap">
          {/* 将addTodo传递给子组件Header,供子组件调用 */}
          <Header addTodo={this.addTodo}/>
          {/* 将todos传递给子组件List */}
          <List todos={todos}/>
          <Footer/>
        </div>
      </div>
    )
  }
}

70. 子组件向父组件传递数据

1.在父组件中定义一个带有参数的函数

a = (data) => {
    // 这个data就是从子组件中传递到父组件的值
    console.log(data)
}

2.并将此函数传递给子组件

<Header a={this.a}/>

3.在子组件中通过this.props.a()调用

this.props.a(data)

在父组件中,就能够接收到子组件在调用a函数时,传递进来的实参

从而拿到子组件传递到父组件中的值。

实现了从子组件向父组件传值。

71. 实现“鼠标移入高亮显示当前背景”的效果

image

image

Item组件

import React, { Component } from "react";
import "./index.css";

export default class Item extends Component {
  state = {
    mouse: false, // 表示鼠标是否进入某个Item,默认为false
  };

  //鼠标移入和鼠标移出事件的回调
  handleMouse = (flag) => {
    return () => {
      this.setState({ mouse: flag });
    };
  };

  render() {
    // 接收从List组件传递进来的todo
    const { name, done } = this.props;
    const { mouse } = this.state;
    return (
      // 给每个Item绑定鼠标进入和鼠标移出事件
      <li
        onMouseLeave={this.handleMouse(false)}
        onMouseEnter={this.handleMouse(true)}
        // 根据mouse动态判断是否需要高亮当前Item
        style={{ backgroundColor: mouse ? "#ddd" : "white" }}
      >
        <label>
          <input type="checkbox" defaultChecked={done} />
          <span>{name}</span>
        </label>
        <button
          className="btn btn-danger"
          // 根据mouse动态判断是否需要显示后面的“删除”按钮
          style={{ display: mouse ? "block" : "none" }}
        >
          删除
        </button>
      </li>
    );
  }
}

72.实现“勾选和取消勾选某个项目”的效果

image

Item组件

image

App组件

1661743045315

Item组件

import React, { Component } from "react";
import "./index.css";

export default class Item extends Component {
  state = {
    mouse: false, // 表示鼠标是否进入某个Item,默认为false
  };

  //鼠标移入和鼠标移出事件的回调
  handleMouse = (flag) => {
    return () => {
      this.setState({ mouse: flag });
    };
  };

  // 勾选和取消勾选的回调
  handleCheck = (id) => {
    return (event) => {
      // console.log(id, event.target.checked);

      // Item组件要改变App组件state中的数据
      // ==> 则要求App组件先要传递给Item组件一个函数
      // => App先传递给List,List再传递给Item
      this.props.updateTodo(id, event.target.checked);
    };
  };

  render() {
    // 接收从List组件传递进来的todo
    const { name, done, id } = this.props;
    const { mouse } = this.state;
    return (
      // 给每个Item绑定鼠标进入和鼠标移出事件
      <li
        onMouseLeave={this.handleMouse(false)}
        onMouseEnter={this.handleMouse(true)}
        // 根据mouse动态判断是否需要高亮当前Item
        style={{ backgroundColor: mouse ? "#ddd" : "white" }}
      >
        <label>
          <input
            type="checkbox"
            defaultChecked={done}
            // 添加onChange事件
            onChange={this.handleCheck(id)}
          />
          <span>{name}</span>
        </label>
        <button
          className="btn btn-danger"
          // 根据mouse动态判断是否需要显示后面的“删除”按钮
          style={{ display: mouse ? "block" : "none" }}
        >
          删除
        </button>
      </li>
    );
  }
}

App组件

import React, { Component } from 'react'
import Header from './components/Header'
import List from './components/List'
import Footer from './components/Footer'
import './App.css'
export default class App extends Component {

  // 将初始化数据todos放在App组件的state中
  state = {
    todos: [
      {id: '001', name: '吃饭', done: true},
      {id: '002', name: '睡觉', done: true},
      {id: '003', name: '打代码', done: false},
      {id: '004', name: '逛街', done: true}
    ]
  }

  // 定义一个函数,供子组件Header调用
  // 子组件在调用addTodo时,会传递todo对象进来
  // 从而实现【子组件向父组件传值】
  addTodo = (todo) => {
    let {todos} = this.state
    let newTodos = [todo, ...todos]
    this.setState({
      todos: newTodos
    })
  }

  // 定义一个函数,供孙组件Item调用
  // 用于勾选和取消勾选
  updateTodo = (id, done) => {
    let {todos} = this.state
    const newTodos = todos.map(todo => {
      if(todo.id === id) return {...todo, done}
      else return todo
    })
    this.setState({
      todos: newTodos
    })
  }

  /**
   * 状态state在哪里,操作状态的方法就在哪里。
   * 
   * 这些方法可能在自身组件中调用,也可能在子组件中调用。
   */

  render() {
    const { todos } = this.state
    return (
      <div className="todo-container">
        <div className="todo-wrap">
          {/* 将addTodo传递给子组件Header,供子组件调用 */}
          <Header addTodo={this.addTodo}/>
          {/* 将todos传递给子组件List */}
          <List todos={todos} updateTodo={this.updateTodo}/>
          <Footer/>
        </div>
      </div>
    )
  }
}

List组件

import React, { Component } from "react";
import Item from "../Item";
import "./index.css";

export default class List extends Component {
  render() {
    // 接收从App组件传递进来的todos
    const { todos, updateTodo } = this.props;
    return (
      <ul className="todo-main">
        {todos.map((todo) => {
          // 将todo传递给子组件Item
          return <Item key={todo.id} {...todo} updateTodo={updateTodo} />;
        })}
      </ul>
    );
  }
}

73. 状态state在哪里,操作state的方法就放在哪里。

1. Header组件添加一个todo到App组件的state中

=》 子组件要改变父组件的数据

=》 state在App组件中, addTodo()也要在App组件中

(父组件App要先把addTodo方法传递给子组件Header,供Header组件在合适的时机调用)

父组件App会接收到子组件Header传递进来的实参,再去改变父组件自身的state

所以说,state在哪里,操作state的方法(比如addTodo方法)就放在哪里

父组件App中的state发生了改变,就会重新调用自身的render(),则List组件就会重新加载,由于List组件渲染的是App父组件中的数据,所以List组件中的展示也会跟着变化。

2. Item组件要更新App组件中某一个todo是否勾选

=》 孙组件Item要改变祖父App组件的数据

=》 state在App组件中, updateTodo()也要在App组件中

(父组件App要先把updateTodo方法传递给子组件List,List再传递给Item,供Item组件在合适的时机调用)

父组件App会接收到孙组件Item传递进来的实参,再去改变父组件自身的state

所以说,state在哪里,操作state的方法(比如updateTodo方法)就放在哪里

74. 实现“删除某个项目”的效果

image

Item组件

image

App组件

image

Item组件

import React, { Component } from "react";
import PropTypes from "prop-types"; // 用于对props校验:(需要自行安装: npm i prop-types)(react脚手架并没有安装这个库)
import "./index.css";

export default class Item extends Component {
  state = {
    mouse: false, // 表示鼠标是否进入某个Item,默认为false
  };

  // 对接收的props进行:类型、必要性的限制
  static propTypes = {
    name: PropTypes.string.isRequired,
    done: PropTypes.bool.isRequired,
    id: PropTypes.string.isRequired,
    updateTodo: PropTypes.func.isRequired,
    deleteTodo: PropTypes.func.isRequired,
  };

  //鼠标移入和鼠标移出事件的回调
  handleMouse = (flag) => {
    return () => {
      this.setState({ mouse: flag });
    };
  };

  // 勾选和取消勾选的回调
  handleCheck = (id) => {
    return (event) => {
      // console.log(id, event.target.checked);

      // Item组件要改变App组件state中的数据
      // ==> 则要求App组件先要传递给Item组件一个函数
      // => App先传递给List,List再传递给Item
      this.props.updateTodo(id, event.target.checked);
    };
  };

  // 点击删除按钮的回调
  handleDelete = (id) => {
    // console.log(id);

    // Item组件要去改变App组件state中的数据,
    // => 需要App组件先定义且传递一个函数给Item,并供Item组件在此处调用这个函数
    // => App先传递给List,List再传递给Item
    this.props.deleteTodo(id);
  };

  render() {
    // 接收从List组件传递进来的todo
    const { name, done, id } = this.props;
    const { mouse } = this.state;
    return (
      // 给每个Item绑定鼠标进入和鼠标移出事件
      <li
        onMouseLeave={this.handleMouse(false)}
        onMouseEnter={this.handleMouse(true)}
        // 根据mouse动态判断是否需要高亮当前Item
        style={{ backgroundColor: mouse ? "#ddd" : "white" }}
      >
        <label>
          <input
            type="checkbox"
            defaultChecked={done}
            // 添加onChange事件
            onChange={this.handleCheck(id)}
          />
          <span>{name}</span>
        </label>
        <button
          className="btn btn-danger"
          // 根据mouse动态判断是否需要显示后面的“删除”按钮
          style={{ display: mouse ? "block" : "none" }}
          // 绑定点击删除按钮事件
          onClick={() => {
            this.handleDelete(id);
          }}
        >
          删除
        </button>
      </li>
    );
  }
}

App组件

import React, { Component } from 'react'
import Header from './components/Header'
import List from './components/List'
import Footer from './components/Footer'
import './App.css'
export default class App extends Component {

  // 将初始化数据todos放在App组件的state中
  state = {
    todos: [
      {id: '001', name: '吃饭', done: true},
      {id: '002', name: '睡觉', done: true},
      {id: '003', name: '打代码', done: false},
      {id: '004', name: '逛街', done: true}
    ]
  }

  // 定义一个函数,供子组件Header调用
  // 子组件在调用addTodo时,会传递todo对象进来
  // 从而实现【子组件向父组件传值】
  addTodo = (todo) => {
    let {todos} = this.state
    let newTodos = [todo, ...todos]
    this.setState({
      todos: newTodos
    })
  }

  // 定义一个函数,供孙组件Item调用
  // 用于勾选和取消勾选
  updateTodo = (id, done) => {
    let {todos} = this.state
    const newTodos = todos.map(todo => {
      if(todo.id === id) return {...todo, done}
      else return todo
    })
    this.setState({
      todos: newTodos
    })
  }

  /**
   * 状态state在哪里,操作状态的方法就在哪里。
   * 
   * 这些方法可能在自身组件中调用,也可能在子组件中调用。
   */

  // 定义一个函数,供孙组件Item调用
  // 用于删除某Item项
  deleteTodo = (id) => {
    let { todos } = this.state
    const newTodos = todos.filter(todo => {
      return todo.id !== id // 返回id不为传递进来的Item项
    })
    this.setState({
      todos: newTodos
    })
  }

  render() {
    const { todos } = this.state
    return (
      <div className="todo-container">
        <div className="todo-wrap">
          {/* 将addTodo传递给子组件Header,供子组件调用 */}
          <Header addTodo={this.addTodo}/>
          {/* 将todos传递给子组件List */}
          <List todos={todos} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo}/>
          <Footer/>
        </div>
      </div>
    )
  }
}

List组件

import React, { Component } from "react";
import PropTypes from "prop-types"; // 用于对props校验:(需要自行安装: npm i prop-types)(react脚手架并没有安装这个库)
import Item from "../Item";
import "./index.css";

export default class List extends Component {
  // 对接收的props进行:类型、必要性的限制
  static propTypes = {
    todos: PropTypes.array.isRequired,
    updateTodo: PropTypes.func.isRequired,
    deleteTodo: PropTypes.func.isRequired,
  };

  render() {
    // 接收从App组件传递进来的todos
    const { todos, updateTodo, deleteTodo } = this.props;
    return (
      <ul className="todo-main">
        {todos.map((todo) => {
          // 将todo传递给子组件Item
          return (
            <Item
              key={todo.id}
              {...todo}
              updateTodo={updateTodo}
              deleteTodo={deleteTodo}
            />
          );
        })}
      </ul>
    );
  }
}

75. 实现“对数组的条件统计”的效果

image

App组件

image

Footer组件

image

App组件

import React, { Component } from 'react'
import Header from './components/Header'
import List from './components/List'
import Footer from './components/Footer'
import './App.css'
export default class App extends Component {

  // 将初始化数据todos放在App组件的state中
  state = {
    todos: [
      {id: '001', name: '吃饭', done: true},
      {id: '002', name: '睡觉', done: true},
      {id: '003', name: '打代码', done: false},
      {id: '004', name: '逛街', done: true}
    ]
  }

  // 定义一个函数,供子组件Header调用
  // 子组件在调用addTodo时,会传递todo对象进来
  // 从而实现【子组件向父组件传值】
  addTodo = (todo) => {
    let {todos} = this.state
    let newTodos = [todo, ...todos]
    this.setState({
      todos: newTodos
    })
  }

  // 定义一个函数,供孙组件Item调用
  // 用于勾选和取消勾选
  updateTodo = (id, done) => {
    let {todos} = this.state
    const newTodos = todos.map(todo => {
      if(todo.id === id) return {...todo, done}
      else return todo
    })
    this.setState({
      todos: newTodos
    })
  }

  /**
   * 状态state在哪里,操作状态的方法就在哪里。
   * 
   * 这些方法可能在自身组件中调用,也可能在子组件中调用。
   */

  // 定义一个函数,供孙组件Item调用
  // 用于删除某Item项
  deleteTodo = (id) => {
    let { todos } = this.state
    const newTodos = todos.filter(todo => {
      return todo.id !== id // 返回id不为传递进来的Item项
    })
    this.setState({
      todos: newTodos
    })
  }

  render() {
    const { todos } = this.state
    return (
      <div className="todo-container">
        <div className="todo-wrap">
          {/* 将addTodo传递给子组件Header,供子组件调用 */}
          <Header addTodo={this.addTodo}/>
          {/* 将todos传递给子组件List */}
          <List todos={todos} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo}/>
          <Footer todos={todos}/>
        </div>
      </div>
    )
  }
}

Footer组件

import React, { Component } from "react";
import "./index.css";

export default class Footer extends Component {
  render() {
    const { todos } = this.props;

    // 已完成的数量
    // const doneCount = todos.filter((todo) => todo.done == true).length;

    /**
     * reduce()用于做数组的条件统计
     * pre表示上一次函数运行的结果
     * todo表示当前项
     */

    // const doneCount = todos.reduce((pre, todo) => {
    //   return pre + (todo.done ? 1 : 0);
    // }, 0);

    // 简写
    const doneCount = todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0);

    // 全部
    const total = todos.length;

    return (
      <div className="todo-footer">
        <label>
          <input
            type="checkbox"
            // 当todos中都是勾选时,全选打勾 ==>当已完成数量=全部时
            checked={doneCount == total && total !== 0 ? true : false}
          />
        </label>
        <span>
          <span>已完成{doneCount}</span> / 全部{total}
        </span>
        <button className="btn btn-danger">清除已完成任务</button>
      </div>
    );
  }
}

76. 实现“全选或取消全选”的效果

image

Footer组件

image

image

App组件

image

Footer组件

import React, { Component } from "react";
import "./index.css";

export default class Footer extends Component {
  // 全选和取消全选的回调
  handleCheckAll = (event) => {
    // 需要改变App中state
    // 则需要在App中先定义一个函数,且传递进来,供Footer组件在此处调用
    this.props.checkAllTodo(event.target.checked);
  };

  render() {
    const { todos } = this.props;

    // 已完成的数量
    // const doneCount = todos.filter((todo) => todo.done == true).length;

    /**
     * reduce()用于做数组的条件统计
     * pre表示上一次函数运行的结果
     * todo表示当前项
     */

    // const doneCount = todos.reduce((pre, todo) => {
    //   return pre + (todo.done ? 1 : 0);
    // }, 0);

    // 简写
    const doneCount = todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0);

    // 全部
    const total = todos.length;

    return (
      <div className="todo-footer">
        <label>
          <input
            type="checkbox"
            // 当todos中都是勾选时,全选打勾 ==>当已完成数量=全部时
            checked={doneCount == total && total !== 0 ? true : false}
            // 当手动勾“全选”或“取消全选”时,去改变App中todos
            onChange={this.handleCheckAll}
          />
        </label>
        <span>
          <span>已完成{doneCount}</span> / 全部{total}
        </span>
        <button className="btn btn-danger">清除已完成任务</button>
      </div>
    );
  }
}

App组件

import React, { Component } from 'react'
import Header from './components/Header'
import List from './components/List'
import Footer from './components/Footer'
import './App.css'
export default class App extends Component {

  // 将初始化数据todos放在App组件的state中
  state = {
    todos: [
      {id: '001', name: '吃饭', done: true},
      {id: '002', name: '睡觉', done: true},
      {id: '003', name: '打代码', done: false},
      {id: '004', name: '逛街', done: true}
    ]
  }

  // 定义一个函数,供子组件Header调用
  // 子组件在调用addTodo时,会传递todo对象进来
  // 从而实现【子组件向父组件传值】
  addTodo = (todo) => {
    let {todos} = this.state
    let newTodos = [todo, ...todos]
    this.setState({
      todos: newTodos
    })
  }

  // 定义一个函数,供孙组件Item调用
  // 用于勾选和取消勾选
  updateTodo = (id, done) => {
    let {todos} = this.state
    const newTodos = todos.map(todo => {
      if(todo.id === id) return {...todo, done}
      else return todo
    })
    this.setState({
      todos: newTodos
    })
  }

  /**
   * 状态state在哪里,操作状态的方法就在哪里。
   * 
   * 这些方法可能在自身组件中调用,也可能在子组件中调用。
   */

  // 定义一个函数,供孙组件Item调用
  // 用于删除某Item项
  deleteTodo = (id) => {
    let { todos } = this.state
    const newTodos = todos.filter(todo => {
      return todo.id !== id // 返回id不为传递进来的Item项
    })
    this.setState({
      todos: newTodos
    })
  }

  // 定义一个函数,供Footer子组件调用
  // 用于全选或者取消全选
  checkAllTodo = (done) => {
    let { todos } = this.state
    const newTodos = todos.map(todo => {
      return {...todo, done}
    })
    this.setState({
      todos: newTodos
    })
  }

  render() {
    const { todos } = this.state
    return (
      <div className="todo-container">
        <div className="todo-wrap">
          {/* 将addTodo传递给子组件Header,供子组件调用 */}
          <Header addTodo={this.addTodo}/>
          {/* 将todos传递给子组件List */}
          <List todos={todos} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo}/>
          <Footer todos={todos} checkAllTodo={this.checkAllTodo}/>
        </div>
      </div>
    )
  }
}

77. 实现“清除已完成项”的效果

image

Footer组件

image

image

App组件

image

Footer组件

import React, { Component } from "react";
import "./index.css";

export default class Footer extends Component {
  // 全选和取消全选的回调
  handleCheckAll = (event) => {
    // 需要改变App中state
    // 则需要在App中先定义一个函数,且传递进来,供Footer组件在此处调用
    this.props.checkAllTodo(event.target.checked);
  };

  // 清除已完成的回调
  handleClearDone = () => {
    this.props.clearDoneTodo();
  };

  render() {
    const { todos } = this.props;

    // 已完成的数量
    // const doneCount = todos.filter((todo) => todo.done == true).length;

    /**
     * reduce()用于做数组的条件统计
     * pre表示上一次函数运行的结果
     * todo表示当前项
     */

    // const doneCount = todos.reduce((pre, todo) => {
    //   return pre + (todo.done ? 1 : 0);
    // }, 0);

    // 简写
    const doneCount = todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0);

    // 全部
    const total = todos.length;
    
    return (
      <div className="todo-footer">
        <label>
          <input
            type="checkbox"
            // 当todos中都是勾选时,全选打勾 ==>当已完成数量=全部时
            checked={doneCount == total && total !== 0 ? true : false}
            // 当手动勾“全选”或“取消全选”时,去改变App中todos
            onChange={this.handleCheckAll}
          />
        </label>
        <span>
          <span>已完成{doneCount}</span> / 全部{total}
        </span>
        <button
          className="btn btn-danger"
          // 去改变App中的todos
          onClick={this.handleClearDone}
        >
          清除已完成任务
        </button>
      </div>
    );
  }
}

App组件

import React, { Component } from 'react'
import Header from './components/Header'
import List from './components/List'
import Footer from './components/Footer'
import './App.css'
export default class App extends Component {

  // 将初始化数据todos放在App组件的state中
  state = {
    todos: [
      {id: '001', name: '吃饭', done: true},
      {id: '002', name: '睡觉', done: true},
      {id: '003', name: '打代码', done: false},
      {id: '004', name: '逛街', done: true}
    ]
  }

  // 定义一个函数,供子组件Header调用
  // 子组件在调用addTodo时,会传递todo对象进来
  // 从而实现【子组件向父组件传值】
  addTodo = (todo) => {
    let {todos} = this.state
    let newTodos = [todo, ...todos]
    this.setState({
      todos: newTodos
    })
  }

  // 定义一个函数,供孙组件Item调用
  // 用于勾选和取消勾选
  updateTodo = (id, done) => {
    let {todos} = this.state
    const newTodos = todos.map(todo => {
      if(todo.id === id) return {...todo, done}
      else return todo
    })
    this.setState({
      todos: newTodos
    })
  }

  /**
   * 状态state在哪里,操作状态的方法就在哪里。
   * 
   * 这些方法可能在自身组件中调用,也可能在子组件中调用。
   */

  // 定义一个函数,供孙组件Item调用
  // 用于删除某Item项
  deleteTodo = (id) => {
    let { todos } = this.state
    const newTodos = todos.filter(todo => {
      return todo.id !== id // 返回id不为传递进来的Item项
    })
    this.setState({
      todos: newTodos
    })
  }

  // 定义一个函数,供子组件Footer调用
  // 用于全选或者取消全选
  checkAllTodo = (done) => {
    let { todos } = this.state
    const newTodos = todos.map(todo => {
      return {...todo, done}
    })
    this.setState({
      todos: newTodos
    })
  }

  // 定义一个函数,供子组件Footer调用
  // 用于清除已完成项
  clearDoneTodo = () => {
    const { todos } = this.state
    const newTodos = todos.filter(todo => !todo.done) // 只返回done为false的项
    this.setState({
      todos: newTodos
    })
  }


  render() {
    const { todos } = this.state
    return (
      <div className="todo-container">
        <div className="todo-wrap">
          {/* 将addTodo传递给子组件Header,供子组件调用 */}
          <Header addTodo={this.addTodo}/>
          {/* 将todos传递给子组件List */}
          <List todos={todos} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo}/>
          <Footer todos={todos} checkAllTodo={this.checkAllTodo} clearDoneTodo={this.clearDoneTodo}/>
        </div>
      </div>
    )
  }
}

78. 总结todolist案例

1.学会拆分组件、实现静态组件,注意:className、style的写法

2. 动态初始化列表,如何确定将数据 放在哪个组件的state中?

  • 某个组件使用:放在其自身的state中

  • 某些组件使用:放在他们共同的父组件state中(官方称此操作为: 状态提升)

3. 关于父子之间通信

-【父组件】给【子组件】传递数据:

    在父组件中,通过给子组件添加标签属性传递;

    在子组件中通过 this.props.属性名接收;

-【子组件】给【父组件】传递数据:

    在父组件中提前定义一个函数,并且通过给子组件添加标签属性的方式,将此函数传递给子组件;
    
    在子组件中通过this.props.函数名()进行调用此函数;

4. 注意defaultChecked和checked的区别

defaultChecked只执行一次,表示默认加载时的状态

5. 状态在哪里,操作状态的方法就在哪里

79. react ajax

1.前置说明

  1. React本身只关注于界面,并不包含发送ajax请求的代码
  2. 前端应用需要通过ajax请求与后台进行交互(json数据)
  3. react应用中需要集成第三方ajax库(或自己封装)

2.常用的ajax请求库

  1. jQuery: 比较重,如果需要,另外引入,(不建议使用)
  2. axios: 轻量级,建议使用
  • 封装XmlHttpRequest对象的ajax

  • promise风格

  • 可以用在浏览器端和node服务器端

80. react中安装并使用axios

1. 安装axios: yarn add axios

image

2. axios的使用

import React, { Component } from 'react'
// 1.安装axios: yarn add axios
// 2.引入axios
import axios from 'axios' 

export default class App extends Component {
  getStudentData = () => {
    // 3. 使用axios发送请求
    axios.get('???').then(
      (response) => {
        console.log('成功了', response.data)
      },
      (error) => {
        console.log('失败了', error)
      }
    )
  }
  render() {
    return (
      <div>
        <button onClick={this.getStudentData}>点击获取学生数据</button>
      </div>
    )
  }
}

81. 开启一个服务器(后端知识,能运行起来一个服务即可)

image

server1.js

image

82. react中【直接用】axios发送请求,会存在“跨域”问题

App组件

image

import React, { Component } from 'react'
// 1.安装axios: yarn add axios
// 2.引入axios
import axios from 'axios' 

export default class App extends Component {
  getStudentData = () => {
    // 3. 使用axios发送请求
    axios.get('http://localhost:5000/students').then(
      (response) => {
        console.log('成功了', response.data)
      },
      (error) => {
        console.log('失败了', error)
      }
    )
  }
  render() {
    return (
      <div>
        <button onClick={this.getStudentData}>点击获取学生数据</button>
      </div>
    )
  }
}

运行结果

image

原因是Ajax引擎受到了同源策略的限制

image

83. 用“代理”解决在react脚手架中直接使用axios的跨域问题的原理

image

84. 配置代理方式一(通过配置代理,解决直接使用axios发送请求引起的跨域问题)

package.json

image

App组件

image

运行结果

客户端client收到了响应

image

服务器server也接收到了请求

image

缺点

这种方式的配置代理,只会把本地没有的,转发给server,若本地有,则不会转给server。

也就是说,通过这种方式的配置代理,server并不会收到所有的请求。

85. 配置代理方式二

1. 创建配置代理文件

 在src下创建配置文件: src/setupProxy.js

(不允许改文件名,react脚手架底层会遍历这个文件)

2. 编写setupProxy.js

const proxy = require('http-proxy-middleware')
   
   module.exports = function(app) {
     app.use(
       proxy.createProxyMiddleware('/api1', {  //api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000)
         target: 'http://localhost:5000', //配置转发目标地址(能返回数据的服务器地址)
         changeOrigin: true, //控制服务器接收到的请求头中host字段的值
         /*
         	changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
         	changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:3000
         	changeOrigin默认值为false,但我们一般将changeOrigin值设为true
         */
         pathRewrite: {'^/api1': ''} //去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置)
       }),
       proxy.createProxyMiddleware('/api2', { 
         target: 'http://localhost:5001',
         changeOrigin: true,
         pathRewrite: {'^/api2': ''}
       })
     )
   }

3. App组件

import React, { Component } from 'react'
// 1.安装axios: yarn add axios
// 2.引入axios
import axios from 'axios' 

export default class App extends Component {
  getStudentData = () => {
    // 3. 使用axios发送请求
    axios.get('http://localhost:3000/api1/students').then(
      (response) => {
        console.log('成功了', response.data)
      },
      (error) => {
        console.log('失败了', error)
      }
    )
  }

  getCarData = () => {
    // 3. 使用axios发送请求
    axios.get('http://localhost:3000/api2/cars').then(
      (response) => {
        console.log('成功了', response.data)
      },
      (error) => {
        console.log('失败了', error)
      }
    )
  }

  render() {
    return (
      <div>
        <button onClick={this.getStudentData}>点击获取学生数据</button>
        <button onClick={this.getCarData}>点击获取汽车数据</button>
      </div>
    )
  }
}

86. github搜索案例的需求

demo_users (2)

87. 拆分组件(步骤1)

image

88. 实现静态组件(步骤2)

image

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <title>React App</title>
    <!-- 引入bootstrap.css,用于实现bootstrap搜索案例 -->
    <link rel="stylesheet" href="./bootstrap.css">
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

Search组件

import React, { Component } from "react";

export default class Search extends Component {
  render() {
    return (
      <section className="jumbotron">
        <h3 className="jumbotron-heading">Search Github Users</h3>
        <div>
          <input type="text" placeholder="enter the name you search" />
          &nbsp;<button>Search</button>
        </div>
      </section>
    );
  }
}

List组件

import React, { Component } from "react";
import "./index.css";

export default class List extends Component {
  render() {
    return (
      <div className="row">
        <div className="card">
          <a href="https://github.com/reactjs" target="_blank" rel="noreferrer">
            <img
              alt="head-portral"
              src="https://avatars.githubusercontent.com/u/6412038?v=3"
              style={{ width: "100px" }}
            />
          </a>
          <p className="card-text">reactjs</p>
        </div>
        <div className="card">
          <a href="https://github.com/reactjs" target="_blank" rel="noreferrer">
            <img
              alt="head-portral"
              src="https://avatars.githubusercontent.com/u/6412038?v=3"
              style={{ width: "100px" }}
            />
          </a>
          <p className="card-text">reactjs</p>
        </div>
        <div className="card">
          <a href="https://github.com/reactjs" target="_blank" rel="noreferrer">
            <img
              alt="head-portral"
              src="https://avatars.githubusercontent.com/u/6412038?v=3"
              style={{ width: "100px" }}
            />
          </a>
          <p className="card-text">reactjs</p>
        </div>
        <div className="card">
          <a href="https://github.com/reactjs" target="_blank" rel="noreferrer">
            <img
              alt="head-portral"
              src="https://avatars.githubusercontent.com/u/6412038?v=3"
              style={{ width: "100px" }}
            />
          </a>
          <p className="card-text">reactjs</p>
        </div>
        <div className="card">
          <a href="https://github.com/reactjs" target="_blank" rel="noreferrer">
            <img
              alt="head-portral"
              src="https://avatars.githubusercontent.com/u/6412038?v=3"
              style={{ width: "100px" }}
            />
          </a>
          <p className="card-text">reactjs</p>
        </div>
      </div>
    );
  }
}

List组件的样式文件

.album {
    min-height: 50rem; /* Can be removed; just added for demo purposes */
    padding-top: 3rem;
    padding-bottom: 3rem;
    background-color: #f7f7f7;
  }
  
  .card {
    float: left;
    width: 33.333%;
    padding: .75rem;
    margin-bottom: 2rem;
    border: 1px solid #efefef;
    text-align: center;
  }
  
  .card > img {
    margin-bottom: .75rem;
    border-radius: 100px;
  }
  
  .card-text {
    font-size: 85%;
  }

App组件

import React, { Component } from 'react'
import Search from './components/Search'
import List from './components/List'

export default class App extends Component {
  render() {
    return (
      <div className="container">
        <Search/>
        <List/>
    </div>
    )
  }
}

89. 实现动态组件(步骤3)

image

Search组件

image

import React, { Component } from "react";
import axios from "axios";

export default class Search extends Component {
  handleSearch = () => {
    // 1.获取用户的输入(连续解构赋值 + 重命名变量)
    const {
      keyWordElement: { value: keyWord },
    } = this;

    // 2.发送请求
    // axios.get(`http://localhost:3000/api1/search/users?q=${keyWord}`).then(

    // 如果站的位置(http://localhost:3000)与要发送请求的位置(http://localhost:3000)相同
    // 则http://localhost:3000可以省略不写
    axios.get(`/api1/search/users?q=${keyWord}`).then(
      (response) => {
        console.log("成功了", response);
      },
      (error) => {
        console.log("失败了", error);
      }
    );
  };
  render() {
    return (
      <section className="jumbotron">
        <h3 className="jumbotron-heading">搜索github用户</h3>
        <div>
          <input
            ref={(c) => (this.keyWordElement = c)}
            type="text"
            placeholder="输入关键词进行搜索"
          />
          &nbsp;<button onClick={this.handleSearch}>搜索</button>
        </div>
      </section>
    );
  }
}

配置代理

image

src/setupProxy.js

const proxy = require('http-proxy-middleware')
   
   module.exports = function(app) {
     app.use(
       proxy.createProxyMiddleware('/api1', {  //api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000)
         target: 'http://localhost:5000', //配置转发目标地址(能返回数据的服务器地址)
         changeOrigin: true, //控制服务器接收到的请求头中host字段的值
         /*
         	changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
         	changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:3000
         	changeOrigin默认值为false,但我们一般将changeOrigin值设为true
         */
         pathRewrite: {'^/api1': ''} //去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置)
       })
     )
   }

90. 连续解构赋值 + 变量重命名

keyWordElement表示input节点

    <input
       ref={(c) => (this.keyWordElement = c)}
       type="text"
       placeholder="输入关键词进行搜索"
      />

将input节点的value值通过连续结构赋值取出,并重新命名value变量为keyWord

const {
      keyWordElement: { value: keyWord },
    } = this;

91. 站的位置与发送的位置相同,可以省略host和端口,只写请求路径

// 本地react脚手架是 localhost:3000
// 发送请求也是 localhost: 3000 (通过配置代理实现)
axios.get(`http://localhost:3000/api1/search/users?q=${keyWord}`).then(
      (response) => {
        console.log("成功了", response);
      },
      (error) => {
        console.log("失败了", error);
      }
    );


// 则可以省略host和端口,只写请求路径
axios.get(`/api1/search/users?q=${keyWord}`).then(
      (response) => {
        console.log("成功了", response);
      },
      (error) => {
        console.log("失败了", error);
      }
    );

92. 实现“首次打开界面、加载中、错误信息、获取数据后展示数据”的4个效果

image

App组件

import React, { Component } from 'react'
import Search from './components/Search'
import List from './components/List'

export default class App extends Component {
  // 初始化数据
  state = {
    users: [], // 获取到的用户数据
    isFirst: true, // 是否为首次打开界面
    isLoading: false, // 是否处于正在加载中
    err: '', //存储请求相关的错误信息
  }

  // 定义一个函数,用于更新App组件的state
  updateAppState = (state) => {
    this.setState(state)
  }

  render() {
    return (
      <div className="container">
        <Search updateAppState={this.updateAppState}/>
        <List {...this.state}/>
    </div>
    )
  }
}

Search组件

import React, { Component } from "react";
import axios from "axios";

export default class Search extends Component {
  handleSearch = () => {
    // 1.获取用户的输入(连续解构赋值 + 重命名变量)
    const {
      keyWordElement: { value: keyWord },
    } = this;

    // loading...
    this.props.updateAppState({ isFirst: false, isLoading: true });

    // 2.发送请求
    // axios.get(`http://localhost:3000/api1/search/users?q=${keyWord}`).then(

    // 如果站的位置(http://localhost:3000)与要发送请求的位置(http://localhost:3000)相同
    // 则http://localhost:3000可以省略不写
    axios.get(`/api1/search/users?q=${keyWord}`).then(
      (response) => {
        this.props.updateAppState({
          users: response.data.items,
          isLoading: false,
        });
      },
      (error) => {
        this.props.updateAppState({ err: error.message, isLoading: false });
      }
    );
  };
  render() {
    return (
      <section className="jumbotron">
        <h3 className="jumbotron-heading">搜索github用户</h3>
        <div>
          <input
            ref={(c) => (this.keyWordElement = c)}
            type="text"
            placeholder="输入关键词进行搜索"
          />
          &nbsp;<button onClick={this.handleSearch}>搜索</button>
        </div>
      </section>
    );
  }
}

List组件

import React, { Component } from "react";
import "./index.css";

export default class List extends Component {
  render() {
    const { users, isLoading, isFirst, err } = this.props;
    return (
      <div className="row">
        {isFirst ? (
          <h2>欢迎,请输入关键字进行搜索</h2>
        ) : isLoading ? (
          <h2>loading...</h2>
        ) : err ? (
          <h2 style={{ color: "red" }}>{err}</h2>
        ) : (
          users.map((user) => {
            return (
              <div className="card" key={user.id}>
                <a href={user.html_url} target="_blank" rel="noreferrer">
                  <img
                    alt="head-portral"
                    src={user.avatar_url}
                    style={{ width: "100px" }}
                  />
                </a>
                <p className="card-text">{user.login}</p>
              </div>
            );
          })
        )}
      </div>
    );
  }
}

93. pubsub-js库,实现任意组件的通信

(重新实现github搜索案例)

image

github库地址

https://github.com/mroderick/PubSubJS

1. 安装 pubsub-js

yarn add pubsub-js

App组件,只做外壳组件

import React, { Component } from 'react'
import Search from './components/Search'
import List from './components/List'

export default class App extends Component {
  render() {
    return (
      <div className="container">
        <Search/>
        <List/>
    </div>
    )
  }
}

在List组件中订阅(subscribe)消息

image

import React, { Component } from "react";
// 1.首先安装 PubSubJS库: yarn add pubsub-js
// 2.再引入 pubsub-js
import PubSub from "pubsub-js";
import "./index.css";

export default class List extends Component {
  // 初始化数据
  state = {
    users: [],
    isFirst: true, // 是否为首次打开界面
    isLoading: false, // 是否处于正在加载中
    err: "", //存储请求相关的错误信息
  };

  // 组件完成挂载
  componentDidMount() {
    // 3.订阅消息:
    // 消息名为 myMessage,
    // 发布消息后,会自动调用第二个参数(回调函数),并收到两个参数
    // 第一个参数为消息名,第二个参数为发布消息时传递的数据
    this.token = PubSub.subscribe("myMessage", (messageName, data) => {
      this.setState(data);
    });
  }

  // 组件卸载时
  componentWillUnmount() {
    // 4. 取消订阅
    PubSub.unsubscribe(this.token);
  }

  render() {
    const { users, isLoading, isFirst, err } = this.state;
    return (
      <div className="row">
        {isFirst ? (
          <h2>欢迎,请输入关键字进行搜索</h2>
        ) : isLoading ? (
          <h2>loading...</h2>
        ) : err ? (
          <h2 style={{ color: "red" }}>{err}</h2>
        ) : (
          users.map((user) => {
            return (
              <div className="card" key={user.id}>
                <a href={user.html_url} target="_blank" rel="noreferrer">
                  <img
                    alt="head-portral"
                    src={user.avatar_url}
                    style={{ width: "100px" }}
                  />
                </a>
                <p className="card-text">{user.login}</p>
              </div>
            );
          })
        )}
      </div>
    );
  }
}

在Search组件中发布(publish)消息

image

import React, { Component } from "react";
// 1.首先安装 PubSubJS库: yarn add pubsub-js
// 2.再引入 pubsub-js
import PubSub from "pubsub-js";
import axios from "axios";

export default class Search extends Component {
  handleSearch = () => {
    // 获取用户的输入(连续解构赋值 + 重命名变量)
    const {
      keyWordElement: { value: keyWord },
    } = this;

    // 3. 发布消息
    // 第一个参数为消息名,第二个参数是携带的数据
    PubSub.publish("myMessage", { isFirst: false, isLoading: true });

    // 发送请求
    axios.get(`/api1/search/users?q=${keyWord}`).then(
      (response) => {
        PubSub.publish("myMessage", {
          users: response.data.items,
          isLoading: false,
        });
      },
      (error) => {
        PubSub.publish("myMessage", { err: error.message, isLoading: false });
      }
    );
  };

  render() {
    return (
      <section className="jumbotron">
        <h3 className="jumbotron-heading">搜索github用户</h3>
        <div>
          <input
            ref={(c) => (this.keyWordElement = c)}
            type="text"
            placeholder="输入关键词进行搜索"
          />
          &nbsp;<button onClick={this.handleSearch}>搜索</button>
        </div>
      </section>
    );
  }
}

94. 总结github搜索案例

1. 设计state时要考虑全面,例如带有网络请求的组件,要考虑请求失败怎么办

2. ES6知识点:连续解构赋值 + 重命名变量

3. 消息“订阅-发布”: PubSubJS库的安装与使用

   1. 安装

   yarn add pubsub-js

   2. 在组件中引入
   
   import PubSub from 'pubsub-js'
    
   3. 在组件中订阅
  
   this.token = PubSub.subscribe('myMessage', (message, data) => {
        // 消息名为 myMessage
        // message 也是消息名,一般用不到
        // data 是发布消息时传递过来的数据
   }) 

  4. 在组件中取消订阅
  
  PubSub.unsubscribe(this.token)

  5. 在另一个组件中发布

  PubSub.publish('myMessage', {name: 1, age:12})

 // {name:1, age:12} 为发布消息时传递的数据

** 先订阅,再发布**

理解:有一种隔空对话的感觉

适用于任意组件间通信

在组件的componentWillUnmount中取消订阅

95. React 路由和SPA的理解

SPA的理解

  1. 单页应用 (single page application) SPA

  2. 整个应用只有一个完整的html页面

  3. 应用React 路由技术,可以实现点击页面中的链接不会刷新页面,只会做页面的局部更新

  4. 数据都需要通过ajax请求获取,并在前端异步展现。

总结:SPA应用是单页面、多组件的应用。

React路由的理解

image

应用了React路由,它是个单页

默认一上来是 127.0.0.1:5501

点击Home 127.0.0.1:5501/home ,且不会引起页面的跳转

理解:

React路由中专门有一个人,专门监测浏览器中path的变化,发现浏览器的path发生了变化(由127.0.0.1:5501变为127.0.0.1:5501/home)

前端路由的工作原理:

每一个路径(path),对应一个组件(component)

96. react-router-dom 库的安装

image

1. yarn add react-router-dom

(同学们注意:由于 react-router-dom在2021年11月升级到了6版本,

我们在此处学的是5版本,要执行 npm i react-router-dom@5

)

(关于react-router-dom的最新6版本,在本教程的127集开始讲,建议学习完整套课程后,再从127学习react-router-dom的6版本)

image

97. react-router-dom 库的使用(Link组件、Route组件、BrowserRouter组件)

App.js

import React, { Component } from 'react'
// 1. 从react-router-dom 库中引入 
// <Link>组件,用于编写路由链接(实现url中path的切换)
// import {Link, Router} from 'react-router-dom'

// <BrowserRouter>组件,路由器--用于包裹Link组件
// <Route>组件,用于实现url中不同path对应不同component
// import {BrowserRouter, Link, Route} from 'react-router-dom'

// 由于整个应用中只需要一个路由器,所以我们不在此处引入<BrowserRouter>,去index.js中引入,并包裹App组件即可
import {Link, Route} from 'react-router-dom'
import About from './components/About'
import Home from './components/Home'

export default class App extends Component {
  render() {
    return (
      <div>
        <div className="row">
          <div className="col-xs-offset-2 col-xs-8">
            <div className="page-header"><h2>React Router Demo</h2></div>
          </div>
        </div>
        <div className="row">
          <div className="col-xs-2 col-xs-offset-2">
            <div className="list-group">

              {/* 原生:html是靠 <a> 标签跳转不同的html页面 */}
              {/* 
              <a className="list-group-item" href="./about.html">About</a>
              <a className="list-group-item active" href="./home.html">Home</a> 
              */}

              {/* 2. 使用react-router-dom
              第一步:使用<Link>组件切换url中不同的path ---- 编写路由链接*/}
              {/* 
              <Link to="/about" className="list-group-item">About</Link>
              <Link to="/home" className="list-group-item active">Home</Link> 
              */}
              {/* Uncaught Error: Invariant failed: You should not use <Link> outside a <Router> */}
              {/* 报错: 应该在Link组件外层包裹一个Router(路由器)*/}

              {/* 
              <Router>
                <Link to="/about" className="list-group-item">About</Link>
                <Link to="/home" className="list-group-item active">Home</Link>
              </Router> 
              */}
              {/* Uncaught TypeError: Cannot read properties of undefined (reading 'location') */}
              {/* 报错:因为没有指定使用哪种路由器,在react-router-dom中有两种类型的路由器:
                  BrowserRouter 和 HashRouter
               */}

              {/* 
              <BrowserRouter>
                <Link to="/about" className="list-group-item">About</Link>
                <Link to="/home" className="list-group-item active">Home</Link>
              </BrowserRouter>  
              */}
              {/* 这样就编写好了:路由链接----可以改变url中的path,且不刷新页面 */}

              {/* 原则:整个react应用,应该只有一个路由器
               BrowserRouter 放在最外层,去包裹App组件,这里的两个BrowserRouter(路由器)就不需要了 */}
              <Link to="/about" className="list-group-item">About</Link>
              <Link to="/home" className="list-group-item active">Home</Link>
            </div>
          </div>
          <div className="col-xs-6">
            <div className="panel">
              <div className="panel-body">
                {/* 
                <About/>
                <Home/> 
                */}

                {/* 第二步:使用<Route>组件使得不同的path对应不同的component----注册路由 */}
                {/* 
                <Route path="/about" component={About}></Route>
                <Route path="/home" component={Home}></Route> 
                */}
                {/* Uncaught Error: Invariant failed: You should not use <Route> outside a <Router> */}
                {/* 报错:应该在Route外层包裹一个路由器 */}

                {/* 
                <BrowserRouter>
                  <Route path="/about" component={About}></Route>
                  <Route path="/home" component={Home}></Route>
                </BrowserRouter> 
                */}
                {/* 点击Link,path改变,但是并不会渲染相应的component */}
                {/* 原因是:观察发现,Link 和 Route 分别在两个不同的路由器里面(BrowserRouter就是一个路由器) 
                    Link组件所在的路由器path发生了变化,并不会通知Route所在的下面这个路由器;
                    所以Route所在的下面这个路由器就不会渲染相应的component

                    解决办法: 把 Link 和 Route 放在一个路由器中
                    原则:整个react应用,应该只有一个路由器。

                    所以我们把 BrowserRouter路由器 放在最外层,去包裹App组件,这里的两个BrowserRouter(路由器)就不需要了
                */}
                
                <Route path="/about" component={About}></Route>
                <Route path="/home" component={Home}></Route>
              </div>
            </div>
          </div>
        </div>
      </div>
    )
  }
}

index.js 入口

// 引入react核心库
import React from 'react';
// 引入ReactDOM
import { createRoot } from 'react-dom/client';
// 引入BrowserRouter组件--路由器,用于包裹App组件,使得其下面所有的<Link>组件和<Route>组件都在同一个路由器中
// 这样当<Link>组件改变了url中的path后,Route组件才能监听到url中path的变化,并去渲染相应的component
import { BrowserRouter } from 'react-router-dom'
// 引入App组件
import App from './App';
// 渲染App组件到页面
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<BrowserRouter><App/></BrowserRouter>);

components/About/index.jsx About组件

import React, { Component } from "react";

export default class About extends Component {
  render() {
    return <h3>我是About的内容</h3>;
  }
}

components/Home/index.jsx Home组件

import React, { Component } from "react";

export default class Home extends Component {
  render() {
    return <h3>我是Home的内容</h3>;
  }
}

98. 总结react-router-dom

  • 使用react-router-dom的步骤:

第一步:编写路由链接

使用 Link 组件切换url中不同的path

第二步:注册路由

使用 Route 组件使得不同的path对应相应的component

  • 注意点:
  1. Link 和 Route 必须从 'react-router-dom' 库中引入,且必须被相同的一个路由器 BrowserRouter 或者 HashRouter 包裹
import {Link, Route, BrowserRouter} from 'react-router-dom'
  1. Link组件使用 to 属性来指定要改变的path:
<Link to="/about">About</Link>

Link组件就是一个路由链接,当点击这个路由链接后,url中的path就会变为/about

  1. Route组件使用 path 和component属性来指定映射关系
<Route path="/about" component={About}></Route>
<Route path="/home" component={Home}></Route>

Route就是一个路由,当url中的path发生变化时,它就会根据path,渲染相应的component

  1. 路由链接Link 和 路由Route 必须都被包裹在同一个路由器BrowserRouter或者HashRouter中

99. Link 组件的小细节

image

100. HashRouter 组件(路由器)的小细节

image

index.js 入口文件

// 引入react核心库
import React from 'react';
// 引入ReactDOM
import { createRoot } from 'react-dom/client';
// 引入HashRouter组件--路由器,用于包裹App组件,使得其下面所有的<Link>组件和<Route>组件都在同一个路由器中
// 这样当<Link>组件改变了url中的path后,Route组件才能监听到url中path的变化,并去渲染相应的component
import { HashRouter } from 'react-router-dom'
// 引入App组件
import App from './App';
// 渲染App组件到页面
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<HashRouter><App/></HashRouter>);

101. 一般组件(components目录)和路由组件(pages目录)

image

App.js

import React, { Component } from 'react'
import {Link, Route} from 'react-router-dom'
import About from './pages/About' // About是路由组件
import Home from './pages/Home' // Home是路由组件
import Header from './components/Header' // Header是一般组件
/** 1. 存放位置不同:
 *  路由组件,放在pages目录下
 *  一般组件,放在components目录下
 */

export default class App extends Component {
  render() {
    return (
      <div>
        <div className="row">
          <div className="col-xs-offset-2 col-xs-8">
            <div className="page-header">
                {/* 2.写法不同:
                  一般组件需要程序员手动写组件标签:<组件名/>
                */}
                <Header/>
            </div>
          </div>
        </div>
        <div className="row">
          <div className="col-xs-2 col-xs-offset-2">
            <div className="list-group">
              <Link to="/about" className="list-group-item">About</Link>
              <Link to="/home" className="list-group-item active">Home</Link>
            </div>
          </div>
          <div className="col-xs-6">
            <div className="panel">
              <div className="panel-body">
                {/* 
                  路由组件,由Route路由自动渲染组件,不需手动写组件标签,
                  只需要指定组件名
                */}
                <Route path="/about" component={About}></Route>
                <Route path="/home" component={Home}></Route>
              </div>
            </div>
          </div>
        </div>
      </div>
    )
  }
}

Home 组件 ---路由组件

import React, { Component } from "react";

export default class Home extends Component {
  render() {
    // 3.接收到的props不同
    console.log(this.props);
    return <h3>我是Home的内容</h3>;
  }
}

image

总结:

存放位置不同

一般组件: components目录

路由组件: pages目录

写法不同

一般组件: <组件名/>

路由组件: <Route path='/demo' component={Demo}></Route>

接收到的props不同

一般组件:给组件标签写什么属性,就在this.props中接收到什么属性

路由组件:会自动接收到一个路由对象,里面包含history、location、match三个重要的属性:

**history**:
    
      go:   f  go()

      goBack:  f goBack()

      goForward:  f goForward()

      push:  f push()

      replace:  f  replace()

**location**:

      pathname: '/about'
      
      search: ''

      state: undefined

**match**: 

      params: {}

      path: '/about'

      url: '/about'

102. NavLink组件(可以实现路由链接的高亮,通过activeClassName指定样式名)

  1. 从'react-router-dom'库中引入NavLink组件
// import { Link } from 'react-router-dom'
// NavLink组件,会自动给当前选中的路由链接,加上 ‘active’ 样式类
import { NavLink } from 'react-router-dom'
  1. 使用 NavLink 组件
 {/* 路由链接 Link组件 */}
 {/* 
 <Link to="/about" className="list-group-item">About</Link>
 <Link to="/home" className="list-group-item">Home</Link> 
 */}

 {/* NavLink组件,会自动给当前选中的路由链接,加上 ‘active’ 样式类
 而Link组件不会
 */}
 {/* 
 <NavLink to="/about" className="list-group-item">About</NavLink>
 <NavLink to="/home" className="list-group-item">Home</NavLink>
 */}

 {/* 也可以给NavLink组件自定义active的类名,使用 activeClassName属性 */}
 <NavLink activeClassName='ljk-active' to="/about" className="list-group-item">About</NavLink>
 <NavLink activeClassName='ljk-active' to="/home" className="list-group-item">Home</NavLink>

App.js

import React, { Component } from 'react'
// import {Link, Route} from 'react-router-dom'
// NavLink组件,会自动给当前选中的路由链接,加上 ‘active’ 样式类
import {NavLink, Route} from 'react-router-dom'
import About from './pages/About' 
import Home from './pages/Home' 
import Header from './components/Header'
import './App.css'
export default class App extends Component {
  render() {
    return (
      <div>
        <div className="row">
          <div className="col-xs-offset-2 col-xs-8">
            <div className="page-header">
                <Header/>
            </div>
          </div>
        </div>
        <div className="row">
          <div className="col-xs-2 col-xs-offset-2">
            <div className="list-group">
              {/* 路由链接 Link组件 */}
              {/* 
              <Link to="/about" className="list-group-item">About</Link>
              <Link to="/home" className="list-group-item">Home</Link> 
              */}

              {/* NavLink组件,会自动给当前选中的路由链接,加上 ‘active’ 样式类
                而Link组件不会
              */}
              {/* 
              <NavLink to="/about" className="list-group-item">About</NavLink>
              <NavLink to="/home" className="list-group-item">Home</NavLink>
               */}

               {/* 也可以给NavLink组件自定义active的类名,使用 activeClassName属性 */}
              <NavLink activeClassName='ljk-active' to="/about" className="list-group-item">About</NavLink>
              <NavLink activeClassName='ljk-active' to="/home" className="list-group-item">Home</NavLink>
            </div>
          </div>
          <div className="col-xs-6">
            <div className="panel">
              <div className="panel-body">
                <Route path="/about" component={About}></Route>
                <Route path="/home" component={Home}></Route>
              </div>
            </div>
          </div>
        </div>
      </div>
    )
  }
}

App.css

.ljk-active {
  background-color: orange !important;
}

image

103. this.props.children属性接收组件标签体内容

  1. 组件标签属性,在内部通过this.props.xxx进行接收
// 组件标签的属性a、属性b、属性c 都在内部通过this.props.a、 this.props.b、 this.props.c 进行接收
<Acomponent a={a} b={b} c={c}>哈哈</Acomponent>

// 组件标签的标签体内容“哈哈”,在组件内部如何接收?
  1. 标签体内容,是一种特殊的标签属性,叫children

也就是说,标签体内容,在组件内部通过this.props.children 进行接收

  1. 举例,对NavLink组件进行二次封装:

App.js

import React, { Component } from 'react'
import {Route} from 'react-router-dom'
import About from './pages/About' 
import Home from './pages/Home' 
import Header from './components/Header'
// 对NavLink组件进行 二次封装
import MyNavLink from './components/MyNavLink'
export default class App extends Component {
  render() {
    return (
      <div>
        <div className="row">
          <div className="col-xs-offset-2 col-xs-8">
            <div className="page-header">
                <Header/>
            </div>
          </div>
        </div>
        <div className="row">
          <div className="col-xs-2 col-xs-offset-2">
            <div className="list-group">
              {/* 直接使用NavLink组件,会使页面中有大量重复的代码 */}
              {/* 
              <NavLink activeClassName='ljk-active' to="/about" className="list-group-item">About</NavLink>
              <NavLink activeClassName='ljk-active' to="/home" className="list-group-item">Home</NavLink> 
              */}

              {/* 对NavLink组件进行 二次封装 
                  目的:使用起来更简洁
              */}
              <MyNavLink to="/about">About</MyNavLink>
              <MyNavLink to="/home">Home</MyNavLink>
              {/* 主要知识点:标签体内容,在组件内部如何接收?*/}
            </div>
          </div>
          <div className="col-xs-6">
            <div className="panel">
              <div className="panel-body">
                <Route path="/about" component={About}></Route>
                <Route path="/home" component={Home}></Route>
              </div>
            </div>
          </div>
        </div>
      </div>
    )
  }
}

components/MyNavLink/index.jsx

// 本组件主要是对NavLink组件进行 二次封装
import React, { Component } from "react";
import { NavLink } from "react-router-dom";
import "./index.css";
export default class MyNavLink extends Component {
  render() {
    // 如何接收标签体内容?
    // 1. 标签体内容,也是一种特殊的标签属性。
    // 2. 标签体内容的key 是 children。
    //    也就是说标签体内容,被搜集在this.props.children属性中。

    // let { to } = this.props;
    // return (
    //   <NavLink activeClassName="ljk-active" className="list-group-item" to={to}>
    //     {this.props.children}
    //   </NavLink>
    // );

    // 简写方式: <标签名 {...this.props}/>
    return (
      <NavLink
        activeClassName="ljk-active"
        className="list-group-item"
        {...this.props}
      />
    );
    // <标签名 to={to} a={a} b={b} c={c}>{this.props.children}</标签名>
    // 也可以写成: <标签名 to={to} a={a} b={b} c={c} children={children}/>
    // 因为标签体内容也是一种特殊的标签属性,叫children属性

    // 所以最终可以简写为: <标签名 {...this.props}/>
    // 把所有的属性批量传递
  }
}

104. Switch组件的使用(用于包裹Route组件,提高路由匹配效率)

image

App.js

import React, { Component } from 'react'
// 使用 Switch组件,提高注册路由时的匹配效率
// 默认不使用Switch只用Route时,会把所有的Route都匹配完一遍
import {Route, Switch} from 'react-router-dom'
import About from './pages/About' 
import Home from './pages/Home' 
import Header from './components/Header'
import MyNavLink from './components/MyNavLink'
import Test from './pages/Test'
export default class App extends Component {
  render() {
    return (
      <div>
        <div className="row">
          <div className="col-xs-offset-2 col-xs-8">
            <div className="page-header">
                <Header/>
            </div>
          </div>
        </div>
        <div className="row">
          <div className="col-xs-2 col-xs-offset-2">
            <div className="list-group">
              <MyNavLink to="/about">About</MyNavLink>
              <MyNavLink to="/home">Home</MyNavLink>
            </div>
          </div>
          <div className="col-xs-6">
            <div className="panel">
              <div className="panel-body">
                {/* 默认不使用Switch组件时,
                    在注册路由时,当匹配到/home时,会显示两个组件Home和Test

                    也就是说,路由匹配会把所有注册的路由都匹配一遍

                    当注册的路由很多时,就会存在效率问题
                */}
                {/* 
                <Route path="/about" component={About}></Route>
                <Route path="/home" component={Home}></Route>
                <Route path="/home" component={Test}></Route> 
                */}

                {/* 使用Switch组件,把Route包裹后,
                    当已经匹配到一个时,就不会继续往下匹配。提高了路由匹配效率。
                */}
                <Switch>
                  <Route path="/about" component={About}></Route>
                  <Route path="/home" component={Home}></Route>
                  <Route path="/home" component={Test}></Route> 
                </Switch>
                {/* 这样就只会显示Home组件,不会在继续匹配Test */}
              </div>
            </div>
          </div>
        </div>
      </div>
    )
  }
}

105. 路由path为二级路径 + 刷新,会出现样式丢失问题

  1. 演示问题:我们把所有的路由前都加上 atguigu前缀,路由的path都变成了二级路由

image

  1. 默认情况下

image

  1. 点击路由链接,path变化,加载相应组件。

image

  1. 当path为二级路由且遇上刷新

image

  1. 分析原因,仔细查看请求

image

样式文件本身就一直存放在public根路径下

image

引入样式的方式是相对路径:href="./bootstrap.css"表示从当前目录出发 ,去找bootstrap.css

image

106. 解决“二级path+刷新”时样式丢失问题的3种办法

根本原因

上述问题存在的根本原因是因为我们在index.html文件中引入bootstrap.css文件时,使用的是相对路径的原因造成的。

所以在请求样式时,会从当前路径(/atguigu)出发,去寻找样式文件

那么,当你在/atguigu/about路径且刷新时,就会请求成了 localhost:3000/atguigu/bootstrap.css。

导致找不到样式文件,直接返回给你了 index.html,造成样式丢失。

  1. 解决方式1:

由相对路径改为绝对路径:表示直接到localhost:3000/这个根路径下去找bootstrap.css文件

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <title>React App</title>
    <!-- 引入bootstrap.css,用于实现bootstrap搜索案例 -->
    <!-- <link rel="stylesheet" href="./bootstrap.css"> -->
    <!-- 当这里写相对路径时,当遇上“二级路由+刷新”时,样式就会丢失
         解决办法如下:
    -->

    <!-- 1. 由相对路径改为绝对路径:表示直接到localhost:3000/这个根路径下去找bootstrap.css文件 -->
    <link rel="stylesheet" href="/bootstrap.css">
   
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
  1. 解决方式2:

由相对路径改为%PUBLIC_URL%,表示直接到 public 目录下去找bootstrap.css文件

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <title>React App</title>
    <!-- 引入bootstrap.css,用于实现bootstrap搜索案例 -->
    <!-- <link rel="stylesheet" href="./bootstrap.css"> -->
    <!-- 当这里写相对路径时,当遇上“二级路由+刷新”时,样式就会丢失
         解决办法如下:
    -->

    <!-- 1. 由相对路径改为绝对路径:表示直接到localhost:3000/这个根路径下去找bootstrap.css文件 -->
    <!-- <link rel="stylesheet" href="/bootstrap.css"> -->
    <!-- 2. 或者由相对路径改为%PUBLIC_URL%,也表示直接到 public 目录下去找bootstrap.css文件 -->
    <link rel="stylesheet" href="%PUBLIC_URL%/bootstrap.css">
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
  1. 解决方式3:

在index.js入口文件中,把路由器,由BrowserRouter换成HashRouter

import React from 'react';

import { createRoot } from 'react-dom/client';

// import { BrowserRouter } from 'react-router-dom'
import { HashRouter } from 'react-router-dom'
// 因为 # 后面的资源默认为前端资源,则在“二级path+刷新”时,就不会把“http://localhost:3000/#/atguigu/about”中的/atguigu/about当成服务器资源,
// 也就不存在把样式文件请求成了 "localhost:3000/atguigu/bootstrap.css"路径这个问题了。

import App from './App';

const container = document.getElementById('root');
const root = createRoot(container);
root.render(
<HashRouter>
  <App/>
</HashRouter>);

107. Route组件的exact属性,用于path的精准匹配(尽量不用)

路由Route,默认情况下是模糊匹配

App.js

import React, { Component } from 'react'
import {Route, Switch} from 'react-router-dom'
import About from './pages/About' 
import Home from './pages/Home' 
import Header from './components/Header'
import MyNavLink from './components/MyNavLink'
export default class App extends Component {
  render() {
    return (
      <div>
        <div className="row">
          <div className="col-xs-offset-2 col-xs-8">
            <div className="page-header">
                <Header/>
            </div>
          </div>
        </div>
        <div className="row">
          <div className="col-xs-2 col-xs-offset-2">
            <div className="list-group">
              <MyNavLink to="/about">About</MyNavLink>
              {/* 由于在注册路由时,给路由Route组件使用了exact属性
                  所以这里to与Route中的path无法精确匹配,
                  则Home组件并不会渲染
              */}
              <MyNavLink to="/home/a/b">Home</MyNavLink>
            </div>
          </div>
          <div className="col-xs-6">
            <div className="panel">
              <div className="panel-body">
                <Switch>
                  {/* 默认情况下:
                      在注册路由时,path是模糊匹配。

                      比如: 路由链接<Link>或者<NavLink>中的 to="/home/a/b",
                            路由<Route>中的path="/home"

                            由于默认是模糊匹配,则可以匹配成功。

                      但是  to="/a/home/b" 与 path="/home"则匹配失败; 因为匹配顺序不一致;
                            to="/home/a/b" 与 path="/home" 且 exact={true},则匹配失败; 因为必须精确匹配。

                  */}
                  
                  {/* 给路由Route 加上 exact属性时,path就必须精确匹配,才会渲染相应的component */}
                  {/* 但是:能不用就不用,因为会引发严重的问题。 */}
                  <Route exact={true} path="/about" component={About}></Route>
                  <Route exact={true} path="/home" component={Home}></Route>
                </Switch>
              </div>
            </div>
          </div>
        </div>
      </div>
    )
  }
}

注意:exact属性能不用就不用,因为会引发严重的问题

108. Redirect组件(注册路由Route时,用于兜底:将url重定向某个path去)

用于当注册路由时,所有路由规则都没有匹配上时, 用Redirect组件,将url重定向到某个path去。

App.js

import React, { Component } from 'react'
// 引入Redirect组件:用于当注册路由时,所有路由规则都没有匹配上时,
// 用Redirect组件,将url重定向到某个path去。
import {Route, Switch, Redirect} from 'react-router-dom'
import About from './pages/About' 
import Home from './pages/Home' 
import Header from './components/Header'
import MyNavLink from './components/MyNavLink'
export default class App extends Component {
  render() {
    return (
      <div>
        <div className="row">
          <div className="col-xs-offset-2 col-xs-8">
            <div className="page-header">
                <Header/>
            </div>
          </div>
        </div>
        <div className="row">
          <div className="col-xs-2 col-xs-offset-2">
            <div className="list-group">
              <MyNavLink to="/about">About</MyNavLink>
              <MyNavLink to="/home">Home</MyNavLink>
            </div>
          </div>
          <div className="col-xs-6">
            <div className="panel">
              <div className="panel-body">
                <Switch>
                  <Route path="/about" component={About}></Route>
                  <Route path="/home" component={Home}></Route>
                  {/* 使用Redirect组件,进行兜底:

                      当上面注册的路由都没有匹配上时,
                      就会定向到Redirect组件的to属性中的路径中去。
                  */}
                  <Redirect to="/home"/>
                  {/* 要把Redirect组件放在Route组件最后面 */}
                  {/* Redirect组件也可以用于:页面一加载,默认去某个界面 */}
                </Switch>
              </div>
            </div>
          </div>
        </div>
      </div>
    )
  }
}

109. 嵌套路由(二级组件的path,最好都带上上一级路由中的path,形成嵌套路由)

实现效果:

react-router demo2

image

嵌套路由:指的是一个组件中包含二级组件,那么二级组件的path最好都加上上级组件的path,形成嵌套路由。

这样做的好处:可以避免注册路由时,父组件永远无法渲染的问题。

路由匹配原理:路由匹配永远是从最开始注册的第一个路由开始进行匹配

Home组件

import React, { Component } from "react";
import { Route, Switch, Redirect } from "react-router-dom";
import MyNavLink from "../../components/MyNavLink";
import News from "./News";
import Message from "./Message";

export default class Home extends Component {
  render() {
    return (
      <div>
        <h3>我是Home的内容</h3>
        <div>
          <ul className="nav nav-tabs">
            <li>
              {/* <MyNavLink to="/news">News</MyNavLink> */}
              <MyNavLink to="/home/news">News</MyNavLink>
            </li>
            <li>
              {/* <MyNavLink to="/message">Message</MyNavLink> */}
              <MyNavLink to="/home/message">Message</MyNavLink>
            </li>
          </ul>
          {/* 这里注意:
              React中路由的注册是有顺序的:
              !!!路由匹配是从最开始注册的第一个路由开始进行匹配!!!

              也就是说,这里<MyNavLink to="/news">News</MyNavLink>使得path变为/news时,
              会从App.js中最开始注册的第一个路由进行匹配:
              App.js:
              <Switch>
                  <Route path="/about" component={About}></Route>
                  <Route path="/home" component={Home}></Route>
                  <Redirect to="/about"/>
              </Switch>

              由于此时,/news 和 /message 并未注册路由,则点击上面的路由链接
              <MyNavLink to="/news">News</MyNavLink> 
              和 <MyNavLink to="/message">Message</MyNavLink>
              直接会Redirect到 /about中去;

          */}

          {/* 下面开始注册路由 */}
          {/* <Switch>
            <Route path="/news" component={News} />
            <Route path="/message" component={Message} />
          </Switch> */}

          {/* 但是这样写会存在一个问题:
              就是当我点击Home组件中的两个路由链接:News和Message时,
              由于路由匹配是从第一个注册的路由规则开始进行的,
              所以/news/和 /message都会导致:
              <Switch>
                  <Route path="/about" component={About}></Route>
                  <Route path="/home" component={Home}></Route>
                  <Redirect to="/about"/>
              </Switch>
              中的前两条规则匹配不上,从而Redirect到/about去。
          */}

          {/* 所以,我们应该把path都写成带上一级目录的path,
              也就是写成二级/三级,或者叫嵌套形式的path,
              就不会导致上一级匹配不上
          */}
          <Switch>
            <Route path="/home/news" component={News} />
            <Route path="/home/message" component={Message} />
            <Redirect to="/home/news" />
          </Switch>

          {/* 同时,也验证了一点:不用随意使用exact属性
              如果在App.js中的Route用了exact,则会导致无法继续匹配二级路由。
          */}
        </div>
      </div>
    );
  }
}

News组件

import React, { Component } from "react";

export default class News extends Component {
  render() {
    return (
      <ul>
        <li>news001</li>
        <li>news002</li>
        <li>news003</li>
      </ul>
    );
  }
}

Message组件

import React, { Component } from "react";

export default class Message extends Component {
  render() {
    return (
      <div>
        <ul>
          <li>
            <a href="/message1">message001</a>&nbsp;&nbsp;
          </li>
          <li>
            <a href="/message2">message002</a>&nbsp;&nbsp;
          </li>
          <li>
            <a href="/message/3">message003</a>&nbsp;&nbsp;
          </li>
        </ul>
      </div>
    );
  }
}

App.js:

import React, { Component } from 'react'
import {Route, Switch, Redirect} from 'react-router-dom'
import About from './pages/About' 
import Home from './pages/Home' 
import Header from './components/Header'
import MyNavLink from './components/MyNavLink'
export default class App extends Component {
  render() {
    return (
      <div>
        <div className="row">
          <div className="col-xs-offset-2 col-xs-8">
            <div className="page-header">
                <Header/>
            </div>
          </div>
        </div>
        <div className="row">
          <div className="col-xs-2 col-xs-offset-2">
            <div className="list-group">
              <MyNavLink to="/about">About</MyNavLink>
              <MyNavLink to="/home">Home</MyNavLink>
            </div>
          </div>
          <div className="col-xs-6">
            <div className="panel">
              <div className="panel-body">
                <Switch>
                  {/* 注册路由 */}
                  <Route path="/about" component={About}></Route>
                  <Route path="/home" component={Home}></Route>
                  {/* 当所有路由规则都不匹配时,则Redirect到/about */}
                  <Redirect to="/about"/>
                </Switch>
              </div>
            </div>
          </div>
        </div>
      </div>
    )
  }
}

110. 向路由组件中传递params参数(3步:传递-声明-接收)

3步: 传递(写路由链接时)---声明(注册路由时)---接收(在路由组件内)

image

  1. 传递

image

image

  1. 声明

image

  1. 接收

image

image

Message组件

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

export default class Message extends Component {
  state = {
    messageArr: [
      {
        id: "01",
        title: "消息1",
      },
      {
        id: "02",
        title: "消息2",
      },
      {
        id: "03",
        title: "消息3",
      },
    ],
  };
  render() {
    const { messageArr } = this.state;
    return (
      <div>
        <ul>
          {messageArr.map((msgObj) => {
            return (
              <li key={msgObj.id}>
                {/* 注意:这里的path一定不能写错:必须带上父级所有的path, 不能只写/detail */}
                {/* <Link to="/home/message/detail">{msgObj.title}</Link> */}

                {/* 接下来讲知识点:如何向路由组件中传递参数?
                    3步: 传递(写路由链接时)---声明(注册路由时)---接收(在路由组件内)
                */}

                {/* 1. 传递:向路由组件传递params参数,直接在path后面写 /id/title 
                       这样就传递了2个参数:id和title
                */}
                <Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>
                  {msgObj.title}
                </Link>
              </li>
            );
          })}
        </ul>
        <hr />
        {/* 注册路由*/}
        {/* <Route path="/home/message/detail" component={Detail}></Route> */}

        {/* 2. 声明:声明接收params参数,直接在path后面写 /:id/:title,声明接收的变量名 
               这样就声明了2个变量:id和title 
        */}
        <Route
          path="/home/message/detail/:id/:title"
          component={Detail}
        ></Route>
      </div>
    );
  }
}

Detail组件

import React, { Component } from "react";

const detailData = [
  {
    id: "01",
    content: "我爱你,中国",
  },
  {
    id: "02",
    content: "我爱你,React",
  },
  {
    id: "03",
    content: "好好爱自己",
  },
];
export default class Detail extends Component {
  render() {
    console.log(this.props);
    // 3. 接收params参数id和title:
    // 通过 this.props.match.params
    const { id, title } = this.props.match.params;

    // 根据拿到的id去detailData数据中查找相应的content
    const findResult = detailData.find((detailObj) => detailObj.id === id);
    return (
      <ul>
        {/* 使用接收到的params参数: id和title */}
        <li>id: {id}</li>
        <li>Title: {title}</li>
        <li>Content:{findResult.content}</li>
      </ul>
    );
  }
}

111. 总结向路由组件传递params参数

  1. 路由链接(携带参数): <Link to="/demo/test/tom/18">详情</Link>

  2. 注册路由(声明接收): <Route path="/demo/test/:name/:age" component={Test} />

  3. 路由组件(接收参数): const {name, age} = this.props.match.params

112. 向路由组件中传递search参数(2步:传递-接收并解析,无需要声明)

  1. 传递

image

注册路由时,无需声明

image

  1. 接收并解析

image

Message组件

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

export default class Message extends Component {
  state = {
    messageArr: [
      {
        id: "01",
        title: "消息1",
      },
      {
        id: "02",
        title: "消息2",
      },
      {
        id: "03",
        title: "消息3",
      },
    ],
  };
  render() {
    const { messageArr } = this.state;
    return (
      <div>
        <ul>
          {messageArr.map((msgObj) => {
            return (
              <li key={msgObj.id}>
                {/* 1. 传递:向路由组件传递search参数,直接在path后面写 ?id=xx&title=xx
                       这样就传递了2个参数:id和title
                */}
                <Link
                  to={`/home/message/detail?id=${msgObj.id}&title=${msgObj.title}`}
                >
                  {msgObj.title}
                </Link>
              </li>
            );
          })}
        </ul>
        <hr />
        {/* 2. 传递search参数,无需声明接收,直接正常注册路由即可 */}
        <Route path="/home/message/detail" component={Detail}></Route>
      </div>
    );
  }
}

Detail组件

import React, { Component } from "react";
import qs from "qs"; // 需要安装: npm i qs

const detailData = [
  {
    id: "01",
    content: "我爱你,中国",
  },
  {
    id: "02",
    content: "我爱你,React",
  },
  {
    id: "03",
    content: "好好爱自己",
  },
];
export default class Detail extends Component {
  render() {
    console.log(this.props);
    // 3. 接收search参数:通过 this.props.location.search 需要自己解析出对象格式

    // 【1】获取search字符串(默认格式是urlencoded格式的字符串: ?key=value&key=value 这种格式就是urlencoded)
    let search = this.props.location.search;
    // ?id=01&title=消息1

    // 【2】去掉字符串前面的 ?(使用slice()截取)
    let searchString = search.slice(1);
    // id=01&title=消息1

    // 【3】使用qs库中的parse(), 可以将上面urlencoded格式的字符串,解析成对象
    let { id, title } = qs.parse(searchString);
    // {id: '01', title: '消息1'}

    // 根据拿到的id去detailData数据中查找相应的content
    const findResult = detailData.find((detailObj) => detailObj.id === id);
    return (
      <ul>
        <li>id: {id}</li>
        <li>Title:{title}</li>
        <li>Content:{findResult.content}</li>
      </ul>
    );
  }
}

113. 总结向路由组件传递search参数

  1. 路由链接(携带参数):<Link to="/demo/test?name=tom&age=18">详情</Link>

  2. 注册路由(无需声明,正常注册即可)<Route path="/demo/test" component={Test} />

  3. 路由组件(接收参数): this.props.location.search

备注: 获取到的search是urlencoded编码字符串,需要借助qs库解析

114. 向路由组件中传递state参数(2步:传递-接收,无需要声明)

Message组件

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

export default class Message extends Component {
  state = {
    messageArr: [
      {
        id: "01",
        title: "消息1",
      },
      {
        id: "02",
        title: "消息2",
      },
      {
        id: "03",
        title: "消息3",
      },
    ],
  };
  render() {
    const { messageArr } = this.state;
    return (
      <div>
        <ul>
          {messageArr.map((msgObj) => {
            return (
              <li key={msgObj.id}>
                {/* 此state参数与React中的state状态无关 */}
                {/* 1. 传递:向路由组件传递state参数,to得写在一个对象
                       对象中的pathname属性,写路由的path路径
                       state对象属性,放置要传递的参数

                       to={{
                          pathname: `/home/message/detail`,
                          state: { 
                            id: xxx,   // 参数1 --id
                            title: xxx // 参数2 --title
                          }
                        }}

                       这样就传递了2个参数:id和title
                */}
                <Link
                  to={{
                    pathname: `/home/message/detail`,
                    state: { id: msgObj.id, title: msgObj.title },
                  }}
                >
                  {msgObj.title}
                </Link>
              </li>
            );
          })}
        </ul>
        <hr />
        {/* 2. 传递state参数,无需声明接收,直接正常注册路由即可 */}
        <Route path="/home/message/detail" component={Detail}></Route>
      </div>
    );
  }
}

Detail组件

import React, { Component } from "react";

const detailData = [
  {
    id: "01",
    content: "我爱你,中国",
  },
  {
    id: "02",
    content: "我爱你,React",
  },
  {
    id: "03",
    content: "好好爱自己",
  },
];
export default class Detail extends Component {
  render() {
    console.log(this.props);
    // 3. 接收state参数:通过 this.props.location.state
    let { id, title } = this.props.location.state;

    // 根据拿到的id去detailData数据中查找相应的content
    const findResult = detailData.find((detailObj) => detailObj.id === id);
    return (
      <ul>
        <li>id: {id}</li>
        <li>Title:{title}</li>
        <li>Content:{findResult.content}</li>
      </ul>
    );
  }
}

image

image

115. 总结向路由组件传递state参数

  1. 路由链接(携带参数):
<Link to={{
         pathname: `/demo/test`,
         state: { 
           name: tom,   // 参数1 --name
           age: 18// 参数2 --age
         }
      }}>详情</Link>
  1. 注册路由(无需声明,正常注册即可)<Route path="/demo/test" component={Test} />

  2. 路由组件(接收参数): this.props.location.state

备注: 刷新时,this.props.location.state也会保留的

116. Link组件的replace属性(设置路由跳转的模式:push或replace)

路由默认为push 模式:压栈

replace模式:替换掉栈顶

image

Message组件

给Message组件中的消息标题改为 replace模式的路由跳转。

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

export default class Message extends Component {
  state = {
    messageArr: [
      {
        id: "01",
        title: "消息1",
      },
      {
        id: "02",
        title: "消息2",
      },
      {
        id: "03",
        title: "消息3",
      },
    ],
  };
  render() {
    const { messageArr } = this.state;
    return (
      <div>
        <ul>
          {messageArr.map((msgObj) => {
            return (
              <li key={msgObj.id}>
                {/* 路由有两种模式: push 和 replace,默认为 push模式

                    可以通过给路由链接,添加 replace属性,把路由跳转改为 replace 模式:
                    
                    replace={true} ---- 开启replace模式
                    replace={false} ---- 开启push模式
                */}
                <Link
                  replace={true}
                  to={{
                    pathname: `/home/message/detail`,
                    state: { id: msgObj.id, title: msgObj.title },
                  }}
                >
                  {msgObj.title}
                </Link>
              </li>
            );
          })}
        </ul>
        <hr />
        <Route path="/home/message/detail" component={Detail}></Route>
      </div>
    );
  }
}

117. 编程式路由导航(在路由组件中,通过history对象身上的方法进行路由跳转)

本节主要讲通过 history 对象身上的方法,来进行路由跳转。(这就是编程式路由导航)

history对象在路由组件中则可以通过 this.props.history 拿到

    
    history:
    
          go:   f  go()

          goBack:  f goBack()

          goForward:  f goForward()

          push:  f push(path, state)

          replace:  f  replace(path, state)

     location:

          pathname: '/about'
          
          search: ''

          state: undefined

      match: 

          params: {}

          path: '/about'

          url: '/about'

Message 组件

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

export default class Message extends Component {
  state = {
    messageArr: [
      {
        id: "01",
        title: "消息1",
      },
      {
        id: "02",
        title: "消息2",
      },
      {
        id: "03",
        title: "消息3",
      },
    ],
  };

  // 本节主要讲通过 history 对象身上的方法,来进行路由跳转。(这就是编程式路由导航)
  // history对象在路由组件中则可以通过 this.props.history 拿到
  /*
     history:
    
          go:   f  go()

          goBack:  f goBack()

          goForward:  f goForward()

          push:  f push(path, state)

          replace:  f  replace(path, state)

     location:

          pathname: '/about'
          
          search: ''

          state: undefined

      match: 

          params: {}

          path: '/about'

          url: '/about'
     */

  handleReplace = (id, title) => {
    // 通过 this.props.history对象身上的方法: 实现编程式路由导航
    this.props.history.replace(`/home/message/detail/${id}/${title}`);
  };

  handlePush = (id, title) => {
    this.props.history.push(`/home/message/detail/${id}/${title}`);
  };

  // replace(path, state) 和 push(path, state)
  // 还可以传递第二个state参数,实现路由跳转时携带state参数
  // this.props.history.replace(`/home/message/detail`, {id, title});
  // this.props.history.push(`/home/message/detail`, {id, title});

  render() {
    const { messageArr } = this.state;
    return (
      <div>
        <ul>
          {messageArr.map((msgObj) => {
            return (
              <li key={msgObj.id}>
                <Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>
                  {msgObj.title}
                </Link>
                <button
                  onClick={() => {
                    this.handlePush(msgObj.id, msgObj.title);
                  }}
                >
                  push方式查看详情
                </button>
                <button
                  onClick={() => this.handleReplace(msgObj.id, msgObj.title)}
                >
                  replace方式查看详情
                </button>
              </li>
            );
          })}
        </ul>
        <hr />
        <Route
          path="/home/message/detail/:id/:title"
          component={Detail}
        ></Route>
      </div>
    );
  }
}

Detail组件

import React, { Component } from "react";

const detailData = [
  {
    id: "01",
    content: "我爱你,中国",
  },
  {
    id: "02",
    content: "我爱你,React",
  },
  {
    id: "03",
    content: "好好爱自己",
  },
];
export default class Detail extends Component {
  render() {
    let { id, title } = this.props.match.params;
    const findResult = detailData.find((detailObj) => detailObj.id === id);
    return (
      <ul>
        <li>id: {id}</li>
        <li>Title:{title}</li>
        <li>Content:{findResult.content}</li>
      </ul>
    );
  }
}

118. withRouter()方法(使得一般组件也可以使用this.props.history)

借助 react-router-dom 中的一个 withRouter() 方法,将一个一般组件,加工成路由组件

Header组件: components/Header/index.jsx

import React, { Component } from "react";
// 所以需要借助react-router-dom中的一个withRouter方法来实现:
// 将一个一般组件,加工成路由组件。返回的是一个新组件:
// 从而可以使用路由组件身上特有的api: 比如history对象身上的push()、replace()、goBack()、goForward()、go()
import { withRouter } from "react-router-dom";

class Header extends Component {
  handleForward = () => {
    this.props.history.goForward();
    // Uncaught TypeError: Cannot read properties of undefined (reading 'goForward')
    // 因为 Header组件是一般组件
    // 而 history 对象只有在路由组件中才会存在。
  };

  handleGoBack = () => {
    this.props.history.goBack();
  };

  handleGo = () => {
    this.props.history.go(2);
  };

  render() {
    return (
      <div>
        <h2>React Router Demo</h2>
        <button onClick={this.handleForward}>前进</button>
        <button onClick={this.handleGoBack}>后退</button>
        <button onClick={this.handleGo}>go</button>
      </div>
    );
  }
}

export default withRouter(Header);

// 去对比看下Message组件中,因为Message组件是路由组件,直接就可以用this.props.history对象身上的方法,
// 进行路由跳转。无需要使用withRouter进行加工。

119. BrowserRouter与HashRouter的区别

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

120. antd的基本使用

image

App.js

import React, { Component } from 'react'
// 第一步:安装 yarn add antd
// 第二步:引入样式 import 'antd/dist/reset.css';
import 'antd/dist/reset.css'; 
// 第三步:引入要使用的组件
import { Button } from 'antd';

export default class App extends Component {
  render() {
    return (
      <div>
        <p>App</p>
        <button>按钮1</button>
        {/* 第4步:使用antd中的Button组件 */}
        <Button type="primary">Button</Button>
      </div>
    )
  }
}

/** antd的基本使用:
 * 
 *  官网:
 *  https://ant.design/index-cn
 * 
 *  安装:
 *  yarn add antd
 * 
 *  学会去官网查阅文档就可以了。
 *
 */

package.json:

image

121. antd样式文件的按需引入

import 'antd/dist/reset.css'; 
// 这样就加载了全部的ant组件的样式 (gzipped 后一共大约 60kb)

具体配置步骤参考官网:

https://3x.ant.design/docs/react/use-with-create-react-app-cn#%E9%AB%98%E7%BA%A7%E9%85%8D%E7%BD%AE

就可以配置成功。

要查看3.x以前的版本,文档写的详细一些。

4.x以后的版本文档写的不那么细

按照官网配置好之后,就不再需要 import 'antd/dist/reset.css'; 代码了

就已经实现了引入组件后自动引入该组件的样式,也就是按需引入样式

注意 :

antd的安装版本也必须回退到 3.x,否则按需引入会报错

yarn remove antd  
yarn add antd@3.26.19

image

config-overrides.js:

const { override, fixBabelImports } = require('customize-cra')

module.exports = override(
  fixBabelImports('import', {
    libraryName: 'antd',
    libraryDirectory: 'es',
    style: 'css',
  }),
);

package.json:

image

App.js:

// import 'antd/dist/reset.css'; 
// 按照官网配置好之后,就不再需要 import 'antd/dist/reset.css'; 代码了
// 就已经实现了引入组件后自动引入该组件的样式,也就是按需引入样式

122. antd自定义主题

config-overrides.js:

/** 本节主要实现:antd的自定义主题
 * 
 *  1. yarn add less less-loader customize-cra-less-loader
 *   
 *  注意:官网文档只写了安装前两个 less 和 less-loader,这里需要记住要安装另外一个 customize-cra-less-loader
 *       否则会报错
 *  
 *  官网:
 *  const { override, fixBabelImports, addLessLoader } = require('customize-cra');  // 报错
 *  正确写法:
 *  const addLessLoader = require("customize-cra-less-loader")
 * 
 * 
 *  如果遇到报错,就去查看这里:可以解决问题
 *  https://zhuanlan.zhihu.com/p/571879317
 */

// 1. 官网写法:  
// const { override, fixBabelImports, addLessLoader } = require('customize-cra');
// 正确写法:
const { override, fixBabelImports } = require('customize-cra')
// 需要先安装 customize-cra-less-loader: 
// yarn add customize-cra-less-loader
// 再从 customize-cra-less-loader 中引入 addLessLoader
const addLessLoader = require("customize-cra-less-loader")  


module.exports = override(
  fixBabelImports('import', {
    libraryName: 'antd',
    libraryDirectory: 'es',
    style: true,
  }),
  // 2. 官网写法:报错
  // addLessLoader({
  //   javascriptEnabled: true,
  //   modifyVars: { '@primary-color': '#1DA57A' },
  // }),
  // 正确写法:
  addLessLoader({
    lessLoaderOptions: {
      lessOptions: {
        javascriptEnabled: true,
        modifyVars: {
          '@primary-color': 'orange'
        },
      }
    }
  }),
)

123. redux是什么?什么情况下需要使用redux?

# redux 是什么?

1. redux 是一个专门用于做 **状态管理**的JS库(不是react插件库)
2. 它可以用在 react, angular, vue 等项目中,但基本与react 配合使用
3. 作用: 集中式管理react应用中多个组件**共享**的状态。

# 什么情况下需要使用redux?

1. 某个组件的状态,需要让其他组件可以随时拿到(共享)
2. 一个组件需要改变另一个组件的状态(通信)
3. 总体原则:能不用就不用,如果不用比较吃力才考虑使用。

124. redux的工作流程

redux原理图

Action Creators的作用

就是为了创建一个action。相当于服务员(只记下客人的需求)。

action 其实就是一个普通的对象 {}。

它有两个重要的属性: typedata。 type是一个字符串,表示要做的事情;data表示要操作的数据。

Store 会直接将拿到的action对象,转发给Reducers

Store就相当于老板(直接把菜单交给厨师),Reducers 就是厨师。

Reducers会根据Store给的 previousState和action,对数据进行加工,然后返回新的数据给Store。

Store拿到新的数据后,组件就可以通过 getState() 这个API来拿到新的数据

Reducers 不仅可以加工数据,还可以初始化数据

125. 求和案例(不使用redux实现)

Count组件

import React, { Component } from "react";

export default class Count extends Component {
  state = {
    count: 0,
  };

  handleIncrement = () => {
    // 加
    const { count } = this.state;
    let { value } = this.selectNode;
    this.setState({
      count: count + value * 1,
    });
  };

  handleDecrement = () => {
    // 减
    const { count } = this.state;
    let { value } = this.selectNode;
    this.setState({
      count: count - value * 1,
    });
  };

  handleIncrementOdd = () => {
    // count为奇数再加
    const { count } = this.state;
    let { value } = this.selectNode;
    if (count % 2 !== 0) {
      this.setState({
        count: count + value * 1,
      });
    }
  };

  handleIncrementAsync = () => {
    // 异步加
    const { count } = this.state;
    let { value } = this.selectNode;
    setTimeout(() => {
      this.setState({
        count: count + value * 1,
      });
    }, 3000);
  };

  render() {
    const { count } = this.state;
    return (
      <div>
        <h1>当前求和为: {count}</h1>
        <select ref={(c) => (this.selectNode = c)}>
          <option value={1}>1</option>
          <option value={2}>2</option>
          <option value={3}>3</option>
        </select>
        &nbsp;
        <button onClick={this.handleIncrement}>+</button>
        &nbsp;
        <button onClick={this.handleDecrement}>-</button>
        &nbsp;
        <button onClick={this.handleIncrementOdd}>当和为奇数时才加</button>
        &nbsp;
        <button onClick={this.handleIncrementAsync}>异步加</button>
      </div>
    );
  }
}

App组件:

import React, { Component } from 'react'
import Count from './components/Count'

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

image

126. redux的安装

yarn add redux

127. 求和案例Redux版(仅使用Store和Reducer版)

1672890980290

src/redux/store.js:

/**
 * 
 * 首先要使用redux,必须要先安装 yarn add redux
 * 
 * 使用redux
 * 1. 在 src目录下新建 redux文件夹
 *      且新建2个重要的文件:store.js 和 count_reducer.js
 * 
 *      [store.js]就是专门用于创建Store对象,整个应用只有一个Store对象
 *      [count_reducer.js]表示是为count组件服务的Reducer。
 *      正常情况下,【每个组件都应该对应单独的一个Reducer】
 * 
 */

// 引入createStore方法,专门用于创建redux中最为核心的Store对象
import { createStore } from 'redux'
// 引入为Count组件服务的reducer
import countReducer from './count_reducer'

const store = createStore(countReducer)

export default store

src/redux/count_reducer.js:

/**
 * Reducer:接到之前的数据,加工数据,并返回新数据
 * 
 * 【本质就是一个函数】
 *  会接到两个参数:分别是之前的状态 prevState 和 action对象
 */

export default function countReducer (preState, action) {
    // 【初始化时,preState的值是undefined】
    if(preState === undefined) preState = 0

    // 从action对象中获取type, data
    const {type, data} = action
    // 根据type决定如何加工数据
    switch (type) {
        // 加
        case 'increment':
            // 返回加工后的数据
            return preState + data
        // 减 
        case 'decrement':
            // 返回加工后的数据
            return preState - data
        // 初始化
        default:
            return preState
    }
}
/** Reducer 是一个纯函数:
 * 
 *  它只管加工数据,所以这里我们只写 加 和 减(不写奇数加和异步加)
 *  它不管细节,只负责加工数据,至于什么时候加,在组件中写好判断,然后告诉Reducer
 * 
 */

/** Reducer第一次被调用时,是Store自动触发的,传递的preState是undefined。
 */

/** 注意:
 *  
 * 【Redux 只负责维护状态】。
 * 但是状态加工后,【能否引起页面的更新,它并不承诺】。
 * 
 * 也就是说,
 * Redux只帮你管理状态,但是默认Redux中状态的更改并不会引起页面更新。
 * 
 * 为什么?
 * 因为Redux不是facebook出品。它只管理状态,更新不更新不关它事。
 * 
 */

Count组件:

import React, { Component } from "react";
// 1.引入store,用于获取redux中保存的状态
import store from "../../redux/store";

export default class Count extends Component {
  handleIncrement = () => {
    // 加
    let { value } = this.selectNode;
    // 通知Store加
    // 3. 使用dispatch() api 来分发一个对象
    store.dispatch({
      type: "increment",
      data: value * 1,
    });

    // 4. 现在有一个问题:默认由于Redux中状态的更改并不会引起页面更新
    // 故需要在组件中去监测Redux中状态更改,再手动render
  };

  componentDidMount() {
    // 5. 在组件这里监测Redux中状态变化,只要变化,就调用render
    // 使用 subscribe() api 订阅
    // 只要Redux中任何一个状态发生了改变,都会自动调用subscribe()中的回调
    store.subscribe(() => {
      // 通过setState手动调用下render【晃了一下】
      this.setState({});
    });
  }

  /** 如果不想在每个组件中都监测Redux中状态变化。则可以在index.js入口文件中监测
   *  好处是:只写一次。
   *  不需要在每个组件中都监测。
   * 
      store.subscribe(()=>{
          root.render(<App/>);
      })
   */

  handleDecrement = () => {
    // 减
    let { value } = this.selectNode;
    store.dispatch({
      type: "decrement",
      data: value * 1,
    });
  };

  handleIncrementOdd = () => {
    // count为奇数再加
    const count = store.getState();
    let { value } = this.selectNode;
    if (count % 2 !== 0) {
      store.dispatch({
        type: "increment",
        data: value * 1,
      });
    }
  };

  handleIncrementAsync = () => {
    // 异步加
    let { value } = this.selectNode;
    setTimeout(() => {
      store.dispatch({
        type: "increment",
        data: value * 1,
      });
    }, 1000);
  };

  render() {
    return (
      <div>
        {/* 2. 使用getState() api来获取Store中的状态 */}
        <h1>当前求和为: {store.getState()}</h1>
        <select ref={(c) => (this.selectNode = c)}>
          <option value={1}>1</option>
          <option value={2}>2</option>
          <option value={3}>3</option>
        </select>
        &nbsp;
        <button onClick={this.handleIncrement}>+</button>
        &nbsp;
        <button onClick={this.handleDecrement}>-</button>
        &nbsp;
        <button onClick={this.handleIncrementOdd}>当和为奇数时才加</button>
        &nbsp;
        <button onClick={this.handleIncrementAsync}>异步加</button>
      </div>
    );
  }
}

128. 求和案例Redux版(使用Store、Reducer和ActionCreator版本)

image

本次主要新增了src/redux/count_action.js文件,

目的是:在组件中无需要再手动写action对象{type: 'xxx',data: yyy}

而只需要调用 count_action.js 中的函数,函数的返回值就是一个对象(action)。【方便在同一个文件中管理所有的action对象】

src/redux/store.js:

import { createStore } from 'redux'
import countReducer from './count_reducer'

const store = createStore(countReducer)

export default store

src/redux/count_reducer.js:

export default function countReducer (preState, action) {
    if(preState === undefined) preState = 0
    const {type, data} = action
    switch (type) {
        // 加
        case 'increment':
            return preState + data
        // 减 
        case 'decrement':
            return preState - data
        // 初始化
        default:
            return preState
    }
}

src/redux/count_action.js:

/**
 * 该文件专门为Count组件生成action对象
 * 
 * 每调用一个不同的函数,将返回一个的action
 */

// 写法1:普通函数

// function createIncrementAction(data) {
//     return {
//         type: 'increment',
//         data
//     }
// }

// function createDecrementAction(data) {
//     return {
//         type: 'decrement',
//         data
//     }
// }

// 写法2:箭头函数

// const createIncrementAction = (data) => {
//     return {
//         type: 'increment',
//         data
//     }
// }

// const createDecrementAction = (data)=> {
//     return {
//         type: 'decrement',
//         data
//     }
// }

// 写法2:箭头函数(简写)

// const createIncrementAction = data => ({
//     type: 'increment',
//     data
// })

// const createDecrementAction = data => ({
//     type: 'decrement',
//     data
// })

// 使用分别暴露将2个函数暴露出去

export const createIncrementAction = data => ({
    type: 'increment',
    data
})

export const createDecrementAction = data => ({
    type: 'decrement',
    data
})

/**
 * 写好了之后,回到组件中去,就不再需要自己动手写action对象了,
 * 直接调用该文件中的函数,从函数的返回值中,就可以拿到一个action对象。
 */

Count组件:

import React, { Component } from "react";
import store from "../../redux/store";
// 1. 引入 actionCreator (count_action.js文件中的函数,专门用于创建action对象),
import {
  createIncrementAction,
  createDecrementAction,
} from "../../redux/count_actions";

export default class Count extends Component {
  handleIncrement = () => {
    // 加
    let { value } = this.selectNode;
    // 2. 派发action时,就不用再手动写{type:xx,data:yy}
    // store.dispatch({
    //   type: "increment",
    //   data: value * 1,
    // });
    // 2. 直接调用count_action.js中的函数,就能直接拿到一个action对象
    store.dispatch(createIncrementAction(value * 1));
  };

  componentDidMount() {
    store.subscribe(() => {
      this.setState({});
    });
  }

  handleDecrement = () => {
    // 减
    let { value } = this.selectNode;
    // store.dispatch({
    //   type: "decrement",
    //   data: value * 1,
    // });
    store.dispatch(createDecrementAction(value * 1));
  };

  handleIncrementOdd = () => {
    // count为奇数再加
    const count = store.getState();
    let { value } = this.selectNode;
    if (count % 2 !== 0) {
      // store.dispatch({
      //   type: "increment",
      //   data: value * 1,
      // });
      store.dispatch(createIncrementAction(value * 1));
    }
  };

  handleIncrementAsync = () => {
    // 异步加
    let { value } = this.selectNode;
    setTimeout(() => {
      // store.dispatch({
      //   type: "increment",
      //   data: value * 1,
      // });
      store.dispatch(createIncrementAction(value * 1));
    }, 1000);
  };

  render() {
    return (
      <div>
        <h1>当前求和为: {store.getState()}</h1>
        <select ref={(c) => (this.selectNode = c)}>
          <option value={1}>1</option>
          <option value={2}>2</option>
          <option value={3}>3</option>
        </select>
        &nbsp;
        <button onClick={this.handleIncrement}>+</button>
        &nbsp;
        <button onClick={this.handleDecrement}>-</button>
        &nbsp;
        <button onClick={this.handleIncrementOdd}>当和为奇数时才加</button>
        &nbsp;
        <button onClick={this.handleIncrementAsync}>异步加</button>
      </div>
    );
  }
}

这样就实现了在组件中dispatch时,无需要手动写{},而是通过调用count_action.js中的函数,通过函数的返回值拿到一个对象去派发给store。

方便在同一个文件中管理所有的action对象

129. 求和案例Redux版(使用Store、Reducer、ActionCreator和constant常量版本)

1672902105986

src/redux/constant.js:

// 定义两个常量,并分别暴露
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'

src/redux/count_action.js:

import { INCREMENT,  DECREMENT} from './constant'
export const createIncrementAction = data => ({
    // type: 'increment',
    // 将用到字符串的地方,改成常量
    type: INCREMENT,
    data
})

export const createDecrementAction = data => ({
    // type: 'decrement',
    type: DECREMENT,
    data
})

src/redux/count_reducer.js:

import { INCREMENT,  DECREMENT} from './constant'
export default function countReducer (preState, action) {
    if(preState === undefined) preState = 0
    const {type, data} = action
    switch (type) {
        // 加
        // case 'increment':
        // 将用到字符串的地方,改成常量
        case INCREMENT:
            return preState + data
        // 减 
        // case 'decrement':
        case DECREMENT:
            return preState - data
        // 初始化
        default:
            return preState
    }
}

store.js:

import { createStore } from 'redux'
import countReducer from './count_reducer'

const store = createStore(countReducer)

export default store

Count组件:

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

export default class Count extends Component {
  handleIncrement = () => {
    // 加
    let { value } = this.selectNode;
    store.dispatch(createIncrementAction(value * 1));
  };

  componentDidMount() {
    store.subscribe(() => {
      this.setState({});
    });
  }

  handleDecrement = () => {
    // 减
    let { value } = this.selectNode;
    store.dispatch(createDecrementAction(value * 1));
  };

  handleIncrementOdd = () => {
    // count为奇数再加
    const count = store.getState();
    let { value } = this.selectNode;
    if (count % 2 !== 0) {
      store.dispatch(createIncrementAction(value * 1));
    }
  };

  handleIncrementAsync = () => {
    // 异步加
    let { value } = this.selectNode;
    setTimeout(() => {
      store.dispatch(createIncrementAction(value * 1));
    }, 1000);
  };

  render() {
    return (
      <div>
        <h1>当前求和为: {store.getState()}</h1>
        <select ref={(c) => (this.selectNode = c)}>
          <option value={1}>1</option>
          <option value={2}>2</option>
          <option value={3}>3</option>
        </select>
        &nbsp;
        <button onClick={this.handleIncrement}>+</button>
        &nbsp;
        <button onClick={this.handleDecrement}>-</button>
        &nbsp;
        <button onClick={this.handleIncrementOdd}>当和为奇数时才加</button>
        &nbsp;
        <button onClick={this.handleIncrementAsync}>异步加</button>
      </div>
    );
  }
}

130. 求和案例Redux版(异步action:action不再是{},而是一个函数)(需要配合redux-thunk中间键)

1672907384436

Count组件

import React, { Component } from "react";
import store from "../../redux/store";
import {
  createIncrementAction,
  createDecrementAction,
  createIncrementAsyncAction,
} from "../../redux/count_actions";

export default class Count extends Component {
  handleIncrement = () => {
    // 加
    let { value } = this.selectNode;
    store.dispatch(createIncrementAction(value * 1));
  };

  componentDidMount() {
    store.subscribe(() => {
      this.setState({});
    });
  }

  handleDecrement = () => {
    // 减
    let { value } = this.selectNode;
    store.dispatch(createDecrementAction(value * 1));
  };

  handleIncrementOdd = () => {
    // count为奇数再加
    const count = store.getState();
    let { value } = this.selectNode;
    if (count % 2 !== 0) {
      store.dispatch(createIncrementAction(value * 1));
    }
  };

  handleIncrementAsync = () => {
    // 异步加
    let { value } = this.selectNode;
    // 1. 把异步动作,由组件中等1000s改为在action中
    // setTimeout(() => {
    //   store.dispatch(createIncrementAction(value * 1));
    // }, 1000);

    // 1. 去count_action.js中创建一个异步的action
    store.dispatch(createIncrementAsyncAction(value * 1, 1000));
  };

  render() {
    return (
      <div>
        <h1>当前求和为: {store.getState()}</h1>
        <select ref={(c) => (this.selectNode = c)}>
          <option value={1}>1</option>
          <option value={2}>2</option>
          <option value={3}>3</option>
        </select>
        &nbsp;
        <button onClick={this.handleIncrement}>+</button>
        &nbsp;
        <button onClick={this.handleDecrement}>-</button>
        &nbsp;
        <button onClick={this.handleIncrementOdd}>当和为奇数时才加</button>
        &nbsp;
        <button onClick={this.handleIncrementAsync}>异步加</button>
      </div>
    );
  }
}

src/redux/count_actions.js:

import { INCREMENT,  DECREMENT} from './constant'
import store from './store'

// 同步action【返回的是一个{}】
export const createIncrementAction = data => ({
    type: INCREMENT,
    data
})

export const createDecrementAction = data => ({
    type: DECREMENT,
    data
})

/**
 * 如果一个actionCreator返回的action是一个 {},则是同步action
 * 
 * 如果返回的是 f() {}, 则是异步的action
 */

// 异步action【返回的是一个f() {}】
export const createIncrementAsyncAction = (data, time) => {
    return () => {
        // 在这里等1000ms之后,再去 加
        setTimeout(() => {
            store.dispatch(createIncrementAction(data))
        }, time)
    }
}

// redux.js:275 Uncaught Error: Actions must be plain objects.
// Instead, the actual type was: 'function'. 
// You may need to add middleware to your store setup to handle dispatching other values, 
// such as 'redux-thunk' to handle dispatching functions. 

/**
 *  Store说,交给Reducer的只能是一个{type:xx,data:yy}。否则Reducer不知道该如何加工数据。
 * 
 *  所以当你把一个函数交给Store,Store直接就报错了。
 * 
 */

/**
 *  这时候,需要把一个中间键请出来,让这个中间键跟Store去对话。
 *  
 *  中间键会告诉Store,(给个面子),你看到我给你的是一个函数,则不需要交给Reducer让它干活。只需要帮我把这个函数调用一下。
 * 
 *  这个中间键就是 redux-thunk
 * 
 *  安装 redux-thunk :   yarn add redux-thunk
 * 
 */

/**
 *  去store.js中,看中间键redux-thunk如何与Store商量
 */

src/redux/store.js:

// 1. 首先需要从redux中请出 applyMiddleware,用于应用中间键
import { createStore, applyMiddleware } from 'redux'
import countReducer from './count_reducer'
// 2. 引入 redux-thunk中间键(需要先安装 yarn add redux-thunk)
import thunk from 'redux-thunk'

// 3. 在创建store时,传入第二个参数
// const store = createStore(countReducer)
const store = createStore(countReducer,applyMiddleware(thunk))

export default store

src/redux/count_reducer.js:

import { INCREMENT,  DECREMENT} from './constant'
export default function countReducer (preState, action) {
    if(preState === undefined) preState = 0
    const {type, data} = action
    switch (type) {
        // 加
        case INCREMENT:
            return preState + data
        // 减 
        case DECREMENT:
            return preState - data
        // 初始化
        default:
            return preState
    }
}

src/redux/constant.js:

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

在redux中使用了redux-thunk 中间键,就可以在actionCreator中写异步action了。

异步action: 就指action不再是 {},而是 f()

总结

1. 明确: 延迟的动作不想交给组件自身,想交给action

2. 何时需要异步action: 想要对状态进行操作,但是具体的数据靠异步任务返回

3. 具体编码:

yarn add redux-thunk,并配置在store中

创建action的函数,不再返回一个一般对象,而是一个函数,该函数中写异步任务

异步任务有结果后,分发一个同步的action去真正操作数据

4. 备注: 异步action不是必须要写的,完全可以自己等异步任务的结果有了再去分发同步action

131. 求和案例Redux版(优化异步action:直接从action函数的参数中获取dispatch)

src/redux/count_action.js:

import { INCREMENT,  DECREMENT} from './constant'
// 2. 这里也就无需再引入store
// import store from './store'

export const createIncrementAction = data => ({
    type: INCREMENT,
    data
})

export const createDecrementAction = data => ({
    type: DECREMENT,
    data
})

export const createIncrementAsyncAction = (data, time) => {
    // 1. store在拿到这个函数后
    // 这个函数中就有一个dispatch参数,无需要再通过store.dispatch来获取
    // return () => {
    //     setTimeout(() => {
    //         store.dispatch(createIncrementAction(data))
    //     }, time)
    // }

    // 1. 可以直接从参数中拿到dispatch
    return (dispatch) => {
        setTimeout(() => {
            dispatch(createIncrementAction(data))
        }, time)
    }
}

132. 总结已经学过哪些

image

133. react-redux 库(react-redux的模型图)

react-redux模型图

134. 实现UI组件、容器组件,并将UI组件和Redux与容器组件进行连接

1672986504519

Count的UI组件 --src/components/Count/index.jsx:(将UI组件中与Redux相关的Api都去掉,改造为一个UI组件)

import React, { Component } from "react";

export default class Count extends Component {
  handleIncrement = () => {
    // 加
    let { value } = this.selectNode;
  };

  handleDecrement = () => {
    // 减
    let { value } = this.selectNode;
  };

  handleIncrementOdd = () => {
    // count为奇数再加
    let { value } = this.selectNode;
  };

  handleIncrementAsync = () => {
    // 异步加
    let { value } = this.selectNode;
  };

  render() {
    return (
      <div>
        <h1>当前求和为: ???</h1>
        <select ref={(c) => (this.selectNode = c)}>
          <option value={1}>1</option>
          <option value={2}>2</option>
          <option value={3}>3</option>
        </select>
        &nbsp;
        <button onClick={this.handleIncrement}>+</button>
        &nbsp;
        <button onClick={this.handleDecrement}>-</button>
        &nbsp;
        <button onClick={this.handleIncrementOdd}>当和为奇数时才加</button>
        &nbsp;
        <button onClick={this.handleIncrementAsync}>异步加</button>
      </div>
    );
  }
}

Count的容器组件 --src/containers/Count/index.jsx:(用于连接UI组件)

/**
 * 该文件是Count组件(UI组件)的容器组件;
 *
 * 容器组件是一个桥梁:左手边是UI组件,右手边是redux;
 *
 * 容器组件不能自己写,得用 react-redux 这个react插件库;
 *
 * 安装react-redux:  yarn add react-redux;
 *
 */

/**
 *  因为容器组件是一个桥梁,
 *  所以在容器组件中,必须引入左手的UI组件和右手的redux
 */

// 引入Count的UI组件
import CountUI from "../../components/Count";

// 引入redux(直接引入redux中最为核心的store)
// import store from "../../redux/store";

// 引入connect用于连接UI组件与redux
import { connect } from "react-redux";

// connect()();

/**
 * connect()--->connect是一个函数
 * connect()() ----> connect函数调用的返回值,依然是一个函数。
 *
 */

/**
 * connect()();
 *
 * connect函数连续调用2次后,就可以在左边接到一个容器组件;
 */

// const container = connect()();

// 在第二次调用connect时,传入UI组件---CountUI
const CountContainer = connect()(CountUI);

/**
 *  这样CountContainer这个容器组件就与 CountUI 这个UI组件
 *  建立好了联系
 *
 */

export default CountContainer;

/**
 * 想法是美好的:希望直接在容器组件中,直接连接左手UI与右手redux
 *
 * 但是事实上,右手的redux并不是在这个容器组件中引入并连接的。
 *
 * 而是在 App.js中引入容器组件的地方,通过props的方式引入redux。
 *
 * 所以 import store from "../../redux/store"; 可以去掉
 */

App.js:

import React, { Component } from 'react'
// 在App中就不能再引入Count的UI组件了,而是引入Count的容器组件
// import Count from './components/Count'  // count的UI组件
import Count from './containers/Count'  // count的容器组件
// 引入redux(直接引入redux中最为核心的store)
import store from './redux/store'

export default class App extends Component {
  render() {
    return (
      <div>
        {/* <Count/> */}
        {/* 在引入容器组件的地方,通过props将右手的redux与容器组件进行连接 */}
        <Count store= {store}/>
      </div>
    )
  }
}

135. 容器组件与UI组件是父子关系

image

从代码关系上,并不能看出容器组件与UI组件是父子关系。

1672989475820

但在浏览器调试中可以看出他们的父子关系。

且是通过 react-redux中的connect进行连接的父子组件

136. 容器组件向UI组件传值(通过connect(a,b)()第一次被调用时的参数a和b)

先理一下:

src/index.js入口文件:

// 引入react核心库
import React from 'react';
// 引入ReactDOM
import { createRoot } from 'react-dom/client';
// 引入App组件
import App from './App';
// 1.引入redux
import store from './redux/store'
// 渲染App组件到页面
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App/>);

// 2.监测中redux中状态的改变,如redux状态发生改变,那么重新渲染App组件
store.subscribe(()=>{
  root.render(<App/>);
})

src/App.js:

import React, { Component } from 'react'
import Count from './containers/Count'
import store from './redux/store'
export default class App extends Component {
  render() {
    return (
      <div>
        {/* 3.给容器组件传递store,使得容器组件与右手边的redux进行连接 */}
        <Count store= {store}/>
      </div>
    )
  }
}

容器组件src/containers/Count/index.jsx:

import CountUI from "../../components/Count";
import { connect } from "react-redux";

/**
 * 容器组件与UI组件是父子关系: 这一点可以在调试工具components这个tab下得到验证
 *
 * 且容器组件与UI组件是通过connect()() 这个api 连接的
 */

// 4. 问题来了:父组件【容器组件】该如何给子组件【UI组件】传递东西?
// const CountContainer = connect()(CountUI);

/**
 * connect() 在第一次调用时,要传入两个参数:
 *
 * 且两个参数必须是函数
 *
 */

const CountContainer = connect(a, b)(CountUI);

// a函数的返回值,作为状态,传递给了UI组件
function a() {
  return {
    count: 900, // 相当于 <CountUI count={900}/>
  };
}

// b函数的返回值,作为操作状态的方法,传递给了UI组件
function b() {
  return {
    jia: () => {
      console.log(1);
    },
    // 相当于 <CountUI jia={()=>{console.log(1)}}/>
  };
}

export default CountContainer;

UI组件src/components/Count/index.jsx:

import React, { Component } from "react";

export default class Count extends Component {
  handleIncrement = () => {
    // 加
    let { value } = this.selectNode;
  };

  handleDecrement = () => {
    // 减
    let { value } = this.selectNode;
  };

  handleIncrementOdd = () => {
    // count为奇数再加
    let { value } = this.selectNode;
  };

  handleIncrementAsync = () => {
    // 异步加
    let { value } = this.selectNode;
  };

  render() {
    // 5.UI组件接收容器组件传递过来的props
    console.log("UI组件收到的props", this.props);
    return (
      <div>
        {/* 6.使用容器组件传递过来的count */}
        <h1>当前求和为: {this.props.count}</h1>
        <select ref={(c) => (this.selectNode = c)}>
          <option value={1}>1</option>
          <option value={2}>2</option>
          <option value={3}>3</option>
        </select>
        &nbsp;
        <button onClick={this.handleIncrement}>+</button>
        &nbsp;
        <button onClick={this.handleDecrement}>-</button>
        &nbsp;
        <button onClick={this.handleIncrementOdd}>当和为奇数时才加</button>
        &nbsp;
        <button onClick={this.handleIncrementAsync}>异步加</button>
      </div>
    );
  }
}

image

image

137. 容器组件从redux中获取状态(redux中的状态就是a函数的参数)

容器组件 src/containers/Count/index.jsx:

import CountUI from "../../components/Count";
import { connect } from "react-redux";

const CountContainer = connect(a, b)(CountUI);

// 1. 容器组件从redux中获取状态
/**
 * 容器组件已经拿到了store: 在App.js中 <Count store= {store}/>
 *
 * react-redux在调用connect()()时,会自动帮你执行store.getState(),
 * 并拿到store中的状态,作为a函数的参数
 *
 */
function a(state) {
  // 2. 拿到redux中的状态---参数state
  return {
    // 3. 把redux中的状态,传递给UI组件
    count: state,
  };
}

function b() {
  return {
    jia: () => {
      console.log(1);
    },
  };
}

export default CountContainer;

image

138. UI组件调用容器组件传递的方法,容器组件再去修改redux中的状态

UI组件 src/components/Count/index.jsx

import React, { Component } from "react";

export default class Count extends Component {
  handleIncrement = () => {
    // 加
    let { value } = this.selectNode;
    // 1. 调用容器组件传递进来的方法
    // 只是在UI组件中调用了父组件【容器组件】传递过来的jia()
    // 注意:并没有调用任何redux相关的api
    this.props.jia(value * 1);

    /**
     * 在 UI组件中,是看不到任何redux相关的api的
     */
  };

  handleDecrement = () => {
    // 减
    let { value } = this.selectNode;
  };

  handleIncrementOdd = () => {
    // count为奇数再加
    let { value } = this.selectNode;
  };

  handleIncrementAsync = () => {
    // 异步加
    let { value } = this.selectNode;
  };

  render() {
    console.log("UI组件收到的props", this.props);
    return (
      <div>
        <h1>当前求和为: {this.props.count}</h1>
        <select ref={(c) => (this.selectNode = c)}>
          <option value={1}>1</option>
          <option value={2}>2</option>
          <option value={3}>3</option>
        </select>
        &nbsp;
        <button onClick={this.handleIncrement}>+</button>
        &nbsp;
        <button onClick={this.handleDecrement}>-</button>
        &nbsp;
        <button onClick={this.handleIncrementOdd}>当和为奇数时才加</button>
        &nbsp;
        <button onClick={this.handleIncrementAsync}>异步加</button>
      </div>
    );
  }
}

容器组件 src/container/Count/index.jsx:

import CountUI from "../../components/Count";
import { connect } from "react-redux";

const CountContainer = connect(a, b)(CountUI);

function a(state) {
  return {
    count: state,
  };
}

// 在b函数中,一定一定需要一个dispatch,用于操作redux中的state
function b(dispatch) {
  // 这个b函数的参数,就是store中的dispatch方法

  /**
   * 容器组件传递给UI组件jia()----让UI组件去调用jia();
   * b函数提供dispatch(),供容器组件调用
   * 目的是:让容器组件可以操作redux中的状态
   */
  return {
    jia: (data) => {
      // 接收到UI组件中传递过来的data
      // 2. 通过redux 加
      /** 问题:怎么通过redux去加?
       *  dispatch(action)
       */
      dispatch({ type: "increment", data });
    },
  };
}

export default CountContainer;

image

139. 优化(dispatch使用actionCreator、优化a函数和b函数的函数名)

容器组件 src/container/Count/index.jsx:

import CountUI from "../../components/Count";
import { connect } from "react-redux";
// 1. 引入actionCreator,目的:调用里面的方法,自动返回一个action对象
import { createIncrementAction } from "../../redux/count_actions";

// const CountContainer = connect(a, b)(CountUI);

// 3. a函数的使命:就是把state变成props传递给UI组件
// function a(state) {
function mapStateToProps(state) {
  // 参数state就是redux中的状态
  return {
    // count就是UI组件中接收到的props
    count: state,
  };
}
/**
 * 所以给a函数改个函数名: mapStateToProps -- 见名知义
 */

// 4. 同理b函数的使命:把dispatch就成props供UI组件调用
// 所以给b函数改个函数名: mapDispatchToProps
// function b(dispatch) {
function mapDispatchToProps(dispatch) {
  return {
    jia: (data) => {
      // 2. 调用actionCreator中的方法
      // 拿到现成的action对象进行派发
      dispatch(createIncrementAction(data));
    },
  };
}

// 这里也改下参数a,b的名字
const CountContainer = connect(mapStateToProps, mapDispatchToProps)(CountUI);

export default CountContainer;

140. 求和案例react-redux版(减、奇数加、异步加-功能)_完整版

容器组件 src/container/Count/index.jsx:

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

// 映射redux中的状态(参数state)
// UI组件中为 this.props.count
function mapStateToProps(state) {
  return {
    count: state,
  };
}

// 映射redux中操作状态的方法(dispatch)
// UI组件中为 this.props.jia、 this.props.jian、 this.props.jiaAsync
function mapDispatchToProps(dispatch) {
  return {
    jia: (data) => {
      dispatch(createIncrementAction(data));
    },
    // 1. 实现 减
    jian: (data) => {
      dispatch(createDecrementAction(data));
    },
    // 2. 异步 加
    jiaAsync: (data, time) => {
      dispatch(createIncrementAsyncAction(data, time));
    },
  };
}

const CountContainer = connect(mapStateToProps, mapDispatchToProps)(CountUI);

export default CountContainer;

UI组件 src/components/Count/index.jsx:

import React, { Component } from "react";

export default class Count extends Component {
  handleIncrement = () => {
    // 加
    let { value } = this.selectNode;
    this.props.jia(value * 1);
  };

  handleDecrement = () => {
    // 减
    let { value } = this.selectNode;
    // 调用父组件【容器组件】传递的jian函数
    this.props.jian(value * 1);
  };

  handleIncrementOdd = () => {
    // count为奇数再加
    let { value } = this.selectNode;
    if (this.props.count % 2 !== 0) {
      this.props.jia(value * 1);
    }
  };

  handleIncrementAsync = () => {
    // 异步加
    let { value } = this.selectNode;
    this.props.jiaAsync(value * 1, 500);
  };

  render() {
    // console.log("UI组件收到的props", this.props);
    return (
      <div>
        <h1>当前求和为: {this.props.count}</h1>
        <select ref={(c) => (this.selectNode = c)}>
          <option value={1}>1</option>
          <option value={2}>2</option>
          <option value={3}>3</option>
        </select>
        &nbsp;
        <button onClick={this.handleIncrement}>+</button>
        &nbsp;
        <button onClick={this.handleDecrement}>-</button>
        &nbsp;
        <button onClick={this.handleIncrementOdd}>当和为奇数时才加</button>
        &nbsp;
        <button onClick={this.handleIncrementAsync}>异步加</button>
      </div>
    );
  }
}

141. 总结 react-redux

  1. 明确两个概念:

    • UI组件:不能使用任何redux的api,只负责页面的呈现,交互等

    • 容器组件:负责和redux通信,并将结果转交给UI组件

  2. 如何创建一个容器组件----靠react-redux的connect函数

    connect(mapStateToProps, mapDispatchToProps)(UI组件)

    • mapStateToProps: 映射状态,返回值是一个对象

    • mapDispatchToProps: 映射操作状态的方法,返回值是一个对象

    mapStateToProps和mapDispatchToProps都是函数

  3. 备注1: 容器组件中的store是靠props传递进去的,而不是在容器组件中直接引入

  4. 备注2:mapDispatchToProps还可以是一个对象

142. 优化1(mapDispatchToProps的简写)(对象形式,react-redux会自动dispatch)

容器组件 src/container/Count/index.jsx:

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

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

// function mapDispatchToProps(dispatch) {
//   return {
//     jia: (data) => {
//       dispatch(createIncrementAction(data));
//     },
//     jian: (data) => {
//       dispatch(createDecrementAction(data));
//     },
//     jiaAsync: (data, time) => {
//       dispatch(createIncrementAsyncAction(data, time));
//     },
//   };
// }

// 1.语法上简写
let mapStateToProps = (state) => ({ count: state });
// mapDispatchToProps = (dispatch) => ({
//   jia: (data) => {
//     dispatch(createIncrementAction(data));
//   },
//   jian: (data) => {
//     dispatch(createDecrementAction(data));
//   },
//   jiaAsync: (data, time) => {
//     dispatch(createIncrementAsyncAction(data, time));
//   },
// });

// 2.api层面简化mapDispatchToProps
// mapDispatchToProps 不仅可以是一个函数,还可以写成对象
/**
 * 在对象中:
 * key 为 UI组件中调用的方法
 * value 为 redux中的 action
 *
 * react-redux会自动dispatch
 */
let mapDispatchToProps = {
  jia: createIncrementAction,
  jian: createDecrementAction,
  jiaAsync: createIncrementAsyncAction,
};

const CountContainer = connect(mapStateToProps, mapDispatchToProps)(CountUI);

export default CountContainer;

143. 优化2(使用了react-redux后,connect()()会自动监测redux状态变化,自动更新组件)(不需要自己监测redux且手动render组件)

src/index.js 入口文件:

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
// import store from './redux/store'
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App/>);


// store.subscribe(()=>{
//   root.render(<App/>);
// })

// 1. 使用了react-redux之后,就不需要再监测redux中状态变化,手动render组件
/** 
 * 因为react-redux使用了connect()()方法,会自动监测redux中状态变化,自动render组件
 * 
 * 所以这里无需要再引入store,无需要再subscribe
 */

144. 优化3(使用react-redux中的Provider组件,实现自动向容器组件分发store)

src/App.js:

import React, { Component } from 'react'
import Count from './containers/Count'
// import store from './redux/store'
export default class App extends Component {
  render() {
    return (
      <div>
        {/* 如果整个应用中有很多容器组件
            就会在这里多次给每个容器组件都传入store
        */}
        {/* 1. 使用 react-redux中的Provider组件,包裹最外层的<App/>组件 
               
               Provider就会自动知道,哪些是容器组件,
               并且把store自动分发给容器组件

               所以我们不在这里给容器组件传递store,去入口文件中
        */}
        {/* <Count store= {store}/> */}
        <Count/>
      </div>
    )
  }
}

src/index.js 入口文件:

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
// 1.引入store
import store from './redux/store'
// 2.引入 react-redux中的 Provider组件,
// 用于自动给容器组件分发store
import { Provider } from 'react-redux' 

const container = document.getElementById('root');
const root = createRoot(container);

// root.render(<App/>);
root.render(<Provider store={store}><App/></Provider>);

145. 优化4(把UI组件与容器组件,整合到一个文件中)(文件目录的优化,由之前的components目录与containers目录,统一放在pages目录)

image

src/pages/Count/index.js

/**
 * 本节主要是把components目录与containers目录整合成一个pages目录
 *
 * 且在一个js文件中写两个组件:UI组件与容器组件
 *
 * 不再把UI组件与容器组件放在不同的目录中,
 *
 * 这样方便维护。
 *
 * 因为UI组件就是给容器组件用的,所以把UI组件直接定义在容器组件中,直接使用
 * 最合适。
 */
import React, { Component } from "react";
import { connect } from "react-redux";
import {
  createIncrementAction,
  createDecrementAction,
  createIncrementAsyncAction,
} from "../../redux/count_actions";

// 1. 定义UI组件
class Count extends Component {
  handleIncrement = () => {
    // 加
    let { value } = this.selectNode;
    this.props.jia(value * 1);
  };

  handleDecrement = () => {
    // 减
    let { value } = this.selectNode;
    this.props.jian(value * 1);
  };

  handleIncrementOdd = () => {
    // count为奇数再加
    let { value } = this.selectNode;
    if (this.props.count % 2 !== 0) {
      this.props.jia(value * 1);
    }
  };

  handleIncrementAsync = () => {
    // 异步加
    let { value } = this.selectNode;
    this.props.jiaAsync(value * 1, 500);
  };

  render() {
    // console.log("UI组件收到的props", this.props);
    return (
      <div>
        <h1>当前求和为: {this.props.count}</h1>
        <select ref={(c) => (this.selectNode = c)}>
          <option value={1}>1</option>
          <option value={2}>2</option>
          <option value={3}>3</option>
        </select>
        &nbsp;
        <button onClick={this.handleIncrement}>+</button>
        &nbsp;
        <button onClick={this.handleDecrement}>-</button>
        &nbsp;
        <button onClick={this.handleIncrementOdd}>当和为奇数时才加</button>
        &nbsp;
        <button onClick={this.handleIncrementAsync}>异步加</button>
      </div>
    );
  }
}

let mapStateToProps = (state) => ({ count: state });
let mapDispatchToProps = {
  jia: createIncrementAction,
  jian: createDecrementAction,
  jiaAsync: createIncrementAsyncAction,
};

const CountContainer = connect(mapStateToProps, mapDispatchToProps)(Count);

export default CountContainer;

App.js:

import React, { Component } from 'react'
// 这里改下文件目录
import Count from './pages/Count'
export default class App extends Component {
  render() {
    return (
      <div>
        <Count/>
      </div>
    )
  }
}

本节主要是把components目录与containers目录整合成一个pages目录

且在一个js文件中写两个组件:UI组件与容器组件

不再把UI组件与容器组件放在不同的目录中,

这样方便维护。

因为UI组件就是给容器组件用的,所以把UI组件直接定义在容器组件中,直接使用

最合适。

146. 最终版(重新写Count组件)

src/pages/Count/index.jsx:

import React, { Component } from "react";
// 2. 当需要与redux打交道,则引入connect
import { connect } from "react-redux";
import { createIncrementAction } from "../../redux/count_actions";

// 1. 定义Count组件(UI组件)
class Count extends Component {
  handleAdd = () => {
    // 7.在UI组件中,操作redux中的状态
    this.props.add(1);
  };
  render() {
    return (
      <div>
        {/* 6.在UI组件中,使用redux中的状态 */}
        <h2>当前求和为:{this.props.count}</h2>
        <button onClick={this.handleAdd}>加1</button>
      </div>
    );
  }

  /***
   * 在UI组件内,没有使用任何与redux相关的api。
   *
   * 只使用了父组件【容器组件】传递进来的props
   */
}

// 3. 连接容器组件与Count组件(UI组件)
export default connect(
  (state) => ({ count: state }), // 4. 映射redux中的状态,给UI组件
  {
    add: createIncrementAction, // 5. 映射操作redux状态的方法,给UI组件
  }
)(Count);

147. 总结 react-redux (2)

使用了 react-redux之后,

  1. 容器组件和UI组件整合成一个文件(放在pages目录下)

  2. 无需自己给容器组件传递store,给包裹一个<Provider store={store}></Provider>即可。

    组件从 react-redux中引入

  3. 无需要自己监测redux中状态改变,容器组件可以自动完成这个工作

  4. mapDispatchToProps也可以简单的写成一个对象

  5. 一个组件要和redux“打交道”要经过哪几步?

  • 定义好UI组件 ----不暴露

  • 引入connect生成一个容器组件,并暴露,写法如下:

connect(
   state => ({key:value}), // 映射状态
   {
    key: xxxxAction // 映射操作状态的方法
   }
)(UI组件)
  • 在UI组件中通过this.props.xxxx读取和操作状态

148. 使用redux中的combineReducers合并多个reducer

1673247191289

  1. 在redux目录下新建两个文件目录: actions和reducers,分别用于存放不同组件对应的action和reducer

  2. 由于store中,store只能与一个reducer进行关联,

    所以需要在redux/reducers目录下新建index.js,将不同组件对应的reducer进行合并,汇总成一个总的reducer,

    并将这个总的reducer与store进行关联

给Person组件添加action的type常量,src/redux/constant.js:

export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
// 1. 为 Person组件准备一个action中type的常量
export const ADD_PERSON = 'add_person'

Person组件对应的action,src/redux/actions/person.js:

import { ADD_PERSON } from '../constant'

// 2. 为Person组件创建actionCreator,
// 用于自动返回一个action对象
export const createAddPersonAction = (personObj) => ({
  type: ADD_PERSON,
  data: personObj
})

Person组件对应的reducer,src/redux/reducers/person.js

import {ADD_PERSON} from '../constant'

// 3.初始化人的列表
const initState = [{id: '001', name: 'tom', age: 18}]

export default function personReducer(preState=initState, action) {
  console.log('personReducer')
  const {type, data} = action
  switch (type) {
    case ADD_PERSON:
      return [data, ...preState]
    default:
      return preState
  }
}

使用redux库中的combineReducers将Count组件与Person组件对应的reducer进行合并,src/redux/reducers/index.js:

// 引入为Count组件服务的reducer
import countReducer from './count'
// 引入为Person组件服务的reducer
import personReducer from './person'

// 4. 使用 combineReducers 合并count和person的reducer
// 并让store引入合并后的总reducer
import { combineReducers } from 'redux'
/**
 * 这里注意:combineReducers是从redux中引入的
 * 
 * 并不是从 react-redux中引入的
 */

// 5. combineReducers函数:!!!参数中的对象就是redux中保存的那个对象!!!!
const allReducer = combineReducers({
  count: countReducer,
  persons: personReducer
})

export default allReducer

将store与合并后的总的reducer进行关联,src/redux/store.js:

import { createStore, applyMiddleware } from 'redux'
// import countReducer from './reducers/count'
// 6. 引入合并之后的总的reducer
import reducer from '../redux/reducers'
import thunk from 'redux-thunk'

// const store = createStore(countReducer,applyMiddleware(thunk))
const store = createStore(reducer, applyMiddleware(thunk))

export default store

Person组件:

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

class Person extends Component {
  handleAddPerson = () => {
    const name = this.nameNode.value;
    const age = this.ageNode.value;
    const personObj = { id: nanoid(), name, age };
    this.props.add(personObj);
  };
  render() {
    return (
      <div>
        <h2>我是Person组件,Count组件的和为{this.props.count}</h2>
        <input
          type="text"
          placeholder="输入名字"
          ref={(c) => (this.nameNode = c)}
        />
        <input
          type="text"
          placeholder="输入年龄"
          ref={(c) => (this.ageNode = c)}
        />
        <button onClick={this.handleAddPerson}>添加</button>
        <ul>
          {this.props.persons.map((personObj) => {
            return (
              <li key={personObj.id}>
                {personObj.name}---{personObj.age}
              </li>
            );
          })}
        </ul>
      </div>
    );
  }
}

export default connect(
  (state) => ({
    count: state.count,
    persons: state.persons,
  }), // 从redux中映射状态
  {
    add: createAddPersonAction,
  } // 映射操作状态的方法
)(Person);

Count组件:

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

class Count extends Component {
  handleIncrement = () => {
    // 加
    let { value } = this.selectNode;
    this.props.jia(value * 1);
  };

  handleDecrement = () => {
    // 减
    let { value } = this.selectNode;
    this.props.jian(value * 1);
  };

  handleIncrementOdd = () => {
    // count为奇数再加
    let { value } = this.selectNode;
    if (this.props.count % 2 !== 0) {
      this.props.jia(value * 1);
    }
  };

  handleIncrementAsync = () => {
    // 异步加
    let { value } = this.selectNode;
    this.props.jiaAsync(value * 1, 500);
  };

  render() {
    return (
      <div>
        <h1>
          当前求和为: {this.props.count}, Person组件的人数是:{" "}
          {this.props.personLength}
        </h1>
        <select ref={(c) => (this.selectNode = c)}>
          <option value={1}>1</option>
          <option value={2}>2</option>
          <option value={3}>3</option>
        </select>
        &nbsp;
        <button onClick={this.handleIncrement}>+</button>
        &nbsp;
        <button onClick={this.handleDecrement}>-</button>
        &nbsp;
        <button onClick={this.handleIncrementOdd}>当和为奇数时才加</button>
        &nbsp;
        <button onClick={this.handleIncrementAsync}>异步加</button>
      </div>
    );
  }
}

let mapStateToProps = (state) => ({
  count: state.count,
  personLength: state.persons.length,
});
let mapDispatchToProps = {
  jia: createIncrementAction,
  jian: createDecrementAction,
  jiaAsync: createIncrementAsyncAction,
};

const CountContainer = connect(mapStateToProps, mapDispatchToProps)(Count);

export default CountContainer;

image

149. redux的reducer函数必须是一个纯函数

import {ADD_PERSON} from '../constant'

const initState = [{id: '001', name: 'tom', age: 18}]

export default function personReducer(preState=initState, action) {
  console.log('personReducer')
  const {type, data} = action
  switch (type) {
    case ADD_PERSON:
      /**
       * 本节主要讲一个概念: 纯函数
       * 
       * 在reducer中,不可以直接改变原数据
       * 只能返回新的数据,否则页面不会更新
       * 
       * 不能用 push、unshift等方法。这些方法改变了原数据,
       * 但是数据的引用没有发生变化,则redux监测不到
       *
       */

      /**
       * redux底层:会判断,如果你返回的数据与之前的数据,引用地址值是一样的,
       * 则不会进行更新
       */

      return [data, ...preState]
    default:
      return preState
  }
}

/**
 * 纯函数:
 * 
 * 1. 不得更改参数数据
 * 2. 不会产生任何副作用,例如网络请求,
 * 3. 不能调用Date.now() 或者Math.random()等不纯的方法
 * 
 * redux的reducer函数必须是一个纯函数
 */

150. redux开发者工具的安装与使用

src/redux/store.js:

/** 本节主要是配置redux开发者工具 (https://github.com/zalmoxisus/redux-devtools-extension#usage)
 * 
 * 1. 安装 redux-devtools-extension
 * yarn add redux-devtools-extension
 * 
 * 2. 引入 
 * import { composeWithDevTools } from 'redux-devtools-extension';
 * 
 * 3. 使用
 * const store = createStore(reducer, composeWithDevTools(
      applyMiddleware(...middleware),
      // other store enhancers if any
    ));
 */

import { createStore, applyMiddleware } from 'redux'
import reducer from '../redux/reducers'
import thunk from 'redux-thunk'
// 1. 安装并引入redux开发者工具
import { composeWithDevTools } from 'redux-devtools-extension';

// 2. 使用redux开发者工具
// const store = createStore(reducer, applyMiddleware(thunk))
const store = createStore(reducer, composeWithDevTools(applyMiddleware(thunk)))

export default store

151. 项目的打包和在本地运行

  1. 项目打包:
npm run build

会在根目录下,生成 build目录:

image

  1. 在本地运行:

安装一个包: npm i serve -g (必须要全局安装)

然后执行:serve build(表示从build目录下开启一个服务)

则会瞬间开启一个服务:

image

image

152. 使用react中的lazy函数和Suspense组件,实现路由组件的懒加载

目录结构:

image

效果:

image

App.js:

/**
 * 本节主要用于实现:路由组件的懒加载
 * 
 * import About from './components/About'
   import Home from './components/Home'
   
   上面这种写法:不管有没有点击About、Home,对应的About和Home组件都会被加载

   如果页面上有100个路由组件,那用户只点击了3个,其他的97个路由组件也会被加载。

   为了避免这种情况,需要在react项目中实现路由组件的懒加载。
 */

/** 实现路由组件的懒加载:
 *  使用 react身上的 lazy函数和Suspense组件
 */

// 1. 从react中引入lazy函数和Suspense组件
import React, { Component, lazy, Suspense } from 'react'
import {Link, Route} from 'react-router-dom'

// 2. 组件的引入方式改写
// import About from './components/About'
// import Home from './components/Home'
const About = lazy(() => import('./components/About'))
const Home = lazy(() => import('./components/Home'))

export default class App extends Component {
  render() {
    return (
      <div>
        <div className="row">
          <div className="col-xs-offset-2 col-xs-8">
            <div className="page-header"><h2>React Router Demo</h2></div>
          </div>
        </div>
        <div className="row">
          <div className="col-xs-2 col-xs-offset-2">
            <div className="list-group">
              <Link to="/about" className="list-group-item">About</Link>
              <Link to="/home" className="list-group-item active">Home</Link>
            </div>
          </div>
          <div className="col-xs-6">
            <div className="panel">
              <div className="panel-body">
                {/* 3. 使用Suspense组件将注册的路由,进行包裹,并同时指定fallback属性:

                        fallback属性可以是jsx,也可以是一个组件(但这个组件不能使用lazy)
                */}
                <Suspense fallback={<h1>loading...</h1>}>
                  <Route path="/about" component={About}></Route>
                  <Route path="/home" component={Home}></Route>
                </Suspense>
              </div>
            </div>
          </div>
        </div>
      </div>
    )
  }
}

About组件:

import React, { Component } from "react";

export default class About extends Component {
  render() {
    return <h3>我是About的内容</h3>;
  }
}

Home组件:

import React, { Component } from "react";

export default class Home extends Component {
  render() {
    return <h3>我是Home的内容</h3>;
  }
}

根路径:

image

点击About组件:

在请求对应的路由组件时,当网络很慢,会先加载react中的Suspense组件的fallback属性指定的内容

image

最后再显示对应的路由组件:

image

image

153. 使用react中的useState函数,实现在函数式组件中使用state

Count组件:

/** 本节主要讲 state Hook
 *  目的:在函数式组件中使用state
 *
 *  useState()
 *
 */

/**
 * 使用类式组件实现“点我+1”
 */
// import React from "react";
// class Count extends React.Component {
//   state = {
//     count: 0,
//   };

//   handleAdd = () => {
//     // 1. setState()中不仅可以传一个对象,
//     // 还可以写一个函数:函数的参数就是组件之前的state,函数的返回值是一个对象
//     this.setState((state) => ({ count: state.count + 1 }));
//   };

//   render() {
//     let { count } = this.state;
//     return (
//       <div>
//         <h2>当前求和为: {count}</h2>
//         <button onClick={this.handleAdd}>点我加1</button>
//       </div>
//     );
//   }
// }

/**
 * 使用函数式组件实现“点我+1”
 */
// 2. 使用react中的useState函数,实现在函数式组件中使用state
import { useState } from "react";

function Count() {
  // 3.useState()中的参数,就是初始值
  // 函数的返回值是一个数组,
  // 数组中的第一项对应state,第二项对应修改state的方法
  let [count, setCount] = useState(0);

  let [name, setName] = useState("tom");

  function handleAdd() {
    // 4. 第一种写法:setCount的参数直接写修改后的值
    // setCount(count + 1);
    // 5. 第二种写法:setCount的参数是一个函数(函数的参数是之前的state),函数的返回值是修改后的值
    setCount((count) => count + 1);
  }

  function handleChangeName() {
    setName((name) => `${name}haha`);
  }

  /** 6. 函数式组件有多个状态时,需要多次使用 useState()
   */
  return (
    <div>
      <h2>当前求和为:{count}</h2>
      <button onClick={handleAdd}>点我加1</button>
      <hr />
      <h2>我的名字:{name}</h2>
      <button onClick={handleChangeName}>点我改名</button>
    </div>
  );
}

export default Count;

image

154. 使用react中的useEffect函数,实现在函数式组件中使用生命周期函数

Count组件:

/**
 * 本节主要讲 effect Hook
 *
 * 用于实现在函数式组件中使用生命周期函数
 *
 * useEffect()
 *
 */

// import React from "react";
// import { createRoot } from "react-dom/client";
// class Count extends React.Component {
//   state = {
//     count: 0,
//   };

//   componentDidMount() {
//     this.timer = setInterval(() => {
//       this.setState((state) => ({ count: state.count + 1 }));
//     }, 1000);
//   }

//   handleAdd = () => {
//     this.setState((state) => ({ count: state.count + 1 }));
//   };

//   handleDistroy = () => {
//     const container = document.getElementById("test");
//     const root = createRoot(container);
//     root.unmount();
//   };

//   componentWillUnmount() {
//     console.log("@@@");
//     clearInterval(this.timer);
//   }

//   render() {
//     let { count } = this.state;
//     return (
//       <div id="test">
//         <h2>当前求和为: {count}</h2>
//         <button onClick={this.handleAdd}>点我加1</button>
//         <button onClick={this.handleDistroy}>点我卸载组件</button>
//       </div>
//     );
//   }
// }

// 1. 使用react中的useEffect,实现在函数式组件中使用生命周期函数
import { useState, useEffect } from "react";
import { createRoot } from "react-dom/client";
function Count() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("tom");

  function handleAdd() {
    setCount((count) => count + 1);
  }

  function handleChangeName() {
    setName((name) => `${name}haha`);
  }

  // 1. useEffect第二个参数不传
  // 相当于 componentDidMount和componentDidUpdate
  // 表示监测全部的state变化
  // useEffect(() => {
  //   console.log("@@@@");
  // });

  // 2. useEffect第二个参数传[]
  // 只相当于componentDidMount
  // 表示state数据全部都不监测
  // useEffect(() => {
  //   console.log("@@@@");
  // }, []);

  // 3. Effect第二个参数中传[key]
  // 数组传入是依赖项,当数组中的状态发生了变化,也会触发useEffect执行
  // 表示监测componentDidMount和state中key的update
  // useEffect(() => {
  //   console.log("@@@@");
  // }, [count]);

  // 【相当于componentDidMount】
  // useEffect(() => {
  //   setInterval(() => {
  //     setCount((count) => count + 1);
  //   }, 1000);
  // }, []);

  //
  useEffect(() => {
    let timer = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);
    // return返回的函数,相当于componentWillUnmout
    return () => {
      console.log("##");
      clearInterval(timer);
    };
  }, []);

  function handleDistroy() {
    const container = document.getElementById("test");
    const root = createRoot(container);
    root.unmount();
  }

  return (
    <div id="test">
      <h2>当前求和为:{count}</h2>
      <button onClick={handleAdd}>点我加1</button>
      <hr />
      <h2>我的名字:{name}</h2>
      <button onClick={handleChangeName}>点我改名</button>
      <hr />
      <button onClick={handleDistroy}>点我卸载组件</button>
    </div>
  );
}

export default Count;

155. 使用react中的useRef函数,实现在函数式组件中创建ref容器

Count组件:

/**
 * 本节讲 ref Hook
 * 用于在函数式组件中创建ref容器
 *
 * useRef()
 */

// import React from "react";
// class Count extends React.Component {
//   // 1. 创建一个容器myRef,放在实例this自身
//   myRef = React.createRef();
//   handleAlert = () => {
//     alert(this.myRef.current.value);
//   };
//   render() {
//     return (
//       <div>
//         {/* 2. 实例自身上的容器myRef,存放input节点 */}
//         <input defaultValue={null} ref={this.myRef} />
//         <button onClick={this.handleAlert}>点我提示输入框中的内容</button>
//       </div>
//     );
//   }
// }

import { useRef } from "react";
function Count() {
  // 1. 创建一个容器myRef
  const myRef = useRef();

  function handleAlert() {
    alert(myRef.current.value);
  }

  return (
    <div>
      {/* 2. 容器myRef,存放input节点 */}
      <input defaultValue={null} ref={myRef} />
      <button onClick={handleAlert}>点我提示输入框中的内容</button>
    </div>
  );
}

export default Count;

156. 使用react中的createRef函数,也可以实现在函数式组件中创建ref容器

Count组件:

/**
 * 在函数组件中,也可以直接使用createRef
 *
 * 与useRef() 一样
 */
import { createRef } from "react";
function Count() {
  // 1. 创建一个容器myRef
  const myRef = createRef();

  function handleAlert() {
    alert(myRef.current.value);
  }

  return (
    <div>
      {/* 2. 容器myRef,存放input节点 */}
      <input defaultValue={null} ref={myRef} />
      <button onClick={handleAlert}>点我提示输入框中的内容</button>
    </div>
  );
}

export default Count;

157. useRef和createRef区别

  • createRef会在组件每次渲染的时候重新创建

  • useRef只会在组件首次渲染时创建

158. 使用react中的Fragment组件,使得在编译后页面不显示该层级的标签

Count组件:

/**
 * 本节讲react中的Fragment组件,
 *
 * 最终编译后不会在页面中显示此标签
 */
import { Fragment } from "react";
function Count() {
  return (
    // 1. 使用Fragement组件,可以写key属性
    // 编译后不会在页面中显示此标签
    // <Fragment>
    //   <h2>哈哈</h2>
    //   <h2>嘻嘻</h2>
    // </Fragment>

    // 2.空标签不能写key属性
    // 编译后也不会在页面中显示此标签
    <>
      <h2>哈哈</h2>
      <h2>嘻嘻</h2>
    </>
  );
}

export default Count;

1673330871781

159. 使用react中的PureComponent组件,解决react中Component组件效率上的问题

Count组件:

/**
 * 本节主要讲:
 *
 * 使用react中的PureComponent组件,可以解决react中的Component组件效率上的问题
 *
 */

/**
 * react中的Component组件,存在效率上的2个问题:
 *
 *  1. 只要执行setState({}),即使不改变状态数据,组件也会重新render  =====>效率低
 *
 *  2. 只要当前组件重新render(),就会自动重新render子组件,纵使子组件没有用到父组件的任何数据 ===>效率低
 *
 *
 * 效率高的做法:
 *
 *  只有当前组件的state或props数据发生改变时,才重新render()
 *
 * 原因:
 *
 * Component中的shouldComponentUpdate() 总是返回 true
 *
 * 解决:
 *
 *  1. 重写shouldComponentUpdate()
 *
 *  2. 使用PureComponent
 *
 *  注意: 使用PureComponent时,不要直接修改state数据!!!
 *         而是要产生新数据
 *
 *        直接修改:
 *         const obj = this.state    // 引用地址的传递
 *         obj.name='jack'
 *         this.setState(obj)
 *
 *        产生新数据:
 *        this.setState({name: 'jack'})   // 字面量形式的新对象
 *
 */

// 1. 引入 PureComponent而不是Component
// import React, { Component } from "react";
import React, { PureComponent } from "react";

// 2. 使用 PureComponent
// export default class Count extends Component {
export default class Count extends PureComponent {
  render() {
    return <div>1111</div>;
  }
}

160. 插槽

Count组件:

/**
 * 本节主要讲:
 *
 * 插槽
 *
 */

import React, { PureComponent } from "react";

class Parent extends PureComponent {
  render() {
    return (
      <div>
        <h2>我是父组件Parent</h2>
        {/* 1. A 和 B 是父子关系 */}
        {/* 
        <A>
          <B />
        </A> 
        */}

        {/* 3. 方法: */}
        <A loadCom={(name) => <B name={name} />}></A>
      </div>
    );
  }
}

class A extends PureComponent {
  /**
   * 2. 问题:怎么把A中的state传递给B?
   */
  state = {
    name: "tom",
  };

  render() {
    return (
      <div>
        <h3>我是A组件</h3>
        {/* {this.props.children} */}
        {this.props.loadCom(this.state.name)}
      </div>
    );
  }
}

class B extends PureComponent {
  render() {
    return (
      <div>
        <h4>我是B组件</h4>
        {this.props.name}
      </div>
    );
  }
}

export default Parent;

image

161. React Router 6.x 与 React Router 5.x 的区别

React Router 以三个不同的包发布到npm上,它们分别为:

  1. react-router: 路由的核心库,提供了很多的:组件、钩子

  2. react-router-dom: 包含react-router所有的内容,并添加一些专门用于DOM的组件,例如<BrowserRouter>

  3. react-router-native:包括react-router所有内容,并添加一些专门用于ReactNative的api,例如<NativeRouter>

与 React Router 5.x 版本相比,改变了什么?

  1. 内置组件的变化: 移除<Switch/>,新增<Routes/>

  2. 语法的变化:component={About} 变为element={<About/>}

  3. 新增多个hook: useParamsuseNavigateuseMatch

  4. 官方明确推荐函数式组件了!!!

...

162. Routes组件和Navigate组件(react-router-dom 6.x中移除Switch组件,取而代之用Routes组件)(移除Redirect组件,用Navigate组件实现路由重定向)

App.js:

/**
 * 本节主要讲: react-router-dom 的最新版本 6.x
 * 
 * 1. 安装  yarn add react-router-dom
 * 
 * 2. 引入并使用BowserRouter组件去包裹App组件   (在index.js入口文件中)
 * 
 * 3. 编写路由链接
 * 
 * 4. 注册路由
 * 
 * 5. 添加重定向路由规则 
 */

// 引入 NavLink 组件,用于编写路由链接(带高亮效果)(Link组件不带高亮)
// 引入 Routes组件和Route组件,用于注册路由。
// Route组件必须使用Routes进行包裹
// 引入Navigate组件,用于路由重定向(5.x中使用的 Redirect组件)
import { NavLink, Routes, Route, Navigate } from 'react-router-dom'
import Header from './components/Header'
import About from './pages/About'
import Home from './pages/Home'

function App() {
  return <div>
  <Header/>
  <div className="row">
    <div className="col-xs-2 col-xs-offset-2">
      <div className="list-group">
        {/* <a className="list-group-item" href="./about.html">About</a>
        <a className="list-group-item active" href="./home.html">Home</a> */}
        {/* 3. 编写路由链接 */}
        <NavLink to="/about" className="list-group-item">About</NavLink>
        <NavLink to="/home" className="list-group-item">Home</NavLink>
      </div>
    </div>
    <div className="col-xs-6">
      <div className="panel">
        <div className="panel-body">
          {/* 4. 注册路由:
            react-router-dom 5.x:
                使用了Switch组件将所有的路由Route进行包裹,用于提升路由匹配效率;
            react-router-dom 6.x:
                Switch组件被移除,且必须使用Routes,将所有的Route组件包裹
          */}

          <Routes>
            {/* 5. Route语法变化:
                react-router-dom 5.x:  <Route path="/about" component={About}/>
                react-router-dom 6.x:  <Route path="/about" element={<About/>}/>
            */}
            {/* <Route path="/about" component={About}/> */}
            <Route path="/about" element={<About/>}/>
            <Route path="/home" element={<Home/>}/>
            {/* 6. 使用Navigate组件,实现路由重定向:
                Navigate组件只要被渲染,就会引起path的改变,视图就会变化
            */}
            <Route path="/" element={<Navigate to="/about"/>}/>
          </Routes>
        </div>
      </div>
    </div>
  </div>
</div>;
}

export default App;

index.js:

import React from 'react';
import ReactDOM from 'react-dom/client';
// 2.引入react-router-dom中的 BrowserRouter组件【路由器】
import { BrowserRouter } from 'react-router-dom'
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    {/* 并使用BowserRouter组件包裹App组件 */}
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

Home组件:src/pages/Home/index.jsx

import React, { useState } from "react";
import { Navigate } from "react-router-dom";

export default function Home() {
  const [sum, setSum] = useState(1);
  return (
    <div>
      <h3>我是Home的内容</h3>
      {/* Navigate组件只要被渲染,就会引起path的改变,视图就会变化 */}
      {sum == 1 ? <h4>当前求和为:{sum}</h4> : <Navigate to="/about" />}
      <button onClick={() => setSum(2)}>点我变为2</button>
    </div>
  );
}

About组件:src/pages/About/index.jsx

import React from "react";

export default function About() {
  return <h3>我是About的内容</h3>;
}

image

163. 给NavLink组件的className指定函数(react-router-dom 6.x中移除了activeClassName属性)(通过函数的参数给NavLink组件动态添加高亮样式)

App.js:

/**
 * 本节主要讲: react-router-dom 的最新版本 6.x
 * 
 * NavLink组件实现如何高亮效果
 * 
 * 1. 通过NavLink组件,给className属性指定一个函数,在该函数中可以拿到一个对象。
 * 通过对象中的isActive属性可以判断url中的path与当前NavLink中的to属性对应的path是否相同,相同为true
 * 不同为false
 * 
 * 2. 然后根据isActive属性来动态添加高亮样式
 */

import { NavLink, Routes, Route, Navigate } from 'react-router-dom'
import Header from './components/Header'
import About from './pages/About'
import Home from './pages/Home'

function App() {
  function handleClassName({isActive}) {
    // 当url中的path与当前路由链接NavLink中的to匹配,则isActive 为 true。否则为false
    return isActive ? 'list-group-item atguigu': 'list-group-item' 
  }
  return <div>
  <Header/>
  <div className="row">
    <div className="col-xs-2 col-xs-offset-2">
      <div className="list-group">
        {/* react-router-dom 5.x 中,直接给NavLink组件添加 activeClassName属性,
            就能指定高亮的样式:
            <NavLink to="/about" activeClassName='atguigu' className="list-group-item">About</NavLink>

            react-router-dom 6.x中:
            不认识activeClassName属性。
            而是要把className属性写成一个函数
        */}

        {/* 
        <NavLink to="/about" className={(a)=>{console.log(666,a)}}>About</NavLink>
        <NavLink to="/home" className="list-group-item">Home</NavLink>
         */}

        <NavLink to="/about" className={handleClassName}>About</NavLink>
        <NavLink to="/home" className={handleClassName}>Home</NavLink>
      </div>
    </div>
    <div className="col-xs-6">
      <div className="panel">
        <div className="panel-body">
          <Routes>
            <Route path="/about" element={<About/>}/>
            <Route path="/home" element={<Home/>}/>
            <Route path="/" element={<Navigate to="/about"/>}/>
          </Routes>
        </div>
      </div>
    </div>
  </div>
</div>;
}

export default App;

image

image

164. 使用useRoutes()函数(实现根据路由表,自动生成路由规则)

App.js:

/**
 * 本节主要讲: react-router-dom 的最新版本 6.x
 * 
 * useRoutes()  Hook
 * 
 * 实现:根据路由表,自动生成路由规则
 * 
 */

// 1. 从react-router-dom 6.x中引入 useRoutes
import { 
  NavLink, 
  // Routes, 
  // Route, 
  Navigate, 
  useRoutes 
} from 'react-router-dom'
import Header from './components/Header'
import About from './pages/About'
import Home from './pages/Home'

function App() {
  // 2.调用useRoutes(),并传入对象数组
  // 其中的[]就是路由表,useRoutes 会根据路由表自动生成路由规则
  // a 就是自动生成的路由规则
  const a = useRoutes([
    {
      path: '/about',
      element: <About/>
    },
    {
      path: '/home',
      element: <Home/>
    },
    {
      path: '/',
      element: <Navigate to="/about"/>
    },
  ])
  return <div>
  <Header/>
  <div className="row">
    <div className="col-xs-2 col-xs-offset-2">
      <div className="list-group">
        <NavLink to="/about" className='list-group-item'>About</NavLink>
        <NavLink to="/home" className='list-group-item'>Home</NavLink>
      </div>
    </div>
    <div className="col-xs-6">
      <div className="panel">
        <div className="panel-body">
          {/* 编写路由规则 */}
          {/* 
          <Routes>
            <Route path="/about" element={<About/>}/>
            <Route path="/home" element={<Home/>}/>
            <Route path="/" element={<Navigate to="/about"/>}/>
          </Routes> 
          */}

          {a}
          {/* 
            3. 优点:使用了useRoutes()之后,无需再手动写Routes、Route等组件,
            useRoutes会自动帮你生成这些结构。
          */}
        </div>
      </div>
    </div>
  </div>
</div>;
}

export default App;

165. 在routes目录中配置路由表

1673417259482

src/routes/index.js:

/**
 * 1. 本文件是路由表
 */
import { Navigate } from 'react-router-dom'
import About from '../pages/About'
import Home from '../pages/Home'

const routes = [
  {
    path: '/about',
    element: <About/>
  },
  {
    path: '/home',
    element: <Home/>
  },
  {
    path: '/',
    element: <Navigate to="/about"/>
  },
]

export default routes

App.js:

/**
 * 本节主要讲: react-router-dom 的最新版本 6.x
 *  
 * 如何把路由表配置在外部,引入并使用
 */

import { NavLink, useRoutes } from 'react-router-dom'
import Header from './components/Header'
import Routes from './routes'

function App() {
  return <div>
  <Header/>
  <div className="row">
    <div className="col-xs-2 col-xs-offset-2">
      <div className="list-group">
        <NavLink to="/about" className='list-group-item'>About</NavLink>
        <NavLink to="/home" className='list-group-item'>Home</NavLink>
      </div>
    </div>
    <div className="col-xs-6">
      <div className="panel">
        <div className="panel-body">
          {useRoutes(Routes)}
        </div>
      </div>
    </div>
  </div>
</div>;
}

export default App;

166. 嵌套路由表,并使用Outlet组件将与url中path对应的路由组件渲染到Outlet所在位置

image

src/routes/index.js:

/**
 * 本节主要实现 react-router-dom 6.x中的路由嵌套
 * 
 * 1. 路由表中,可以使用children属性-实现
 * 
 */
import { Navigate } from 'react-router-dom'
import About from '../pages/About'
import Home from '../pages/Home'
import News from '../pages/Home/News'
import Message from '../pages/Home/Message'


const routes = [
  {
    path: '/about',
    element: <About/>
  },
  {
    path: '/home',
    element: <Home/>,
    // 1. children属性实现路由嵌套
    children: [
      {
        // 子级path,不再需要嵌套所有的父级path
        // 也不用写 /
        path: 'news',
        element: <News/>
      },
      {
        path: 'message',
        element: <Message/>
      }
    ]
  },
  {
    path: '/',
    element: <Navigate to="/about"/>
  },
]

export default routes

Home组件: src/pages/Home/index.jsx

import React from "react";
// 2. 使用Outlet组件
import { NavLink, Outlet } from "react-router-dom";

export default function Home() {
  return (
    <div>
      <h3>我是Home的内容</h3>
      <div>
        <ul className="nav nav-tabs">
          <li>
            {/* 3. 这里的to就不需要写成 “/home/news” 
              直接写 news
            */}
            <NavLink to="news" className="list-group-item">
              News
            </NavLink>
          </li>
          <li>
            <NavLink to="message" className="list-group-item">
              Message
            </NavLink>
          </li>
        </ul>
        {/* 4. Outlet组件:
          会自动根据当前url中的path,渲染对应的路由组件到当前Outlet所在的位置
         */}
        <Outlet />
      </div>
    </div>
  );
}

App.js:

import { NavLink, useRoutes } from 'react-router-dom'
import Header from './components/Header'
import Routes from './routes'

function App() {
  return <div>
  <Header/>
  <div className="row">
    <div className="col-xs-2 col-xs-offset-2">
      <div className="list-group">
        <NavLink to="/about" className='list-group-item'>About</NavLink>
        {/* 5. end 属性用于实现:
            子菜单被高亮时,父菜单取消高亮
        */}
        <NavLink to="/home" end className='list-group-item'>Home</NavLink>
      </div>
    </div>
    <div className="col-xs-6">
      <div className="panel">
        <div className="panel-body">
          {useRoutes(Routes)}
        </div>
      </div>
    </div>
  </div>
</div>;
}

export default App;

image

167. 使用useParams()函数(接收params参数)

Message组件:

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

export default function Message() {
  const [messages] = useState([
    {
      id: "001",
      title: "message001",
      content: "消息1内容",
    },
    {
      id: "002",
      title: "message002",
      content: "消息2内容",
    },
    {
      id: "003",
      title: "message003",
      content: "消息3内容",
    },
  ]);
  return (
    <div>
      <ul>
        {messages.map((m) => {
          return (
            <li key={m.id}>
              {/* 1. 传递params参数 */}
              <Link to={`detail/${m.id}/${m.title}/${m.content}`}>
                {m.title}
              </Link>
              &nbsp;&nbsp;
            </li>
          );
        })}
      </ul>
      <hr />
      <Outlet />
    </div>
  );
}

路由表:src/routes/index.js

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


const routes = [
  {
    path: '/about',
    element: <About/>
  },
  {
    path: '/home',
    element: <Home/>,
    children: [
      {
        path: 'news',
        element: <News/>
      },
      {
        path: 'message',
        element: <Message/>,
        children: [
          {
            // 2. 在路由表中,path属性上声明并接收变量
            path: 'detail/:id/:title/:content',
            element: <Detail/>
          }
        ]
      }
    ]
  },
  {
    path: '/',
    element: <Navigate to="/about"/>
  },
]

export default routes

Detail组件:

import React from "react";
// 3. 使用 useParams 接收params参数
import { useParams } from "react-router-dom";

export default function Detail() {
  const a = useParams();
  console.log(a); // {id: '003', title: 'message003', content: '消息3内容'}
  return (
    <ul>
      <li>消息的id:{a.id}</li>
      <li>消息的标题:{a.title}</li>
      <li>消息的内容:{a.content}</li>
    </ul>
  );
}

image

168. 使用useSearchParams()函数(接收search参数)

Message组件:

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

export default function Message() {
  const [messages] = useState([
    {
      id: "001",
      title: "message001",
      content: "消息1内容",
    },
    {
      id: "002",
      title: "message002",
      content: "消息2内容",
    },
    {
      id: "003",
      title: "message003",
      content: "消息3内容",
    },
  ]);
  return (
    <div>
      <ul>
        {messages.map((m) => {
          return (
            <li key={m.id}>
              {/* 1. 传递 search参数 */}
              <Link
                to={`detail?id=${m.id}&title=${m.title}&content=${m.content}`}
              >
                {m.title}
              </Link>
              &nbsp;&nbsp;
            </li>
          );
        })}
      </ul>
      <hr />
      <Outlet />
    </div>
  );
}

Detail组件:

import React from "react";
// 2. 使用 useSearchParams() 接收search参数
import { useSearchParams } from "react-router-dom";

export default function Detail() {
  const a = useSearchParams();
  console.log(a);
  // a 拿到的是一个数组
  const [search, setSearch] = a;
  // 数组的第一项是拿到的search参数,第二项是修改search参数的方法

  // 3. 需要注意:必须通过search.get(key)拿到参数
  const id = search.get("id");
  const title = search.get("title");
  const content = search.get("content");

  return (
    <div>
      {/* 4. 通过 setSearch 修改传递的search参数*/}
      <button
        onClick={() => {
          setSearch("id=008&title=哈哈&content=嘻嘻");
        }}
      >
        修改传递过来的search参数
      </button>
      <ul>
        <li>消息的id: {id}</li>
        <li>消息的标题:{title}</li>
        <li>消息的内容:{content}</li>
      </ul>
    </div>
  );
}

路由表:无需要声明接收参数变量(src/routes/index.js)

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


const routes = [
  {
    path: '/about',
    element: <About/>
  },
  {
    path: '/home',
    element: <Home/>,
    children: [
      {
        path: 'news',
        element: <News/>
      },
      {
        path: 'message',
        element: <Message/>,
        children: [
          {
            path: 'detail',
            element: <Detail/>
          }
        ]
      }
    ]
  },
  {
    path: '/',
    element: <Navigate to="/about"/>
  },
]

export default routes

1

点击修改search参数按钮:

2

169. 使用useLocation()函数(接收state参数)

Message组件:

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

export default function Message() {
  const [messages] = useState([
    {
      id: "001",
      title: "message001",
      content: "消息1内容",
    },
    {
      id: "002",
      title: "message002",
      content: "消息2内容",
    },
    {
      id: "003",
      title: "message003",
      content: "消息3内容",
    },
  ]);
  return (
    <div>
      <ul>
        {messages.map((m) => {
          return (
            <li key={m.id}>
              {/* 1. 传递state参数: 直接给Link添加state属性,值是一个对象*/}
              <Link
                to="detail"
                state={{
                  id: m.id,
                  title: m.title,
                  content: m.content,
                }}
              >
                {m.title}
              </Link>
              &nbsp;&nbsp;
            </li>
          );
        })}
      </ul>
      <hr />
      <Outlet />
    </div>
  );
}

Detail组件:

import React from "react";
/**
 * 回顾:
 *
 * params   ---  useParams()
 * search   ---  useSearchParams()
 * state    ---  useLocation()

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

export default function Detail() {
  const a = useLocation();
  console.log(a); // location对象身上有state参数

  // 连续结构赋值
  const {
    state: { id, title, content },
  } = a;
  return (
    <div>
      <ul>
        <li>消息的id: {id}</li>
        <li>消息的标题:{title}</li>
        <li>消息的内容:{content}</li>
      </ul>
    </div>
  );
}

路由表:无需要声明接收参数变量(src/routes/index.js)

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


const routes = [
  {
    path: '/about',
    element: <About/>
  },
  {
    path: '/home',
    element: <Home/>,
    children: [
      {
        path: 'news',
        element: <News/>
      },
      {
        path: 'message',
        element: <Message/>,
        children: [
          {
            path: 'detail',
            element: <Detail/>
          }
        ]
      }
    ]
  },
  {
    path: '/',
    element: <Navigate to="/about"/>
  },
]

export default routes

image

170. 使用useNavigate()函数(实现编程式路由导航)

Message组件:

import React, { useState } from "react";
// 1. 使用 useNavigate(),实现编程式路由导航
import { Link, Outlet, useNavigate } from "react-router-dom";

export default function Message() {
  const [messages] = useState([
    {
      id: "001",
      title: "message001",
      content: "消息1内容",
    },
    {
      id: "002",
      title: "message002",
      content: "消息2内容",
    },
    {
      id: "003",
      title: "message003",
      content: "消息3内容",
    },
  ]);

  // 2. 调用useNavigate(),拿到一个函数a
  const a = useNavigate();

  function handleDetail(m) {
    // 3. 调用函数a
    // 第一个参数写跳转后的path,
    // 第二个参数写配置项
    a("detail", {
      replace: false, // push or replace
      state: {
        // 【state】属性中写要携带的参数
        id: m.id,
        title: m.title,
        content: m.content,
      },
    });
  }

  return (
    <div>
      <ul>
        {messages.map((m) => {
          return (
            <li key={m.id}>
              <Link
                to="detail"
                state={{
                  id: m.id,
                  title: m.title,
                  content: m.content,
                }}
              >
                {m.title}
              </Link>
              &nbsp;&nbsp;
              <button onClick={() => handleDetail(m)}>查看详情</button>
            </li>
          );
        })}
      </ul>
      <hr />
      <Outlet />
    </div>
  );
}

Header组件: src/components/Header/index.jsx

import React from "react";
// 1. 使用useNavigate()在一般组件中实现编程式路由导航
import { useNavigate } from "react-router-dom";
/**
 * react-router-dom 5.x中,一般组件使用的是withRouter()
 * 来实现编程式路由导航
 *
 * react-router-dom 6.x:
 * 使用 useNavigate()
 */

export default function Header() {
  let a = useNavigate();
  function back() {
    a(-1);
  }
  function forward() {
    a(1);
  }
  return (
    <div className="row">
      <div className="col-xs-offset-2 col-xs-8">
        <div className="page-header">
          <h2>React Router Demo</h2>
          <button onClick={back}>前进</button>
          <button onClick={forward}>后退</button>
        </div>
      </div>
    </div>
  );
}

1673428572035

⚠️ **GitHub.com Fallback** ⚠️