Skip to content

React 脚手架

React 在 VSCode 中推荐安装的插件。

“ES7 React/Redux/GraphQL/React-Native snippets” 是一个非常流行的 VS Code 插件,它提供了一系列的代码片段,可以帮助你更快地编写 React、Redux、GraphQL 和 React Native 代码。常用的快捷键:

  • clg:控制台打印。

    jsx
    console.log()
  • imr:导入 React。

    jsx
    import React from 'react';
  • imrd:导入 ReactDOM。

    jsx
    import ReactDOM from 'react-dom';
  • impt:导入 PropTypes。

    jsx
    import PropTypes from 'prop-types';
  • imrc:导入 React 和 Component。

    jsx
    import React, { Component } from 'react';
  • rcc:创建一个 React class 组件。

    jsx
    import React, { Component } from 'react'
    
    export default class FileName extends Component {
      render() {
        return <div>$2</div>
      }
    }
  • rpc:以后实际工作中常常使用这个来进行类组件开发

    jsx
    import React, { PureComponent } from 'react'
    
    export default class FileName extends PureComponent {
      render() {
        return <div>$2</div>
      }
    }
  • rcredux

    jsx
    import 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

    jsx
    import React, { Component } from 'react'
    
    export class FileName extends Component {
      render() {
        return <div>$2</div>
      }
    }
    
    export default FileName
  • rpce

    jsx
    import React, { PureComponent } from 'react'
    import PropTypes from 'prop-types'
    
    export class FileName extends PureComponent {
      static propTypes = {}
    
      render() {
        return <div>$2</div>
      }
    }
    
    export default FileName
  • rfc:创建一个 React 函数组件。

    jsx
    import React from 'react'
    
    export default function $1() {
      return <div>$0</div>
    }
  • rmc

    jsx
    import React, { memo } from 'react'
    
    export default memo(function $1() {
      return <div>$0</div>
    })
  • rmcp

    jsx
    import 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。

bash
npm i -g create-react-app

2)然后可以新建一个文件夹用于存放项目。在新建的文件夹下执行:

bash
create-react-app [project-name]

3)再在生成好的 hello-react 文件夹中执行:

bash
npm start

提示

React 目前可以说是处在动荡年代,新版特性与旧版功能弃用同时进行。在 2025 年 2 月 14 日 React 发布了逐步淘汰 Create React App,以后创建工程的方式采用框架创建新的 React 应用

bash
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. 脚手架项目结构

bash
[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 文件中的代码意思。

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: 【注意:创建好的组件一定要暴露出去】

jsx
// 创建外壳组件APP
import React from 'react'

class App extends React.Component{
  render(){
    return (
      <div>Hello world</div>
    )
  }
}

export default App

index.js: 【主要的作用其实就是将 App 这个组件渲染到页面上】

jsx
// 引入核心库
import React from 'react'
import ReactDOM from 'react-dom'
// 引入组件
import App from './App'

ReactDOM.render(<App />, document.getElementById("root"))

这样在重新启动应用,就成功了。

不建议这样直接将内容放入 App 组件中,尽量还是用内部组件。

我们编写一个 Hello 组件:

jsx
import React,{Componet} from 'react'

export default class Hello extends Componet{
  render() {
    return (
      <h1>Hello world</h1>
    )
  }
}

在 App 组件中,进行使用。

jsx
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)引入并使用的时候改变方式

jsx
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:3000xxx:4000 会有跨域问题,xxx:3000abc:3000 有跨域问题。

那接下来我们采用配置代理的方式去解决这个问题。

一、全局代理

它直接将代理配置在了配置文件 package.json 中。

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 属性用于去除请求前缀,因为通过代理请求时,需要在请求地址前添加一个标志,但是实际的地址是不存在这个标志的,所以一定要去除这个前缀。

配置一个代理的完整代码如下:

javascript
// 引入中间件
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)函数式组件实现

在父组件中定义一个回调函数:

jsx
function Parent() {
  const handleChildData = (data) => {
    console.log('Received from child:', data);
    // 处理从子组件接收到的数据
  };

  return <Child onDataFromChild={handleChildData} />;
}

在子组件中,通过 props 接收这个函数并在适当的时候调用它:

jsx
function Child({ onDataFromChild }) {
  const sendDataToParent = () => {
    const data = 'Some data from child';
    onDataFromChild(data);
  };

  return <button onClick={sendDataToParent}>Send data to parent</button>;
}

这样,当子组件中的按钮被点击时,就会调用父组件传递的函数,并将数据传递给父组件。

2)类组件实现

父组件

jsx
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;

子组件

jsx
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 库

bash
yarn add pubsub-js

2)引入这个库

javascript
import PubSub from 'pubsub-js'

3)订阅消息

通过 subscribe 来订阅消息,它接收两个参数。第一个参数是消息的名称,第二个是消息成功的回调,回调中也接受两个参数,一个是消息名称,一个是返回的数据。

javascript
PubSub.subscribe('search',(msg,data)=>{
  console.log(msg, data);
})

4)发布消息

采用 publish 来发布消息,用法如下。

javascript
PubSub.publish('search',{name:'tom',age:18})

三、父给后传

1. Context API

1)类组件实现

当想要给子类的子类传递数据时,马上想到 redux 的做法,这里在介绍一种方法。

首先,创建一个 ThemeContext。

jsx
// ThemeContext.js
import React from 'react';

{/*
  React.createContext(defaultValue): defaultValue 是当组件在往顶层查找过程中没有找到对应的 Provider,那么就使用默认值 */}
const ThemeContext = React.createContext({
  theme: 'light',
  toggleTheme: () => {},
});

export default ThemeContext;

然后,创建一个 ThemeProvider 组件。

