从基础到原理带你全面掌握React-Router

391 阅读5分钟

1、路由方式

React-Router 中的 3 个核心:

  • 路由器,比如 BrowserRouter 和 HashRouter
  • 路由,比如 Route 和 Switch
  • 导航,比如 Link、NavLink、Redirect

路由主要分为两种方式,一种是 history 模式,另一种是 Hash 模式;

  • history 模式下:www.xxx.com/home import { BrowserRouter } from 'react-router-dom'
  • hash 模式下: www.xxx.com/#/home import { HashRouter } from 'react-router-dom'

2、React-Router 基本构成

2.1 history,location,match

  • history 对象:history对象保存改变路由方法 push ,replace,和监听路由方法 listen 等。
  • location 对象:可以理解为当前状态下的路由信息,包括 pathname ,state 等。
  • match 对象:这个用来证明当前路由的匹配信息的对象。存放当前路由path 等信息。

2.2 exact

Route 可以加上exact,来进行精确匹配,精确匹配原则,pathname 必须和 Route 的 path 完全匹配,才能展示该路由信息。

2.3 使用react-router-config

使用react-router-config库来配置路由:

import { renderRoutes } from "react-router-config";
const RouteList = [
    {
        name: '首页',
        path: '/router/home',  
        exact:true,
        component:Home
    },
    {
        name: '列表页',
        path: '/router/list',  
        render:()=><List />
    },
    {
        name: '详情页',
        path: '/router/detail',  
        component:detail
    },
    {
        name: '我的',
        path:'/router/person',
        component:personal
    }
] 
function Index(){
    return <div>
        <Meuns/>
        { renderRoutes(RouteList) }
    </div> 
}

2.4 Switch

Switch 作用是先通过匹配选出一个正确路由 Route 进行渲染。

<Switch>
   <Route path='/home'  component={Home}  />
   <Route path='/list'  component={List}  />
   <Route path='/my'  component={My}  />
</Switch>

2.5 Redirect

Redirect 可以在路由不匹配情况下跳转指定某一路由。

<Switch>
   <Route path='/router/home'  component={Home}  />
   <Route path='/router/list'  component={List}  />
   <Route path='/router/my'  component={My}  />
   <Redirect from={'/router/*'} to={'/router/home' }  />
</Switch>

3、路由使用方式

3.1 路由组件 props

被 Route 包裹的路由组件 props 中会默认混入 history 等信息;

class Home extends React.Component{
    render(){
        return <div>
            <Children {...this.props}  />
        </div>
    }
}

3.2 withRouter

对于距离路由组件比较远的深层次组件,通常可以用 react-router 提供的 withRouter 高阶组件方式获取 histroy ,loaction 等信息。

import { withRouter } from 'react-router-dom'
@withRouter
class Home extends React.Component{
    componentDidMount(){
        console.log(this.props.history)
    }
    render(){
        return <div>
            { /* ....*/ }
        </div>
    }
}

3.3 useHistory 和 useLocation

对于函数组件,可以用 React-router 提供的自定义 hooks 中的 useHistory 获取 history 对象,用 useLocation 获取 location 对象。

import { useHistory ,useLocation  } from 'react-router-dom'
function Home(){
    const history = useHistory() /* 获取history信息 */
    const useLocation = useLocation() /* 获取location信息 */
}

4、使用技巧

4.1 路由跳转

路由跳转有声明式路由函数式路由两种。

  • 声明式:<NavLink to='/home' /> ,利用 react-router-dom 里面的 Link 或者 NavLink 。
  • 函数式:history.push('/home')

4.2 参数传递

url拼接

const name = '小明'
const age = '15'
history.push(`/home?name=${name}&age=${age}`)

state路由状态

const name = '小明'
const age = '15'
history.push({
    pathname:'/home',
    state:{
        name,
        age
    }
})

// 获取
const {state:{name ,age}} = props.location

动态路径参数路由

// 参数做路径
<Route path="/post/:id"  />

// 跳转
history.push('/post/'+id) // id为动态的文章id

5、路由扩展

5.1 嵌套路由

嵌套路由子路由一定要跟随父路由。比如父路由是 /home ,那么子路由的形式就是 /home/xxx ,否则路由页面将展示不出来。

/* 第二层嵌套路由 */
function Home(){
    return <div>
        <Route path='/home/home1' component={home1}   />
        <Route path='/home/home2' component={home2}  />
    </div>
}

/* 第一层父级路由 */
function Index(){
    return ( 
  	 <Switch>
        <Route path="/home" component={Home}  />
    </Switch>
   )
}

5.2 自定义路由

// 定义组件
function CustomRouter(props){
    return  xxx(判断条件)? <Route  {...props}  /> : null />
}

// 使用
<CustomRouter  path='/index' component={Index}  />

6、路由本质

路由本质就是对 url 进行改变和监听,来让某个 dom 节点显示对应的视图。

7、浏览器的 hash 与 history

我们先来了解一下浏览器对hash模式和history模式的处理。

7.1 hash模式

