脚手架可以帮助我们快速的搭建一个项目结构,在我们之前学习
webpack
的过程中,每次都需要配置webpack.config.js
文件,用于配置我们项目的相关loader
、plugin
,这些操作比较复杂,但是它的重复性很高,而且在项目打包时又很有必要,那 React
脚手架就帮助我们做了这些,它不需要我们人为的去编写webpack
配置文件,它将这些配置文件全部都已经提前的配置好了。
一、脚手架项目结构
hello-react
├─ .gitignore // 自动创建本地仓库
├─ package.json // 相关配置文件
├─ public // 公共资源
│ ├─ favicon.ico // 浏览器顶部的icon图标
│ ├─ index.html // 应用的 index.html入口
│ ├─ logo192.png // 在 manifest 中使用的logo图
│ ├─ logo512.png // 同上
│ ├─ manifest.json // 应用加壳的配置文件
│ └─ robots.txt // 爬虫给协议文件
├─ src // 源码文件夹
│ ├─ App.css // App组件的样式
│ ├─ App.js // App组件
│ ├─ App.test.js // 用于给APP做测试
│ ├─ index.css // 样式
│ ├─ index.js // 入口文件
│ ├─ logo.svg // logo图
│ ├─ reportWebVitals.js // 页面性能分析文件
│ └─ setupTests.js // 组件单元测试文件
└─ yarn.lock
再介绍一下public目录下的 index.html 文件中的代码意思
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<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>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
第5行
指定浏览器图标的路径,这里直接采用 %PUBLIC_URL%
原因是 webpack
配置好了,它代表的意思就是 public
文件夹
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
第6行
用于做移动端网页适配
<meta name="viewport" content="width=device-width, initial-scale=1" />
第七行
用于配置安卓手机浏览器顶部颜色,兼容性不大好
<meta name="theme-color" content="#000000" />
8到11行
用于描述网站信息
<meta
name="description"
content="Web site created using create-react-app"
/>
第12行
苹果手机触摸版应用图标
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
第13行
应用加壳时的配置文件
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
二、TodoList 案例
TodoList
案例在前端学习中挺重要的,从原生 JavaScript 的增删查改,到现在 React 的组件通信,都是一个不错的案例,这篇文章主要记录,还原一下通过 React
实现 TodoList
的全过程
1、拆分组件
首先第一步需要做的是将这个页面拆分成几个组件
首先顶部的输入框,可以完成添加项目的功能,可以拆分成一个 Header 组件
中间部分可以实现一个渲染列表的功能,可以拆分成一个 List 组件
在这部分里面,每一个待办事项都可以拆分成一个 Item 组件
最后底部显示当前完成状态的部分,可以拆分成一个 Footer 组件
在拆分完组件后,我们下一步要做的就是去实现这些组件的静态效果
2、实现静态组件
首先,我们可以先写好这个页面的静态页面,然后再分离组件,所以这就要求我们
以后写静态页面的时候,一定要有明确的规范
1.打好注释
2.每个部分的 CSS 要写在一个地方,不要随意写
3.命名一定要规范
4.CSS 选择器不要关联太多层级
5.在写 HTML 时就要划分好布局
这样有利于我们分离组件
首先,我们在 src
目录下,新建一个 Components
文件夹,用于存放我们的组件,然后在文件夹下,新建 Header
、Item
、List
、Footer
组件文件夹,再创建其下的 index.jsx
,index.css
文件,用于创建对应组件及其样式文件
todolist
├─ package.json
├─ public
│ ├─ favicon.ico
│ └─ index.html
├─ src
│ ├─ App.css
│ ├─ App.jsx
│ ├─ Components
│ │ ├─ Footer
│ │ │ ├─ index.css
│ │ │ └─ index.jsx
│ │ ├─ Header
│ │ │ ├─ index.css
│ │ │ └─ index.jsx
│ │ ├─ item
│ │ │ ├─ index.css
│ │ │ └─ index.jsx
│ │ └─ List
│ │ ├─ index.css
│ │ └─ index.jsx
│ └─ index.js
└─ yarn.lock
最终目录结构如上
然后我们将每个组件,对应的 HTML 结构 CV 到对应组件的 index.jsx
文件中 return
出来,再将 CSS 样式添加到 index.css
文件中
记得,在 index.jsx
中一定要引入 index.css
文件
实现了静态组件后,我们需要添加事件等,来实现动态组件
3、实现动态组件
🍎 1. 动态展示列表
我们目前实现的列表项是固定的,我们需要它通过状态来维护,而不是通过组件标签来维护
首先我们知道,父子之间传递参数,可以通过 state 和 props 实现
我们通过在父组件也就是 App.jsx 中设置状态
// 初始化状态
state = {
todos: [
{ id: '001', name: '吃饭', done: true },
{ id: '002', name: '睡觉', done: true },
{ id: '003', name: '打代码', done: false },
{ id: '004', name: '逛街', done: true },
]
}
再将它传递给对应的渲染组件 List
const { todos } = this.state
<List todos={todos}/>
这样在 List
组件中就能通过 props
来获取到 todos
我们通过解构取出 todos
const { todos } = this.props
再通过 map
遍历渲染 Item
数量
todos.map(todo => {
return <Item key={todo.id} {...todo}/>
})
同时由于我们的数据渲染最终是在 Item
组件中完成的,所以我们需要将数据传递给 Item
组件
这里有两个注意点
1、关于 key
的作用在 diff
算法的文章中已经有讲过了,需要满足唯一性
2、这里采用了简写形式 {...todo}
,这使得代码更加简洁,它代表的意思是
id = {todo.id} name = {todo.name} done = {todo.done}
在 Item
组件中通过解构取出 props
即可使用
const { id, name, done } = this.props
这样我们更改 APP.jsx
文件中的 state
就能驱动着 Item
组件的更新,如图
同时这里需要注意的是
对于复选框的选中状态,这里采用的是 defaultChecked = {done}
,相比于 checked
属性,这个设定的是默认值,能够更改
🍍 2. 添加事项功能
首先我们需要在 Header 组件中,绑定键盘事件,判断按下的是否为回车,如果为回车,则将当前输入框中的内容传递给 App 组件
因为,在目前的学习知识中,Header 组件和渲染组件 List 属于兄弟组件,没有办法进行直接的数据传递,因此可以将数据传递给 APP 再由 APP 转发给 List。
//键盘事件的回调
handleKeyUp = (event)=>{
//解构赋值获取keyCode,target
const {keyCode,target} = event
//判断是否是回车按键
if(keyCode !== 13) return
//准备好一个todo对象
const todoObj = {id:nanoid(),name:target.value,done:false}
//将todoObj传递给App
this.props.addTodo(todoObj)
}
⚠️注意1:这里除了绑定键盘事件,判断按下的是否为回车键,用 addTodo
传递参数给App
组件,还有一些细节
- 添加的
todo
名字不能为空,所以需要判断一下target.value
值是否为空串,trim()
用来去除空格
//添加的todo名字不能为空
if(target.value.trim() === ''){
alert('输入不能为空')
return
}
- 传递完后,要把输入框内的数据清空,直接将
target.value
值设为空串即可
//清空输入
target.value = ''
⚠️注意2:我们在 App.jsx
中添加了事件 addTodo
,这样可以将 Header
组件传递的参数,维护到 App
的状态中
// App.jsx
addTodo = (todoObj) => {
const { todos } = this.state
// 追加一个 todo
const newTodos = [todoObj, ...todos]
this.setState({ todos: newTodos })
}
在这小部分中,需要我们注意的是,我们新建的 todo
对象,一定要保证它的 id
的唯一性
这里采用的 nanoid
库,这个库的每一次调用都会返回一个唯一的值
npm i nanoid #或者用 yarn add nanoid
安装这个库,然后哪里使用就在哪里引入,这里我们在Header
引入
import {nanoid} from 'nanoid'
通过 nanoid()
即可生成唯一值
console.log(nanoid()) //wUncb6ywbx-9CWzHhrOj7
🍋 3. 实现鼠标悬浮效果
接下来我们需要实现每个 Item
中的小功能
首先是鼠标移入时的变色效果
我的逻辑是,通过一个状态来维护是否鼠标移入,比如用一个 state={mouse:flag }
,当鼠标移入时,设定状态为 true
;当鼠标移出时设为 false
。然后我们只需要在 style
中用 mouse
去设定样式的两种不同情况即可
下面我们来代码实现
在 Item
组件中,先设定状态
// 标识鼠标移入,移出
state = { mouse: flag }
给元素 li
绑定上鼠标移入,移出事件
<li onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)} ><li/>
当鼠标移入时,会触发 onMouseEnter
事件,调用 handleMouse
事件传入参数 true
表示鼠标进入,更新组件状态
// 鼠标移入、移出的回调
handleMouse = (flag) => {
return () => {
this.setState({ mouse: flag })
}
再在 li
身上添加由 mouse
控制的背景颜色
style={{ backgroundColor: this.state.mouse ? '#ddd' : 'white' }}
同时在 button
上通过 mouse
来控制删除按钮的显示和隐藏
style={{ display: mouse ? 'block' : 'none' }}
🍉 4. 复选框状态维护
我们需要将当前复选框的状态,维护到 state
当中
我们的思路是
在复选框中添加一个 onChange
事件来进行数据的传递,通过 id
我们知道改变状态的是哪一个 Item
,当事件触发时我们执行 handleCheck
函数,这个函数可以通过 App
组件内定义的 updateTodo
向 App
组件中传递参数 id
和 done
,这样再在 App
中改变状态即可
首先在 input
复选框绑定一个 onChange
事件
// Item/index.jsx
<input type="checkbox" defaultChecked={done} onChange={this.handleCheck(id)} />
勾选、取消勾选某一个todo
的事件回调函数
// 勾选、取消勾选某一个todo的回调
handleCheck = (id) => {
return (event) => {
this.props.updateTodo(id,event.target.checked)
}
}
⚠️注意:由于我们需要传递 id
来记录状态更新的对象,因此我们需要采用高阶函数的写法,让 this.handleCheck(id)
的返回值是一个函数,不然会报错,复选框的状态我们可以通过 event.target.checked
来获取
我们将需要改变状态的 Item
的 id
和复选框的状态 done
传递给 App 组件,此时需要通过 App
组件内定义的 updateTodo
,来接受参数并更新 App
组件中的状态
我们传递了两个参数 id
和 done
,通过 map
遍历找出该 id
对应的 todo
对象,更改它的 done
即可,最后更新状态
// App.jsx
// updateTodo用于更新一个todo对象
updateTodo = (id, done) => {
// 获取状态中的todos
const { todos } = this.state
// 匹配处理数据
const newTodos = todos.map((todoObj) => {
if (todoObj.id == id) return { ...todoObj, done }
else return todoObj
})
// 更新状态
this.setState({ todos: newTodos })
}
这里更改的方式是 { ...todoObj, done }
,首先会展开 todoObj
的每一项,再对 done
属性做覆盖
⚠️注意:App是List的父元素,List是Item的父元素,所以App是Item的祖先元素。要想让App与Item传递数据,必须先让App给List传递函数,再在List里给Item传递函数。
和传递 todos
一样,在 List
组件中可以通过 props
来获取到 updateTodo
函数
将这个函数传递给 List 组件
<List todos={todos} updateTodo={this.updateTodo} />
在List
组件里通过解构取出 updateTodo
函数
// List index.jsx
const { todos, updateTodo } = this.props
在通过 map
遍历渲染 Item
时,将 updateTodo={updateTodo}
再次通过 props
传递给 Item
todos.map(todo => {
return <Item key={todo.id} {...todo} updateTodo={updateTodo} />
最后在 Item
,这里没解构取出,直接用 this.props.updateTodo(id,done)
了,当然你也可以解构
this.props.updateTodo(id,event.target.checked)
🍏 5. 限制参数类型
在我们前面写的东西中,我们并没有对参数的类型以及必要性进行限制
在前面我们也学过这个,我们需要借助 propTypes
这个库
npm i prop-types #或者 yarn add prop-types
首先我们需要引入这个库
import PropTypes from 'prop-types'
然后对 props
进行限制
在Header
组件中需要接收一个 addTodo
函数,所以我们进行一下限制
// 对接收的props进行类型以及必要性的限制
static propTypes = {
addTodo: PropTypes.func.isRequired
}
在 List
组件中也需要进行对 todos
以及 updateTodo
的限制
// 对接收的props进行类型以及必要性的限制
static propTypes = {
todos:PropTypes.array.isRequired,
updateTodo:PropTypes.func.isRequired
}
如果传入的参数不符合限制,则会报 warning
🍒 6. 删除按钮
现在我们需要实现删除按钮的效果,这个和前面的挺像的
首先我们需要在 Item
组件上的按钮绑定删除的点击事件 handleDelete
。在App组件中定义一个 deleteTodo
,将被点击删除按钮的 Item
的 id
值,通过 props
传递给父元素 List
,再通过在 List
中绑定的 App
组件的 deleteTodo
,将 id
传递给 App
来改变 state
状态
⚠️注意:因为 setState
每次调用会重新执行 render
渲染,所以 state
中如果没有了要删除的 todo
对象,再次渲染后就达到了删除的效果。
首先我们先编写 handleDelete
点击事件
// Item/index.jsx
// 删除一个todo的回调
handleDelete = (id) => {
this.props.deleteTodo(id)
}
子组件想影响父组件的状态,需要父组件传递一个函数,因此我们在 App 中添加一个 deleteTodo
函数
// app.jsx
// deleteTodo用于删除一个todo对象
deleteTodo = (id) => {
const { todos } = this.state
const newTodos = todos.filter(todoObj => {
return todoObj.id !== id
})
this.setState({ todos: newTodos })
}
然后将这个函数传递给 List 组件
<List todos={todos} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo} />
在List
组件里通过解构取出 updateTodo
函数
// List index.jsx
const { todos, updateTodo, deleteTodo } = this.props
再将这个函数传递给 Item
组件
//List index.jsx
todos.map(todo => {
return <Item key={todo.id} {...todo} updateTodo={updateTodo} deleteTodo={deleteTodo} />
})
顺便对接收的props进行类型以及必要性的限制
deleteTodo: PropTypes.func.isRequired
再传递给 Item
在 Item
的 handleDelete
回调函数里增加一个判断,弹窗问是否确认删除
if(window.confirm('确认删除吗?')) {
this.props.deleteTodo(id)
}
⚠️注意:这里用了 confirm
,记得前面加上 window
🍓 7. 获取完成数量
我们在 App
中向 Footer
组件传递 todos
数据,再去统计数据
统计 done
为 true
的个数
const doneCount = todos.reduce((pre, todo) => {
return pre + (todo.done ? 1 : 0)
}, 0)
再渲染数据即可
🍊 8. 全选按钮
首先我们需要在按钮上绑定事件,由于子组件需要改变父组件的状态,所以我们的操作和之前的一样,先绑定事件,再在 App 中传一个函数给 Footer ,再在 Footer 中调用这个函数并传入参数即可
这里需要特别注意的是
defaulChecked
只有第一次会起作用,所以我们需要将前面写的改成 checked
并添加 onChange
事件即可
首先我们先在 App 中给 Footer 传入一个函数 checkAllTodo
,它需要传入一个参数done来决定全选还是全不选
// App.jsx
// checkAllTodo用于全选
checkAllTodo = (done) => {
// 获取原来的todos
const { todos } = this.state
// 加工数据
const newTodos = todos.map((todoObj) => {
return { ...todoObj, done }
})
// 更新状态
this.setState({ todos: newTodos })
}
然后将这个函数传递给 Footer 组件
<Footer todos={todos} checkAllTodo={this.checkAllTodo}/>
然后我们需要在 Footer
中调用一下,这里我们通过事件传入了一个参数:当前按钮的状态,用于全选和取消全选
handleCheckAll = (event) => {
this.props.checkAllTodo(event.target.checked)
}
同时我们需要排除总数 total 为 0 时的干扰
<input type="checkbox" checked={doneCount === total && total !== 0? true : false} onChange={this.handleCheckAll} />
🥭 9. 删除已完成
给删除按钮添加一个点击事件,回调中调用 App 中添加的删除已完成的函数,全都一个套路
首先在 App 中定义函数,过滤掉 done
为 true
的,再更新状态即可
// App.jsx
clearAllDone = () => {
const { todos } = this.state
const newTodos = todos.filter((todoObj) => {
return todoObj.done !== true
})
this.setState({ todos: newTodos })
}
然后将这个函数传递给 Footer
组件
<Footer todos={todos} checkAllTodo={this.checkAllTodo} clearAllDone={this.clearAllDone}/>
给Footer的button绑定点击事件
<button onClick={this.handleClearAllDone} className="btn btn-danger">清除已完成任务</button>
然后我们需要在 Footer 的点击事件中调用 App 组件定义的函数
//清除已完成任务的回调
handleClearAllDone=()=>{
this.props.clearAllDone()
}
总结
1.拆分组件、实现静态组件,注意:className、style 写法
2.动态初始化列表,如何确定将数据放在那个组件的state中?
- 某个组件使用:放在自身的state中
- 某些组件使用:放在他们公共的父组件state中(官方称此操作为:状态提升)
3.关于父子之间通信:
- 父组件给子组件传递数据,通过 props传递
- 子组件给父组件传递数据,通过 props,要求父提前给子传递一个函数
4.注意 defaultChecked
和 checked
的区别,类似的还有 defaultValue
和 value
5、状态在哪里,操作状态的方法就在哪里
三、脚手架配置代理
React 使用 axios 发送客户端请求
React 本身只关注于页面,并不包含发送 Ajax 请求的代码,所以一般都是集成第三方的包,或者自己封装的
自己封装的话,比较麻烦,而且也可能考虑不全
常用的有两个库,一个是JQuery
,一个是 axios
1、JQuery
这个比较重,因为 Ajax
服务也只是它这个库里的一小块功能,它主要做的还是 DOM
操作,而这不利于 React
,不推荐使用
2、axios
这个就比较轻,而且采用 Promise
风格,代码的逻辑会相对清晰,推荐使用
因此我们这里采用 axios
来发送客户端请求,记得在 App.jsx
里引入
import axios from 'axios'
1、用代理解决跨域问题
以前,我们在发送请求的时候,经常会遇到一个很重要的问题:跨域!
在我以前的学习中,基本上都需要操作后端服务器代码才能解决跨域的问题,配置请求头,利用 script,这些都需要后端服务器的配合,因此我们前端需要自己解决这个问题的话,就需要这个技术了:代理。
在说代理之前,先谈谈为什么会出现跨域?
这个应该是源于浏览器的同源策略。所谓同源(即指在同一个域)就是两个页面具有相同的协议,主机和端口号, 当一个请求 URL 的协议、域名、端口三者之间任意一个与当前页面 URL 不同即为跨域 。
也就是说 xxx:3000
和 xxx:4000
会有跨域问题,xxx:3000
与 abc:3000
有跨域问题
那接下来我们采用配置代理的方式去解决这个问题
2、全局代理
第一种方法,我把它叫做全局代理,因为它直接将代理配置在了配置文件 package.json
中
"proxy":"http://localhost:5000"
// "proxy":"请求的地址"
这样配置代理时,首先会在原请求地址上访问,如果访问不到文件,就会转发到这里配置的地址上去请求
创建 button
按钮并绑定事件 getStudentData
<button onClick={this.getStudentData}>点我获取学生数据</button>
编写 getStudentData
函数, get
学生数据
getStudentData = () => {
axios.get('http://localhost:3000/students').then(
response => { console.log('成功了', response.data) },
error => { console.log('失败了', error) }
)
}
⚠️注意:这样配置代理时,首先会在抓原请求地址(3000端口)上访问,如果访问不到文件,就会转发到我们配置的地址(5000端口)上去请求
按下按钮,我们就得到了学生数据
优缺点
优点:配置简单,前端请求资源可以不加任何前缀
缺点:不能配置多个代理
工作方式:上述方法配置代理,当请求了 3000 不存在的资源,那么该请求会转发给 5000(优先匹配前端资源)
所以这种方法我们使用时会有一些问题:
它会先向我们请求的地址,也就是这里的 3000 端口下请求数据,如果在 3000 端口中存在我们需要访问的文件,会直接返回,不会再去转发
同时因为这种方式采用的是全局配置的关系,导致只能转发到一个地址,不能配置多个代理
3、单独配置
这是自己起的名字,这种配置方式,可以给多个请求配置代理,非常不错
它的工作原理和全局配置是一样的,但是写法不同
首先 我们需要在React
脚手架 src
目录下,创建代理配置文件 setupProxy.js
⚠️注意:这个文件只能叫这个名字,脚手架在启动的时候,会自动执行这些文件
然后 在 setupProxy.js
里配置代理
配置一个代理的完整代码如下:
const {createProxyMiddleware} = require('http-proxy-middleware')
module.exports = function(app) {
app.use(
createProxyMiddleware('/api1',{ //遇见/apil前缀的请求就会触发
target: 'http://localhost:5000', //配置转发目标地址
changeOrigin: true, //控制服务器接收到的请求头中host字段的值
pathRewrite: {'^/api1': ''} //去除请求前缀址(必须配置)
}),
createProxyMiddleware('/api2', {
target: 'http://localhost:5001',
changeOrigin: true,
pathRewrite: {'^/api1': ''}
})
)
}
在我们的 React
脚手架 App.jsx
里再创建一个 button
按钮并绑定事件 getCarData
<button onClick={this.getCarData}>点我获取汽车数据</button>
重写 getStudentData
函数,并新编写 getCarData
函数
getStudentData = () => {
axios.get('http://localhost:3000/api1/students').then(
response => { console.log('成功了', response.data) },
error => { console.log('失败了', error) }
)
}
getCarData = () => {
axios.get('http://localhost:3000/api2/students').then(
response => { console.log('成功了', response.data) },
error => { console.log('失败了', error) }
)
}
路径前缀分别为 api1 和 api2 ,启动 React 脚手架
按下按钮,我们就得到了 server1.js 的学生数据和 server2.js 的汽车数据
4、配置具体的代理规则
1.首先我们需要引入这个 http-proxy-middleware
中间件
const {createProxyMiddleware} = require('http-proxy-middleware')
2.然后需要导出一个对象,这里建议使用函数 function 并暴露它,使用对象的话兼容性不大好
module.exports = function(app) {}
3.然后需要在 app.use
中配置代理规则,首先 createProxyMiddleware
接收的第一个参数 ‘/api1’ 是需要转发的请求,我的理解是一个标志的作用,当有这个标志的时候,预示着我们需要采用代理,例如 /api1
,就需要在我们 axios
的请求路径中,加上 /api1
,这样所有添加了 /api1
前缀的请求都会转发到这里
module.exports = function(app) {
app.use(createProxyMiddleware('/api1'))
}
4.第二个参数接受的是一个对象,用于配置代理。
createProxyMiddleware('/api1', {
target: 'http://localhost:5000', //配置转发目标地址
changeOrigin: true, //控制服务器接收到的请求头中host字段的值
pathRewrite: {'^/api1': ''} //去除请求前缀址,重写请求路径(必须配置)
}
target
属性用于配置转发目标地址,也就是我们数据的地址
hangeOrigin
属性用于控制服务器收到的响应头中 host 字段的值,可以理解成伪装效果
pathRewrite
属性用于去除请求前缀(重写请求路径)
因为我们通过代理请求时,需要在请求地址前添加一个标志,但是实际的地址是不存在这个标志的,所以我们一定要去除这个前缀,这里采用的有点类似于正则替换的方式
优缺点
优点:可以配置多个代理,可以灵活的控制请求是否走代理
缺点:配置繁琐,前端请求资源时必须加前缀
四、GitHub 搜索案例
本部分主要介绍 React 学习中 Github
搜索案例,这个案例主要涉及到了 Axios
发送请求,数据渲染以及一些中间交替效果的实现
个人感觉在做完 TodoList
案例之后,这个案例会很轻松,只是多加了一个 Loading
效果的实现思路,以及一些小细节的完善
1、实现静态组件
1.拆分组件
和之前的 TodoList 案例一样,我们需要先实现静态组件
在实现静态组件之前,我们还需要拆分组件,这个页面的组件,我们可以将它拆成以下两个组件,第一个组件是 Search
,第二个是 List
2.实现静态组件
接下来我们需要将提前写好的静态页面,对应拆分到组件当中
首先,我们在 src
目录下,新建一个 components
文件夹,用于存放我们的组件,然后在文件夹下,新建 List
、Search
组件文件夹,再创建其下的 index.jsx
,index.css
文件,用于创建对应组件及其样式文件
其次,在 public
下创建 css
文件夹,放入 bootstrap.css
文件,记得引入 css
文件
Github搜索案例
├─ package.json
├─ public
│ ├─ css
│ │ ├─ bootstrap.css
│ ├─ favicon.ico
│ └─ index.html
├─ src
│ ├─ App.jsx
│ ├─ components
│ │ ├─ List
│ │ │ ├─ index.css
│ │ │ └─ index.jsx
│ │ └─ Search
│ │ └─ index.jsx
│ └─ index.js
└─ yarn.lock
最终目录结构如上
⚠️注意:各 jsx 编写的时候需要注意
- class 需要改成
className
- style 的值需要使用双花括号的形式
img
标签,一定要添加alt
属性表示图片加载失败时的提示a
标签要添加rel="noreferrer"
属性,不然会有大量的警告出现
展示页面效果:
2、axios 发送请求
在实现静态组件之后,我们需要通过向 github
发送请求,来获取相应的用户信息
首先在 Search
的 index.jsx
里,对 button
按钮绑定事件 search
<button onClick={this.search}>搜索</button>
编写 serach
函数,需要获取用户的输入以及发送网络请求
search = () => {
// 获取用户的输入
// 发送网络请求
)
}
在需要触发事件的 input 标签中,添加 ref 属性,这里采用 回调形式refs
<input ref={c => this.keyWordElement = c} type="text" placeholder="输入关键词点击搜索" />
在 search 函数中,我们通过连续的解构赋值,解构到 value
后,并将 value
重命名为 keyWord
const { keyWordElement: { value: keyWord } } = this
此时 value 被重命名为 keyWord
,获取到了 keyWord
值,接下来我们就需要发送请求了
console.log(keyWord) // 返回你输入框输入的内容
然后 在 setupProxy.js
里配置代理
配置一个代理的完整代码如下:
// setupProxy.js
const {createProxyMiddleware} = require('http-proxy-middleware')
module.exports = function(app) {
app.use(
createProxyMiddleware('/api1',{
target: 'http://localhost:5000',
changeOrigin: true,
pathRewrite: {'^/api1': ''}
}),
)
}
在 search 函数中,调用 axios.get
发送网络请求
axios.get(`https://localhost:3000/api1/search/users?q=${keyWord}`).then(
response => {console.log('成功了',response.data)},
error => {console.log('失败了', error)}
)
我们将 keyWord
接在请求地址的后面,来传递参数,以获得相关数据
因为配置了代理来解决跨域的问题,我们需要在请求地址前(端口号后面)加上启用代理的标志 /api1
⚠️注意:
- 此时我们站在 3000 向 3000 发送请求,因为 3000 有代理,所以转发给 5000 从而解决了跨域的问题
- 当我们站在 3000 向 3000 发送请求时(站的位置就是要发请求的地方),其实可以简写如下
axios.get(`/api1/search/users?q=${keyWord}`).then()
这样我们就能成功的获取到了数据
3、渲染数据
在获取到了数据之后,我们需要对数据进行分析,并将这些数据渲染到页面上
目前的学习知识中,Search 和 List 属于兄弟组件,没有办法进行直接的数据传递,因此可以将数据传递给 APP 再由 APP 转发给子组件
父子组件之间传递参数的方式,可以通过 state
和 props
实现
我们通过在父组件也就是 App.jsx
中设置状态
// 初始化状态,users初始值
state = { users: [] }
我们在 App.jsx
中添加 saveUsers
函数,这样可以将 Search
组件传递的参数,维护到 App 的状态中
// 保存users数据
saveUsers = (users) => {
this.setState({ users })
}
然后将这个函数通过 props 传递给 Search 组件
<Search saveUsers={this.saveUsers}/>
在 Search 组件的 serach 函数中,调用 saveUsers
函数,将 Search 组件传递的参数,维护在 App 的状态中
// Search inde.jsx
// 发送网络请求
axios.get(`/api1/search/users?q=${keyWord}`).then(
response => {this.props.saveUsers(response.data.items)},
error => {console.log('失败了', error)}
)
在 App 组件我们通过解构取出状态中的 users
const {users} = this.state
再将它通过 props 传递给 List 组件
<List users={users}/>
在 List 组件中,通过 map 遍历整个返回的数据,渲染用户数据
⚠️注意:我们获取到的用户个数是动态的,因此我们需要通过遍历的方式去实现
this.props.users.map((userObj) => {
return (
<div key={userObj.id} className="card">
<a rel="noreferrer" href={userObj.html_url} target="_blank">
<img alt="avatar" src={userObj.avatar_url} style={{ width: '100px' }} />
</a>
<p className="card-text">{userObj.login}</p>
</div>
)
})
⚠️注意:我们新建的 user 对象,一定要保证它的 id 的唯一性,用数据里的用户 id 作为 key={userObj.id}
同时将一些用户信息添加到其中,需要添加的信息有用户链接 html_url 、用户头像 avatar_url 、用户昵称 login
这样我们就能成功的渲染用户数据
4、增加交互
做到这里其实已经完成了一大半了,但是似乎少了点交互
- 第一次进入页面时 List 组件中的 欢迎使用字样
- 加载时的 Loading… 效果
- 在报错时应该 提示错误信息
这一些都预示着我们不能单纯的将用户数据直接渲染
我们需要添加一些判断,什么时候该渲染数据,什么时候渲染 loading,什么时候渲染 err
首先我们需要增加一些状态,来指示我们该渲染什么,比如
- 采用 isFrist 来判断页面是否第一次启动,初始值给 true,点击搜索后改为 false
- 采用 isLoading 来判断是否应该显示 Loading 动画,初始值给 false,在点击搜索后改为 true,在拿到数据后改为false
- 采用 err 来判断是否渲染错误信息,当报错时填入报错信息,初始值给空
// 初始化状态
state = {
users: [], // users初始化为数组
isFirst:true, // 是否为第一次打开页面
isLoading:false, // 标识是否处于加载中
err:'', // 存储请求相关错误信息
}
重写 saveUsers 函数方法为 updateAppState ,采用更新状态的方式,接收一个状态对象来更新数据
// 接收一个状态对象
updateAppState = (stateObj) => {
this.setState(stateObj)
}
⚠️注意:我们需要改变数据传递方式,这样就不用去指定什么时候更新什么,减少了很多不必要的函数声明
然后将这个函数通过 props 传递给 Search 组件
<Search updateAppState={this.updateAppState} />
在 Search 组件的 search 函数中,多次调用 updateAppState 函数
// Search/index.jsx
search = () => {
// 获取用户的输入
const { keyWordElement: { value: keyWord } } = this
// 发送请求前通知App更新状态
this.props.updateAppState({ isFirst: false, isLoading: true })
// 发送网络请求
axios.get(`/api1/search/users?q=${keyWord}`).then(
response => {
// 请求成功后通知App更新状态
this.props.updateAppState({ isLoading: false, users: response.data.items })
},
error => {
// 请求失败通知App更新状态
this.props.updateAppState({ isLoading: false, err: error.message })
}
)
}
我们的状态更新是在 Search 组件中实现的,在点击搜索之后
- 发送请求前通知 App 更新状态 isFirst: false, isLoading: true
- 请求成功后通知 App 更新状态 isLoading: false, users: response.data.items
- 请求失败通知 App 更新状态 isLoading: false, err: error.message
在 App 组件给 List 组件传递数据时,我们可以采用批量传递的方式,将状态里的对象都传递给 List ,这样可以减少代码量
<List {...this.state} />
⚠️注意:此时之前的解构取出状态中的 users就可以删掉了
然后我们只需要在 List 组件中,判断这些状态的值,来显示即可,这里采用连续三目运算符
// List/index.jsx
// 对象解构
const { users, isFirst, isLoading, err } = this.props
// 判断
{
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="avatar" src={userObj.avatar_url} style={{ width: '100px' }} />
</a>
<p className="card-text">{userObj.login}</p>
</div>
)
})
}
连续三目运算符判断:
- 我们需要先判断是否第一次
- 再判断是不是正在加载
- 再判断有没有报错
- 通过层层判断,如果不是第一次、不是正在加载、没有报错,最后再渲染数据
以上就是 Github 搜索案例的实现过程
最终效果图:
五、消息订阅发布
在上一节写的 Github 案例中,我们采用的是 axios 发送请求来获取数据,同时我们需要将数据从 Search 组件传给 App,再由 App 组件将数据传给 List 组件,这个过程会显得多此一举。
同时我们要将 state 状态存放在 App 组件当中,但是这些 state 状态都是在 List 组件中使用的。而在 Search 组件中做的,只是更新这些数据。这样也会显得很没有必要,我们完全可以将 state 状态存放在 List 组件中,但是这样我们又会遇到技术难题,兄弟组件间的数据通信。
这里我们就学习一下如何利用 消息订阅发布 来解决 兄弟组件间的通信
1、消息发布订阅
我们先简单的说一下消息订阅和发布的机制
就拿我们平常订杂志来说,我们和出版社说我们要订一年的足球周刊,那每次有新的足球周刊,它都会寄来给你。
换到代码层面上,我们订阅了一个消息假设为 A,当另一个人发布了 A 消息时,因为我们订阅了消息 A ,那么我们就可以拿到 A 消息,并获取数据
那我们要怎么实现呢?
1.pubsub-js
我们需要先安装这个库
yarn add pubsub-js
引入这个库 ,哪里用哪里引入
import PubSub from 'pubsub-js'
2.订阅消息
我们通过 subscribe 来订阅消息,它接收两个参数,第一个参数是消息的名称,第二个是消息成功的回调,回调中也接受两个参数,一个是消息名称,一个是返回的数据
PubSub.subscribe('search',(msg,data)=>{
console.log(msg,data);
})
3.发布消息
我们采用 publish
来发布消息,用法如下
PubSub.publish('search',{name:'tom',age:18})
4.举例
比如在Github搜索案例中,将 App 组件中的所有状态和方法全部去除
在Search组件中重写 search 函数,发布消息
// Search/index.jsx
search = () => {
console.log('Search组件发布消息了')
PubSub.publish('atguigu',{name:'tom',age:18})
}
在List组件中,在componentDidMount()
钩子中订阅消息
componentDidMount(){
PubSub.subscribe('atguigu',(_,data)=>{
console.log('List组件收到数据了',data)
})
}
效果展示:
输入123按下搜索键,显示发布消息与订阅消息
2、重写Github搜索案例
1.去除App中的初始化状态和数据传递函数
有了消息订阅发布,我们可以将 App 组件中的所有状态和方法全部去除,因为本来就不是在 App 组件中直接使用这些方法的,App 组件只是一个中间媒介而已
2.初始化状态
之前 state 状态存放在 App 组件当中,但是这些 state 状态都是在 List 组件中使用的,所以我们将状态放在 List 组件里
在List组件中,初始化状态
// List/index.jsx
// 初始化状态,users初始值为数组
state = {
users: [], // users初始化为数组
isFirst: true, // 是否为第一次打开页面
isLoading: false, // 标识是否处于加载中
err: '', // 存储请求相关错误信息
}
3.重写search函数发布消息
在Search组件重写 search 函数,完整代码如下:
// Search/index.jsx
search = () => {
// 获取用户的输入
const { keyWordElement: { value: keyWord } } = this
// 发送请求前通知List更新状态
PubSub.publish('search',{ isFirst: false, isLoading: true })
// 发送网络请求
axios.get(`/api1/search/users?q=${keyWord}`).then(
response => {
// 请求成功后通知List更新状态
PubSub.publish('search',{ isLoading: false, users: response.data.items })
},
error => {
// 请求失败通知List更新状态
PubSub.publish('search',{ isLoading: false, err: error.message })
}
)
}
⚠️注意重写的部分:将数据的更新通过 publish 来传递
例如:在发送请求之前,我们需要出现 loading 字样
// 之前的写法
this.props.updateAppState({ isFirst: false, isLoading: true })
// 改为发布订阅方式
PubSub.publish('search',{ isFirst: false, isLoading: true })
4.页面挂载后订阅消息
在List组件中订阅这个消息即可,并将返回的数据用于更新状态即可
⚠️注意:这里写在 componentDidMount()
钩子里,页面一挂载就订阅消息,从而调用setState
,触发更新
// List/index.jsx
componentDidMount(){
PubSub.subscribe('search',(msg,stateObj)=>{
this.setState(stateObj)
})
}
5.页面卸载时取消订阅
在页面要卸载时,在 componentWillUnmount()
钩子里,通过 unsubscribe
来取消指定的订阅
以上就是 Github 搜索案例的改进过程
六、React 路由
1、什么是路由?
路由是根据不同的 URL 地址展示不同的内容或页面
在 SPA 应用中,大部分页面结果不改变,只改变部分内容的使用
- 一个路由就是一个映射关系
(key:value)
key
为路径,value
可能是function
或component
1.后端路由
理解:value 是 function,用来处理客户端提交的请求。
1、注册路由:router.get(path, function(req,res))
2、工作过程:当 node 接收到一个请求时,根据请求路径找到匹配的路由,调用路由中的函数来处理请求,返回响应数据
3、之前server.js本地服务器就使用了后端路由
2.前端路由
1、浏览器端路由,value
是 component
,用于展示页面内容。
2、注册路由:<Route path="/test" component={Test}>
3、工作过程:当浏览器的 path
变为 /test
时,当前路由组件就变为 Test
组件
2、路由的基本使用
react-router有三个库,一个专门给web人员使用,一个给native,还有一个any两者都行
这react-router-dom就是专门给web人员使用的库
1.划分布局
首先我们要明确好页面的布局 ,分好导航区、展示区
其中红色区域的header和导航区是一直存在的,而蓝色内容展示区需要切换显示
2.引入所需文件
要引入 react-router-dom 库,暴露一些属性 Link、BrowserRouter、Route
import { Link, BrowserRouter, Route } from 'react-router-dom'
要引入 Home
组件和 About
组件
import Home from './components/Home'
import About from './components/About'
3.导航区 Link 标签
在 APP 组件中,导航区的 <a>
标签改为 <Link>
标签
// 在React中靠路由链接实现切换组件
<Link className="list-group-item" to="/about">About</Link>
<Link className="list-group-item" to="/home">Home</Link>
⚠️注意:
1、<Link>
将 <a>
中的 herf
换成了 to
2、to
里面不写大小写因为不识别大小写
3、to
里不写 ./XXX.html
,而是写 /XXX
4.内容展示区 Route 标签
内容展示区现实的内容要放在 Home 组件和 About 组件中
// Home/index.jsx
<h3>我是Home的内容</h3>
// About/index.jsx
<h3>我是About的内容</h3>
而在 APP 组件中,我们需要用 Route 标签,来进行路径的匹配,从而实现不同路径的组件切换
// 注册路由
<Route path="/about" component={About} />
<Route path="/home" component={Home} />
当导航区的<Link>
标签被触发后,就会来进行路径匹配,切换不同路径的组件
5.路由器标签 BrowserRouter
这样之后我们还需要一步,加个路由器,在上面我们写了两组路由,同时还会报错指示我们需要添加 Router 来解决错误,这就是需要我们添加路由器来管理路由
⚠️注意:如果我们在 Link 和 Route 中分别用路由器管理,那这样是实现不了的,只有在一个路由器的管理下才能进行页面的跳转工作。
方法一(不推荐)
我们可以在 App 组件里 Link 和 Route 标签的外层标签采用 BrowserRouter 包裹
⚠️注意:但是这样当我们的路由过多向外扩展时,我们要不停的更改标签包裹的位置
方法二(推荐方式)
我们回到 src 目录下的 index.js 文件,将整个 组件标签用 BrowserRouter 标签去包裹,这样整个 App 组件都在一个路由器的管理下
6.路由组件和一般组件
在我们前面的内容中,我们是把 Home 和 About 当成是一般组件来使用,我们将它们写在了 src 目录下的 components 文件夹下,但引入它们的时候我们是通过 {Home} 来引用的,而不是 <Demo/>
。
这就涉及一般组件和路由组件的差异,其实 Home 和 About 是路由组件,要改一下
最重要的一点就是它们接收到的 props 不同
在一般组件中,如果我们不进行传递,就不会收到值;如果传递什么,就能收到什么
// App.jsx
<Header a={1}/>
// Header/index.jsx
console.log('Header组件收到的props是',this.props)
而对于路由组件而言,它会接收到 3 个固定属性 history 、location 以及 match
// About/index.jsx
console.log('About组件收到的props是',this.props)
两者收到的参数:
路由组件:接受到三个固定属性,history、location、match
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"
7.NavLink 标签
NavLink 标签是和 Link 标签作用相同的,但是它又比 Link 更加强大。
在前面的 demo 展示中,你可能会发现点击的按钮并没有出现高亮的效果,正常情况下我们给标签多添加一个 active 的类就可以实现高亮的效果
<Link className="list-group-item active" to="/about">About</Link>
而 NavLink 标签正可以帮助我们实现这一步
当我们选中某个 NavLink 标签时,就会自动的在类上添加一个 active 属性
<NavLink className="list-group-item" to="/about">About</NavLink>
上图可以看到右侧的元素类名在不断的切换,当然 NavLink 标签是默认的添加上 active 类,我们也可以改变它,在标签上添加一个属性 activeClassName
例如 activeClassName="aaa"
在触发这个 NavLink
时,会自动添加一个 aaa 类
这里我们改成自定义的 milo 类
<NavLink activeClassName="milo" className="list-group-item" to="/about">About</NavLink>
在 public 的 index.html 添加 milo 类 style
<style>
.milo{
background-color: rgb(209,137,4) !important;
color: white !important;
}
</style>
⚠️注意:由于 index.html
引入了 bootstrap.css
,它的优先级较高,所以自定义的 style 要加上 !important
8.NavLink 封装
在上面的 NavLink 标签中,我们可以发现我们每次都需要重复的去写 className 或者是 activeClassName ,这并不是一个很好的情况,代码过于冗余。
那我们是不是可以想想办法封装一下它们呢?
我们可以采用 MyNavLink 组件,对 NavLink 进行封装
首先我们需要新建一个 MyNavLink 组件
内容就是对 NavLink 进行封装
<NavLink className="list-group-item" {...this.props} />
在 MyNavLink 组件中引入 react-router-dom 库
import { NavLink } from 'react-router-dom'
在 App 组件中引入 MyNavLink 组件
import MyNavLink from './components/MyNavLink' // MyNavLink是一般组件
调用并传递参数
在 App 组件中调用 MyNavLink 组件时,直接写
<MyNavLink to="/home">home</MyNavLink>
⚠️注意:我们在标签体内写的内容都会成为一个 children 属性,因此我们在调用 MyNavLink 时,在标签体中写的内容,都会成为 props 中的一部分,从而能够实现参数传递
总结
- NavLink 可以实现路由链接的高亮,通过
activeClassName
指定样式名 - 标签体内容是一个特殊的标签属性
- 通过
this.props.children
可以获得标签体内容
七、React 路由传参
1、Switch 解决相同路径问题
首先我们看一段这样的代码
<Route path="/home" component={Home}></Route>
<Route path="/about" component={About}></Route>
<Route path="/about" component={About}></Route>
这是两个路由组件,在2,3行中,我们同时使用了相同的路径 /about
我们发现它出现了两个 about 组件的内容,那这是为什么呢?
其实是因为,Route 的机制,当匹配上了第一个 /about
组件后,它还会继续向下匹配,因此会出现两个 About 组件,这时我们可以采用 Switch 组件进行包裹
在使用 Switch 时,我们需要先从 react-router-dom 中暴露出 Switch 组件
import { Route,Switch } from 'react-router-dom'
用 Switch 组件包裹路由们
<Switch>
<Route path="/home" component={Home}></Route>
<Route path="/about" component={About}></Route>
<Route path="/about" component={About}></Route>
</Switch>
这样我们就能成功的解决掉这个问题了
2、 解决多级路由样式丢失的问题
⚠️注意:当我们将路径改写成 path="/milo/about"
这样的形式时,如果我们强制刷新页面,页面的 CSS 样式会消失。
{/* 在React中靠路由链接实现切换组件 */}
<MyNavLink to="/milo/home">Home</MyNavLink>
<MyNavLink to="/milo/about">About</MyNavLink>
{/* 注册路由 */}
<Switch>
<Route path="/milo/about" component={About} />
<Route path="/milo/home" component={Home} />
</Switch>
这是因为,我们在 src 下的 index.html 中,引入样式文件时用的是相对路径
<link rel="stylesheet" href="./css/bootstrap.css">
当我们使用二级路由的时候,会使得请求的路径发生改变,浏览器会向 localhost:3000/milo 下请求 css 样式资源,这并不是我们想要的,因为我们的样式存放于公共文件下的 CSS 文件夹中
将样式引入的路径改成绝对路径
在 src 下的 index.html 中,引入样式文件时用绝对路径
<!-- %PUBLIC_URL%代表public文件夹的路径 -->
<link rel="stylesheet" href="%PUBLIC_URL%/css/bootstrap.css">
引入样式文件时不带.
在 src 下的 index.html 中,引入样式文件时不带 .
<link rel="stylesheet" href="/css/bootstrap.css">
使用 HashRouter
之前我们在 src 目录下的 index.js 文件,将整个 <App/>
组件标签用 BrowserRouter 标签去包裹
这里我们换成 HashRouter 标签去包裹即可
// index.js
<HashRouter>
<App/>
</HashRouter>
3、路由的精准匹配和模糊匹配
路由的匹配有两种形式,一种是精准匹配一种是模糊匹配,React 中默认开启的是模糊匹配
模糊匹配可以理解为,在匹配路由时,只要有匹配到的就好了
精准匹配就是,两者必须相同
1.模糊匹配
默认使用的是模糊匹配:【输入的路径】必须包含【要匹配的路径】,且顺序要一致
一个模糊匹配的小例子:
{/* 在React中靠路由链接实现切换组件 */}
<MyNavLink to = "/home/a/b" >Home</MyNavLink>
这个标签输入的路由,我们可以拆分成 home => a => b
,将会根据这个先后顺序匹配路由
{/* 注册路由 */}
<Route path="/home"component={Home}/>
模糊匹配就可以匹配到上面的注册路由,因为【输入的路径 /home/a/b 】包含【要匹配的路径 /home 】
当要匹配的路由改成下面这样时,就会失败。
<Route path="/a" component={Home}/>
模糊匹配遵循最左前缀法则,它会按照第一个来匹配,如果第一个没有匹配上,那就会失败,这里的 /a 和 /home 没有匹配上,就不会继续往后找 /home/a
2.精准匹配
当注册路由时,用 exact 来开启精准匹配(一般不开启)
<Route exact={true} path="/home" component={Home}/>
当我们开启了精准匹配后,需要完全一样的值才能匹配成功
还有简写方式
<Route exact path="/home" component={Home}/>
4、重定向路由
在我们写好了这些之后,我们会发现,我们需要点击任意一个按钮,才会去匹配一个组件,这并不是我们想要的,我们想要页面一加载上来,默认的就能匹配到一个组件。
1.Redirecrt
这个时候我们就需要用 Redirecrt
进行默认匹配了。
{/* 注册路由 */}
<Switch>
<Route path="/about" component={About} />
<Route path="/home" component={Home} />
<Redirect to="/about" />
</Switch>
一般写在所有路由注册的最下方,当所有路由都无法匹配时,跳转到Redirect指定的路由 /about
这里当我们请求3000端口时,就会重定向到 /about 这样就能够实现我们想要的效果了
5、嵌套路由
嵌套路由也就是我们前面有提及的多级路由,嵌套路由包括了二级、三级…还有很多级路由,比如当我们需要在一个路由组件中添加两个路由组件,就是二级路由
1.划分布局
首先我们要明确好页面的布局 ,分好导航区、展示区
这里我们需要在 Home 组件的内容展示区内添加新的导航区和内容展示区
2.引入所需文件
要引入 react-router-dom 库,暴露一些属性 Switch、Route、Redirect
import {Route,Switch,Redirect} from 'react-router-dom'
创建 News 组件和 Message 组件,这里我们将 News 和 Message 放到 Home 中
src
├─ App.jsx
├─ components
├─ pages
│ ├─ Home
│ │ ├─ index.jsx
│ │ ├─ News
│ │ │ └─ index.jsx
│ │ └─ Message
│ │ └─ index.jsx
│ └─ About
└─index.js
引入 News 组件、 Message 组件和 MyNavLink 组件
import News from './News'
import Message from './Message'
import MyNavLink from '../../components/MyNavLink'
3.导航区 MyNavLink标签
在 Home 组件中,导航区的 <a>
标签改为 <MyNavLink>
标签
// 在React中靠路由链接实现切换组件
<MyNavLink to="/home/news">News</MyNavLink>
<MyNavLink to="/home/message">Message</MyNavLink>
⚠️注意:在这里我们需要使用嵌套路由的方式,才能完成匹配,匹配子路由时要写上父路由的 path 值
4.内容展示区 Route 标签
内容展示区的内容要放在 News 组件和 Message 组件中
// News/index.jsx
<ul>
<li>news001</li>
<li>news002</li>
<li>news003</li>
</ul>
// Message/index.jsx
<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>
而在 Home 组件中,我们需要用 Route 标签,来进行路径的匹配,从而实现不同路径的组件切换
{/* 注册路由 */}
<Switch>
<Route path="/home/news" component={News} />
<Route path="/home/message" component={Message} />
<Redirect to="/home/news" />
</Switch>
⚠️注意:在这里我们需要使用嵌套路由的方式,才能完成匹配,注册子路由时要写上父路由的 path
值
这里我们套上了 <Switch>
标签解决相同路径问题
这里我们用 Redirecrt
对 News
默认匹配,它先高亮
5.路由的匹配按照注册路由的顺序执行
React 中的路由注册是有顺序的。当我们匹配时,由于 Home 组件先注册路由,会先去找 /home
进行匹配,由于是模糊匹配,匹配成功。
然后会在 Home 组件里,再匹配输入的路由,从而找到 /home/news
进行匹配,因此找到 News 组件,进行匹配渲。
如果开启精确匹配的话,第一步的 /home/news
匹配 /home
就会卡住不动,这个时候就不会显示有用的东西了!
6、传递 params 参数
我们需要实现的效果是,点击消息列表,展示出消息的详细内容
这个案例实现的方法有三种,第一种就是传递 params 参数,由于我们所显示的数据都是从数据集中取出来的,因此我们需要有数据的传输给 Detail 组件
首先我们要明确好页面的布局 ,分好导航区、展示区
这里我们需要在 Message 组件的内容展示区内添加新的导航区和内容展示区
引入所需文件
要引入 react-router-dom 库,暴露一些属性 Link,Route
import {Link,Route} from 'react-router-dom'
创建 Detail 组件,这里我们将 Detail 放到 Message 中
src
├─ App.jsx
├─ components
├─ pages
│ ├─ Home
│ │ ├─ index.jsx
│ │ ├─ News
│ │ └─ Message
│ │ ├─ index.jsx
│ │ └─ Detail
│ │ └─ index.jsx
│ └─ About
└─index.js
引入 Detail 组件
import Detail from './Detail'
数据初始化
由划分布局的图可见,导航区显示消息列表,内容展示区展示详细内容的数据列表
将消息列表保存在 Message 组件的 state 状态中
// Message/index.jsx
state = {
messageArr: [
{ id: '01', title: '消息1' },
{ id: '02', title: '消息2' },
{ id: '03', title: '消息3' },
]
将详细内容的数据列表,保存在 Detail 组件的 DetailData 中
// Detail/index.jsx
const Detaildata = [
{id:'01',content:'你好,中国'},
{id:'02',content:'你好,tianyang'},
{id:'03',content:'你好,未来的自己'},
]
导航区 Link 标签
首先结构赋值,取出 state 中的数组
const { messageArr } = this.state
由于是消息列表,这里用 map 的方式将 state 中 messageArr 遍历并生成 <li>
在 Message 组件中,导航区的 <a>
标签改为 <Link>
标签
messageArr.map((msgObj) => {
return (
<li key={msgObj.id}>
<Link to={"/home/message/detail/"}>{msgObj.title}</Link>
</li>
)
})
⚠️注意点:
- 添加唯一值 key 来区分节点,这里用 id 作为 key
- 这里因为不需要高亮,所以用
<Link>
- 嵌套路由匹配子路由时要写上父路由的 path 值
内容展示区 Route 标签
内容展示区的内容要放在 Detail 组件中
// Detail/index.jsx
<ul>
<li>ID:???</li>
<li>TITLE:???</li>
<li>CONTENT:???</li>
</ul>
在 Message 组件中,用 Route 标签来进行路径的匹配
// Message/index.jsx 注册路由
<Route path="/home/message/detail" component={Detail}/>
⚠️注意:嵌套路由注册子路由时要写上父路由的 path 值
向路由组件传递params参数
我们通过将数据拼接在路由地址末尾,来实现数据的传递(携带参数)
// Message/index.jsx 向路由组件传递params参数
<Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link>
⚠️注意:需要采用模板字符串以及 $ +{}
的方式来进行数据的获取
如上,将消息列表的 id 和 title 拼接路由地址后面
声明接收 params 参数
在注册路由时,我们可以通过 :数据名
来接收数据(声明接收)
// Message/index.jsx 声明接收params参数
<Route path="/home/message/detail/:id/:title" component={Detail} />
如上,使用了 /:id/:title
成功的接收了由 Link 传递过来的 id 和 title 数据
这样我们既成功的实现了路由的跳转,又将需要获取的数据传递给了 Detail 组件
验证
我们在 Detail 组件中 render 下打印 this.props 来查看当前接收的数据情况
console.log(this.props)
我们可以发现,我们传递的数据被接收到了对象的 match 属性下的 params 中
接收参数
因此我们可以在 Detail 组件中获取到由 Message 组件传递来的 params 数据
在 Detail 组件 render 下解构赋值得到 params 数据中的 id 和 title
const { id, title } = this.props.match.params
并通过 params 数据中的 id 值,在详细内容的数据列表 DetailData 中查找出指定 id 的详细内容
const findResult = DetailData.find((detailObj) => {
return detailObj.id === id
})
这里 findResult 会返回得到指定 id 及其详细内容 content
console.log(findResult)
最后在renturn里把相应的数据替换并渲染即可
<ul>
<li>ID:{id}</li>
<li>TITLE:{title}</li>
<li>CONTENT:{findResult.content}</li>
</ul>
总结
1、路由链接(携带参数):<Link to='/demo/test/milo/18'>详情</Link>
2、注册路由(声明接收):<Route path="/demo/test/:name/:age" component={Test}/>
3、接收参数:const {id,title} = this.props.match.params
7、传递 search 参数
我们还可以采用传递 search 参数的方法来实现,传递参数前步骤都一样
向路由组件传递 search 参数
我们通过将数据拼接在路由地址末尾,来实现数据的传递(携带参数)
// Message/index.jsx 向路由组件传递search参数
<Link to={`/home/message/detail?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link>
⚠️注意:采用 ? 符号的方式来表示后面的为可用数据
如上,将消息列表的 id 和 title 拼接路由地址后面
无需声明,正常注册路由
采用 search 传递的方式,无需在 Route 中再次声明,可以在 Detail 组件中直接获取到
// Message/index.jsx search参数无需声明接收
<Route path="/home/message/detail" component={Detail}/>
验证
我们在 Detail 组件中 render 下打印 this.props 来查看当前接收的数据情况
console.log(this.props)
我们可以发现,我们的数据保存在了 location 对象下的 search 中
总结
1、路由链接(携带参数):<Link to='/demo/test?name=milo&age=18'>详情</Link>
2、注册路由(无需声明):<Route path="/demo/test" component={Test}/>
3、接收参数:const {search} = this.props.location
const {id,title} = qs.parse(search)
备注:获取到的 search 是 urlencoded 编码字符串,需要借助 qs 解析
8、传递 state 参数
向路由组件传递 state 参数
我们通过传递一个路由对象,来实现数据的传递(携带参数)
// Message/index.jsx 向路由组件传递state参数
<Link to={{pathname:'/home/message/detail',state:{id:msgObj.id,title:msgObj.title}}}>{msgObj.title}</Link>
⚠️注意:在 Link 中需要传递一个路由对象,包括一个跳转地址名 pathname 和一个数据对象 state
如上,将消息列表的 id 和 title 传递给 Detail 组件
无需声明,正常注册路由
采用 state 参数传递的方式,无需在 Route 中再次声明,可以在 Detail 组件中直接获取到
// Message/index.jsx state参数无需声明接收
<Route path="/home/message/detail" component={Detail}/>
验证
我们在 Detail 组件中 render 下打印 this.props 来查看当前接收的数据情况
console.log(this.props)
接收参数
解构赋值,取出我们所传递的数据 id 和 title
const { id, title } = this.props.location.state
成功的获取数据,并进行渲染,后面步骤一样
⚠️注意:解决清除缓存造成报错的问题,我们可以在获取不到数据的时候用空对象来替代,例如,
const { id, title } = this.props.location.state || {}
当获取不到 state 时,则用空对象代替
这里的 state 和状态里的 state 不是一个东西!!!
总结:
1、路由链接(携带参数):
<Link to={{pathname:'/demo/test',state:{id:'milo',age:18}}}>详情</Link>
2、注册路由(无需声明):<Route path="/demo/test" component={Test}/>
接收参数:const {id,title} = this.props.location.state
备注:用 <BrowserRouter>
时刷新可以保留住参数,但用 <HashRouter>
刷新参数消失
八、路由跳转
1、push 与 replace 模式
push
默认情况下,开启的是 push 模式,也就是说,每次点击跳转,都会向栈中压入一个新的地址,在点击返回时,可以返回到上一个打开的地址
replace
当我们在读消息的时候,有时候我们可能会不喜欢这种繁琐的跳转,我们可以开启 replace 模式,这种模式与 push 模式不同,它会将当前地址替换成点击的地址,也就是替换了新的栈顶
我们只需要在需要开启的链接上加上 replace
即可
<Link replace to={{ pathname: '/home/message/detail', state: { id: msgObj.id, title: msgObj.title } }}>{msgObj.title}</Link>
2、编程式路由导航
我们可以采用绑定事件的方式实现路由的跳转
我们在 Message 组件的 return 下,map 遍历里创建两个按钮,并在按钮上绑定 onClick 事件
<button onClick={()=> this.pushShow(msgObj.id,msgObj.title)}>push查看</button>
<button onClick={()=> this.replaceShow(msgObj.id,msgObj.title)}>replace查看</button>
当事件触发时,我们执行回调 pushShow 和 replaceShow
pushShow = (id,title) => {
// push跳转+携带params参数
this.props.history.push(`/home/message/detail/${id}/${title}`)
// push跳转+携带params参数
this.props.history.push(`/home/message/detail?id=${id}&title=${title}`)
// push跳转+携带state参数
this.props.history.push(`/home/message/detail`,{id,title})
}
replaceShow = (id,title) => {
// replace跳转+携带params参数
this.props.history.replace(`/home/message/detail/${id}/${title}`)
// replace跳转+携带query参数
this.props.history.replace(`/home/message/detail?id=${id}&title=${title}`)
// replace跳转+携带state参数
this.props.history.replace(`/home/message/detail`,{id,title})
}
⚠️注意事项:
1、回调函数接收两个参数,用来仿制默认的跳转方式,第一个是点击的 id 第二个是标题 title
2、我们在回调中,调用 this.props.history 对象下的 push 和 replace 方法
3、共有三种携带参数的方式,params、query 和 state,写法各不相同,注册路由、匹配路由以及接收参数也需要随之改变
go、goBack 和 goForward
我们可以借助 this.props.history
身上的 API 实现路由的跳转,例如 go
、goBack
、goForward
我们在 Message 组件的 return 下,注册路由 Route 标签后面,创建三个按钮并绑定相应的事件
<button onClick={this.back}>回退</button>
<button onClick={this.forward}>前进</button>
<button onClick={this.go}>go</button>
当事件触发时,我们执行回调 back 、forward 和 go
back = ()=>{this.props.history.goBack();}
forward = ()=>{this.props.history.goForward();}
go = ()=>{this.props.history.go(-2);}
我们在回调中,调用 this.props.history 对象下的 push 和 replace 方法
延迟跳转
想在 News 组件加一个延迟跳转,2秒后自动跳转到 Message
// News/index.jsx
componentDidMount(){
setTimeout(()=>{
this.props.history.push('/home/message')
},2000)
}
在 componentDidMount()
钩子下用一个定时器,2秒后调用 this.props.history.push
,实现跳转
3、withRouter
有这样一个需求:在Header组件里加三个按钮实现编程式路由跳转
创建按钮并绑定事件
我们在 Header 组件的 return 下,创建三个按钮并绑定相应的事件
<button onClick={this.back}>回退</button>
<button onClick={this.forward}>前进</button>
<button onClick={this.go}>go</button>
当事件触发时,我们执行回调 back 、forward 和 go
back = ()=>{this.props.history.goBack();}
forward = ()=>{this.props.history.goForward();}
go = ()=>{this.props.history.go(-2);}
我们在回调中,调用 this.props.history
对象下的 push 和 replace 方法
⚠️注意:发现报错,因为采用的是一般组件,没有 history 对象!!
当我们需要在页面内部添加回退前进等按钮时,由于这些组件大多通过一般组件的方式去编写,因此会遇到一个问题,无法获得 history 对象,这正是因为我们采用的是一般组件造成的。
withRouter 包装一般组件
我们可以利用 react-router-dom
下的 withRouter
函数来对我们导出的 Header 组件进行包装,这样我们就能获得一个拥有 history 对象的一般组件
我们需要对哪个组件包装就在哪个组件下引入
// Header/index.jsx
import { withRouter } from 'react-router-dom'
在最后导出对象时,用 withRouter 函数对 index 进行包装
// 原来的Header不暴露
class Header extends Component {
... ... ...
}
// 暴露用 withRouter 函数包装过的Header
export default withRouter(Header)
⚠️注意:
1、withRouter
可以加工一般组件,让一般组件具备路由组件所特有的API
2、withRouter
的返回值是一个新组件
这样就能让一般组件获得路由组件所特有的 API
4、BrowserRouter 和 HashRouter 的区别
1、底层实现原理不一样
BrowserRouter 是用的是 H5 的 history API,不兼容 IE9 及以下版本
这里的 history 和浏览器中的 history 有所不同,通过操作这些 API 来实现路由的保存等操作,但是这些 API 是 H5 中提出的,因此不兼容 IE9 以下版本。
HashRouter 使用的是 URL 的哈希值
我们可以理解为是锚点跳转,因为锚点跳转不会给服务器发请求但会保存历史记录,从而让 HashRouter 有了相关的前进后退操作,且不会将 # 符号后面的内容请求。兼容性更好!
2、url 表现形式不一样
BrowserRouter的路径中没有 # ,例如: localhost:3000/demo/test
HashRouter 的路径中包含 # ,例如: localhost:3000/#/demo/test
3、刷新后路由 state 参数的影响
BrowserRouter 没有任何影响,因为 state 保存在history 对象中,刷新不会丢失
BrowserRouter 是用的是 H5 的 history API,state 保存在history 对象中,所以刷新不会丢失
HashRouter 刷新回导致路由 state 参数的丢失
由于HashRouter 没有使用 history API,所以没有 history对象。HashRouter 可以用于解决一些路径错误相关的问题。
备注:BrowserRouter 用的比较多,但不兼容 IE9 及以下版本