39. B站 学习react全家桶笔记 尚硅谷【React简介】 - yiqunkeke/react-jianshu-shangguigu-zty GitHub Wiki
- 发送请求获取数据
- 处理数据(过滤、整理格式等)
- 操作DOM呈现页面
React 只做第3步。
由FaceBook开发,且开源。
- 之前的方式:原生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开发的痛点。
-
采用 组件化 模式、声明式编码 ,提高了组件复用率和开发效率。
命令式编码:向左走,拿起水杯,接水,送到我这里,让我喝
声明式编码:接好水,喝吧
-
在React Native中可以使用React语法进行 移动端开发 。
-
使用 虚拟DOM + 优秀的 Diffing算法 ,尽量减少与真实DOM的交互。
关键点来了: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高效的原因。
使用虚拟DOM,不总是直接操作页面真实DOM。,而且在操作真实DOM之前,会把两个不同的虚拟DOM进行比较,比较出有差异的那一部分,再去更新页面,所以说人家高效。
DOM Diffing算法,最小化页面重绘。
- 判断this 的指向
- class(类)
- ES6语法规范
- npm 包管理器
- 原型、原型链
- 数组常用方法
- 模块化
学习react,我们从最原始的方式开始学习,回想下一开始是如何学习jquery的?
就是在html页面中直接引入jquery.js文件,然后去查阅相关文档,学习怎么使用$符号操作dom。
学习react也是一样,我们从最简单的开始,只需要在html页面中引入react就可以了。那都需要引入哪些文件?
-
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>
运行结果如下图:
由 const VDOM = <h1>Hello, React</h1> /*此处一定等号后面一定不要写引号,因为VDOM不是字符串*/
思考这样一个问题: 为什么不用原生的js,非要用jsx?
React官方要求我们使用jsx。
那React官方为什么要出jsx语法?
接下来用一个案例来解释这个问题。
<!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>
我们来看下虚拟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>
-
全称 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()
-
react定义的一种类似于XML的JS扩展语法: JS + XML
-
本质是 React.createElement(component, props, ...children)的语法糖
-
作用:用来简化创建虚拟DOM
a. 写法: `var ele = <h1>Hello JSX!</h1>` b. 注意1:它不是字符串(等号后面不需加引号),也不是 html/xml 标签 c: 注意2:它最终产生的就是一个JS对象
-
标签名任意: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>
代码实现过程:
<!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表达式。不能写语句。
(1) 模块:向外提供特定功能的js程序,一般就是一个js文件
(2) 为什么要拆成模块?
随着业务逻辑的增加,代码越来越我且复杂
(3) 作用:复用js,简化js的编写,提高js运行效率
总结模块,就是js模块,只是拆js
(1) 组件:用来实现局部功能代码和资源的组合
(2) 为什么:一个界面的功能可能更复杂
(3) 作用:复用代码,简化项目编码,提高运行效率
模块化: 当应用的js都以模块来编写的,这个应用就是一个模块化的应用
组件化: 当应用是以多组件的方式实现,这个应用就是一个组件化的应用
一拆到底
这个工具 React Developer Tools 是基于chrome 浏览器的
React小图标:
红色+小虫子:代表网站没有打包上线,是在开发环境
蓝色:代表线上环境
components这个tab, 可以用来观察网页由什么组件组成,每个组件中有哪些属性
profiler这个tab,用于记录网站的性能,比如渲染组件用了多久?哪个组件加载最慢?
<!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>
<!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>
<!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>
上图结果可以看出,在类的原型对象上,确实有render()方法
上图结果可以看到MyComponent类的实例对象
-
复杂组件:有state
-
简单组件:无state
-
state:
人: 状态 影响 行为
组件: 状态 驱动 页面
组件的状态里边存着数据,
数据的改变驱动着页面的展示。
-
state是谁身上的?
-不是类本身上的
-是由这个类缔造的实例身上的。(看16节中的结果截图,是render中的this身上的。也就是实例对象上的。)
所以说是“组件实例的三大核心属性之一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>
<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>
运行结果:
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>
(实现案例:今天天气很炎热/凉爽 点击进行切换)
<!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>
(实现案例:今天天气很炎热/凉爽 点击进行切换)
<!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>
如上图,在上面两个红色框内,都能够访问到this。
构造器中的this,就是Weather类的组件实例对象
render中的this,也是Weather类的组件实例对象
而类外部的方法根本访问不到类的实例对象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>
<!-- 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>
原因如下:
直接报错:
正确写法如下:
<!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>
<!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>
(实现案例:今天天气很炎热/凉爽 点击进行切换)
<!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,不可能指向实例对象自身
(实现案例:今天天气很炎热/凉爽 点击进行切换)
<!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>
(实现案例:今天天气很炎热/凉爽 点击进行切换)
<!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>
因为你写了组件标签,然后react中自动帮你new Weather()了一个实例对象,比如叫w1,并且通过【w1.render()调用】的render函数,
所以render中的this指向实例对象,也没问题。
因为changeWeather根本就【不是w1.调用】的。
而是【作为事件的回调在使用】。(等触发了点击事件时,直接把changeWeather函数拉出来,直接调用)
而且由于类内部的方法开启了局部严格模式,
所以changeWeather()中的this指向了undefined
也就是说类的所有内部方法中的this,都会指向undefined
如下图
(1)因为我们要对Weather类的实例对象,进行一些初始化的操作(初始化状态)
(2)为事件处理函数绑定实例
感觉我们的构造器写的很被动。
<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>
如下图:
(实现案例:今天天气很炎热/凉爽 点击进行切换)
<!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>
原理如下图:
实现以下案例:
<!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>
<!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>
<!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>
<!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>
运行结果:
render() {
// 【不允许修改】传递进来的组件标签属性
this.props.name = 'jack'
return (
<ul>
<li>姓名:{this.props.name}</li>
<li>性别:{this.props.sex}</li>
{/*下面这行是【运算】,不是修改*/}
<li>年龄:{this.props.age+1}</li>
</ul>
)
}
<!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>
<!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>
<!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>
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 属性
- 组件标签的所有属性都保存在props中
作用:
-
给组件标签添加属性,可以从组件外部向组件内部,传递变化的数据
-
注意:组件内部不要修改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的关系探讨
需求:
页面中有两个input,一个button
-
点击button,提示第一个输入框中的值
-
第二个输入框失去焦点时,提示这个输入框中的值
<!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="点击按钮获取数据"/> */}
{/* (2)React给JSX标签打标识:给JSX标签添加 ref属性 */}
<input ref="input1" type="text" placeholder="点击按钮获取数据"/>
<button onClick={this.showData}>点击按钮</button>
<input ref="input2" onBlur={this.handleBlur} type="text" placeholder="鼠标失去焦点获取数据"/>
{/*
理解:
组件内的标签可以定义ref来标识自己。
(就类似于原生中的id)
*/}
</div>
)
}
}
ReactDOM.render(<Demo />, document.getElementById('test'))
</script>
</body>
</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>
<!-- 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="点击按钮获取数据"/> */}
{/*
回调函数能收到什么参数?
取决于谁调的它。
换句话说:回调函数能收到什么参数,取决于回调函数的调用者
ref={(a)=>{console.log(a)}} 查看下参数a
a正是当前这个ref所处的DOM节点
*/}
{/* <input ref={(a)=>{console.log(a)}} type="text" placeholder="点击按钮获取数据"/> */}
{/*
this.input1 = a
把当前ref所处的节点,挂在实例对象自身上,并取名为input1
*/}
{/* <input ref={(a)=>{this.input1 = a}} type="text" placeholder="点击按钮获取数据"/> */}
{/*
精简写法:
箭头函数的参数只有一个,则箭头左侧的()可以省略
箭头函数的函数体中只有一句,则箭头右侧的 {} 可以省略
*/}
<input ref={c => this.input1 = c} type="text" placeholder="点击按钮获取数据"/>
<button onClick={this.showData}>点击按钮</button>
<input ref={c => this.input2 = c} onBlur={this.handleBlur} type="text" placeholder="鼠标失去焦点获取数据"/>
</div>
)
}
}
ReactDOM.render(<Demo />, document.getElementById('test'))
</script>
</body>
</html>
如果ref回调函数是以【内联函数】的方式定义的,在【更新】过程中它会被执行两次。
内联函数:
ref={c => this.input1 = c}
这种形式就是内联函数
换句话说:在JSX标签内直接写函数就是内联函数
更新:
在render中不会存在这个问题,只有在更新时才会被执行两次
案例:综合天气炎热/凉爽与点击按钮获取取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" /> */}
{/* 把ref回调函数放在实例自身 */}
<input ref={this.saveNode} type="text" />
<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内联回调函数】
<!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="点击按钮获取数据"/>
<button onClick={this.showData}>点击按钮</button>
<input ref={this.myRef2} onBlur={this.handleBlur} type="text" placeholder="鼠标失去焦点获取数据"/>
</div>
)
}
}
ReactDOM.render(<Demo />, document.getElementById('test'))
</script>
</body>
</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>
<!-- 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="点击按钮获取数据"/>
<button onClick={this.showData}>点击按钮</button>
<input onBlur={this.handleBlur} type="text" placeholder="鼠标失去焦点获取数据"/>
</div>
)
}
}
ReactDOM.render(<Demo />, document.getElementById('test'))
</script>
</body>
</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>
<!-- 非受控组件:现用现取数据 -->
<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>
<!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>
<!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>
<!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>
需求:
1.让指定的文本作显示/隐藏的渐变动画
2.从完全可见,到彻底消失,耗时2s
3.点击“不活了”按钮,从界面中卸载组件
<!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>
理解:
-
组件从创建到死亡它会经历一些特定的阶段
-
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>
<!-- 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>
1.初始化阶段:由ReactDOM.render()触发---初次渲染
-
constructor()
-
componentWillMount()
-
render()
-
componentDidMount()
常用
一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
2.更新阶段:由组件内部this.setState() 或者父组件render触发
-
shouldComponentUpdate()
-
componentWillUpdate()
-
render()
-
componentDidUpdate()
-
componentWillReceiveProps()
3.卸载组件:由ReactDOM.unmountComponentAtNode()触发
-
componentWillUnmount()
常用
一般在这个钩子中做一些收尾的事:例如关闭定时器、取消订阅消息
4. 新版本的react中的生命周期
在新版本 18.x 中,所有带will的除了componentWillUnmount()之外的钩子,都要加上 UNSAFE_ 前缀:
5. 需要加UNSAFE_前缀的都有哪些?
UNSAFE_componentWillMount()
UNSAFE_componentWillUpdate()
UNSAFE_componentWillReceiveProps()
(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) 使用脚手架开发的项目特点:模块化、组件化、工程化
第一步:全局安装 npm install -g create-react-app
必须全局安装这个命令,才能使用这个命令创建基于react脚手架的模板项目
第二步:切换到想创建项目的目录,使用命令:create-react-app hello-react
第三步:进入项目文件夹:cd hello-react
第四步:启动项目: npm start
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>
src/index.js 入口文件
// 引入 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();
-
整个项目在启动后,首先来到src/index.js,在这里面
- 引入了react核心库
- 引入了reactDOM
- 引入了通用样式
- 引入了App组件
- 最后触发了
ReactDOM.render(<App/>,document.getElementById('root'))
其实渲染的是App组件,且由于document.getElementById('root')写了root节点,则react脚手架就会自动去public/index.html中找id为root的节点
-
所以说 src/index.js 会主动找public/index.html
因此,src/index.js和public/index.html不要随意更改文件名。
-
ReactDOM.render(<App/>,document.getElementById('root'))
执行完之后,App组件就显示到页面上了 -
在App组件内部,由于引入了src/index.css样式文件,所以通用样式也就生效了。
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>
)
}
}
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>;
}
}
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样式,发生样式冲突
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文件中的样式冲突
用了这个插件,就可以使用 rcc 快速定义一个react class component
用 rfc 快速定义一个 react function component
rcc
运行结果
rfc
运行结果
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;
}
1.动态显示初始化数据
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.交互(从绑定事件监听开始)
子组件Header
父组件App
子组件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>
)
}
}
1.在父组件中定义一个带有参数的函数
a = (data) => {
// 这个data就是从子组件中传递到父组件的值
console.log(data)
}
2.并将此函数传递给子组件
<Header a={this.a}/>
3.在子组件中通过this.props.a()调用
this.props.a(data)
则在父组件中,就能够接收到子组件在调用a函数时,传递进来的实参。
从而拿到子组件传递到父组件中的值。
实现了从子组件向父组件传值。
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>
);
}
}
Item组件
App组件
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>
);
}
}
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方法)就放在哪里
Item组件
App组件
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>
);
}
}
App组件
Footer组件
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>
);
}
}
Footer组件
App组件
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>
)
}
}
Footer组件
App组件
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>
)
}
}
1.学会拆分组件、实现静态组件,注意:className、style的写法
2. 动态初始化列表,如何确定将数据 放在哪个组件的state中?
-
某个组件使用:放在其自身的state中
-
某些组件使用:放在他们共同的父组件state中(官方称此操作为: 状态提升)
3. 关于父子之间通信
-【父组件】给【子组件】传递数据:
在父组件中,通过给子组件添加标签属性传递;
在子组件中通过 this.props.属性名接收;
-【子组件】给【父组件】传递数据:
在父组件中提前定义一个函数,并且通过给子组件添加标签属性的方式,将此函数传递给子组件;
在子组件中通过this.props.函数名()进行调用此函数;
4. 注意defaultChecked和checked的区别
defaultChecked只执行一次,表示默认加载时的状态
5. 状态在哪里,操作状态的方法就在哪里
1.前置说明
- React本身只关注于界面,并不包含发送ajax请求的代码
- 前端应用需要通过ajax请求与后台进行交互(json数据)
- react应用中需要集成第三方ajax库(或自己封装)
2.常用的ajax请求库
- jQuery: 比较重,如果需要,另外引入,(不建议使用)
- axios: 轻量级,建议使用
封装XmlHttpRequest对象的ajax
promise风格
可以用在浏览器端和node服务器端
1. 安装axios: yarn add axios
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>
)
}
}
server1.js
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:5000/students').then(
(response) => {
console.log('成功了', response.data)
},
(error) => {
console.log('失败了', error)
}
)
}
render() {
return (
<div>
<button onClick={this.getStudentData}>点击获取学生数据</button>
</div>
)
}
}
运行结果
原因是Ajax引擎受到了同源策略的限制
package.json
App组件
运行结果
客户端client收到了响应
服务器server也接收到了请求
缺点
这种方式的配置代理,只会把本地没有的,转发给server,若本地有,则不会转给server。
也就是说,通过这种方式的配置代理,server并不会收到所有的请求。
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>
)
}
}
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" />
<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>
)
}
}
Search组件
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="输入关键词进行搜索"
/>
<button onClick={this.handleSearch}>搜索</button>
</div>
</section>
);
}
}
配置代理
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': ''} //去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置)
})
)
}
keyWordElement表示input节点
<input
ref={(c) => (this.keyWordElement = c)}
type="text"
placeholder="输入关键词进行搜索"
/>
将input节点的value值通过连续结构赋值取出,并重新命名value变量为keyWord
const {
keyWordElement: { value: keyWord },
} = this;
// 本地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);
}
);
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="输入关键词进行搜索"
/>
<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>
);
}
}
(重新实现github搜索案例)
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)消息
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)消息
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="输入关键词进行搜索"
/>
<button onClick={this.handleSearch}>搜索</button>
</div>
</section>
);
}
}
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中取消订阅
SPA的理解
-
单页应用 (single page application) SPA
-
整个应用只有一个完整的html页面
-
应用React 路由技术,可以实现点击页面中的链接不会刷新页面,只会做页面的局部更新。
-
数据都需要通过ajax请求获取,并在前端异步展现。
总结:SPA应用是单页面、多组件的应用。
React路由的理解
应用了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)。
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版本)
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>;
}
}
- 使用react-router-dom的步骤:
第一步:编写路由链接
使用 Link 组件切换url中不同的path
第二步:注册路由
使用 Route 组件使得不同的path对应相应的component
- 注意点:
- Link 和 Route 必须从 'react-router-dom' 库中引入,且必须被相同的一个路由器 BrowserRouter 或者 HashRouter 包裹
import {Link, Route, BrowserRouter} from 'react-router-dom'
- Link组件使用 to 属性来指定要改变的path:
<Link to="/about">About</Link>
Link组件就是一个路由链接,当点击这个路由链接后,url中的path就会变为/about
- Route组件使用 path 和component属性来指定映射关系
<Route path="/about" component={About}></Route>
<Route path="/home" component={Home}></Route>
Route就是一个路由,当url中的path发生变化时,它就会根据path,渲染相应的component
- 路由链接Link 和 路由Route 必须都被包裹在同一个路由器BrowserRouter或者HashRouter中。
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>);
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>;
}
}
总结:
存放位置不同:
一般组件: 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'
- 从'react-router-dom'库中引入NavLink组件
// import { Link } from 'react-router-dom'
// NavLink组件,会自动给当前选中的路由链接,加上 ‘active’ 样式类
import { NavLink } from 'react-router-dom'
- 使用 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;
}
- 组件标签属性,在内部通过this.props.xxx进行接收
// 组件标签的属性a、属性b、属性c 都在内部通过this.props.a、 this.props.b、 this.props.c 进行接收
<Acomponent a={a} b={b} c={c}>哈哈</Acomponent>
// 组件标签的标签体内容“哈哈”,在组件内部如何接收?
- 标签体内容,是一种特殊的标签属性,叫children
也就是说,标签体内容,在组件内部通过this.props.children 进行接收
- 举例,对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}/>
// 把所有的属性批量传递
}
}
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>
)
}
}
- 演示问题:我们把所有的路由前都加上 atguigu前缀,路由的path都变成了二级路由
- 默认情况下
- 点击路由链接,path变化,加载相应组件。
- 当path为二级路由且遇上刷新
- 分析原因,仔细查看请求
样式文件本身就一直存放在public根路径下
引入样式的方式是相对路径:href="./bootstrap.css"
表示从当前目录出发 ,去找bootstrap.css
根本原因
上述问题存在的根本原因是因为我们在index.html文件中引入bootstrap.css文件时,使用的是相对路径的原因造成的。
所以在请求样式时,会从当前路径(/atguigu)出发,去寻找样式文件。
那么,当你在/atguigu/about路径且刷新时,就会请求成了 localhost:3000/atguigu/bootstrap.css。
导致找不到样式文件,直接返回给你了 index.html,造成样式丢失。
- 解决方式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>
- 解决方式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>
- 解决方式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>);
路由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属性能不用就不用,因为会引发严重的问题。
用于当注册路由时,所有路由规则都没有匹配上时, 用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>
)
}
}
实现效果:
嵌套路由:指的是一个组件中包含二级组件,那么二级组件的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>
</li>
<li>
<a href="/message2">message002</a>
</li>
<li>
<a href="/message/3">message003</a>
</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>
)
}
}
3步: 传递(写路由链接时)---声明(注册路由时)---接收(在路由组件内)
- 传递
- 声明
- 接收
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>
);
}
}
-
路由链接(携带参数):
<Link to="/demo/test/tom/18">详情</Link>
-
注册路由(声明接收):
<Route path="/demo/test/:name/:age" component={Test} />
-
路由组件(接收参数):
const {name, age} = this.props.match.params
- 传递
注册路由时,无需声明
- 接收并解析
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>
);
}
}
-
路由链接(携带参数):
<Link to="/demo/test?name=tom&age=18">详情</Link>
-
注册路由(无需声明,正常注册即可):
<Route path="/demo/test" component={Test} />
-
路由组件(接收参数): this.props.location.search
备注: 获取到的search是urlencoded编码字符串,需要借助qs库解析
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>
);
}
}
- 路由链接(携带参数):
<Link to={{
pathname: `/demo/test`,
state: {
name: tom, // 参数1 --name
age: 18// 参数2 --age
}
}}>详情</Link>
-
注册路由(无需声明,正常注册即可):
<Route path="/demo/test" component={Test} />
-
路由组件(接收参数): this.props.location.state
备注: 刷新时,this.props.location.state也会保留的
路由默认为push 模式:压栈
replace模式:替换掉栈顶
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>
);
}
}
本节主要讲通过 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>
);
}
}
借助 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进行加工。
/**
* 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可以用于解决一些路径错误相关的问题。(比如样式丢失)
*/
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:
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
config-overrides.js:
const { override, fixBabelImports } = require('customize-cra')
module.exports = override(
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: 'css',
}),
);
package.json:
App.js:
// import 'antd/dist/reset.css';
// 按照官网配置好之后,就不再需要 import 'antd/dist/reset.css'; 代码了
// 就已经实现了引入组件后自动引入该组件的样式,也就是按需引入样式
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'
},
}
}
}),
)
# redux 是什么?
1. redux 是一个专门用于做 **状态管理**的JS库(不是react插件库)
2. 它可以用在 react, angular, vue 等项目中,但基本与react 配合使用
3. 作用: 集中式管理react应用中多个组件**共享**的状态。
# 什么情况下需要使用redux?
1. 某个组件的状态,需要让其他组件可以随时拿到(共享)
2. 一个组件需要改变另一个组件的状态(通信)
3. 总体原则:能不用就不用,如果不用比较吃力才考虑使用。
Action Creators的作用:
就是为了创建一个action。相当于服务员(只记下客人的需求)。
action 其实就是一个普通的对象 {}。
它有两个重要的属性: type 和 data。 type是一个字符串,表示要做的事情;data表示要操作的数据。
Store 会直接将拿到的action对象,转发给Reducers。
Store就相当于老板(直接把菜单交给厨师),Reducers 就是厨师。
Reducers会根据Store给的 previousState和action,对数据进行加工,然后返回新的数据给Store。
Store拿到新的数据后,组件就可以通过 getState() 这个API来拿到新的数据。
Reducers 不仅可以加工数据,还可以初始化数据。
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>
<button onClick={this.handleIncrement}>+</button>
<button onClick={this.handleDecrement}>-</button>
<button onClick={this.handleIncrementOdd}>当和为奇数时才加</button>
<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>
)
}
}
yarn add redux
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>
<button onClick={this.handleIncrement}>+</button>
<button onClick={this.handleDecrement}>-</button>
<button onClick={this.handleIncrementOdd}>当和为奇数时才加</button>
<button onClick={this.handleIncrementAsync}>异步加</button>
</div>
);
}
}
本次主要新增了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>
<button onClick={this.handleIncrement}>+</button>
<button onClick={this.handleDecrement}>-</button>
<button onClick={this.handleIncrementOdd}>当和为奇数时才加</button>
<button onClick={this.handleIncrementAsync}>异步加</button>
</div>
);
}
}
这样就实现了在组件中dispatch时,无需要手动写{},而是通过调用count_action.js中的函数,通过函数的返回值拿到一个对象去派发给store。
方便在同一个文件中管理所有的action对象。
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>
<button onClick={this.handleIncrement}>+</button>
<button onClick={this.handleDecrement}>-</button>
<button onClick={this.handleIncrementOdd}>当和为奇数时才加</button>
<button onClick={this.handleIncrementAsync}>异步加</button>
</div>
);
}
}
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>
<button onClick={this.handleIncrement}>+</button>
<button onClick={this.handleDecrement}>-</button>
<button onClick={this.handleIncrementOdd}>当和为奇数时才加</button>
<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
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)
}
}
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>
<button onClick={this.handleIncrement}>+</button>
<button onClick={this.handleDecrement}>-</button>
<button onClick={this.handleIncrementOdd}>当和为奇数时才加</button>
<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>
)
}
}
从代码关系上,并不能看出容器组件与UI组件是父子关系。
但在浏览器调试中可以看出他们的父子关系。
且是通过 react-redux中的connect进行连接的父子组件。
先理一下:
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>
<button onClick={this.handleIncrement}>+</button>
<button onClick={this.handleDecrement}>-</button>
<button onClick={this.handleIncrementOdd}>当和为奇数时才加</button>
<button onClick={this.handleIncrementAsync}>异步加</button>
</div>
);
}
}
容器组件 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;
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>
<button onClick={this.handleIncrement}>+</button>
<button onClick={this.handleDecrement}>-</button>
<button onClick={this.handleIncrementOdd}>当和为奇数时才加</button>
<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;
容器组件 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;
容器组件 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>
<button onClick={this.handleIncrement}>+</button>
<button onClick={this.handleDecrement}>-</button>
<button onClick={this.handleIncrementOdd}>当和为奇数时才加</button>
<button onClick={this.handleIncrementAsync}>异步加</button>
</div>
);
}
}
-
明确两个概念:
-
UI组件:不能使用任何redux的api,只负责页面的呈现,交互等
-
容器组件:负责和redux通信,并将结果转交给UI组件
-
-
如何创建一个容器组件----靠react-redux的connect函数
connect(mapStateToProps, mapDispatchToProps)(UI组件)
-
mapStateToProps: 映射状态,返回值是一个对象
-
mapDispatchToProps: 映射操作状态的方法,返回值是一个对象
mapStateToProps和mapDispatchToProps都是函数
-
-
备注1: 容器组件中的store是靠props传递进去的,而不是在容器组件中直接引入
-
备注2:mapDispatchToProps还可以是一个对象
容器组件 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;
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
*/
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>);
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>
<button onClick={this.handleIncrement}>+</button>
<button onClick={this.handleDecrement}>-</button>
<button onClick={this.handleIncrementOdd}>当和为奇数时才加</button>
<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组件直接定义在容器组件中,直接使用
最合适。
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);
使用了 react-redux之后,
-
容器组件和UI组件整合成一个文件(放在pages目录下)
-
无需自己给容器组件传递store,给包裹一个
<Provider store={store}></Provider>
即可。组件从 react-redux中引入
-
无需要自己监测redux中状态改变,容器组件可以自动完成这个工作
-
mapDispatchToProps也可以简单的写成一个对象
-
一个组件要和redux“打交道”要经过哪几步?
-
定义好UI组件 ----不暴露
-
引入connect生成一个容器组件,并暴露,写法如下:
connect(
state => ({key:value}), // 映射状态
{
key: xxxxAction // 映射操作状态的方法
}
)(UI组件)
- 在UI组件中通过this.props.xxxx读取和操作状态
-
在redux目录下新建两个文件目录: actions和reducers,分别用于存放不同组件对应的action和reducer
-
由于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>
<button onClick={this.handleIncrement}>+</button>
<button onClick={this.handleDecrement}>-</button>
<button onClick={this.handleIncrementOdd}>当和为奇数时才加</button>
<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;
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函数必须是一个纯函数
*/
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
- 项目打包:
npm run build
会在根目录下,生成 build目录:
- 在本地运行:
安装一个包: npm i serve -g
(必须要全局安装)
然后执行:serve build
(表示从build目录下开启一个服务)
则会瞬间开启一个服务:
目录结构:
效果:
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>;
}
}
根路径:
点击About组件:
在请求对应的路由组件时,当网络很慢,会先加载react中的Suspense组件的fallback属性指定的内容
最后再显示对应的路由组件:
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;
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;
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;
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;
-
createRef会在组件每次渲染的时候重新创建
-
useRef只会在组件首次渲染时创建
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;
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>;
}
}
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;
React Router 以三个不同的包发布到npm上,它们分别为:
-
react-router: 路由的核心库,提供了很多的:组件、钩子
-
react-router-dom: 包含react-router所有的内容,并添加一些专门用于DOM的组件,例如
<BrowserRouter>
等 -
react-router-native:包括react-router所有内容,并添加一些专门用于ReactNative的api,例如
<NativeRouter>
等
与 React Router 5.x 版本相比,改变了什么?
-
内置组件的变化: 移除
<Switch/>
,新增<Routes/>
等 -
语法的变化:
component={About}
变为element={<About/>}
等 -
新增多个hook:
useParams
、useNavigate
、useMatch
等 -
官方明确推荐函数式组件了!!!
...
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>;
}
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;
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;
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;
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;
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>
</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>
);
}
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>
</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
点击修改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. 传递state参数: 直接给Link添加state属性,值是一个对象*/}
<Link
to="detail"
state={{
id: m.id,
title: m.title,
content: m.content,
}}
>
{m.title}
</Link>
</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
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>
<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>
);
}