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';
  • imrc:导入 React 和 Component。

    jsx
    import React, { Component } from 'react';
  • impt:导入 PropTypes。

    jsx
    import PropTypes from 'prop-types';
  • rcc:创建一个 React class 组件。

    jsx
    import React, { Component } from 'react';
    
    class FileName extends Component {
      render() {
        return <div></div>;
      }
    }
    
    export default FileName;
  • rfc:创建一个 React 函数组件。

    jsx
    import React from 'react';
    
    function FileName() {
      return <div></div>;
    }
    
    export default FileName;
  • rrd:创建一个 React Redux 组件。

    jsx
    import React, { Component } from 'react';
    import { connect } from 'react-redux';
    
    class FileName extends Component {
      render() {
        return <div></div>;
      }
    }
    
    const mapStateToProps = (state) => ({});
    
    export default connect(mapStateToProps)(FileName);

第一章:认识脚手架

一、什么是 React 脚手架?

在我们的现实生活中,脚手架最常用的使用场景是在工地,它是为了保证施工顺利的、方便的进行而搭建的,在工地上搭建的脚手架可以帮助工人们高校的去完成工作,同时在大楼建设完成后,拆除脚手架并不会有任何的影响。

在我们的 React 项目中,脚手架的作用与之有异曲同工之妙。

React 脚手架其实是一个工具帮我们快速的生成项目的工程化结构,每个项目的结构其实大致都是相同的,所以 React 给我提前的搭建好了,这也是脚手架强大之处之一,也是用 React 创建 SPA 应用(单页应用程序,只有一个 HTML 页面,不利于 SEO)的最佳方式。

总结:脚手架可以帮助我们快速的搭建一个项目结构。

二、为什么要用脚手架?

在之前学习 webpack 的过程中,每次都需要配置 webpack.config.js 文件,用于配置我们项目的相关 loader、plugin,这些操作比较复杂,但是它的重复性很高,而且在项目打包时又很有必要,那 React 脚手架就帮助我们做了这些,它不需要我们人为的去编写webpack 配置文件,它将这些配置文件全部都已经提前的配置好了。

三、怎么用 React 脚手架?

1. 使用步骤

首先确保安装了 npm 和 Node。

然后打开 cmd 命令行工具,全局安装 create-react-app

bash
npm i create-react-app -g

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

bash
create-react-app hello-react

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

bash
npm start

2. 脚手架项目结构

hello-react
├─ 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    // 页面性能分析文件
│  └─ 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 传递给子组件,子组件在合适的时候调用并传入实参。

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. 仅适用于类式组件

当想要给子类的子类传递数据时,马上想到 redux 的做法。这里介绍的 Context 我觉得也类似于 Redux。

需要引入一个 MyContext 组件,然后引用 MyContext 下的 Provider

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

Provider 标签包裹 A 组件内的 B 组件,并通过 value 值,将数据传递给子组件,这样以 A 组件为父代组件的所有子组件都能够接受到数据。

jsx
<Provider value={{ username, age }}>
    <B />
</Provider>

但是需要在使用数据的组件中引入 MyContext

jsx
static contextType = MyContext;

在使用时,直接从 this.context 上取值即可。

jsx
const {username, age} = this.context

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:pushStatereplaceState,原理类似于 Hash 实现。 用 H5 实现,单页路由的 URL 不会多出一个 # 号,这样会更加的美观。

点我查看代码
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 的老版本浏览器中也可以使用。

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

  // 隐藏所有的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

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 className="list-group-item" 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
<NavLink className="list-group-item" activeClassName="active" {...this.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.location 对象下的 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}`)
}

同时还可以借助 this.props.history 身上的 API 实现路由的跳转,例如 go(正负数)goBack()goForward()

3)withRouter

当需要在页面内部添加回退前进等按钮时,由于这些组件是一般组件的方式去编写,因此会遇到一个问题,无法获得 history 对象,这正是因为采用的是一般组件造成的。

只有路由组件才能获取到 history 对象。如何解决这个问题呢?

利用 react-router-dom 对象下的 withRouter 函数来对导出的 Header 组件进行包装,这样就能获得一个拥有 history 对象的一般组件。

需要对哪个组件包装就在哪个组件下引入。

jsx
// Header/index.jsx
import { withRouter } from 'react-router-dom'
// ......

class Header extends Component {
    // ......
}

// 在最后导出对象时,用 `withRouter` 函数对 index 进行包装
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)来阻止路由的改变。

Released under the MIT License.