hash 的改变:我们可以通过 location 暴露出来的属性,直接去修改当前 URL 的 hash 值:

window.location.hash = 'index';

// 路径就会变为 xx网址/#index

hash 的感知:通过监听 “hashchange”事件,可以用 JS 来捕捉 hash 值的变化,进而决定我们页面内容是否需要更新:

// 监听hash变化,点击浏览器的前进后退会触发
window.addEventListener('hashchange', function(event){ 
    // 根据 hash 的变化更新内容
},false)

7.2 history 模式

使用history.pushState 和 history.replaceState:

history.pushState(data[,title][,url]); // 向浏览历史中追加一条记录

// history.pushState({}, '', 'index')
// 路径就会变为 xx网址/index
history.replaceState(data[,title][,url]); // 修改(替换)当前页在浏览历史中的信息

popstate用来控制浏览器后退:

window.addEventListener('popstate', function(e) {
  console.log(e)
});

8、BrowserRouter & HashRouter

react-router-dom 提供了 BrowserRouter 和 HashRouter 两个组件来实现应用的 UI 和 URL 同步。 我们先来看 BrowserRoute 组件。

// react-router-dom/modules/BrowserRouter.js
// ...
import { Router } from "react-router"
import { createBrowserHistory as createHistory } from "history"

class BrowserRouter extends React.Component {
  history = createHistory(this.props) // 创建一个 history 对象

  render() {
    return <Router history={this.history} children={this.props.children} />
  }
}
// ...

在来看一下HashRouter

// react-router-dom/modules/HashRouter.js
// 最主要的区别就是引用了 history 的不同函数,其他核心代码都是完全一致的。
import { createHashHistory as createHistory } from "history"

可以看出,Router 组件是 BrowserRoute 与 HashRouter 的底层组件,将 history 作为 props 传递给 Router 组件。

9、实现React-Router

9.1 Router

从上面Router 的核心原理就是通过 Provider 把 location 和 history 等路由关键信息传递给子组件,并且在路由发生变化的时候要让子组件可以感知到。

我们使用ts来实现一个简易版:

// 存储 history.listen 的回调函数
let listeners: Listener[] = [];
// listen方法
function listen(fn: Listener) {
  listeners.push(fn);
  return function() {
    listeners = listeners.filter(listener => listener !== fn);
  };
}


import React, { useState, useEffect, ReactNode } from 'react';
import { history, Location } from './history';
interface RouterContextProps {
  history: typeof history;
  location: Location;
}

export const RouterContext = React.createContext<RouterContextProps | null>(
  null,
);

export const Router: React.FC = ({ children }) => {
  const [location, setLocation] = useState(history.location);
  useEffect(() => {
    // 监听
    const unlisten = history.listen(location => {
      setLocation(location);
    });
    return unlisten;
  }, []);

  return (
    <RouterContext.Provider value={{ history, location }}>
      {children}
    </RouterContext.Provider>
  );
};
  1. 在组件创建前使用 history 库的 listen 函数注册监听 history 的改变。当切换路由时会更新 location 的值,会触发 render 重新渲染,Router 会向下传递history和location ,下层组件获取最新的内容来判断是否渲染;
  2. 在组件卸载时,也就是在useEffect return卸载;
  3. historylocation 属性以上下文方式传给后代组件。

9.2 Route

一句话概括 Route 的作用是:匹配路由并渲染组件,代码也很简单。

import { useLocation } from "./hooks";

interface RouteProps {
  path: string;
  children: JSX.Element;
}

export const Route = ({ path, children }: RouteProps) => {
  // 就是上层传递的localtion
  const { pathname } = useLocation();
  const matched = path === pathname;

  if (matched) {
    return children;
  }
  return null;
};

9.3 浏览器跳转功能

push方法主要是用作浏览器页面跳转:


function push(to: string, state?: State) {
  location = getNextLocation(to, state); 
  window.history.pushState(state, '', to); // 向浏览历史中追加一条记录
  listeners.forEach(fn => fn(location)); 
}

9.4 浏览器后退功能

popstate方法用于处理浏览器前进后退操作:

// 用于处理浏览器前进后退操作
window.addEventListener('popstate', () => {
  location = getLocation();
  listeners.forEach(fn => fn(location));
});

10、实现效果

示例代码:

import { Router, Route, useHistory } from "./Router";

const Home = () => {
  const history = useHistory();

  const go = (path: string) => {
    const state = { name: path };
    history.push(path, state);
  };

  return (
    <div>
      我是home页面 <button onClick={() => go("/list")}>list</button>
    </div>
  );
};

const List = () => <div>我是list页面</div>;


export default () => {
  return (
    <div>
      <Router>
        {/* <Links /> */}
        <Route path="/index">
          <Home />
        </Route>
        <Route path="/list">
          <List />
        </Route>
      </Router>
    </div>
  );
};

index页面效果:

image-20220125113252118.png

点击list按钮跳转到list页面效果:

image-20220125111003728.png

11、参考链接

1、深入探索前端路由,手写 react-mini-router

12、源码

地址:github.com/linhexs/min…