jsx
// 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;

现在,创建一个使用主题的组件。

jsx
// 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;

最后,在主应用中使用这些组件:

jsx
// 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() 创建的变量。

jsx
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。

jsx
const { Provider, Consumer} = MyContext;

然后通过 value 取值即可。

jsx
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 不会多出一个 # 号,这样会更加的美观。

最精简版本

点我查看代码
javascript
// 路由配置
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 库来简化路由实现

点我查看代码
html
<!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()">&lt;= 回退</button>
  <button onClick="forword()">前进 =&gt;</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 的老版本浏览器中也可以使用。

点我查看代码
html
<!-- 创建一些链接,点击这些链接会改变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 ...

jsx
import { Link, BrowserRouter, Route } from 'react-router-dom'

导航区的 a 标签改为 Link 标签。

jsx
<Link to="/about">About</Link>

同时需要用 Route 标签来进行路径的匹配,从而实现不同路径的组件切换。

jsx
<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 组件。

jsx
// index.js
<BrowserRouter>
  < App />
</BrowserRouter>

NavLink 标签

<NavLink> 组件会在其指向的路由与当前 URL 匹配时,给自己添加一个"active"的类或样式。可以通过 activeStyle 属性来设置激活时的内联样式,或者通过 activeClassName 属性来设置激活时的类名。

jsx
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 一个结构。

jsx
const MyNavLink = (props) => {
  return (
    <NavLink className="list-group-item" activeClassName="active" {...props} />
  );
};

有一点非常重要的是,在标签体内写的内容都会成为一个 children 属性,因此在调用 MyNavLink 时,在标签体中写的内容,都会成为 props 中的一部分,从而能够实现。

接下来在调用时,直接写。

jsx
<MyNavLink to="/home">home</MyNavLink>

即可实现相同的效果。

Switch 的使用

Route 的机制是当匹配上了第一个 <Route path="/xxx" component={xxx}></Route> 组件后,它还会继续向下匹配,如果下面还有一个 path 与现在一样的,那么就会一起展示。但是,一般情况下,一个路径只对应一个组件,怎么解决效率低的问题?

可以采用 Switch 对组件进行包裹。

jsx
<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 组件。

样式错误

  1. 使用 %PUBLIC_URL%。
  2. 引入样式文件时不带 .
  3. 使用 HashRouter。

2. 重定向路由

jsx
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 页面。

jsx
<Redirect to="/home" />

3. 嵌套路由

当点击 /home/news 的 Link 标签时,会先从最开始注册的路由找,找到 Home 组件,之后渲染。

jsx
<Switch>
  <Route path="/about" component={About}></Route> {/* 不匹配 */}
  <Route path="/home" component={Home}></Route> {/* 匹配,渲染此组件 */}
  <Redirect to="/404" />
</Switch>

紧接着到 Home 组件里,发现又注册了路由,继续匹配。

jsx
<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 已经被替换掉了。

jsx
<NavLink replace to="/about" activeClassName="active">About</NavLink>
2)实现编程式导航

采用绑定事件的方式实现路由的跳转。在按钮上绑定一个 onClick 事件,当事件触发时,执行一个 replaceShow 回调。这个函数接收两个参数,用来仿制默认的跳转方式,第一个是点击的 id、第二个是标题。

在回调中,调用 this.props.history 对象下的 replace 方法。

jsx
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。

jsx
// 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 上添加传递的参数。

jsx
<Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link>

2)在 Route 标签的 path 上添加占位符 /:数据名

jsx
<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 中采用 ? 符号的方式来表示后面的为可用数据。

jsx
<Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link>

2)采用 search 传递的方式,无需在 Route 中再次声明,可以在 Detail 组件中直接获取到。

可以发现,数据都保存在了 location 对象下的 search 中,是以字符串的形式保存的。因此,可以引用一个库来进行转化 querystring。

jsx
import qs from 'querystring'

这个库是 React 中自带的,它有两个方法,一个是 parse、一个是 stringify。基本用法如下:

jsx
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 方法,将字符串转化为键值对形式的对象。

jsx
const qs = require('querystring');

const { search } = this.props.location
const { id, title } = qs.parse(search.slice(1))

这样就能成功的获取数据,并进行渲染。

3. 传递 state 参数

采用传递 state 参数的方法,是最完美的一种方法,因为它不会将数据携带到地址栏上,采用内部的状态来维护。

jsx
<Link
  to={
    {
      pathname: '/home/message/detail',
      state: { id: msgObj.id, title: msgObj.title }
    }
  }>
  {msgObj.title}
</Link>

首先,需要在 Link 中注册跳转时,传递一个路由对象,包括一个跳转地址名,一个 state 数据,这样就可以在 Detail 组件中获取到这个传递的 state 数据。

注意:采用这种方式传递,无需声明接收。

可以在 Detail 组件中的 location 对象下的 state 中取出所传递的数据。

jsx
const { id, title } = this.props.location.state

解决清除缓存造成报错的问题,我们可以在获取不到数据的时候用空对象来替代,例如:

jsx
const { id, title } = this.props.location.state || {}

当获取不到 state 时,则用空对象代替。

这里的 state 和状态里的 state 有所不同。

四、路由守卫

如何使用 <Prompt> 组件或者路由的生命周期钩子(例如 history.block)来阻止路由的改变。

这个问题涉及到React Router中的路由拦截功能,主要用于在用户尝试离开当前页面时进行提示或阻止。让我详细解释一下:

1. <Prompt> 组件

<Prompt> 是React Router提供的一个组件,用于在用户试图离开当前页面时显示一个确认对话框。

jsx
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,允许你以编程方式阻止导航。

jsx
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,导航会被阻止。

preview
图片加载中
预览

Released under the MIT License.