React之路由运用及封装

377 阅读11分钟

本文将带你了解到:路由原理、react-router的使用、企业级封装

一、路由机制

HASH路由

  • 改变页面的哈希值(#/xxx),主页面是不会刷新的
  • 根据不同的哈希值,让容器中渲染不同的内容「组件」

简单实现

<body>
    <nav class="nav-box">
        <a href="#/">首页</a>
        <a href="#/product">产品中心</a>
        <a href="#/personal">个人中心</a>
    </nav>
    <div class="view-box"></div>

    <script>
        // 获取渲染内容的容器
        const viewBox = document.querySelector('.view-box');
        // 构建一个路由匹配表:每当我们重新加载页面、或者路由切换(切换哈希值),都先到这个路由表中进行匹配;根据当前页面的哈希值,匹配出要渲染的内容(组件)!!
        const routes = [
            { path: '/', component: '首页的内容' },
            { path: '/product', omponent: '产品中心的内容' },
            { path: '/personal', component: '个人中心的内容' }
        ];

        // 路由匹配的办法
        const routerMatch = function routerMatch() {
            let hash = location.hash.substring(1),
                text = "";
            routes.forEach(item => {
                if (item.path === hash) text = item.component;
            });
            viewBox.innerHTML = text;
        };

        // 一进来要展示的是首页的信息,所以默认改变一下HASH值
        location.hash = '/';
        routerMatch();

        // 监测HASH值的变化,重新进行路由匹配
        window.onhashchange = routerMatch;
    </script>
</body>

history路由

  • 利用了H5中的HistoryAPI来实现页面地址的切换「可以不刷新页面」
  • 根据不同的地址,到路由表中进行匹配,让容器中渲染不同的内容「组件」

问题:我们切换的地址,在页面不刷新的情况下是没有问题的但是如果页面刷新,这个地址是不存在的,会报404错误!!

解决方案:此时我们需要服务器的配合:在地址不存在的情况下,也可以把主页面内容返回!!

简单实现

<body>
    <nav class="nav-box">
        <a href="/">首页</a>
        <a href="/product">产品中心</a>
        <a href="/personal">个人中心</a>
    </nav>
    <div class="view-box"></div>

    <script>
        const viewBox = document.querySelector('.view-box'),
            navBox = document.querySelector('.nav-box');

        // 点击A实现页面地址切换,但是不能刷新页面
        navBox.onclick = function (ev) {
            let target = ev.target;
            if (target.tagName === 'A') {
                ev.preventDefault(); // 阻止A标签页面跳转&刷新的默认行为
                history.pushState({}, "", target.href);
                routerMatch(); // 去路由匹配
            }
        };

        // 路由匹配的办法
        const routes = [
            { path: '/', component: '首页的内容' },
            { path: '/product', omponent: '产品中心的内容' },
            { path: '/personal', component: '个人中心的内容' }
        ];
        const routerMatch = function routerMatch() {
            let path = location.pathname,
                text = "";
            routes.forEach(item => {
                if (item.path === path) {
                    text = item.component;
                }
            });
            viewBox.innerHTML = text;
        };

        // 默认展示首页
        history.pushState({}, "", "/");
        routerMatch();

        // 监听popstate地址变化事件;此事件:执行go/forward/back等方法(或者点击前进后退按钮)可以触发,但是执行pushState/replaceState等方法无法触发!!
        window.onpopstate = routerMatch;
    </script>
</body>

二、react-router-dom V5

1.简单使用

import React from "react";
import { HashRouter, Route, Switch, Redirect, Link } from 'react-router-dom';

/* 导入组件 */
import A from './views/A';
import B from './views/B';
import C from './views/C';

/* 导航区域的样式 */
import styled from "styled-components";
const NavBox = styled.nav`
   a{
    margin-right: 10px;
    color: #000;
   }
`;

const App = function App() {
    /* 
    基于<HashRouter>把所有要渲染的内容包起来,开启HASH路由 
      + 后续用到的<Route>、<Link>等,都需要在HashRouter/BrowserRouter中使用
      + 开启后,整个页面地址,默认会设置一个 #/ 哈希值

    Link实现路由切换/跳转的组件
      + 最后渲染完毕的结果依然是A标签
      + 它可以根据路由模式,自动设定点击A切换的方式
    */
    return <HashRouter>
        {/* 导航部分 */}
        <NavBox>
            <Link to="/">A</Link>
            <Link to="/b">B</Link>
            <Link to="/c">C</Link>
        </NavBox>

        {/* 路由容器:每一次页面加载或者路由切换完毕,都会根据当前的哈希值,到这里和每一个Route进行匹配,把匹配到的组件,放在容器中渲染!! */}
        <div className="content">
            {/* 
             Switch:确保路由中,只要有一项匹配,则不再继续向下匹配
             exact:设置匹配模式为精准匹配
             */}
            <Switch>
                <Route exact path="/" component={A} />
                <Route path="/b" component={B} />
                <Route path="/c" render={() => {
                    // 当路由地址匹配后,先把render函数执行,返回的返回值就是我们需要渲染的内容
                    // 在此函数中,可以处理一些事情,例如:登录态检验....
                    let isLogin = true;
                    if (isLogin) {
                        return <C />;
                    }
                    return <Redirect to="/login" />
                }} />
                {/* 
                  // 放在最后一项,path设置※或者不写,意思是:以上都不匹配,则执行这个规则
                  <Route path="*" component={404组件} /> 
                  // 当然也可以不设置404组件,而是重定向到默认 / 地址:
                  <Redirect from="" to="" exact/>
                    + from:从哪个地址来
                    + to:重定向的地址
                    + exact是对from地址的修饰,开启精准匹配
                */}
                <Redirect to="/" />
            </Switch>
        </div>
    </HashRouter>;
};

export default App;

2.关于withRouter

/*
 只要在<HashRouter>/<BrowserRouter>中渲染的组件:
   我们在组件内部,基于useHistory/useLocation/useRouteMatch这些Hook函数,就可以获取history/location/match这些对象信息!!
   即便这个组件并不是基于<Route>匹配渲染的!!
 只有基于<Route>匹配渲染的组件,才可以基于props属性,获取这三个对象信息!!

 问题:如果当前组件是一个类组件,在<HashRouter>内,但是并没有经过<Route>匹配渲染,我们如何获取三个对象信息呢?
   解决方案:基于函数高阶组件,自己包裹一层进行处理!!
   在react-router-dom v5版本中,自带了一个高阶组件 withRouter ,就是用来解决这个问题的!!
 */
class HomeHead extends React.Component {
    render() {
        // console.log(this.props); //有三个对象信息了
        return <NavBox>
            {/* 
             NavLink VS Link
               都是实现路由跳转的,语法上几乎一样,区别就是:
               每一次页面加载或者路由切换完毕,都会拿最新的路由地址,和NavLink中to指定的地址「或者pathname地址」进行匹配
                 + 匹配上的这一样,会默认设置active选中样式类「我们可以基于activeClassName重新设置选中的样式类名」
                 + 我们也可以设置exact精准匹配
               基于这样的机制,我们就可以给选中的导航设置相关的选中样式!!
             */}
            <NavLink to="/a">A</NavLink>
            <NavLink to="/b">B</NavLink>
            <NavLink to="/c">C</NavLink>
        </NavBox>;
    }
}
export default withRouter(HomeHead);

3.传参方式

import React from "react";
import { useHistory } from 'react-router-dom';
import qs from 'qs';

const B = function B() {
    let history = useHistory();

    return <div className="box">
        B组件的内容
        <button onClick={() => {
            /* 
            传参方案一:问号传参 
              + 传递的信息出现在URL地址上:丑、不安全、长度限制
              + 信息是显式的,即便在目标路由内刷新,传递的信息也在
            // history.push('/c?id=100&name=zhufeng');
            history.push({
                pathname: '/c',
                // search存储的就是问号传参信息,要求是urlencoded字符串
                search: qs.stringify({
                    id: 100,
                    name: 'zhufeng'
                })
            });
            */

            /* 
            传参方案二:路径参数「把需要传递的值,作为路由路径中的一部分」
              + 传递的信息也在URL地址中:比问号传参看起来漂亮一些、但是也存在安全和长度的限制
              + 因为信息都在地址中,即便在目标组件刷新,传递的信息也在
            history.push(`/c/100/zhufeng`);
            */

            /*
             方案三:隐式传参 
               + 传递的信息不会出现在URL地址中:安全、美观,也没有限制
               + 在目标组件内刷新,传递的信息就丢失了
             */
            history.push({
                pathname: '/c',
                state: {
                    id: 100,
                    name: 'zhufeng'
                }
            });
        }}>按钮</button>
    </div>;
};
export default B;

const C = function C() {
    /* const location = useLocation();
    // console.log(location.search); //"?id=100&name=zhufeng"
    // 获取传递的问号参数信息
    let { id, name } = qs.parse(location.search.substring(1));
    console.log(id, name);
    // 也可以基于URLSearchParams来处理
    let usp = new URLSearchParams(location.search);
    console.log(usp.get('id'), usp); */

    /* // const match = useRouteMatch();
    // console.log(match.params); //=>{id:100,name:'zhufeng'}

    let params = useParams();
    console.log(params); //=>{id:100,name:'zhufeng'} */

    /* const location = useLocation();
    console.log(location.state); */

    return <div className="box">
        C组件的内容
    </div>;
};

4.企业级封装

路由表

/* 
配置路由表:数组,数组中每一项就是每一个需要配置的路由规则
   + redirect:true 此配置是重定向
   + from:来源的地址
   + to:重定向的地址
   + exact:是否精准匹配
   + path:匹配的路径
   + component:渲染的组件
   + name:路由名称(命名路由)
   + meta:{} 路由元信息「包含当前路由的一些信息,当路由匹配后,我们可以拿这些信息做一些事情...」
   + children:[] 子路由
   + ...
*/
import { lazy } from 'react';
import A from '../views/A';
import aRoutes from './aRoutes';

// 一级路由的理由表
const routes = [{
    redirect: true,
    from: '/',
    to: '/a',
    exact: true
}, {
    path: '/a',
    name: 'a',
    component: A,
    meta: {},
    children: aRoutes
}, {
    path: '/b',
    name: 'b',
    component: lazy(() => import('../views/B')),
    meta: {}
}, {
    path: '/c/:id?/:name?',
    name: 'c',
    component: lazy(() => import('../views/C')),
    meta: {}
}, {
    redirect: true,
    to: '/a'
}];
export default routes;

二级路由表

// A组件的二级路由表
import { lazy } from 'react';
const aRoutes = [{
    redirect: true,
    from: '/a',
    to: '/a/a1',
    exact: true
}, {
    path: '/a/a1',
    name: 'a-a1',
    component: lazy(() => import(/* webpackChunkName:"AChild" */'../views/a/A1')),
    meta: {}
}, {
    path: '/a/a2',
    name: 'a-a2',
    component: lazy(() => import(/* webpackChunkName:"AChild" */'../views/a/A2')),
    meta: {}
}, {
    path: '/a/a3',
    name: 'a-a3',
    component: lazy(() => import(/* webpackChunkName:"AChild" */'../views/a/A3')),
    meta: {}
}];
export default aRoutes;

全局路由入口router/index.js

import React, { Suspense } from "react";
import { Switch, Route, Redirect } from 'react-router-dom';

/* 调用组件的时候,基于属性传递路由表进来,我们根据路由表,动态设定路由的匹配规则 */
const RouterView = function RouterView(props) {
    // 获取传递的路由表
    let { routes } = props;
    return <Switch>
        {/* 循环设置路由匹配规则 */}
        {routes.map((item, index) => {
            let { redirect, from, to, exact, path, component: Component } = item,
                config = {};
            if (redirect) {
                // 重定向的规则
                config = { to };
                if (from) config.from = from;
                if (exact) config.exact = true;
                return <Redirect key={index} {...config} />;
            }
            // 正常匹配规则
            config = { path };
            if (exact) config.exact = true;
            return <Route key={index} {...config} render={(props) => {
                // 统一基于RENDER函数处理,当某个路由匹配,后期在这里可以做一些其它事情
                // Suspense.fallback:在异步加载的组件没有处理完成之前,先展示的Loading效果!!
                return <Suspense fallback={<>正在处理中...</>}>
                    <Component {...props} />
                </Suspense>;
            }} />;
        })}
    </Switch>;
};
export default RouterView;

三、react-router-dom V6

1.基本使用

import React from "react";
import { HashRouter, Routes, Route, Navigate } from 'react-router-dom';
import HomeHead from './components/HomeHead';
/* 导入需要的组件 */
import A from './views/A';
import B from './views/B';
import C from './views/C';
import A1 from './views/a/A1';
import A2 from './views/a/A2';
import A3 from './views/a/A3';

const App = function App() {
    return <HashRouter>
        <HomeHead />
        <div className="content">
            {/* 
            所有的路由匹配规则,放在<Routes>中;
            每一条规则的匹配,还是基于<Route>;
              + 路由匹配成功,不再基于component/render控制渲染的组件,而是基于element,语法格式是<Component/>
              + 不再需要Switch,默认就是一个匹配成功,就不在匹配下面的了
              + 不再需要exact,默认每一项匹配都是精准匹配
            原有的<Redirect>操作,被 <Navigate to="/" /> 代替!!
              + 遇到 <Navigate/> 组件,路由就会跳转,跳转到to指定的路由地址
              + 设置 replace 属性,则不会新增立即记录,而是替换现有记录
              + <Navigate to={{...}}/> to的值可以是一个对象:pathname需要跳转的地址、search问号传参信息
            */}
            <Routes>
                <Route path="/" element={<Navigate to="/a" />} />
                <Route path="/a" element={<A />}>
                    {/* v6版本中,要求所有的路由(二级或者多级路由),不在分散到各个组件中编写,而是统一都写在一起进行处理!! */}
                    <Route path="/a" element={<Navigate to="/a/a1" />} />
                    <Route path="/a/a1" element={<A1 />} />
                    <Route path="/a/a2" element={<A2 />} />
                    <Route path="/a/a3" element={<A3 />} />
                </Route>
                <Route path="/b" element={<B />} />
                <Route path="/c/:id?/:name?" element={<C />} />
                {/* 如果以上都不匹配,我们可以渲染404组件,也可以重定向到A组件「传递不同的问号参数信息」 */}
                <Route path="*" element={<Navigate to={{
                    pathname: '/a',
                    search: '?from=404'
                }} />} />
            </Routes>
        </div>
    </HashRouter>;
};

export default App;

2.Outlet

v5 是 RouterView

const A = function A() {
    return <DemoBox>
        <div className="menu">
            <Link to="/a/a1">A1</Link>
            <Link to="/a/a2">A2</Link>
            <Link to="/a/a3">A3</Link>
        </div>
        <div className="view">
            {/* Outlet:路由容器,用来渲染二级(多级)路由匹配的内容 */}
            <Outlet />
        </div>
    </DemoBox>;
};
export default A;

3.路由传参

import React from "react";
import { useNavigate } from 'react-router-dom';
import qs from 'qs';

/*
 在react-router-dom v6中 ,实现路由跳转的方式:
   + <Link/NavLink to="/a" > 点击跳转路由
   + <Navigate to="/a" /> 遇到这个组件就会跳转
   + 编程式导航:取消了history对象,基于navigate函数实现路由跳转
     import { useNavigate } from 'react-router-dom';
     const navigate = useNavigate();
     navigate('/c');
     navigate('/c', { replace: true });
     navigate({
        pathname: '/c'
     });
     navigate({
        pathname: '/c',
        search: '?id=100&name=zhufeng'
     });
     ...
 */

/*
 在react-router-dom v6中 ,即便当前组件是基于<Route>匹配渲染的,也不会基于属性,把history/location/match传递给组件!!想获取相关的信息,我们只能基于Hook函数处理!!
   + 首先要确保,需要使用“路由Hook”的组件,是在Router「HashRouter或BrowserRouter」内部包着的,否则使用这些Hook会报错!!
   + 只要在<Router>内部包裹的组件,不论是否是基于<Route>匹配渲染的 
     + 默认都不可能再基于props获取相关的对象信息了
     + 只能基于“路由Hook”去获取!!

 为了在类组件中也可以获取路由的相关信息:
 1. 稍后我们构建路由表的时候,我们会想办法:继续让基于<Route>匹配渲染的组件,可以基于属性获取需要的信息
 2. 不是基于<Route>匹配渲染的组件,我们需要自己重写withRouter「v6中干掉了这个API」,让其和基于<Route>匹配渲染的组件,具备相同的属性!!
 */

const B = function B() {
    const navigate = useNavigate();
    const handle = () => {
        /* 
        // 问号传参
        navigate({
            pathname: '/c',
            search: qs.stringify({
                id: 100,
                name: 'zhufeng'
            })
        }); 
        */

        /* // 路径参数
        navigate(`/c/100/zhufeng`); */

        // 隐式传参
        navigate('/c', {
            //历史记录池替换现有地址
            replace: true,
            //隐式传参信息
            state: {
                id: 100,
                name: 'zhufeng'
            }
        });
    };
    return <div className="box">
        B组件的内容
        <button onClick={handle}>按钮</button>
    </div>;
};
export default B;

/* 
在react-router-dom v6中,常用的路由Hook
+ useNavigate  -> 代替5中的 useHistory   :实现编程式导航
+ useLocation 「5中也有」:获取location对象信息  pathname/search/state….
+ useSearchParams「新增的」:获取问号传参信息,取到的结果是一个URLSearchParams对象
+ useParams「5中也有」:获取路径参数匹配的信息
———————
+ useMatch(pathname) -> 代替5中的 useRouteMatch「5中的这个Hook有用,可以基于params获取路径参数匹配的信息;但是在6中,这个Hook需要我们自己传递地址,而且params中也没有获取匹配的信息,用的就比较少了!!」
*/

const C = function C() {
    /* 
    const location = useLocation();
    // location.search:"?id=100&name=zhufeng"
    const usp = new URLSearchParams(location.search);
    console.log(usp.get('id'), usp.get('name')); 

    let [usp] = useSearchParams();
    console.log(usp.get('id'), usp.get('name'));
    */

    /* const params = useParams();
    console.log(params); //=>{id:100,name:'zhufeng'} */

    const location = useLocation();
    console.log(location.state);

    return <div className="box">
        C组件的内容
    </div>;
};

4.企业级封装

路由表配置

import { Navigate } from 'react-router-dom';
import { lazy } from 'react';
import A from '../views/A';

/* A版块的二级路由 */
const aRoutes = [{
    path: '/a',
    component: () => <Navigate to="/a/a1" />
}, {
    path: '/a/a1',
    name: 'a-a1',
    component: lazy(() => import(/* webpackChunkName:"AChild" */'../views/a/A1')),
    meta: {}
}, {
    path: '/a/a2',
    name: 'a-a2',
    component: lazy(() => import(/* webpackChunkName:"AChild" */'../views/a/A2')),
    meta: {}
}, {
    path: '/a/a3',
    name: 'a-a3',
    component: lazy(() => import(/* webpackChunkName:"AChild" */'../views/a/A3')),
    meta: {}
}];

/* 一级路由 */
const routes = [{
    path: '/',
    component: () => <Navigate to="/a" />
}, {
    path: '/a',
    name: 'a',
    component: A,
    meta: {},
    children: aRoutes
}, {
    path: '/b',
    name: 'b',
    component: lazy(() => import('../views/B')),
    meta: {}
}, {
    path: '/c/:id?/:name?',
    name: 'c',
    component: lazy(() => import('../views/C')),
    meta: {}
}, {
    path: '*',
    component: () => {
        return <Navigate to={{
            pathname: '/a',
            search: '?from=404'
        }} />;
    }
}];
export default routes;

全局路由入口router/index.js

import { Suspense } from 'react';
import routes from "./routes";
import { Routes, Route, useNavigate, useLocation, useParams, useSearchParams } from 'react-router-dom';

/* 统一渲染的组件:在这里可以做一些事情「例如:权限/登录态校验,传递路由信息的属性...」 */
const Element = function Element(props) {
    let { component: Component } = props;

    // 把路由信息先获取到,最后基于属性传递给组件:只要是基于<Route>匹配渲染的组件,都可以基于属性获取路由信息
    const navigate = useNavigate(),
        location = useLocation(),
        params = useParams(),
        [usp] = useSearchParams();

    // 最后要把Component进行渲染
    return <Component navigate={navigate} location={location} params={params} usp={usp} />;
};

/* 递归创建Route */
const createRoute = function createRoute(routes) {
    return <>
        {routes.map((item, index) => {
            let { path, children } = item;
            // 每一次路由匹配成功,不直接渲染我们设定的组件,而是渲染Element;在Element做一些特殊处理后,再去渲染我们真实要渲染的组件!!
            return <Route key={index} path={path} element={<Element {...item} />}>
                {/* 基于递归方式,绑定子集路由 */}
                {Array.isArray(children) ? createRoute(children) : null}
            </Route>;
        })}
    </>;
};

/* 路由容器 */
export default function RouterView() {
    return <Suspense fallback={<>正在处理中...</>}>
        <Routes>
            {createRoute(routes)}
        </Routes>
    </Suspense>;
};

/* 创建withRouter */
export const withRouter = function withRouter(Component) {
    // Component:真实要渲染的组件
    return function HOC(props) {
        // 提前获取路由信息,作为属性传递给Component
        const navigate = useNavigate(),
            location = useLocation(),
            params = useParams(),
            [usp] = useSearchParams();
        return <Component {...props} navigate={navigate} location={location} params={params} usp={usp} />;
    };
};