一、React脚手架
1、创建并启动
第一步,全局安装:npm install -g create-react-app
第二步,切换到想创项目的目录,使用命令:create-react-app 项目名称
或 create-react-app 项目名称 --template typescript
第三步,进入项目文件夹:cd hello-react
第四步,启动项目:npm start
相关命令
如果使用yarn
,只需把npm替换为yarn
即可
启动
npm start
打包为静态文件
npm run build
以测试方式运行(基本不用)
npm test
默认关于webpack的配置文件时隐藏的,可通过以下命令可视化,不过之后无法再次隐藏
npm run eject
执行第二步报错
PS G:\zz_learn_front\1_react_demo> create-react-app react_demo
create-react-app : 无法加载文件 G:\codesoftware\install\node\node_global\create-react-app.ps1,因为在此系统上禁止运行脚本。有关详细信息,请参阅 https
:/go.microsoft.com/fwlink/?LinkID=135170 中的 about_Execution_Policies。
所在位置 行:1 字符: 1
+ create-react-app react_demo
+ ~~~~~~~~~~~~~~~~
+ CategoryInfo : SecurityError: (:) [],PSSecurityException
+ FullyQualifiedErrorId : UnauthorizedAccess
解决方法:
window左下角搜索 Windos PowerShell
,并以管理员身份运行,依次执行以下命令
set-ExecutionPolicy RemoteSigned
输入A,回车,之后查看当前状态
get-ExecutionPolicy
项目部署
全局安装serve
npm i serve -g
运行
serve -s build
2、脚手架文件结构
public ---- 静态资源文件夹
favicon.icon ------ 网站页签图标
index.html -------- 主页面 (重要)
logo192.png ------- logo图
logo512.png ------- logo图
manifest.json ----- 应用加壳的配置文件
robots.txt -------- 爬虫协议文件
src ---- 源码文件夹
App.css -------- App组件的样式
App.js --------- App组件 (重要)
App.test.js ---- 用于给App做测试
index.css ------ 样式
index.js ------- 入口文件 (重要)
logo.svg ------- logo图
reportWebVitals.js
--- 页面性能分析文件(需要web-vitals库的支持)
setupTests.js
---- 组件单元测试的文件(需要jest-dom库的支持)
3、index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- %PUBLIC_URL% : 代表public文件夹路径 -->
<!-- 网站图标 -->
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<!-- 开启理想视口,用于做移动端网页的适配 -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- 用于配置浏览器页签+地址栏的颜色(仅支持安卓手机浏览器) -->
<meta name="theme-color" content="#000000" />
<!-- 网站描述信息,用于百度搜索 -->
<meta
name="description"
content="Web site created using create-react-app"
/>
<!-- 网站被添加到手机主屏幕的图标,只用于苹果手机 -->
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!-- 应用加壳时的配置文件 -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
</head>
<body>
<!-- 当浏览器不支持js时展示 noscript 的内容 -->
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
4、index.js
入口文件
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
// 用于性能测试
import reportWebVitals from './reportWebVitals';
//渲染虚拟DOM到页面
ReactDOM.render(
/* <React.StrictMode> 用于检查 App及其子组件的代码是否合理 */
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// 用于性能测试
reportWebVitals();
5、组件引入
- 为了区分组件和普通js文件,可以把定义组件的js文件后缀改成jsx
- 一个组件一个文件夹
- 引入js文件或者jsx文件时,可以不写后缀名
- 组件文件夹中的文件可以都命名为index,例如index.jsx/index.css,引入的时候可以直接引到目录名就行了
// 引入 react
import React,{Component} from 'react'
import './index.css'
export default class Welcome extends Component{
render(){
return <h2 className="title">Welcome</h2>
}
}
6、css模块化
当多个组件引入各自的css样式后,挂载到app组件中时,可能多个组件加载的css具有重名样式,此时后引入的会覆盖之前引入的。
可将css文件名加上module,模块化
index.module.css
.title{
background-color: orange;
}
index.jsx
import React,{Component} from 'react'
import hello from './index.module.css'
export default class Hello extends Component{
render(){
return <h2 className={hello.title}>Hello,React!</h2>
}
}
7、快速创建组件
webstorm可以使用以下快捷命令创建组件基本结构
rcc
:简单的类式组件
rcfc
:具有所有生命周期的类式组件
rsf
:简单的函数式组件
8、组件化编程流程
1. 拆分组件: 拆分界面,抽取组件
2. 实现静态组件: 使用组件实现静态页面效果
3. 实现动态组件
3.1 动态显示初始化数据
3.1.1 数据类型
3.1.2 数据名称
3.1.2 保存在哪个组件?
3.2 交互(从绑定事件监听开始)
二、案例
1、各模块思路
整体
分为五个组件:
- App组件:外层容器组件
- Header组件:输入框
- Item组件:单个可选单元
- List组件:包含多个Item组件
- Footer组件:底部
输入名称回车添加列表项
在Header组件监听输入键位,当监听到Enter
键时,将输入框中的内容传到父组件(子组件通过调用父组件定义的方法实现数据传递)中,父组件设置新的状态值,实现新增列表项
获取选中的列表项,并设置done属性
在Item组件中监听复选框的选中状态是否变化,如果变化,则通过App父组件定义的方法实现数据传递,将状态变化的列表项的id和变化后的选中状态传递到父组件App中,并在App中设置done属性值
2、App.js
import React, {Component} from 'react';
import './App.css';
import Header from './components/Header'
import Footer from './components/Footer'
import List from './components/List'
class App extends Component {
// 初始化状态
state = {
todos: [
{id: 1,name: "吃饭",done: true},
{id: 2,name: "睡觉",done: true},
{id: 3,name: "敲代码",done: false},
{id: 4,name: "看书",done: false}
]
}
//用于子组件传递数据给父组件,原理是父组件通过 props 向子组件传递一个函数,子组件通过调用这个函数将数据作为返回值传递到父组件
addTodo = (todoObj)=>{
console.log("接收到子组件传递的数据",todoObj)
// 获取原来的todos数组
const {todos} = this.state
//追加一个todo,新todo数组最前面是子组件传递的todo对象,后面是原本的todo对象
const newTodos = [todoObj,...todos];
//更新状态
this.setState({todos:newTodos})
}
//勾选item
selectItem = (id,isDone)=>{
const {todos} = this.state;
// 为选中的todo的done设置值
const newTodos = todos.map((todoObj)=>{
if(todoObj.id === id){
return {...todoObj,done:isDone}
}else{
return todoObj
}
})
this.setState({todos: newTodos});
}
//删除列表项
delTodo = (id)=>{
console.log(id)
const {todos} = this.state;
// 删除指定id的todo
const newTodos = todos.filter((todoObj)=>{
return todoObj.id !== id;
})
this.setState({todos: newTodos});
}
//checkAllTodo用于全选
checkAllTodo = (done)=>{
//获取原来的todos
const {todos} = this.state
//加工数据
const newTodos = todos.map((todoObj)=>{
return {...todoObj,done}
})
//更新状态
this.setState({todos:newTodos})
}
//清楚已完成任务
handleClearAllDone = ()=>{
const {todos} = this.state;
const newTodos = todos.filter((todo)=>{
return !todo.done;
})
this.setState({todos: newTodos})
}
render() {
const {todos} = this.state;
return (
<div className="todo-container">
<div className="todo-wrap">
<Header addTodo={this.addTodo}/>
<List todos={todos} selectItem={this.selectItem} delTodo={this.delTodo}/>
<Footer todos={todos} checkAllTodo={this.checkAllTodo} handleClearAllDone={this.handleClearAllDone}/>
</div>
</div>
);
}
}
export default App;
3、Header.jsx
import React, {Component} from 'react';
// 通过 npm i nanoid 安装,可通过调用 nanoid() 生成 uuid
import {nanoid} from "nanoid";
// 通过 npm install --save prop-types 安装,用于限制属性类型
import PropTypes from "prop-types";
class Index extends Component {
// 对接收的 props 进行:类型和必要性的限制
static propTypes = {
//函数类型,必须有
addTodo: PropTypes.func.isRequired
}
change = (event)=>{
const {key,target} = event;
// 判断是否按下回车
if (key !== "Enter"){
return;
}
console.log(target.value);
// 封装todo对象
const todoObj = {id: nanoid(),name: target.value,done: false}
//将todo对象返回到父组件
this.props.addTodo(todoObj);
//清空输入
target.value = '';
}
render() {
return (
<div className="todo-header">
<input onKeyUp={this.change} type="text" placeholder="请输入你的任务名称,按回车键确认"/>
</div>
);
}
}
export default Index;
4、Item.jsx
import React, {Component} from 'react';
class Index extends Component {
state = {
showDel: false, // 标志鼠标是否移入
}
// 判断鼠标是否移入,如果移入则当前行展示删除按钮,并且背景变灰
handleMouse = (b)=>{
return ()=>{
this.setState({showDel:b})
}
}
//复选框回调
changeDone = (event)=>{
console.log(this.props.todo.id,event.target.checked)
this.props.selectItem(this.props.todo.id,event.target.checked)
}
//删除列表项
delTodo = ()=>{
return ()=>{
if (window.confirm("确定删除吗?")){
this.props.delTodo(this.props.todo.id)
}
}
}
render() {
const {todo} = this.props;
const {showDel} = this.state;
return (
<li onMouseEnter={this.handleMouse(true)}
onMouseLeave={this.handleMouse(false)}
style={{backgroundColor: showDel ? '#ddd': 'white'}}>
<label>
<input type="checkbox" checked={todo.done}
onChange={this.changeDone}/>
<span>{todo.name}</span>
</label>
<button onClick={this.delTodo()}
className="btn btn-danger"
style={{display: showDel?'':'none'}}>删除</button>
</li>
);
}
}
export default Index;
5、List.jsx
import React, {Component} from 'react';
import Item from '../Item'
class Index extends Component {
render() {
const {todos,selectItem,delTodo} = this.props;
return (
<ul className="todo-main">
{
// map函数,用于遍历数组
todos.map((todo)=>{
return <Item todo={todo} key={todo.id} selectItem={selectItem} delTodo={delTodo}/>
})
}
</ul>
);
}
}
export default Index;
6、Footer.jsx
import React, {Component} from 'react';
class Index extends Component {
//全选checkbox的回调
handleCheckAll = (event)=>{
this.props.checkAllTodo(event.target.checked)
}
//清除已完成任务的回调
handleClearAllDone = ()=>{
return ()=>{
this.props.handleClearAllDone()
}
}
render() {
const {todos,handleClearAllDone} = this.props
//已完成的个数
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"
onChange={this.handleCheckAll}
checked={doneCount === total && total !== 0 ? true : false}/>
</label>
<span>
<span>已完成{doneCount}</span> / 全部{total}
</span>
<button className="btn btn-danger"
onClick={handleClearAllDone}>清除已完成任务</button>
</div>
);
}
}
export default Index;
7、总结
1、拆分组件、实现静态组件,注意:className、style的写法
2、动态初始化列表,如何确定将数据放在哪个组件的state中?
- 某个组件使用:放在其 自身 的state中
- 某些组件使用:放在他们共同的 父组件 state中(官方称此操作为:状态提升)
3、关于父子之间通信:
【父组件】给【子组件】传递数据:通过props传递
【子组件】给【父组件】传递数据:通过props传递,要求父提前给子传递一个函数
4、注意defaultChecked (只在第一次指定的时候有作用,之后就没作用了)和 checked的区别,类似的还有:defaultValue 和 value
5、状态在哪里,操作状态的方法就在哪里
三、配置代理
用于解决跨域问题,原理是在前端的端口上在起一个服务器,接收前端请求,并转发到后端,从而解决跨域
方法一
在package.json中追加如下配置
"proxy":"http://localhost:5000"
说明:
- 优点:配置简单,前端请求资源时可以不加任何前缀。
- 缺点:不能配置多个代理。
- 工作方式:上述方式配置代理,当请求了3000不存在的资源时,那么该请求会转发给5000 (优先匹配前端资源)
方法二
第一步:创建代理配置文件(文件名固定)
在src下创建配置文件:src/setupProxy.js
编写setupProxy.js配置具体代理规则:
const proxy = require('http-proxy-middleware') module.exports = function(app) { app.use( proxy('/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('/api2', { target: 'http://localhost:5001', changeOrigin: true, pathRewrite: {'^/api2': ''} }) ) }
说明:
- 优点:可以配置多个代理,可以灵活的控制请求是否走代理。
- 缺点:配置繁琐,前端请求资源时必须加前缀。
四、网络请求
1、axios方式
采用axios,见vue的文章
2、fetch方式(了解)
fetch: 原生函数,不再使用XmlHttpRequest对象提交ajax请求
老版本浏览器可能不支持
get
fetch(url).then(function(response) {
return response.json()
}).then(function(data) {
console.log(data)
}).catch(function(e) {
console.log(e)
});
post
fetch(url, {
method: "POST",
body: JSON.stringify(data),
}).then(function(data) {
console.log(data)
}).catch(function(e) {
console.log(e)
})
结合 async 和 await
search = async()=>{
//获取用户的输入(连续解构赋值+重命名)
const {keyWordElement:{value:keyWord}} = this
//发送请求前通知List更新状态
PubSub.publish('atguigu',{isFirst:false,isLoading:true})
//发送网络请求---使用fetch发送(未优化)
/* fetch(`/api1/search/users2?q=${keyWord}`).then(
response => {
console.log('联系服务器成功了');
return response.json()
},
error => {
console.log('联系服务器失败了',error);
return new Promise(()=>{})
}
).then(
response => {console.log('获取数据成功了',response);},
error => {console.log('获取数据失败了',error);}
) */
//发送网络请求---使用fetch发送(优化)
try {
const response= await fetch(`/api1/search/users2?q=${keyWord}`)
const data = await response.json()
console.log(data);
PubSub.publish('atguigu',{isLoading:false,users:data.items})
} catch (error) {
console.log('请求出错',error);
PubSub.publish('atguigu',{isLoading:false,err:error.message})
}
}
五、组件之间通信
1、父传子
(1)类式组件
- 你不能改变 props。当你需要交互性时,你可以设置 state。
1、利用
props
即可
父组件:
<List todos={todos}/>
子组件:
const {todos} = this.props;
2、接收标签体内容
标签体内容是特殊的标签属性,子组件通过this.props.children
可以获取标签体内容,同时也可以通过设置该属性动态设置标签体。
(2)函数式组件
1、父组件将 props 传递给子组件
export default function Profile() {
return (
<Avatar
person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }}
size={100}
/>
);
}
如果 person=
后面的双花括号让你感到困惑,请记住,在 JSX 花括号中,它们只是一个对象。
2、子组件中读取 props
你可以通过在 function Avatar 之后直接列出它们的名字 person, size 来读取这些 props。这些 props 在 ({ 和 }) 之间,并由逗号分隔。这样,你可以在 Avatar 的代码中使用它们,就像使用变量一样。
function Avatar(props) {
let person = props.person;
let size = props.size;
// ...
}
结构后,也可写成下面的代码
function Avatar({ person, size }) {
// 在这里 person 和 size 是可访问的
}
(3)指定默认值
如果你想在没有指定值的情况下给 prop 一个默认值,你可以通过在参数后面写 =
和默认值来进行解构:
function Avatar({ person, size = 100 }) {
// ...
}
现在, 如果 <Avatar person={...} />
渲染时没有 size
prop, size
将被赋值为 100
。
默认值仅在缺少 size
prop 或 size={undefined}
时生效。 但是如果你传递了 size={null}
或 size={0}
,默认值将 不 被使用。
(4)展开语法传递 props
有时候,传递 props 会变得非常重复:
function Profile({ person, size, isSepia, thickBorder }) {
return (
<div className="card">
<Avatar
person={person}
size={size}
isSepia={isSepia}
thickBorder={thickBorder}
/>
</div>
);
}
重复代码没有错(它可以更清晰)。但有时你可能会重视简洁。一些组件将它们所有的 props 转发给子组件,正如 Profile
转给 Avatar
那样。因为这些组件不直接使用他们本身的任何 props,所以使用更简洁的“展开”语法是有意义的:
function Profile(props) {
return (
<div className="card">
<Avatar {...props} />
</div>
);
}
这会将 Profile
的所有 props 转发到 Avatar
,而不列出每个名字。
(5)随时间变化
export default function Clock({ color, time }) {
return (
<h1 style={{ color: color }}>
{time}
</h1>
);
}
这个例子说明,一个组件可能会随着时间的推移收到不同的 props。 Props 并不总是静态的!在这里,time
prop 每秒都在变化。当你选择另一种颜色时,color
prop 也改变了。Props 反映了组件在任何时间点的数据,并不仅仅是在开始时。
2、子传父
父组件
//用于子组件传递数据给父组件,原理是父组件通过 props 向子组件传递一个函数,
//子组件通过调用这个函数将数据作为返回值传递到父组件
addTodo = (todoObj)=>{
console.log("接收到子组件传递的数据",todoObj)
// 获取原来的todos数组
const {todos} = this.state
//追加一个todo,新todo数组最前面是子组件传递的todo对象,后面是原本的todo对象
const newTodos = [todoObj,...todos];
//更新状态
this.setState({todos:newTodos})
}
render() {
const {todos} = this.state;
return (
<Header addTodo={this.addTodo}/>
);
}
子组件:(通过将数据作为参数调用父组件的方法实现传递)
this.props.addTodo(todoObj);
3、发布订阅
1、工具库: PubSubJS
2、下载: npm install pubsub-js --save
3、可用户任意组件之间的通信,父子、兄弟、祖孙等。
4、使用:
import PubSub from 'pubsub-js' //引入
var token = PubSub.subscribe('topic', function(topic,data){ }); //订阅
PubSub.unsubscribe(token); //取消订阅
PubSub.publish('topic', data) //发布消息
(1)发布
import React, { Component } from 'react'
import PubSub from 'pubsub-js'
import axios from 'axios'
export default class Search extends Component {
search = ()=>{
//获取用户的输入(连续解构赋值+重命名)
const {keyWordElement:{value:keyWord}} = this
//发送请求前通知List更新状态
PubSub.publish('atguigu',{isFirst:false,isLoading:true})
//发送网络请求
axios.get(`/api1/search/users?q=${keyWord}`).then(
response => {
//请求成功后通知List更新状态
PubSub.publish('topic',{isLoading:false,users:response.data.items})
},
error => {
//请求失败后通知App更新状态
PubSub.publish('atguigu',{isLoading:false,err:error.message})
}
)
}
render() {
return (
<section className="jumbotron">
<h3 className="jumbotron-heading">搜索github用户</h3>
<div>
<input ref={c => this.keyWordElement = c} type="text" placeholder="输入关键词点击搜索"/>
<button onClick={this.search}>搜索</button>
</div>
</section>
)
}
}
(2)订阅
import React, { Component } from 'react'
import PubSub from 'pubsub-js'
import './index.css'
export default class List extends Component {
state = { //初始化状态
users:[], //users初始值为数组
isFirst:true, //是否为第一次打开页面
isLoading:false,//标识是否处于加载中
err:'',//存储请求相关的错误信息
}
componentDidMount(){
// 订阅主题为'topic'的消息
this.token = PubSub.subscribe('topic',(topic,stateObj)=>{
this.setState(stateObj)
})
}
componentWillUnmount(){
//取消订阅
PubSub.unsubscribe(this.token)
}
render() {
const {users,isFirst,isLoading,err} = this.state
return (
<div className="row">
{
isFirst ? <h2>欢迎使用,输入关键字,随后点击搜索</h2> :
isLoading ? <h2>Loading......</h2> :
err ? <h2 style={{color:'red'}}>{err}</h2> :
users.map((userObj)=>{
return (
<div key={userObj.id} className="card">
<a rel="noreferrer" href={userObj.html_url} target="_blank">
<img alt="head_portrait" src={userObj.avatar_url} style={{width:'100px'}}/>
</a>
<p className="card-text">{userObj.login}</p>
</div>
)
})
}
</div>
)
}
}
4、组件通信总结
组件间的关系:
- 父子组件
- 兄弟组件(非嵌套组件)
- 祖孙组件(跨级组件)
几种通信方式:
1.props:
(1).children props
(2).render props
2.消息订阅-发布:
pubs-sub、event等等
3.集中式管理:
redux、dva等等
4.conText:
生产者-消费者模式
比较好的搭配方式:
父子组件:props
兄弟组件:消息订阅-发布、集中式管理
祖孙组件(跨级组件):消息订阅-发布、集中式管理、conText(开发用的少,封装插件用的多)
六、路由
新版https://juejin.cn/post/7234058088930934842
1、react-router-dom
下载react-router-dom: npm install react-router-dom
基本使用
1、明确好界面中的导航区、展示区
2、导航区的a标签改为Link标签(可在Link中嵌套图片等,实现点击图片跳转)<Link to="/xxxxx">Demo</Link>
3、展示区写Route标签进行路径的匹配<Route path='/xxxx' component={Demo}/>
4、<App>
的最外侧包裹了一个<BrowserRouter>
或<HashRouter>
相关 API
1. 内置组件
<BrowserRouter>
<HashRouter>
<Route>
<Redirect>
<Link>
<NavLink>
<Switch>
2. 其它
history对象
match对象
withRouter函数
2、基本使用
index.js
//引入react核心库
import React from 'react'
//引入ReactDOM
import ReactDOM from 'react-dom'
//
import {BrowserRouter} from 'react-router-dom'
//引入App
import App from './App'
ReactDOM.render(
<BrowserRouter>
<App/>
</BrowserRouter>,
document.getElementById('root')
)
app.jsx
import React, { Component } from 'react'
import {Link,Route} from 'react-router-dom'
import Home from './components/Home'
import About from './components/About'
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>跳转不同的页面 */}
{/* <a className="list-group-item" href="./about.html">About</a>
<a className="list-group-item active" href="./home.html">Home</a> */}
{/* 在React中靠路由链接实现切换组件--编写路由链接 */}
<Link className="list-group-item" to="/about">About</Link>
<Link className="list-group-item" to="/home">Home</Link>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
{/* 注册路由,根据情况渲染 About和Home组件 */}
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
</div>
</div>
</div>
</div>
</div>
)
}
}
3、路由组件和一般组件
1.写法不同:
一般组件:<Demo/>
路由组件:<Route path="/demo" component={Demo}/>
2.存放位置不同:
一般组件:components
路由组件:pages
3.接收到的props不同:
一般组件:写组件标签时传递了什么,就能收到什么
路由组件:接收到三个固定的属性
history:
go: ƒ go(n)
goBack: ƒ goBack()
goForward: ƒ goForward()
push: ƒ push(path, state)
replace: ƒ replace(path, state)
location:
pathname: "/about"
search: ""
state: undefined
match:
params: {}
path: "/about"
url: "/about"
4、NavLink使用与封装
NavLink可以实现路由链接的高亮(用于替换Link标签),通过activeClassName
属性指定样式名,默认是"active"
<NavLink activeClassName="demo" className="list-group-item" to="/home">Home</NavLink>
封装 NavLink
import React, { Component } from 'react'
import {NavLink} from 'react-router-dom'
export default class MyNavLink extends Component {
render() {
// console.log(this.props);
return (
<NavLik activeClassName="demo" className="list-group-item" {...this.props} />
)
}
}
标签体内容是特殊的标签属性通过this.props.children
可以获取标签体内容
使用封装的组件
<MyNavLink to="/about">About</MyNavLink>
<MyNavLink to="/home">Home</MyNavLink>
5、Switch标签
- 通常情况下,path和component是一一对应的关系。
- Switch可以提高路由匹配效率(单一匹配)。这样只要匹配到了第一个就不会再往下匹配了
<Switch>
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
<Route path="/home" component={Test}/>
</Switch>
6、多级路径刷新时样式丢失
- public/index.html 中 引入样式时不写
./
写/
(常用)【绝对路径】 - public/index.html 中 引入样式时不写
./
写%PUBLIC_URL%
(常用) - 路由标签使用
HashRouter
,替代BrowserRouter
(用得少)
7、严格匹配与模糊匹配
- 默认使用的是模糊匹配(简单记:【输入的路径】必须包含要【匹配的路径】,且顺序要一致)
- 开启严格匹配:
<Route exact={true} path="/about" component={About}/>
- 严格匹配不要随便开启,需要再开,有些时候开启会导致无法继续匹配二级路由
8、Redirect【重定向】
- 一般写在所有路由注册的最下方,当所有路由都无法匹配时,跳转到Redirect指定的路由
- 具体编码:
<Switch>
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
<Redirect to="/about"/>
</Switch>
9、嵌套路由
1、注册子路由时要写上父路由的path值
<NavLink className="list-group-item" to="/home/news">News</NavLink>
<Route path='/home/news' component={News} />
2、路由的匹配是按照注册路由的顺序进行的
3、不可以开启严格模式
Home 组件
Home组件内部还有嵌套一层路由
import React, { Component } from 'react'
import { Route, NavLink,Redirect,Switch } from 'react-router-dom'
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>
<NavLink className="list-group-item" to="/home/news">News</NavLink>
</li>
<li>
<NavLink className="list-group-item" to="/home/message">Message</NavLink>
</li>
</ul>
<Switch>
<Route path='/home/news' component={News} />
<Route path='/home/message' component={Message} />
<Redirect to='/home/news' />
</Switch>
</div>
</div>
)
}
}
10、向路由组件传参
(1)params参数方式
1、路由链接(携带参数):<Link to='/demo/test/tom/18'}>详情</Link>
2、注册路由(声明接收):<Route path="/demo/test/:name/:age" component={Test}/>
3、接收参数(params是一个对象):this.props.match.params
父组件传递参数
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}>
{/* 向路由组件传递params参数 */}
<Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link>
</li>
)
})
}
</ul>
<hr />
{/* 声明接收params参数 */}
<Route path="/home/message/detail/:id/:title" component={Detail}/>
</div>
)
}
}
子组件接收参数
// 接收params参数
const {id, title} = this.props.match.params
(2)search参数方式
1、路由链接(携带参数):
<Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>
{msgObj.title}
</Link>
2、注册路由(无需声明,正常注册即可):<Route path="/demo/test" component={Test}/>
3、接收参数:this.props.location.search
接收到的search为字符串 ?name=tom&age=18'
,需自己处理
4、备注:获取到的search是urlencoded
编码字符串,需要借助querystring
解析
querystring 使用
import qs from 'querystring' //react自带,无需下载
// 对象转urlencoded
let obj = {name:'tom', age:18}
console.log(qs.stringify(obj)) // name=tom&age=18
// urlencoded转对象
let str = 'carName=Benz&price=199'
console.log(qs.parse(str)) // {carName: 'Benz', price: 199}
子组件接收参数,并解析
// 接收search参数
const {search} = this.props.location
const {id,title} = qs.parse(search.slice(1))
(3)state参数方式
1、路由链接(携带参数)(to属性为对象):
<Link to={{pathname:'/home/message/detail',state:{id:obj.id,title:obj.title}}}>
{obj.title}
</Link>
2、注册路由(无需声明,正常注册即可):<Route path="/demo/test" component={Test}/>
3、接收参数:this.props.location.state
4、备注:不会在地址栏中展示参数数据,刷新也可以保留住参数【history对象记录着】,但是清除浏览器缓存会导致参数丢失,需要注意防止出现 undefind
导致报错
子组件接收参数
const {id,title} = this.props.location.state || {}
const findResult = DetailData.find((detailObj)=>{
return detailObj.id === id
}) || {}
(4)总结
params
用的比较多
state
用于携带比较敏感的信息
传递参数
export default class Message extends Component {
render() {
const {messageArr} = this.state
return (
<div>
<ul>
{
messageArr.map((msgObj)=>{
return (
<li key={msgObj.id}>
{/* 向路由组件传递params参数 */}
{/* <Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link> */}
{/* 向路由组件传递search参数 */}
{/* <Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link> */}
{/* 向路由组件传递state参数 */}
<Link to={{pathname:'/home/message/detail',state:{id:msgObj.id,title:msgObj.title}}}>{msgObj.title}</Link>
</li>
)
})
}
</ul>
<hr/>
{/* 声明接收params参数 */}
{/* <Route path="/home/message/detail/:id/:title" component={Detail}/> */}
{/* search参数无需声明接收,正常注册路由即可 */}
{/* <Route path="/home/message/detail" component={Detail}/> */}
{/* state参数无需声明接收,正常注册路由即可 */}
<Route path="/home/message/detail" component={Detail}/>
</div>
)
}
}
接收参数
import React, { Component } from 'react'
// import qs from 'querystring'
export default class Detail extends Component {
render() {
console.log(this.props);
// 接收params参数
// const {id,title} = this.props.match.params
// 接收search参数
// const {search} = this.props.location
// const {id,title} = qs.parse(search.slice(1))
// 接收state参数
const {id,title} = this.props.location.state || {}
const findResult = DetailData.find((detailObj)=>{
return detailObj.id === id
}) || {}
return (
<ul>
<li>ID:{id}</li>
<li>TITLE:{title}</li>
<li>CONTENT:{findResult.content}</li>
</ul>
)
}
}
11、路由跳转模式
(1)push模式
默认的模式,无需配置
采用压栈的方式,记录每个页面的跳转,可返回上一个页面,即出栈,上一个页面可以是同级路由的页面
(2)replace模式
同样是采用压栈的方式,区别是replace模式当跳转到同级页面时,是采用替换栈顶的方式,所以只能返回上一级路由的页面,不可返回到同级路由页面
只需在NavLink
,新增属性replace
,即可开启该模式
<NavLink replace to="/home/news" className="list-group-item"/>
12、编程式路由跳转
借助this.prosp.history对象上的API对操作路由跳转、前进、后退
this.prosp.history.push()
:实现push跳转this.prosp.history.replace()
:实现replace跳转this.prosp.history.goBack()
:回退this.prosp.history.goForward()
:前进this.prosp.history.go()
:跳转到指定页面,参数为数字,正数为前进几个页面,负数为返回
push 跳转
//push跳转+携带params参数
this.props.history.push(`/home/message/detail/${id}/${title}`)
//push跳转+携带search参数
this.props.history.push(`/home/message/detail?id=${id}&title=${title}`)
//push跳转+携带state参数
this.props.history.push(`/home/message/detail`,{id,title})
replace 跳转
//replace跳转+携带params参数
this.props.history.replace(`/home/message/detail/${id}/${title}`)
//replace跳转+携带search参数
this.props.history.replace(`/home/message/detail?id=${id}&title=${title}`)
//replace跳转+携带state参数
this.props.history.replace(`/home/message/detail`,{id,title})
前进和回退
this.props.history.goBack()
this.props.history.goForward()
this.props.history.go(-2)
13、一般组件转路由组件
在一些情况下一般组件中可能需要使用路由组件中的API,此时需要把一般组件转化为路由组件,可通过withRouter
实现
withRouter
可以加工一般组件,让一般组件具备路由组件所特有的APIwithRouter
的返回值是一个新组件
使用步骤
1、引入 import {withRouter} from 'react-router-dom'
2、修改暴露组件代码 export default withRouter(Header)
3、此时一般组件可使用路由组件的API
import React, { Component } from 'react'
import {withRouter} from 'react-router-dom'
class Header extends Component {
back = ()=>{
this.props.history.goBack()
}
forward = ()=>{
this.props.history.goForward()
}
go = ()=>{
this.props.history.go(-2)
}
render() {
console.log('Header组件收到的props是',this.props);
return (
<div className="page-header">
<h2>React Router Demo</h2>
<button onClick={this.back}>回退</button>
<button onClick={this.forward}>前进</button>
<button onClick={this.go}>go</button>
</div>
)
}
}
export default withRouter(Header)
14、BrowserRouter与HashRouter的区别
1.底层原理不一样:
BrowserRouter使用的是H5的history API(与 this.props.history不同,this.props.history 是react对H5的history API的封装),不兼容IE9及以下版本。
HashRouter使用的是URL的哈希值。
2.path表现形式不一样
BrowserRouter的路径中没有#,例如:localhost:3000/demo/test
HashRouter的路径包含#,例如:localhost:3000/#/demo/test
3.刷新后对路由state参数的影响
(1).BrowserRouter没有任何影响,因为state保存在history对象中。
(2).HashRouter刷新后会导致路由state参数的丢失!!!
4.备注:HashRouter可以用于解决一些路径错误相关的问题。
七、Ant Design
蚂蚁金服提供的React第三方ui组件库,中文官网:
https://ant-design.gitee.io/components/overview-cn/
引入方式
npm add antd
1、按需引入
需要对 create-react-app 的默认配置进行自定义,这里我们使用 react-app-rewired (一个对 create-react-app 进行自定义配置的社区解决方案)。
引入 react-app-rewired 并修改 package.json 里的启动配置。由于新的 react-app-rewired@2.x 版本的关系,你还需要安装 customize-cra。
npm add react-app-rewired customize-cra
修改 package.json(将以下三行替换 )
/* package.json */
"scripts": {
/* 删除 */
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
/* 新增 */
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test"
}
然后在项目根目录创建一个 config-overrides.js
用于修改默认配置。
module.exports = function override(config, env) {
// do stuff with the webpack config...
return config;
};
使用 babel-plugin-import
注意:antd 默认支持基于 ES module 的 tree shaking,js 代码部分不使用这个插件也会有按需加载的效果。
babel-plugin-import 是一个用于按需加载组件代码和样式的 babel 插件(原理),现在我们尝试安装它并修改 config-overrides.js
文件。
yarn add babel-plugin-import
config-overrides.js
const { override, fixBabelImports } = require('customize-cra');
module.exports = override(
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: 'css',
}),
);
然后移除前面在 src/App.css
里全量添加的 @import '~antd/dist/antd.css';
样式代码,并且按下面的格式引入模块。
// src/App.js
import React, { Component } from 'react';
- import Button from 'antd/es/button';
+ import { Button } from 'antd';
import './App.css';
class App extends Component {
render() {
return (
<div className="App">
<Button type="primary">Button</Button>
</div>
);
}
}
export default App;
最后重启 yarn start
访问页面,antd 组件的 js 和 css 代码都会按需加载,你在控制台也不会看到这样的警告信息。关于按需加载的原理和其他方式可以阅读这里。
2、国际化
index.js,中引入以下2个import语句,并用<ConfigProvider locale={zhCN}>
包裹 app组件
import zhCN from 'antd/lib/locale/zh_CN';
import { ConfigProvider } from 'antd';
ReactDOM.render(
<ConfigProvider locale={zhCN}>
<App />
</ConfigProvider>,
document.getElementById('root')
);
八、redux
1、简介
文档
中文文档: http://www.redux.org.cn/
Github: https://github.com/reactjs/redux
下载
npm add redux
是什么
- redux是一个专门用于做
状态管理
的JS库(不是react插件库)。 - 可以用在react, angular, vue等项目中, 但基本与react配合使用。
- 作用: 集中式管理react应用中多个组件
共享
的状态。
之前学过vuex,这两个差不多,都是状态管理用的
使用场景
- 某个组件的状态,需要让其他组件可以随时拿到(共享)
- 一个组件需要改变另一个组件的状态(通信)
- 总体原则:能不用就不用, 如果不用比较吃力才考虑使用
工作流程
2、redux的三个核心概念
(1)action
动作的对象
包含 2 个属性
type:标识属性, 值为字符串, 唯一, 必要属性
data:数据属性, 值类型任意, 可选属性
例子:{ type: ‘ADD_STUDENT’,data:{name: ‘tom’,age:18} }
(2)reducer
用于初始化状态、加工状态。
加工时,根据旧的state和action, 产生新的state的纯函数。
(3)store
将state、action、reducer联系在一起的对象
如何得到此对象?
import {createStore} from 'redux'
import reducer from './reducers'
const store = createStore(reducer)
此对象的功能?
getState(): 得到state
dispatch(action): 分发action, 触发reducer调用, 产生新的state
subscribe(listener): 注册监听, 当产生了新的state时, 自动调用
3、API
(1)createStore()
作用:创建包含指定reducer的store对象
(2)store对象
① 作用
redux库最核心的管理对象
它内部维护着:
state
reducer
② 核心方法
getState() //用于获取state
dispatch(action) //执行store中的方法,修改状态
subscribe(listener) //订阅redux中的状态改变
③ 具体编码
store.getState()
store.dispatch({type:'INCREMENT', number})
store.subscribe(render)
(3)applyMiddleware()
作用:应用上基于redux的中间件(插件库)
(4)combineReducers()
作用:合并多个reducer函数
4、求和案例
实现步骤
(1).去除Count组件自身的状态
(2).src下建立:
-redux
-store.js
-count_reducer.js
-count_action.js 专门用于创建action对象
-constant.js 存放常量
(3).store.js:
1).引入redux中的createStore函数,创建一个store
2).createStore调用时要传入一个为其服务的reducer
3).记得暴露store对象
(4).count_reducer.js:
1).reducer的本质是一个函数,接收:preState,action,返回加工后的状态
2).reducer有两个作用:初始化状态,加工状态
3).reducer被第一次调用时,是store自动触发的,
传递的preState是undefined,
传递的action是:{type:'@@REDUX/INIT_a.2.b.4}
(5).在根目录index.js中监测store中状态的改变,一旦发生改变重新渲染App组件
备注:redux只负责管理状态,至于状态的改变驱动着页面的展示,要靠我们自己写。
(6)在自己的组件中调用 store.dispatch({type:'increment',data:value*1}) 更新store中的状态
(1)store.js
/*
该文件专门用于暴露一个store对象,整个应用只有一个store对象
*/
//引入createStore,专门用于创建redux中最为核心的store对象
import {createStore} from 'redux'
//引入为Count组件服务的reducer
import countReducer from './count_reducer'
//暴露store
export default createStore(countReducer)
(2)count_redux.js
/*
1.该文件是用于创建一个为Count组件服务的reducer,reducer的本质就是一个函数
2.reducer函数会接到两个参数,分别为:之前的状态(preState),动作对象(action)
*/
import {INCREMENT,DECREMENT} from './constant'
const initState = 0 //初始化状态
export default function countReducer(preState=initState,action){
// console.log(preState);
//从action对象中获取:type、data
const {type,data} = action
//根据type决定如何加工数据
switch (type) {
case INCREMENT: //如果是加
return preState + data
case DECREMENT: //若果是减
return preState - data
default:
return preState
}
}
(3)count_action.js
/*
该文件专门为Count组件生成action对象
*/
import {INCREMENT,DECREMENT} from './constant'
export const createIncrementAction = data => ({type:INCREMENT,data})
export const createDecrementAction = data => ({type:DECREMENT,data})
(4)constant.js
/*
该模块是用于定义action对象中type类型的常量值,目的只有一个:便于管理的同时防止程序员单词写错
*/
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
(5)index.js
根路径下的 index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import store from './redux/store'
ReactDOM.render(<App/>,document.getElementById('root'))
store.subscribe(()=>{
ReactDOM.render(<App/>,document.getElementById('root'))
})
(6)自定义组件中使用
import React, { Component } from 'react'
//引入store,用于获取redux中保存状态
import store from '../../redux/store'
//引入actionCreator,专门用于创建action对象
import {createIncrementAction,createDecrementAction} from '../../redux/count_action'
export default class Count extends Component {
state = {carName:'奔驰c63'}
//加法
increment = ()=>{
const {value} = this.selectNumber
store.dispatch(createIncrementAction(value*1))
}
//减法
decrement = ()=>{
const {value} = this.selectNumber
store.dispatch(createDecrementAction(value*1))
}
//奇数再加
incrementIfOdd = ()=>{
const {value} = this.selectNumber
const count = store.getState()
if(count % 2 !== 0){
store.dispatch(createIncrementAction(value*1))
}
}
//异步加
incrementAsync = ()=>{
const {value} = this.selectNumber
setTimeout(()=>{
store.dispatch(createIncrementAction(value*1))
},500)
}
render() {
return (
<div>
<h1>当前求和为:{store.getState()}</h1>
<select ref={c => this.selectNumber = c}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<button onClick={this.incrementIfOdd}>当前求和为奇数再加</button>
<button onClick={this.incrementAsync}>异步加</button>
</div>
)
}
}
5、异步Action
(1).明确:延迟的动作不想交给组件自身,想交给action
(2).何时需要异步action:想要对状态进行操作,但是具体的数据靠异步任务返回。
(3).具体编码:
1).yarn add redux-thunk,并配置在store中
2).创建action的函数不再返回一般对象,而是一个函数,该函数中写异步任务。
3).异步任务有结果后,分发一个同步的action去真正操作数据。
(4).备注:异步action不是必须要写的,完全可以自己等待异步任务的结果了再去分发同步action。
(1)store.js
/*
该文件专门用于暴露一个store对象,整个应用只有一个store对象
*/
//引入createStore,专门用于创建redux中最为核心的store对象
import {createStore,applyMiddleware} from 'redux'
//引入为Count组件服务的reducer
import countReducer from './count_reducer'
//引入redux-thunk,用于支持异步action
import thunk from 'redux-thunk'
//暴露store
export default createStore(countReducer,applyMiddleware(thunk))
(2)count_action.js
/*
该文件专门为Count组件生成action对象
*/
import {INCREMENT,DECREMENT} from './constant'
//同步action,就是指action的值为Object类型的一般对象
export const createIncrementAction = data => ({type:INCREMENT,data})
export const createDecrementAction = data => ({type:DECREMENT,data})
//异步action,就是指action的值为函数,异步action中一般都会调用同步action,异步action不是必须要用的。
export const createIncrementAsyncAction = (data,time) => {
return (dispatch)=>{
setTimeout(()=>{
dispatch(createIncrementAction(data))
},time)
}
}
(3)异步调用
在自己的组件中调用即可将异步任务交给redux完成
store.dispatch(createIncrementAsyncAction(value*1,500))
九、react-redux
1、简介
是什么
- 一个React插件库
- 专门用来简化React应用中使用redux
下载安装
npm install react-redux
UI组件
只负责 UI 的呈现,不带有任何业务逻辑
通过props接收数据(一般数据和函数)
不使用任何 Redux 的 API
一般保存在components文件夹下
容器组件
- 负责管理数据和业务逻辑,不负责UI的呈现
- 使用 Redux 的 API
- 一般保存在containers文件夹下
2、相关API
Provider:让所有组件都可以得到state数据
connect:用于包装 UI 组件生成容器组件
mapStateToprops:将外部的数据(即state对象)转换为UI组件的标签属性
mapDispatchToProps:将分发action的函数转换为UI组件的标签属性
3、基本使用
(1).明确两个概念:
1).UI组件:不能使用任何redux的api,只负责页面的呈现、交互等。
2).容器组件:负责和redux通信,将结果交给UI组件。
(2).如何创建一个容器组件————靠react-redux 的 connect函数
connect(mapStateToProps,mapDispatchToProps)(UI组件)
-mapStateToProps:映射状态,返回值是一个对象
-mapDispatchToProps:映射操作状态的方法,返回值是一个对象
(3).备注1:容器组件中的store是靠props传进去的,而不是在容器组件中直接引入
(4).备注2:mapDispatchToProps,也可以是一个对象
(1)下载安装
npm install react-redux
(2)components/Count/index.jsx
UI组件
import React, { Component } from 'react'
export default class Count extends Component {
state = {carName:'奔驰c63'}
//加法
increment = ()=>{
const {value} = this.selectNumber
this.props.jia(value*1)
}
//减法
decrement = ()=>{
const {value} = this.selectNumber
this.props.jian(value*1)
}
//奇数再加
incrementIfOdd = ()=>{
const {value} = this.selectNumber
if(this.props.count % 2 !== 0){
this.props.jia(value*1)
}
}
//异步加
incrementAsync = ()=>{
const {value} = this.selectNumber
this.props.jiaAsync(value*1,500)
}
render() {
//console.log('UI组件接收到的props是',this.props);
return (
<div>
<h1>当前求和为:{this.props.count}</h1>
<select ref={c => this.selectNumber = c}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<button onClick={this.incrementIfOdd}>当前求和为奇数再加</button>
<button onClick={this.incrementAsync}>异步加</button>
</div>
)
}
}
(3)containers/Count/index.jsx
容器组件
//引入Count的UI组件
import CountUI from '../../components/Count'
//引入action
import {
createIncrementAction,
createDecrementAction,
createIncrementAsyncAction
} from '../../redux/count_action'
//引入connect用于连接UI组件与redux
import {connect} from 'react-redux'
/*
1.mapStateToProps函数返回的是一个对象;
2.返回的对象中的key就作为传递给UI组件props的key,value就作为传递给UI组件props的value
3.mapStateToProps用于传递状态
*/
function mapStateToProps(state){
return {count:state}
}
/*
1.mapDispatchToProps函数返回的是一个对象;
2.返回的对象中的key就作为传递给UI组件props的key,value就作为传递给UI组件props的value
3.mapDispatchToProps用于传递操作状态的方法
*/
function mapDispatchToProps(dispatch){
return {
jia:number => dispatch(createIncrementAction(number)),
jian:number => dispatch(createDecrementAction(number)),
jiaAsync:(number,time) => dispatch(createIncrementAsyncAction(number,time)),
}
}
//使用connect()()创建并暴露一个Count的容器组件
export default connect(mapStateToProps,mapDispatchToProps)(CountUI)
(4)App.jsx
给容器组件传递store
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 */}
<Count store={store} />
</div>
)
}
}
4、优化
(1)简写mapDispatchToProps
//使用connect()()创建并暴露一个Count的容器组件
export default connect(
state => ({count:state}),
//mapDispatchToProps的一般写法
/* dispatch => ({
jia:number => dispatch(createIncrementAction(number)),
jian:number => dispatch(createDecrementAction(number)),
jiaAsync:(number,time) => dispatch(createIncrementAsyncAction(number,time)),
}) */
//mapDispatchToProps的简写
{
jia:createIncrementAction,
jian:createDecrementAction,
jiaAsync:createIncrementAsyncAction,
}
)(CountUI)
(2)Provider
容器组件可以检测redux中的状态改变,并渲染页面,所以不需要在index.js中检测了
不要在App.jsx中给子组件传递store了
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import store from './redux/store'
import {Provider} from 'react-redux'
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('root')
)
(3)整合UI和容器组件
每个组件两个文件夹太麻烦了,直接整合在一起就好了~
containers/Count/index.jsx
import React, { Component } from 'react'
//引入action
import {
createIncrementAction,
createDecrementAction,
createIncrementAsyncAction
} from '../../redux/count_action'
//引入connect用于连接UI组件与redux
import {connect} from 'react-redux'
//定义UI组件
class Count extends Component {
state = {carName:'奔驰c63'}
//加法
increment = ()=>{
const {value} = this.selectNumber
this.props.jia(value*1)
}
//减法
decrement = ()=>{
const {value} = this.selectNumber
this.props.jian(value*1)
}
//奇数再加
incrementIfOdd = ()=>{
const {value} = this.selectNumber
if(this.props.count % 2 !== 0){
this.props.jia(value*1)
}
}
//异步加
incrementAsync = ()=>{
const {value} = this.selectNumber
this.props.jiaAsync(value*1,500)
}
render() {
//console.log('UI组件接收到的props是',this.props);
return (
<div>
<h1>当前求和为:{this.props.count}</h1>
<select ref={c => this.selectNumber = c}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<button onClick={this.incrementIfOdd}>当前求和为奇数再加</button>
<button onClick={this.incrementAsync}>异步加</button>
</div>
)
}
}
//使用connect()()创建并暴露一个Count的容器组件
export default connect(
state => ({count:state}),
//mapDispatchToProps的一般写法
/* dispatch => ({
jia:number => dispatch(createIncrementAction(number)),
jian:number => dispatch(createDecrementAction(number)),
jiaAsync:(number,time) => dispatch(createIncrementAsyncAction(number,time)),
}) */
//mapDispatchToProps的简写
{
jia:createIncrementAction,
jian:createDecrementAction,
jiaAsync:createIncrementAsyncAction,
}
)(Count)
5、开发者工具
1、谷歌浏览器安装Redux Dev Tools
插件
2、npm install redux-devtools-extension
3、在store中进行配置
import {composeWithDevTools} from 'redux-devtools-extension'
const store = createStore(allReducer,composeWithDevTools(applyMiddleware(thunk)))
6、最终版
(1)App.jsx
import React, { Component } from 'react'
import Count from './containers/Count' //引入的Count的容器组件
import Person from './containers/Person' //引入的Person的容器组件
export default class App extends Component {
render() {
return (
<div>
<Count/>
<hr/>
<Person/>
</div>
)
}
}
(2)src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import store from './redux/store'
import {Provider} from 'react-redux'
ReactDOM.render(
/* 此处需要用Provider包裹App,目的是让App所有的后代容器组件都能接收到store */
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('root')
)
(3)redux
store.js
/*
该文件专门用于暴露一个store对象,整个应用只有一个store对象
*/
//引入createStore,专门用于创建redux中最为核心的store对象
import {createStore,applyMiddleware} from 'redux'
//引入汇总之后的reducer
import reducer from './reducers'
//引入redux-thunk,用于支持异步action
import thunk from 'redux-thunk'
//引入redux-devtools-extension
import {composeWithDevTools} from 'redux-devtools-extension'
//暴露store
export default createStore(reducer,composeWithDevTools(applyMiddleware(thunk)))
constant.js
/*
该模块是用于定义action对象中type类型的常量值,目的只有一个:便于管理的同时防止程序员单词写错
*/
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
export const ADD_PERSON = 'add_person'
reducers/index.js
/*
该文件用于汇总所有的reducer为一个总的reducer
*/
//引入combineReducers,用于汇总多个reducer
import {combineReducers} from 'redux'
//引入为Count组件服务的reducer
import count from './count'
//引入为Person组件服务的reducer
import persons from './person'
//汇总所有的reducer变为一个总的reducer
export default combineReducers({
count,
persons
})
reducers/count.js
/*
1.该文件是用于创建一个为Count组件服务的reducer,reducer的本质就是一个函数
2.reducer函数会接到两个参数,分别为:之前的状态(preState),动作对象(action)
*/
import {INCREMENT,DECREMENT} from '../constant'
const initState = 0 //初始化状态
export default function countReducer(preState=initState,action){
// console.log('countReducer@#@#@#');
//从action对象中获取:type、data
const {type,data} = action
//根据type决定如何加工数据
switch (type) {
case INCREMENT: //如果是加
return preState + data
case DECREMENT: //若果是减
return preState - data
default:
return preState
}
}
reducers/person.js
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: //若是添加一个人
//preState.unshift(data) //此处不可以这样写,这样会导致preState被改写了,personReducer就不是纯函数了。
return [data,...preState]
default:
return preState
}
}
actions/count.js
/*
该文件专门为Count组件生成action对象
*/
import {INCREMENT,DECREMENT} from '../constant'
//同步action,就是指action的值为Object类型的一般对象
export const increment = data => ({type:INCREMENT,data})
export const decrement = data => ({type:DECREMENT,data})
//异步action,就是指action的值为函数,异步action中一般都会调用同步action,异步action不是必须要用的。
export const incrementAsync = (data,time) => {
return (dispatch)=>{
setTimeout(()=>{
dispatch(increment(data))
},time)
}
}
actions/person.js
import {ADD_PERSON} from '../constant'
//创建增加一个人的action动作对象
export const addPerson = personObj => ({type:ADD_PERSON,data:personObj})
(4)containers
Count/index.jsx
import React, { Component } from 'react'
//引入action
import {
increment,
decrement,
incrementAsync
} from '../../redux/actions/count'
//引入connect用于连接UI组件与redux
import {connect} from 'react-redux'
//定义UI组件
class Count extends Component {
state = {carName:'奔驰c63'}
//加法
increment = ()=>{
const {value} = this.selectNumber
this.props.increment(value*1)
}
//减法
decrement = ()=>{
const {value} = this.selectNumber
this.props.decrement(value*1)
}
//奇数再加
incrementIfOdd = ()=>{
const {value} = this.selectNumber
if(this.props.count % 2 !== 0){
this.props.increment(value*1)
}
}
//异步加
incrementAsync = ()=>{
const {value} = this.selectNumber
this.props.incrementAsync(value*1,500)
}
render() {
//console.log('UI组件接收到的props是',this.props);
return (
<div>
<h2>我是Count组件,下方组件总人数为:{this.props.renshu}</h2>
<h4>当前求和为:{this.props.count}</h4>
<select ref={c => this.selectNumber = c}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<button onClick={this.incrementIfOdd}>当前求和为奇数再加</button>
<button onClick={this.incrementAsync}>异步加</button>
</div>
)
}
}
//使用connect()()创建并暴露一个Count的容器组件
export default connect(
state => ({
count:state.count,
personCount:state.persons.length
}),
{increment,decrement,incrementAsync}
)(Count)
Person/index.jsx
import React, { Component } from 'react'
import {nanoid} from 'nanoid'
import {connect} from 'react-redux'
import {addPerson} from '../../redux/actions/person'
class Person extends Component {
addPerson = ()=>{
const name = this.nameNode.value
const age = this.ageNode.value*1
const personObj = {id:nanoid(),name,age}
this.props.addPerson(personObj)
this.nameNode.value = ''
this.ageNode.value = ''
}
render() {
return (
<div>
<h2>我是Person组件,上方组件求和为{this.props.count}</h2>
<input ref={c=>this.nameNode = c} type="text" placeholder="输入名字"/>
<input ref={c=>this.ageNode = c} type="text" placeholder="输入年龄"/>
<button onClick={this.addPerson}>添加</button>
<ul>
{
this.props.persons.map((p)=>{
return <li key={p.id}>{p.name}--{p.age}</li>
})
}
</ul>
</div>
)
}
}
export default connect(
state => ({
persons:state.persons,
count:state.count
}),//映射状态
{addPerson}//映射操作状态的方法
)(Person)
十、扩展
1. setState
注意:setState 是异步更新,在调用之后立即获取state中的数据,获取不到更新后的数据,要获取的话需要在参数后加一个后调函数,如下
setState更新状态的2种写法
(1). setState(stateChange, [callback])------对象式的setState
1.stateChange为状态改变对象(该对象可以体现出状态的更改)
2.callback是可选的回调函数, 它在状态更新完毕、界面也更新后(render调用后)才被调用
(2). setState(updater, [callback])------函数式的setState
1.updater为返回stateChange对象的函数。
2.updater可以接收到state和props。
4.callback是可选的回调函数, 它在状态更新、界面也更新后(render调用后)才被调用。
总结:
1.对象式的setState是函数式的setState的简写方式(语法糖)
2.使用原则:
(1).如果新状态不依赖于原状态 ===> 使用对象方式
(2).如果新状态依赖于原状态 ===> 使用函数方式
(3).如果需要在setState()执行后获取最新的状态数据,
要在第二个callback函数中读取
add = ()=>{
//对象式的setState
//1.获取原来的count值
const {count} = this.state
//2.更新状态
this.setState({count:count+1},()=>{
console.log(this.state.count);
})
//console.log('12行的输出',this.state.count); //0
//函数式的setState
this.setState( state => ({count:state.count+1}))
}
2. lazyLoad
路由组件的lazyLoad,实现按需加载,提高性能
//1.引入 lazy、Suspense
import React, { Component,lazy,Suspense} from 'react'
import {NavLink,Route} from 'react-router-dom'
import Loading from './Loading'
// import Home from './Home'
// import About from './About'
//2.替换上面注释的两行
//通过React的lazy函数配合import()函数动态加载路由组件 ===> 路由组件代码会被分开打包
const Home = lazy(()=> import('./Home') )
const About = lazy(()=> import('./About'))
// ....
//3.通过<Suspense>指定在加载得到路由打包文件前显示一个自定义loading界面
<Suspense fallback={<Loading/>}>
{/* 注册路由 */}
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
</Suspense>
3. Hooks
(1)是什么?
(1). Hook是React 16.8.0版本增加的新特性/新语法
(2). 可以让你在函数组件中使用 state 以及其他的 React 特性
(2)三个常用的Hook
(1). State Hook: React.useState()
(2). Effect Hook: React.useEffect()
(3). Ref Hook: React.useRef()
(3)State Hook
(1). State Hook让函数组件也可以有state状态, 并进行状态数据的读写操作
(2). 语法: const [xxx, setXxx] = React.useState(initValue)
(3). useState()说明:
参数: 第一次初始化指定的值在内部作缓存,也就是默认值
返回值: 包含2个元素的数组, 第1个为内部当前状态值, 第2个为更新状态值的函数
(4). setXxx()2种写法:
setXxx(newValue): 参数为非函数值, 直接指定新的状态值, 内部用其覆盖原来的状态值
setXxx(value => newValue): 参数为函数, 接收原本的状态值, 返回新的状态值, 内部用其覆盖原来的状态值
const [count,setCount] = React.useState(0)
//加的回调
function add(){
//setCount(count+1) //第一种写法
setCount(count => count+1 )
}
(4)Effect Hook
(1). Effect Hook 可以让你在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)
(2). React中的副作用操作:
发ajax请求数据获取
设置订阅 / 启动定时器
手动更改真实DOM
(3). 语法和说明:
useEffect(() => {
// 在此可以执行任何带副作用操作
return () => { // 在组件卸载前执行
// 在此做一些收尾工作, 比如清除定时器/取消订阅等
}
}, [stateValue]) // 如果指定的是[], 回调函数只会在第一次render()后执行
(4).useEffect(() => {},[]) 参数说明:
1、回调函数,调用时机由第二个参数决定
2、监测对象
1) 数组为空时,不监测数据,仅在挂载时调回调函数
2) 数组不为空时,监测指定数据,会在数据改变、挂载时,调用回调函数
3) 第二个参数省略时,监测所有数据,会在数据改变、挂载时,调用回调函数
(5). 可以把 useEffect Hook 看做如下三个函数的组合
componentDidMount()
componentDidUpdate()
componentWillUnmount()
React.useEffect(()=>{
let timer = setInterval(()=>{
setCount(count => count+1 )
},1000)
return ()=>{
clearInterval(timer)
}
},[])
(5)Ref Hook
(1). Ref Hook可以在函数组件中存储/查找组件内的标签或任意其它数据
(2). 语法: const refContainer = useRef()
(3). 作用:保存标签对象,功能与React.createRef()一样
//提示输入的回调
function show(){
alert(myRef.current.value)
}
<input type="text" ref={myRef}/>
<button onClick={show}>点我提示数据</button>
(6)代码
function Demo(){
//console.log('Demo');
const [count,setCount] = React.useState(0)
const myRef = React.useRef()
React.useEffect(()=>{
let timer = setInterval(()=>{
setCount(count => count+1 )
},1000)
return ()=>{
clearInterval(timer)
}
},[])
//加的回调
function add(){
//setCount(count+1) //第一种写法
setCount(count => count+1 )
}
//提示输入的回调
function show(){
alert(myRef.current.value)
}
//卸载组件的回调
function unmount(){
ReactDOM.unmountComponentAtNode(document.getElementById('root'))
}
return (
<div>
<input type="text" ref={myRef}/>
<h2>当前求和为:{count}</h2>
<button onClick={add}>点我+1</button>
<button onClick={unmount}>卸载组件</button>
<button onClick={show}>点我提示数据</button>
</div>
)
}
export default Demo
4. Fragment
当不得不用一个没有实际作用的标签去包裹代码时,可以使用Fragment
或空标签,该标签不会在前端中渲染,为的是骗过jsx的语法检测。
Fragment
或空标签,的区别是Fragment
可以有一个属性key
,可用于遍历,而空标签不能有属性,Fragment
只能使用key属性,不能有其他属性
<Fragment><Fragment>
<></>
作用:可以不用必须有一个真实的DOM根标签了
5. Context
理解
一种组件间通信方式, 常用于【祖组件】与【后代组件】间通信
使用
1) 创建Context容器对象:(写在组件外)
const XxxContext = React.createContext()
2) 渲染子组时,外面包裹xxxContext.Provider, 通过value属性给后代组件传递数据:
<xxxContext.Provider value={数据}>
子组件
</xxxContext.Provider>
3) 后代组件读取数据:
//第一种方式:仅适用于类组件
static contextType = xxxContext // 声明接收context
this.context // 读取context中的value数据
//第二种方式: 函数组件与类组件都可以
<xxxContext.Consumer>
{value => `${value.username},年龄是${value.age}`}
</xxxContext.Consumer>
注意
在应用开发中一般不用context, 一般都用它的封装react插件
6. PureComponent
Component的2个问题
只要执行setState(),即使不改变状态数据, 组件也会重新render() ==> 效率低
只当前组件重新render(), 就会自动重新render子组件,纵使子组件没有用到父组件的任何数据 ==> 效率低
效率高的做法
只有当组件的state或props数据发生改变时才重新render()
原因
Component中的shouldComponentUpdate()总是返回true
解决
办法1:
重写shouldComponentUpdate()方法
比较新旧state或props数据, 如果有变化才返回true, 如果没有返回false
办法2:
使用PureComponent,类继承使用PureComponent而不是component
PureComponent重写了shouldComponentUpdate(), 只有state或props数据有变化才返回true
注意:
只是进行state和props数据的浅比较, 如果只是数据对象内部数据变了, 返回false
不要直接修改state数据, 而是要产生新数据
项目中一般使用PureComponent来优化
export default class Parent extends PureComponent {}
7. render props
如何向组件内部动态传入带内容的结构(标签)?
Vue中:
使用slot(插槽)技术, 也就是通过组件标签体传入结构 <A><B/></A>
React中:
使用children props: 通过组件标签体传入结构
使用render props: 通过组件标签属性传入结构,而且可以携带数据,一般用render函数属性
就是说在子组件中预留一个位置,父组件可以通过render props
,动态设置子组件中该位置渲染的组件,而不需要改变子组件
children props
<A>
<B>xxxx</B>
</A>
{this.props.children}
问题: 如果B组件需要A组件内的数据, ==> 做不到
render props
<A render={(data) => <C data={data}></C>}></A>
A组件(预留组件位置): {this.props.render(内部state数据)}
C组件: 读取A组件传入的数据显示 {this.props.data}
只改变父组件Parent
的代码即可动态渲染 子组件A
中预留区域的组件,可更换为其他组件等。
import React, { Component } from 'react'
import './index.css'
import C from '../1_setState'
export default class Parent extends Component {
render() {
return (
<div className="parent">
<h3>我是Parent组件</h3>
<A render={(name)=><C name={name}/>}/>
</div>
)
}
}
class A extends Component {
state = {name:'tom'}
render() {
console.log(this.props);
const {name} = this.state
return (
<div className="a">
<h3>我是A组件</h3>
{/*预留组件位置*/}
{this.props.render(name)}
</div>
)
}
}
class B extends Component {
render() {
console.log('B--render');
return (
<div className="b">
<h3>我是B组件,{this.props.name}</h3>
</div>
)
}
}
8. 错误边界
理解:
错误边界(Error boundary):用来捕获后代组件错误,渲染出备用页面,避免因为一个组件出错而导致所有页面无法展示。
特点:
只能捕获后代组件生命周期
产生的错误,不能捕获自己组件产生的错误和其他组件在合成事件、定时器中产生的错误
使用方式:
getDerivedStateFromError配合componentDidCatch
注意:错误边界只在生产环境中有效,开发环境只会展示一会儿备用页面,然后还是会报错。
// 生命周期函数,一旦后台组件报错,就会触发
static getDerivedStateFromError(error) {
console.log(error);
// 在render之前触发
// 返回新的state
return {
hasError: true,
};
}
// 子组件报错时调用,也是一个生命周期函数
componentDidCatch(error, info) {
// 统计页面的错误。发送请求发送到后台去
console.log(error, info);
}
十一、操作缓存和cookie
1、操作localStorage
除非被清除,否则永久保存
大小一般为5MB
仅在客户端(即浏览器)中保存,不参与和服务器的通信
存在 XSS 注入的风险,只要打开控制台,就可以随意修改它们的值
(1)下载引入
npm install localStorage --save
# 如果使用typescript ,还得引入下面依赖
npm i --save-dev @types/localStorage
引入
// 引入
import localStorage from "localStorage";
(2)存
localStorage.setItem("phone", "123")
//对象
let obj = {"name":"xiaoming", "age":"16"}
localStorage.setItem("phone", JSON.stringify(obj));
(3)取
localStorage.getItem("phone")
//对象
let user = JSON.parse(localStorage.getItem("phone"))
(4)删
//指定删
localStorage.removeItem('phone');
//全删
localStorage.clear();
(5)设置过期时间
//设置缓存,设置localStorageSet 过期时间
const localStorageSet = (name, data) => {
const obj = {
data,
expire: new Date().getTime() + 1000 * 60 * 30
};
localStorage.setItem(name, JSON.stringify(obj));
};
//读取缓存,且比较时间戳是否过期
const localStorageGet = name => {
const storage = localStorage.getItem(name);
const time = new Date().getTime();
let result = null;
if (storage) {
const obj = JSON.parse(storage);
if (time < obj.expire) {
result = obj.data;
} else {
localStorage.removeItem(name);
}
}
return result;
};
2、操作Cookie
可设置失效时间。如果在浏览器端生成Cookie,默认是关闭浏览器后失效
大小4K左右
每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题
存在 XSS 注入的风险,只要打开控制台,就可以随意修改它们的值
1、下载依赖
cnpm install react-cookies --save-dev
2、引入
import cookie from 'react-cookies'
3、存
cookie.save('userId', "123");
4、取
cookie.load('userId')
5、删
cookie.remove('userId')
6、设置失效
let inFifteenMinutes = new Date(new Date().getTime() + 24 * 3600 * 1000);//一天
cookie.save('userId', "123",{ expires: inFifteenMinutes });