React 脚手架
React 在 VSCode 中推荐安装的插件。
“ES7 React/Redux/GraphQL/React-Native snippets” 是一个非常流行的 VS Code 插件,它提供了一系列的代码片段,可以帮助你更快地编写 React、Redux、GraphQL 和 React Native 代码。常用的快捷键:
clg:控制台打印。
jsxconsole.log()imr:导入 React。
jsximport React from 'react';imrd:导入 ReactDOM。
jsximport ReactDOM from 'react-dom';impt:导入 PropTypes。
jsximport PropTypes from 'prop-types';imrc:导入 React 和 Component。
jsximport React, { Component } from 'react';rcc:创建一个 React class 组件。
jsximport React, { Component } from 'react' export default class FileName extends Component { render() { return <div>$2</div> } }rpc:以后实际工作中常常使用这个来进行类组件开发
jsximport React, { PureComponent } from 'react' export default class FileName extends PureComponent { render() { return <div>$2</div> } }rcredux
jsximport React, { Component } from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' export class FileName extends Component { static propTypes = { $2: $3, } render() { return <div>$4</div> } } const mapStateToProps = state => ({}) const mapDispatchToProps = {} export default connect( mapStateToProps, mapDispatchToProps, )(FileName)rce
jsximport React, { Component } from 'react' export class FileName extends Component { render() { return <div>$2</div> } } export default FileNamerpce
jsximport React, { PureComponent } from 'react' import PropTypes from 'prop-types' export class FileName extends PureComponent { static propTypes = {} render() { return <div>$2</div> } } export default FileNamerfc:创建一个 React 函数组件。
jsximport React from 'react' export default function $1() { return <div>$0</div> }rmc
jsximport React, { memo } from 'react' export default memo(function $1() { return <div>$0</div> })rmcp
jsximport React, { memo } from 'react' import PropTypes from 'prop-types' const $1 = memo(function $1(props) { return <div>$0</div> }) $1.propTypes = {} export default $1
第一章:认识脚手架
一、什么是 React 脚手架?
在我们的现实生活中,脚手架最常用的使用场景是在工地,它是为了保证施工顺利的、方便的进行而搭建的,在工地上搭建的脚手架可以帮助工人们高效的去完成工作,同时在大楼建设完成后,拆除脚手架并不会有任何的影响。
在我们的 React 项目中,脚手架的作用与之有异曲同工之妙。
React 脚手架其实是一个工具帮我们快速的生成项目的工程化结构,每个项目的结构其实大致都是相同的,所以 React 给我提前的搭建好了,这也是脚手架强大之处之一,也是用 React 创建 SPA 应用(单页应用程序,只有一个 HTML 页面,不利于 SEO)的最佳方式。
总结:脚手架可以帮助我们快速的搭建一个项目结构。
二、为什么要用脚手架?
在之前学习 webpack 的过程中,每次都需要配置 webpack.config.js 文件,用于配置我们项目的相关 loader、plugin,这些操作比较复杂,但是它的重复性很高,而且在项目打包时又很有必要,那 React 脚手架就帮助我们做了这些,它不需要我们人为的去编写 webpack 配置文件,它将这些配置文件全部都已经提前的配置好了。
三、怎么用 React 脚手架?
1. 使用步骤
首先确保安装了 Node 和 npm。
1)然后打开 cmd 命令行工具,全局安装 create-react-app。
npm i -g create-react-app2)然后可以新建一个文件夹用于存放项目。在新建的文件夹下执行:
create-react-app [project-name]3)再在生成好的 hello-react 文件夹中执行:
npm start提示
React 目前可以说是处在动荡年代,新版特性与旧版功能弃用同时进行。在 2025 年 2 月 14 日 React 发布了逐步淘汰 Create React App,以后创建工程的方式采用框架创建新的 React 应用。
npx create-next-app@latest [project-name] [options]options 取值如下:
| 选项 | 描述 |
|---|---|
-h 或 --help | 显示所有可用选项 |
-v 或 --version | 输出版本号 |
--ts 或 --typescript | 初始化为 TypeScript 项目(默认) |
--js 或 --javascript | 初始化为 JavaScript 项目 |
--use-npm | 明确告诉 CLI 使用 npm 引导应用程序 |
--use-pnpm | 明确告诉 CLI 使用 pnpm 引导应用程序 |
--use-yarn | 明确告诉 CLI 使用 Yarn 引导应用程序 |
2. 脚手架项目结构
[project-name]
├─ node_modules/
├─ 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 # 页面性能分析文件
│ ├─ serviceWorker.js # 默认帮助我们写好的注册 PWA (渐进式 Web 应用) 相关的代码
│ └─ setupTests.js # 组件单元测试文件
├─ .gitignore # 自动创建本地仓库
├─ package.json # 相关配置文件
├─ README.md
└─ yarn.lock再介绍一下 public 目录下的 index.html 文件中的代码意思。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- 直接采用 `%PUBLIC_URL%` 原因是 `webpack` 配置好了,它代表的意思就是 `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>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>3. 第一个脚手架应用
1)public 目录
保持 public 中的 index.html 不变。
2)src 目录
修改 src 下面的 APP.js 以及 index.js 文件。
App.js: 【注意:创建好的组件一定要暴露出去】
// 创建外壳组件APP
import React from 'react'
class App extends React.Component{
render(){
return (
<div>Hello world</div>
)
}
}
export default Appindex.js: 【主要的作用其实就是将 App 这个组件渲染到页面上】
// 引入核心库
import React from 'react'
import ReactDOM from 'react-dom'
// 引入组件
import App from './App'
ReactDOM.render(<App />, document.getElementById("root"))这样在重新启动应用,就成功了。
不建议这样直接将内容放入 App 组件中,尽量还是用内部组件。
我们编写一个 Hello 组件:
import React,{Componet} from 'react'
export default class Hello extends Componet{
render() {
return (
<h1>Hello world</h1>
)
}
}在 App 组件中,进行使用。
import React,{Componet} from 'react'
import Hello from './Hello'
class App extends Component{
render(){
return (
<div>
<Hello />
</div>
)
}
}这样的结果和前面是一样的。
企业常见约束
① 在 src 目录下创建 components 文件夹,用来存放除了 APP 组件的其他组件。
② 在 components 文件夹下创建以组件名为文件夹名字的文件夹,里面存放 index.js 和 index.css(导入的时候只需要写到组件文件夹即可,其他的 Node.js 会补全的)。
③ 最好组件使用 jsx 文件后缀。
4. 样式冲突
当组件逐渐增多起来的时候,我们发现,组件的样式也是越来越丰富,这样就很有可能产生两个组件中样式名称有可能会冲突,就会根据引入 App 这个组件的先后顺序,后面的会覆盖前面的。
为了避免这样的样式冲突,我们采用下面的形式:
1)将 css 文件名修改:hello.css ==> hello.module.css
2)引入并使用的时候改变方式
import React,{Component} from 'react'
import hello from './hello.module.css' // 引入的时候给一个名称
export default class Hello extends Component {
render() {
return (
<h1 className={hello.title}>Hello</h1> // 通过大括号进行调用
)
}
}第二章:脚手架配置代理
React 本身只关注于页面,并不包含发送 Ajax 请求的代码,所以一般都是集成第三方的包,或者自己封装的。自己封装的话,比较麻烦,而且也可能考虑不全。
常用的有两个库,一个是 JQuery,一个是 axios。
① JQuery 比较重,因为 Ajax 服务也只是它这个库里的一小块功能,它主要做的还是 DOM 操作,而 React 自己就能操作 DOM,所以不推荐使用。
② axios 这个就比较轻,而且采用 Promise 风格,代码的逻辑会相对清晰,推荐使用。
因此这里采用 axios 来发送客户端请求。
以前,在发送请求的时候,经常会遇到一个很重要的问题:跨域!

在以前的学习中,基本上都需要操作后端服务器代码才能解决跨域的问题,配置请求头 …… 这些都需要后端服务器的配合,因此我们前端需要自己解决这个问题的话,就需要这个技术了:代理。
在说代理之前,先谈谈为什么会出现跨域?
这个应该是源于浏览器的同源策略。所谓同源(即指在同一个域)就是两个页面具有相同的协议,主机和端口号, 当一个请求 URL 的协议、域名、端口三者之间任意一个与当前页面 URL 不同即为跨域 。
也就是说 xxx:3000和 xxx:4000 会有跨域问题,xxx:3000 与 abc:3000 有跨域问题。
那接下来我们采用配置代理的方式去解决这个问题。
一、全局代理
它直接将代理配置在了配置文件 package.json 中。
"proxy":"http://localhost:5000" // "proxy":"请求的地址"这样配置代理时,首先会在原请求地址上访问,如果访问不到文件,就会转发到这里配置的地址上去请求。

我们需要做的就是在我们的请求代码中,将请求的地址改到转发的地址即可。
但是这样会有一些问题,它会先向我们请求的地址,也就是这里的 3000 端口下请求数据,如果在 3000 端口中存在我们需要访问的文件,会直接返回,不会再去转发。
因此这就会出现问题,同时因为这种方式采用的是全局配置的关系,导致只能转发到一个地址,不能配置多个代理。
二、单独配置
这种配置方式,可以给多个请求配置代理,非常不错。
第一步:创建代理配置文件
在 src 目录下,创建代理配置文件 setupProxy.js。
注意:这个文件只能叫这个名字,脚手架在启动的时候,会自动执行这些文件。
第二步:编写 setupProxy.js 配置具体代理规则
1)引入 http-proxy-middleware 中间件。然后需要导出一个对象,这里建议使用函数,使用对象的话兼容性不大好。
2)在 app.use 中配置我们的代理规则。首先 proxy 接收的第一个参数是需要转发的请求,当有这个标志的时候,预示着需要采用代理。例如 /api1 ,就需要在我们 axios 的请求路径中,加上 /api1 ,这样所有添加了 /api1 前缀的请求都会转发到这。
3)第二个参数接受的是一个对象,用于配置代理。
- target 属性用于配置转发目标地址,也就是数据的地址。
- changeOrigin 属性用于控制服务器收到的请求头中 host 字段,可以理解为一个伪装效果,为 true 时,收到的 host 就为请求数据的地址。
- pathRewrite 属性用于去除请求前缀,因为通过代理请求时,需要在请求地址前添加一个标志,但是实际的地址是不存在这个标志的,所以一定要去除这个前缀。
配置一个代理的完整代码如下:
// 引入中间件
const proxy = require('http-proxy-middleware')
// 导出一个函数
module.exports = function(app) {
app.use(
proxy('/api1', { // api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000)
target: 'http://localhost:5000', // 配置转发目标地址(能返回数据的服务器地址)
/*
changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:3000
changeOrigin默认值为false,但我们一般将changeOrigin值设为true
*/
changeOrigin: true, // 控制服务器接收到的请求头中host字段的值
pathRewrite: {'^/api1': ''} // 去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置)
}),
// 还可以继续写规则......
proxy('/api2', {
target: 'http://localhost:5001',
changeOrigin: true,
pathRewrite: {'^/api2': ''}
})
)
}第三章:组件间通信
一、父给子传
props 属性。
二、子给父传
1. 回调函数
父组件把一个函数通过 props 传递给子组件,子组件在合适的时候调用并传入实参。
1)函数式组件实现
在父组件中定义一个回调函数:
function Parent() {
const handleChildData = (data) => {
console.log('Received from child:', data);
// 处理从子组件接收到的数据
};
return <Child onDataFromChild={handleChildData} />;
}在子组件中,通过 props 接收这个函数并在适当的时候调用它:
function Child({ onDataFromChild }) {
const sendDataToParent = () => {
const data = 'Some data from child';
onDataFromChild(data);
};
return <button onClick={sendDataToParent}>Send data to parent</button>;
}这样,当子组件中的按钮被点击时,就会调用父组件传递的函数,并将数据传递给父组件。
2)类组件实现
父组件
import React from 'react';
import ChildComponent from './ChildComponent';
class ParentComponent extends React.Component {
handleChildData = (data) => {
console.log('Received from child:', data);
// 处理从子组件接收到的数据
}
render() {
return (
<div>
<h1>Parent Component</h1>
<ChildComponent onDataFromChild={this.handleChildData} />
</div>
);
}
}
export default ParentComponent;子组件
import React from 'react';
class ChildComponent extends React.Component {
sendDataToParent = () => {
const data = 'Some data from child';
this.props.onDataFromChild(data);
}
render() {
return (
<div>
<h2>Child Component</h2>
<button onClick={this.sendDataToParent}>Send data to parent</button>
</div>
);
}
}
export default ChildComponent;2. 消息订阅发布
说明
也适用于兄弟间传递。任意组件传递。
消息订阅和发布的机制:订阅了一个消息假设为 A,当另一个人发布了 A 消息时,因为我们订阅了消息 A ,那么我们就可以拿到 A 消息,并获取数据。
那要怎么实现呢?
1)安装 pubsub-js 库
yarn add pubsub-js2)引入这个库
import PubSub from 'pubsub-js'3)订阅消息
通过 subscribe 来订阅消息,它接收两个参数。第一个参数是消息的名称,第二个是消息成功的回调,回调中也接受两个参数,一个是消息名称,一个是返回的数据。
PubSub.subscribe('search',(msg,data)=>{
console.log(msg, data);
})4)发布消息
采用 publish 来发布消息,用法如下。
PubSub.publish('search',{name:'tom',age:18})三、父给后传
1. Context API
1)类组件实现
当想要给子类的子类传递数据时,马上想到 redux 的做法,这里在介绍一种方法。
首先,创建一个 ThemeContext。
// ThemeContext.js
import React from 'react';
{/*
React.createContext(defaultValue): defaultValue 是当组件在往顶层查找过程中没有找到对应的 Provider,那么就使用默认值 */}
const ThemeContext = React.createContext({
theme: 'light',
toggleTheme: () => {},
});
export default ThemeContext;然后,创建一个 ThemeProvider 组件。
// ThemeProvider.js
import React from 'react';
import ThemeContext from './ThemeContext';
class ThemeProvider extends React.Component {
constructor(props) {
super(props);
this.state = {
theme: 'light',
};
}
toggleTheme = () => {
this.setState(prevState => ({
theme: prevState.theme === 'light' ? 'dark' : 'light',
}));
}
render() {
return (
<ThemeContext.Provider
value={{
theme: this.state.theme,
toggleTheme: this.toggleTheme,
}}
>
{this.props.children}
</ThemeContext.Provider>
);
}
}
export default ThemeProvider;现在,创建一个使用主题的组件。
// ThemedButton.js
import React from 'react';
import ThemeContext from './ThemeContext';
class ThemedButton extends React.Component {
render() {
return (
<ThemeContext.Consumer>
{
({theme, toggleTheme}) => (
<button
onClick={toggleTheme}
style={{
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff',
padding: '10px',
border: 'none',
cursor: 'pointer'
}}
>
Toggle Theme
</button>
)
}
</ThemeContext.Consumer>
);
}
}
export default ThemedButton;最后,在主应用中使用这些组件:
// App.js
import React from 'react';
import ThemeProvider from './ThemeProvider';
import ThemedButton from './ThemedButton';
class App extends React.Component {
render() {
return (
<ThemeProvider>
<div style={{padding: '20px'}}>
<h1>Theme Toggler Example</h1>
<ThemedButton />
</div>
</ThemeProvider>
);
}
}
export default App;还有一种方式取数据,但不常用。在类组件中声明 static contextType = XXX; 时,React 会自动为这个类组件设置一个名为 context 的属性。其中,XXX 是之前使用 React.createContext() 创建的变量。
import React from 'react';
// 创建一个 Context
const UserContext = React.createContext({
username: 'Guest',
age: 0
});
// 使用 Context 的类组件
class UserInfo extends React.Component {
static contextType = UserContext;
render() {
const { username, age } = this.context;
return (
<div>
<h2>User Information</h2>
<p>Username: {username}</p>
<p>Age: {age}</p>
</div>
);
}
}
// 另一个使用相同 Context 的类组件
class Greeting extends React.Component {
static contextType = UserContext;
render() {
const { username } = this.context;
return <h1>Welcome, {username}!</h1>;
}
}
// 父组件
class App extends React.Component {
state = {
user: { username: 'Alice', age: 25 }
};
render() {
return (
<UserContext.Provider value={this.state.user}>
<div>
<Greeting />
<UserInfo />
<button onClick={() => this.setState({ user: { username: 'Bob', age: 30 } })}>
Change User
</button>
</div>
</UserContext.Provider>
);
}
}
export default App;2)函数式组件
由于函数式组件没有自己的 this,所以不能通过 this.context 来获取数据。
这里就需要从 Context 身上暴露出一个 Consumer。
const { Provider, Consumer} = MyContext;然后通过 value 取值即可。
function C() {
return (
<div>
<h3>我是C组件,我要从A接收到数据</h3>
<Consumer>
{
(value) => {
{/* 基于 context 值进行渲染 */}
return `${value.username},年龄是${value.age}`;
}
}
</Consumer>
</div>
);
}第四章:路由
说明
此处是旧版 react 路由(React Router 5)。
一、相关概念
1. SPA
单页 Web 应用(single page web application, SPA)。整个应用只有一个完整的页面。
点击页面中的链接不会刷新页面,只会做页面的局部更新。
数据都需要通过 ajax 请求获取,并在前端异步展现。
2. 路由的原理
1)实现方式
history
前端路由主要依靠的是 history,也就是浏览器的历史记录。history 是 BOM 对象下的一个属性。在 H5 中新增了一些操作 history 的 API。
浏览器的历史记录就类似于一个栈的数据结构,前进就相当于入栈,后退就相当于出栈。并且历史记录上可以采用 listen 来监听请求路由的改变,从而判断是否改变路径。
在 H5 中新增了 createBrowserHistory 的 API ,用于创建一个 history 栈,允许我们手动操作浏览器的历史记录。新增 API:pushState、replaceState,原理类似于 Hash 实现。 用 H5 实现,单页路由的 URL 不会多出一个 # 号,这样会更加的美观。
最精简版本
点我查看代码
// 路由配置
const routes = {};
// 注册路由函数
const route = (path, cb) => routes[path] = cb;
// 使用
route('/', () => document.body.innerHTML = '<h1>首页</h1>');
route('/about', () => document.body.innerHTML = '<h1>关于</h1>');
// 监听后退
window.onpopstate = () => routes[location.pathname]?.();
// 跳转
const push = (path) => {
history.pushState(null, '', path);
routes[location.pathname]?.();
};
// 拦截点击
document.addEventListener('click', (e) => {
if (e.target.tagName === 'A') {
e.preventDefault();
push(e.target.pathname);
}
});下面例子通过一个 history 库来简化路由实现
点我查看代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>前端路由的基石_history</title>
</head>
<body>
<!-- 有 onclick 事件,不会跳转,只会调用函数 -->
<a href="https://www.baidu.com" onclick="return push('/test1') ">push test1</a><br><br>
<button onClick="push('/test2')">push test2</button><br><br>
<button onClick="replace('/test3')">replace test3</button><br><br>
<button onClick="back()"><= 回退</button>
<button onClick="forword()">前进 =></button>
<script type="text/javascript" src="https://cdn.bootcss.com/history/4.7.2/history.js"></script>
<script type="text/javascript">
// let history = History.createBrowserHistory() // 方法一:直接使用 H5 推出的 history 身上的 API
let history = History.createHashHistory() // 方法二:hash 值(锚点)
// 向浏览器的历史记录中放一些记录
function push (path) {
history.push(path)
return false
}
// replace 是替换栈顶的那条历史记录
function replace (path) {
history.replace(path)
}
function back() {
history.goBack()
}
function forword() {
history.goForward()
}
// 监听 url 路径的变化
history.listen((location) => {
console.log('请求路由路径变化了', location)
})
</script>
</body>
</html>hash
使用锚点(也称为哈希或者片段标识符)来实现一种简单的前端路由。这种方式的优点是兼容性好,即使在不支持 HTML5 History API 的老版本浏览器中也可以使用。
点我查看代码
<!-- 创建一些链接,点击这些链接会改变URL的哈希 -->
<a href="#home">Home</a>
<a href="#about">About</a>
<a href="#contact">Contact</a>
<!-- 创建一些div,我们会根据URL的哈希来显示或隐藏这些div -->
<div id="home">This is the home page.</div>
<div id="about">This is the about page.</div>
<div id="contact">This is the contact page.</div>
<script>
// 首先,隐藏所有的div
document.querySelectorAll('div').forEach(div => {
div.style.display = 'none';
});
// 然后,根据URL的哈希来显示对应的div
function route() {
// 获取URL的哈希
const hash = window.location.hash; // 会返回 URL 中 # 号后面的内容, 包括 # 号
// 隐藏所有的div
document.querySelectorAll('div').forEach(div => {
div.style.display = 'none';
});
// 显示对应的div
const div = document.querySelector(hash);
if (div) {
div.style.display = 'block';
}
}
// 当URL的哈希改变时,调用route函数
window.addEventListener('hashchange', route);
// 当页面加载时,调用route函数
window.addEventListener('load', route);
</script>2)BrowserRouter 和 HashRouter 的区别
底层实现原理不一样
对于 BrowserRouter 来说它使用的是 React 为它封装的 history API,这里的 history 和浏览器中的 history 有所不同噢!通过操作这些 API 来实现路由的保存等操作,但是这些 API 是 H5 中提出的,因此不兼容 IE9 以下版本。
对于 HashRouter 而言,它实现的原理是通过 URL 的哈希值(锚点跳转。因为锚点跳转会保存历史记录,从而让 HashRouter 有了相关的前进后退操作,HashRouter 不会将 # 符号后面的内容请求。兼容性更好!)。
地址栏的表现形式不一样
HashRouter 的路径中包含 #,例如 localhost:3000/#/demo/test。
刷新后路由 state 参数改变
在 BrowserRouter 中,state 保存在 history 对象中,刷新不会丢失。
HashRouter 则刷新会丢失 state。
3. 路由组件和一般组件
1)写法不一样
一般组件:<Demo>
路由组件:<Route path="/demo" component={Demo}/>
2)存放的位置一般不同
一般组件:components
路由组件:pages / views
3)接收的内容【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"二、路由实现
1. 基本路由
react-router-dom 专门给 web 人员使用的库。
引入 react-router-dom 库,暴露一些属性 Link、BrowserRouter ...
import { Link, BrowserRouter, Route } from 'react-router-dom'导航区的 a 标签改为 Link 标签。
<Link to="/about">About</Link>同时需要用 Route 标签来进行路径的匹配,从而实现不同路径的组件切换。
<Route path="/about" component={About}></Route>
<Route path="/home" component={Home}></Route>这样之后我们还需要一步,加个路由器,在上面我们写了两组路由,同时还会报错,指示我们需要添加 Router 来解决错误,这就是需要我们添加路由器来管理路由,如果我们在 Link 和 Route 中分别用路由器管理,那这样是实现不了的,只有在一个路由器的管理下才能进行页面的跳转工作。

在 Link 和 Route 标签的外层标签采用 BrowserRouter 包裹,但是这样当我们的路由过多时,我们要不停的更改标签包裹的位置,因此可以回到 App.jsx 目录下的 index.js 文件,将整个 App 组件标签采用 BrowserRouter 标签去包裹,这样整个 App 组件都在一个路由器的管理下。此外还有 HashRouter 组件。
// index.js
<BrowserRouter>
< App />
</BrowserRouter>NavLink 标签
<NavLink> 组件会在其指向的路由与当前 URL 匹配时,给自己添加一个"active"的类或样式。可以通过 activeStyle 属性来设置激活时的内联样式,或者通过 activeClassName 属性来设置激活时的类名。
import { NavLink } from 'react-router-dom';
// ...
<NavLink to="/home" activeStyle={{ color: 'red' }}>Home</NavLink>
<NavLink to="/about" activeClassName="active">About</NavLink>属性
<NavLink> 默认会在其 to 属性匹配当前 URL 的开头时就会激活(即部分匹配)。如果只想在完全匹配时才激活,需要添加 exact 属性。例如,<NavLink to="/" exact> 只会在 URL 完全等于"/"时激活,而不会在 URL 为"/about"或"/contact"时激活。
NavLink 封装
在上面的 NavLink 标签中,可以发现每次都需要重复的去写这些样式名称或者是 activeClassName,这并不是一个很好的情况,代码过于冗余。那是不是可以想想办法封装一下它们呢?
可以采用 MyNavLink 组件,对 NavLink 进行封装。
首先需要新建一个 MyNavLink 组件。return 一个结构。
const MyNavLink = (props) => {
return (
<NavLink className="list-group-item" activeClassName="active" {...props} />
);
};有一点非常重要的是,在标签体内写的内容都会成为一个 children 属性,因此在调用 MyNavLink 时,在标签体中写的内容,都会成为 props 中的一部分,从而能够实现。
接下来在调用时,直接写。
<MyNavLink to="/home">home</MyNavLink>即可实现相同的效果。
Switch 的使用
Route 的机制是当匹配上了第一个 <Route path="/xxx" component={xxx}></Route> 组件后,它还会继续向下匹配,如果下面还有一个 path 与现在一样的,那么就会一起展示。但是,一般情况下,一个路径只对应一个组件,怎么解决效率低的问题?
可以采用 Switch 对组件进行包裹。
<Switch>
<Route path="/home" component={Home}></Route>
<Route path="/about" component={About}></Route>
<Route path="/about" component={About}></Route>
</Switch>在使用 Switch 时,需要先从 react-router-dom 中暴露出 Switch 组件。
样式错误
- 使用 %PUBLIC_URL%。
- 引入样式文件时不带
.。- 使用 HashRouter。
2. 重定向路由
import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom';
function App() {
return (
<Router>
<Switch>
<Route exact path="/">
<HomePage />
</Route>
<Route path="/about">
<AboutPage />
</Route>
<Redirect from="/old-path" to="/new-path" />
<Route>
<NotFoundPage />
</Route>
</Switch>
</Router>
);
}
function HomePage() {
return <h2>Home</h2>;
}
function AboutPage() {
return <h2>About</h2>;
}
function NotFoundPage() {
return <h2>404 Not Found</h2>;
}
export default App;定义了三个路由:"/"(Home 页面)、"/about"(About 页面)和一个捕获所有未匹配路由的 404 页面。我们还定义了一个重定向,当 URL 为"/old-path"时,会自动重定向到"/new-path"。
请注意,<Switch> 组件会渲染与当前 URL 匹配的第一个 <Route> 或 <Redirect>。所以,我们把 404 页面的 <Route> 放在了最后,这样只有当所有其他路由都不匹配时,才会渲染 404 页面。
另外,404 页面的 <Route> 没有 path 属性,所以它会匹配所有的 URL。如果你想让它只匹配某些特定的 URL,你可以添加一个 path 属性,例如 <Route path="/404">。
还可以不写 from 属性,那么页面找不到指定路径时,就会重定向到 /home 页面。
<Redirect to="/home" />3. 嵌套路由
当点击 /home/news 的 Link 标签时,会先从最开始注册的路由找,找到 Home 组件,之后渲染。
<Switch>
<Route path="/about" component={About}></Route> {/* 不匹配 */}
<Route path="/home" component={Home}></Route> {/* 匹配,渲染此组件 */}
<Redirect to="/404" />
</Switch>紧接着到 Home 组件里,发现又注册了路由,继续匹配。
<Switch>
<Route path="/home/news" component={About}></Route> {/* 匹配,不在继续向下查找了 */}
<Route path="/home/message" component={Home}></Route>
</Switch>因此,不能在 <Route path="/home" component={Home}></Route> 标签上添加 exact 属性。因为添加上后,/home 与 /home/news 就不匹配了,直接到 /404 路由。
4. 编程式导航
1)路由跳转模式
push 与 replace 模式。
push (默认) 方法会在浏览器历史中添加一个新的记录,然后跳转到这个新的 URL。这就像用户点击了一个链接或者在地址栏输入了一个新的 URL。当用户点击浏览器的后退按钮时,他们会返回到上一个 URL。
replace 方法会替换浏览器历史中的当前记录,然后跳转到这个新的 URL。这就像用户在地址栏输入了一个新的 URL,然后按下了回车键。当用户点击浏览器的后退按钮时,他们会返回到上上一个 URL,因为当前的 URL 已经被替换掉了。
<NavLink replace to="/about" activeClassName="active">About</NavLink>2)实现编程式导航
采用绑定事件的方式实现路由的跳转。在按钮上绑定一个 onClick 事件,当事件触发时,执行一个 replaceShow 回调。这个函数接收两个参数,用来仿制默认的跳转方式,第一个是点击的 id、第二个是标题。
在回调中,调用 this.props.history 对象下的 replace 方法。
replaceShow = (id, title) => {
// params 参数
this.props.history.replace(`/home/message/detail/${id}/${title}`)
// search 参数
// this.props.history.replace(`/home/message/detail?id=${id}&title=${title}`)
// state 参数
// this.props.history.replace(`/home/message/detail`, {id, title})
}
replaceShow = (id, title) => {
// params 参数
this.props.history.push(`/home/message/detail/${id}/${title}`)
// 同样支持 search & state 参数
}同时还可以借助 this.props.history 身上的 API 实现路由的跳转,例如 go(正负数)、goBack() 、goForward()。
3)withRouter
当需要在页面内部添加回退前进等按钮时,由于这些组件是一般组件的方式去编写,因此会遇到一个问题,无法获得 history 对象,这正是因为采用的是一般组件造成的。
只有路由组件才能获取到 history 对象。如何解决这个问题呢?
利用 React Router 提供的一个高阶组件(HOC),是在 react-router-dom 对象下的 withRouter 函数,使用此函数来对导出的组件进行包装。它可以将一个普通组件包装成一个类似于路由组件的组件,使其能够访问到 history、location 和 match 这些 props。
// Header/index.jsx
import { withRouter } from 'react-router-dom' // 需要对哪个组件包装就在哪个组件下引入
// ......
class Header extends Component {
// ......
}
// 在最后导出对象时,用 `withRouter` 函数对 Header 进行包装
export default withRouter(Header);这样就能让一般组件获得路由组件所特有的 API。
三、路由传参
实现路由传参的路由,也叫动态路由。
1. 传递 params 参数
1)在 Link 标签的 to 上添加传递的参数。
<Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link>2)在 Route 标签的 path 上添加占位符 /:数据名。
<Route path="/home/message/detail/:id/:title" component={Detail} />那之后怎么在 detail 组件中拿到传递过来的数据呢?还记得前面说过 路由组件:接收到三个固定的属性【history, location, match】,这些属性是以 props 传递的,也就是说,在 props 属性中有这三个属性。打印 this.props 来查看当前接收的数据情况。

明白啦,使用 const { id, title } = this.props.match.params 就能拿到传递过来的数据了。
2. 传递 search 参数
1)在 Link 中采用 ? 符号的方式来表示后面的为可用数据。
<Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link>2)采用 search 传递的方式,无需在 Route 中再次声明,可以在 Detail 组件中直接获取到。

可以发现,数据都保存在了 location 对象下的 search 中,是以字符串的形式保存的。因此,可以引用一个库来进行转化 querystring。
import qs from 'querystring'这个库是 React 中自带的,它有两个方法,一个是 parse、一个是 stringify。基本用法如下:
const querystring = require('querystring');
// 解析查询字符串
let parsed = querystring.parse('key1=value1&key2=value2');
console.log(parsed); // 输出:{ key1: 'value1', key2: 'value2' }
// 格式化对象为查询字符串
let str = querystring.stringify({ key1: 'value1', key2: 'value2' });
console.log(str); // 输出:'key1=value1&key2=value2'那么,使用 parse 方法,将字符串转化为键值对形式的对象。
const qs = require('querystring');
const { search } = this.props.location
const { id, title } = qs.parse(search.slice(1))这样就能成功的获取数据,并进行渲染。
3. 传递 state 参数
采用传递 state 参数的方法,是最完美的一种方法,因为它不会将数据携带到地址栏上,采用内部的状态来维护。
<Link
to={
{
pathname: '/home/message/detail',
state: { id: msgObj.id, title: msgObj.title }
}
}>
{msgObj.title}
</Link>首先,需要在 Link 中注册跳转时,传递一个路由对象,包括一个跳转地址名,一个 state 数据,这样就可以在 Detail 组件中获取到这个传递的 state 数据。
注意:采用这种方式传递,无需声明接收。

可以在 Detail 组件中的 location 对象下的 state 中取出所传递的数据。
const { id, title } = this.props.location.state解决清除缓存造成报错的问题,我们可以在获取不到数据的时候用空对象来替代,例如:
const { id, title } = this.props.location.state || {}当获取不到 state 时,则用空对象代替。
这里的 state 和状态里的 state 有所不同。
四、路由守卫
如何使用 <Prompt> 组件或者路由的生命周期钩子(例如 history.block)来阻止路由的改变。
这个问题涉及到React Router中的路由拦截功能,主要用于在用户尝试离开当前页面时进行提示或阻止。让我详细解释一下:
1. <Prompt> 组件
<Prompt> 是React Router提供的一个组件,用于在用户试图离开当前页面时显示一个确认对话框。
import { Prompt } from 'react-router-dom';
function MyComponent() {
return (
<div>
<h1>My Component</h1>
<Prompt
when={formIsHalfFilledOut}
message="Are you sure you want to leave? You'll lose your unsaved changes."
/>
</div>
);
}当 formIsHalfFilledOut 为 true 时,如果用户尝试离开页面,会看到一个确认对话框。
2. history.block
这是一个更底层的 API,允许你以编程方式阻止导航。
import { useHistory } from 'react-router-dom';
function MyComponent() {
const history = useHistory();
useEffect(() => {
const unblock = history.block((tx) => {
if (window.confirm("Are you sure you want to leave this page?")) {
unblock();
return true;
}
return false;
});
return () => {
unblock();
};
}, [history]);
return <div>My Component</div>;
}使用 history.block 来注册一个回调函数。每当用户尝试导航时,这个函数都会被调用。如果函数返回 false,导航会被阻止。
