前端宝典之四:深度解析React-router原理并手写实现

一、本文主要内容:

1、react-router基本原理,手写实现一个react-router

2、react-router分类和主要hooks

3、处理路由加载失败情况的方法

4、错误边界组件在路由加载失败时的工作原理

react-router中有四种不同路由方式
1、BrowserRouter:官方推荐使用
2、HashRouter:不推荐
3、MemoryRouter:用于内嵌页面或者测试页面
4、StaticRouter:用于ssr

优缺点和使用场景的分析:

二、分类

1、BrowserRouter

通过路由器原生路由进行路由态管理,页面跳转通过pushStatepopState方法实现

1.1 优点

  • 提供了真实的 URL,用户体验更友好,与现代Web应用的标准URL格式一致,有利于搜索引擎优化(SEO)。
  • 支持浏览器的前进、后退按钮,用户可以自然地在历史记录中导航。
  • 官方最推荐的方式,因此,官方文档这样写的:We recommend all web projects use createBrowserRouter.

2.2 缺点

  • 在一些旧版本的浏览器中可能存在兼容性问题,因为它依赖HTML5的历史API。
  • 对于需要支持IE9及以下版本等老旧浏览器的项目不太适用,因为这些浏览器可能不完全支持HTML5的新特性。
  • 在服务器端,如果没有正确配置路由的处理,当用户直接访问一个子路由时,服务器可能会返回404错误,因为服务器不知道如何处理单页应用的路由。

2.3 使用场景

  • 适用于面向广大普通用户的现代化Web应用,对SEO有要求,且不需要考虑极端老旧浏览器兼容性的项目。
  • 当应用需要有一个与传统网站类似的URL结构,并且希望用户能够方便地使用浏览器的历史记录功能时,如大多数的电商网站、资讯类网站等。

2.4 代码实例

import {createBrowserRouter, RouterProvider, Link, Outlet,useLocation,useMatches,useNavigate} from "react-router-dom"
import {Home} from './components/home'
import {Page1} from './components/page1'
import {Page2} from './components/page2'

const router = createBrowserRouter([
  {
    path: "/",
    element: <div>Hello world</div>
  },
  {
    path: "/home",
    element: <Home/>,
    children: [
      {
        path: "page1",
        element: <Page1/>,
      },
      {
        path: "page2",
        element: <Page2 />,
      },
    ],
  },
  {
    path:'*',
    element:<div>404</div>
  }
])
function App() {
  return (
    <div>
      <RouterProvider router={router}></RouterProvider>
    </div>
  );
}

export default App;

2.5 注意事项

使用BrowserRouter,需要使用Ngnix做静态资源代理,另外需要注意404的情况,这里推荐添加try_files处理

Nginx try_files:

location / {
	try_files $uri /index.html
}

2、HashRouter

2.1 优点

  • 兼容性好,所有浏览器都支持哈希路由,包括老旧的浏览器。
  • 因为哈希的变化不会触发服务器请求,所以在用户离线状态下也能正常工作。
  • 服务器不需要做任何特殊的路由配置,因为哈希部分不会被发送到服务器,服务器只需要处理根路径的请求。

2.2 缺点

  • URL中带有哈希符号,看起来不太美观和自然,不太符合标准的URL格式。
  • 对于一些对URL格式有严格要求或者追求完美URL的应用来说,可能不太合适。
  • 官方不推荐

2.3 使用场景

  • 适用于需要兼容低版本浏览器的项目。
  • 在一些简单的单页应用或者原型开发阶段,HashRouter可以快速搭建起路由而无需过多考虑服务器配置问题。
  • 对于一些可能会在离线环境下使用的应用,如部分企业内部应用或者特定的移动应用场景。

3、MemoryRouter

3.1 优点

  • 非常适合在测试环境中使用,因为它可以模拟路由变化而不依赖于浏览器的实际历史记录或地址栏。
  • 在一些不需要与浏览器历史或地址栏交互的特定场景下,如某些嵌入式的Web组件或特定的UI组件库测试中很有用。

3.2 缺点

  • 由于路由状态存储在内存中,所以在页面刷新或重新加载时,路由状态会丢失。
  • 不适合需要持久化路由历史或与实际浏览器历史交互的应用。

3.3 使用场景

  • 主要用于React应用的单元测试和集成测试场景,帮助开发者测试路由相关的组件和逻辑。
  • 在一些特殊的无界面环境或者只在内存中运行的应用场景中可能会用到。
  • 低代码项目

4、StaticRouter

4.1 优点

  • 专为服务器端渲染设计,能够在服务器端根据请求的URL准确地渲染对应的组件。
  • 可以与服务器端的路由配置和逻辑紧密结合,确保服务器端和客户端的路由一致性。
  • 有助于提高首屏加载速度和SEO,因为服务器可以直接返回渲染好的HTML。

4.2 缺点

  • 仅在服务器端使用,不能在客户端独立运行。
  • 需要在服务器端进行额外的配置和开发工作来处理路由和渲染逻辑。

4.3 使用场景

  • 在进行服务器端渲染的React应用中是必不可少的。
  • 对于需要快速呈现内容给用户、提高搜索引擎索引效果以及对首屏加载速度有较高要求的应用,如大型企业级应用、内容丰富的新闻网站等在进行服务器端渲染时会使用。

三、Hooks

这里介绍几个经常遇到的hooks:

  • useLocation
  • useMatch
  • useNavigate
  • Data Api: lazy、loader、errorElement、useLoaderData

1、useLocation

官方解释:此钩子返回当前位置对象。如果您想在当前位置更改时执行一些功能,这可能很有用。
useLocationreact-router-dom 中的一个 React Hook,用于获取当前路由的位置信息。

以下是关于 useLocation 的详细介绍:

1.1 功能与用途

  1. 获取当前路由路径和查询参数

    • useLocation 返回一个包含当前 URL 信息的 location 对象。
    • 可以通过 location.pathname 获取当前路由的路径部分。例如,如果当前 URL 是 /about,那么 location.pathname 的值就是 /about
    • 使用 location.search 可以获取 URL 中的查询参数部分。例如,对于 URL /products?category=electronicslocation.search 的值就是 ?category=electronics
  2. 监听路由变化

    • 在函数组件中,当路由发生变化时,组件会重新渲染,再次调用 useLocation 可以获取到更新后的位置信息。
    • 这对于根据路由变化来更新组件的状态或执行特定的逻辑非常有用。例如,当用户切换到不同的路由页面时,根据不同的页面路径加载不同的数据或显示不同的内容。

1.2 示例用法

import { useLocation } from 'react-router-dom';

function MyComponent() {
    const location = useLocation();

    // 获取路径
    const path = location.pathname;

    // 解析查询参数
    const searchParams = new URLSearchParams(location.search);
    const category = searchParams.get('category');

    return (
        <div>
            <p>当前路径: {path}</p>
            {category && <p>当前分类: {category}</p>}
        </div>
    );
}

在上述示例中:

  • MyComponent 组件使用 useLocation 获取当前路由的位置信息。
  • 首先获取 pathname 来显示当前路径。
  • 然后通过 URLSearchParams 解析查询参数,获取 category 参数的值并进行显示(如果存在的话)。

1.3 与其他 React Router 功能的结合

  1. 结合路由守卫

    • 可以在路由守卫(例如通过自定义的高阶组件或 React RouterRoute 组件的 rendercomponent 属性中的逻辑)中使用 useLocation 来根据当前位置信息决定是否允许用户访问特定的路由。
    • 例如,检查用户是否已经登录,根据登录状态和当前路由来决定是否允许访问某些页面。
  2. useNavigate 配合使用

    • useNavigate 用于进行页面导航。可以结合 useLocation 获取当前位置信息,然后根据特定条件使用 useNavigate 进行路由切换。
    • 例如,当用户在某个页面完成特定操作后,根据当前位置和操作结果,导航到其他页面。

总的来说,useLocationreact-router-dom 中一个非常有用的 Hook,它为函数组件提供了一种方便的方式来获取当前路由的位置信息,从而可以根据这些信息实现各种与路由相关的功能和逻辑。

2、useMatch和useNavigate

这两个钩子很简单
useMatch用于判断是否匹配到了path
useNavigate用于navigate去到不同的路径

export const Page1 = ()=>{
    const match  = useMatch('/home/page1')
    const navigate = useNavigate()
    return <div>
        page 1 {match && 'Yes'}
        <button onClick={()=>{navigate('../page2')}}>navigate to page2</button>
    </div>
}

3、 lazy、loader、errorElement、useLoaderData

这几个是data-router数据路由的新功能,在6.4版本后出现

  • lazy 函数进行懒加载。
  • loader 是加载器函数,用于获取数据。
  • ErrorBoundary 和 ErrorFallback 组件用于处理加载错误,当出现错误时会显示错误信息和一个重试按钮。
  • useLoaderData用于获取由 loader 函数获取的数据。

代码实例

以下是一个使用 react-router-dom 中的 lazyloadererrorElementuseLoaderData 的代码实例:

首先,假设我们有两个页面组件:

Page1.jsx:

import React from 'react';

const Page1 = () => {
    const data = useLoaderData();
    return (
        <div>
            <h1>Page 1</h1>
            <p>Data from loader: {data.message}</p>
        </div>
    );
};

export default Page1;

Page2.jsx:

import React from 'react';

const Page2 = () => {
    return (
        <div>
            <h1>Page 2</h1>
        </div>
    );
};

export default Page2;

然后在路由模块中:

import React, { Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route, lazy, useLoaderData } from 'react-router-dom';
import { ErrorBoundary } from 'react-error-boundary';

// 懒加载页面组件
const Page1 = lazy(() => import('./Page1'));
const Page2 = lazy(() => import('./Page2'));

// 定义加载器函数
const page1Loader = async () => {
    // 模拟数据获取,例如从 API 获取数据
    const response = await fetch('https://example.com/api/data');
    const data = await response.json();
    return data;
};

// 定义错误边界组件,用于处理错误
const ErrorFallback = ({ error, resetErrorBoundary }) => (
    <div role="alert">
        <p>Something went wrong:</p>
        <pre style={{ whiteSpace: 'pre-wrap' }}>{error.message}</pre>
        <button onClick={resetErrorBoundary}>Try again</button>
    </div>
);

function App() {
    return (
        <Router>
            <ErrorBoundary FallbackComponent={ErrorFallback}>
                <Suspense fallback={<div>Loading...</div>}>
                    <Routes>
                        <Route
                            path="/page1"
                            element={<Page1 />}
                            loader={page1Loader}
                        />
                        <Route path="/page2" element={<Page2 />} />
                    </Routes>
                </Suspense>
            </ErrorBoundary>
        </Router>
    );
}

export default App;

在这个例子中:

  • Page1Page2 组件通过 lazy 函数进行懒加载。
  • page1LoaderPage1 的加载器函数,用于获取数据。
  • ErrorBoundaryErrorFallback 组件用于处理加载错误,当出现错误时会显示错误信息和一个重试按钮。
  • useLoaderDataPage1 组件中用于获取由 loader 函数获取的数据。
  • Suspense 用于在组件加载时显示 fallback 内容,这里是 "Loading..."

四、手写react-router

以下是一个简单的手写 react-router 的示例代码及详细解析:

1、准备工作

  1. 创建项目目录和文件
    • 创建一个项目文件夹,例如 react-router-example
    • 在项目文件夹中创建以下文件:
      • index.html:HTML 入口文件。
      • index.js:JavaScript 入口文件。
      • App.js:主应用组件文件。
      • Home.js:首页组件文件。
      • About.js:关于页面组件文件。

2、代码示例

  1. index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>React Router Example</title>
    </head>
    <body>
        <div id="root"></div>
        <script src="index.js"></script>
    </body>
    </html>
    
  2. index.js

    import React from 'react';
    import ReactDOM from 'react-dom';
    import { BrowserRouter } from './BrowserRouter';
    import App from './App';
    
    ReactDOM.render(
        <BrowserRouter>
            <App />
        </BrowserRouter>,
        document.getElementById('root')
    );
    
  3. BrowserRouter.js

       import React from 'react';
    import { useState, useEffect } from 'react';
    
    const BrowserRouter = ({ children }) => {
        const [currentPath, setCurrentPath] = useState(window.location.pathname);
    
        useEffect(() => {
            const handlePopState = () => {
                setCurrentPath(window.location.pathname);
            };
    
            window.addEventListener('popstate', handlePopState);
    
            return () => {
                window.removeEventListener('popstate', handlePopState);
            };
        }, []);
    
        const navigate = (path) => {
            window.history.pushState({}, '', path);
            setCurrentPath(path);
        };
    
        return React.Children.map(children, (child) =>
            React.cloneElement(child, { currentPath, navigate })
        );
    };
    
    export default BrowserRouter;
    
  4. App.js

    import React from 'react';
    import { Link } from './Link';
    import Home from './Home';
    import About from './About';
    
    const App = () => {
        return (
            <div>
                <nav>
                    <Link to="/">Home</Link>
                    <Link to="/about">About</Link>
                </nav>
                {window.location.pathname === '/'? <Home /> : <About />}
            </div>
        );
    };
    
    export default App;
    
  5. Link.js

    import React from 'react';
    
    const Link = ({ to, children }) => {
        const handleClick = (e) => {
            e.preventDefault();
            window.history.pushState({}, '', to);
        };
    
        return (
            <a href="#" onClick={handleClick}>
                {children}
            </a>
        );
    };
    
    export default Link;
    
  6. Home.js

    const Home = () => {
        return <h1>Home Page</h1>;
    };
    
    export default Home;
    
  7. About.js

    const About = () => {
        return <h1>About Page</h1>;
    };
    
    export default About;
    

3、代码解析

  • BrowserRouter.js

    • 使用 useState 来管理当前的路径 currentPath
    • useEffect 用于添加和移除 popstate 事件监听器,当浏览器的历史记录发生变化时(如用户点击后退或前进按钮),更新 currentPath
    • navigate 函数用于改变浏览器的历史记录和更新 currentPath
    • React.Children.mapReact.cloneElement 用于将 currentPathnavigate 传递给子组件。
  • App.js

    • 展示一个导航栏,包含两个 Link 组件。
    • 根据当前路径 currentPath 来决定渲染 Home 组件还是 About 组件。
  • Link.js

    • 创建一个自定义的 Link 组件,当用户点击链接时,阻止默认的链接行为(页面刷新),并通过 window.history.pushState 改变浏览器的历史记录。
  • Home.jsAbout.js

    • 简单的页面组件,分别显示不同的内容。

五、处理路由加载失败情况的方法

1、使用错误边界(Error Boundaries)

在 React 中,可以创建错误边界组件来捕获和处理组件树中的 JavaScript 错误。对于路由加载失败,可以利用这一特性:

  1. 创建错误边界组件:

    import React, { Component } from 'react';
    
    class ErrorBoundary extends Component {
      constructor(props) {
        super(props);
        this.state = { hasError: false };
      }
    
      static getDerivedStateFromError(error) {
        // 更新 state 以触发重新渲染
        return { hasError: true };
      }
    
      componentDidCatch(error, errorInfo) {
        // 这里可以记录错误信息,例如发送到错误报告服务
        console.error('Error caught in error boundary:', error, errorInfo);
      }
    
      render() {
        if (this.state.hasError) {
          // 当发生错误时,显示备用 UI 或错误消息
          return <h1>路由加载出现问题。请稍后再试。</h1>;
        }
    
        return this.props.children;
      }
    }
    
    export default ErrorBoundary;
    
  2. 在路由组件外部包裹错误边界:

    import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
    import ErrorBoundary from './ErrorBoundary';
    import YourComponent from './YourComponent';
    
    const App = () => (
      <Router>
        <ErrorBoundary>
          <Routes>
            <Route path="/your-route" element={<YourComponent />} />
          </Routes>
        </ErrorBoundary>
      </Router>
    );
    
    export default App;
    

2、设置超时和重试机制

  1. 对于异步数据加载(例如在路由的 loader 函数中):

    • 使用 Promisetimeout 函数来设置超时时间:

      import { timeout } from 'promise-timeout';
      
      const fetchDataForRoute = async () => {
        try {
          const response = await timeout(5000, fetch('your-api-url'));
          const data = await response.json();
          return data;
        } catch (error) {
          // 处理超时或其他错误
          throw new Error('数据加载超时或失败');
        }
      };
      
    • 添加重试逻辑,例如在一定次数内重新尝试数据加载:

      const retryFetchData = async (retryCount = 3) => {
        try {
          return await fetchDataForRoute();
        } catch (error) {
          if (retryCount > 0) {
            return retryFetchData(retryCount - 1);
          }
          throw error;
        }
      };
      
  2. 在组件中处理加载失败:

    import React, { useState, useEffect } from 'react';
    
    const YourComponent = () => {
      const [data, setData] = useState(null);
      const [loadingError, setLoadingError] = useState(false);
    
      useEffect(() => {
        retryFetchData()
         .then((data) => setData(data))
         .catch((error) => {
            setLoadingError(true);
            console.error(error);
          });
      }, []);
    
      if (loadingError) {
        return <div>加载失败,点击重试</div>;
      }
    
      if (!data) {
        return <div>正在加载...</div>;
      }
    
      return <div>{/* 使用数据渲染组件 */}</div>;
    };
    
    export default YourComponent;
    

3、显示友好的错误页面或提示信息

  1. 创建一个专门的错误页面组件:

    import React from 'react';
    
    const ErrorPage = () => (
      <div>
        <h1>路由加载错误</h1>
        <p>很抱歉,出现了一些问题导致路由无法加载。</p>
        <button onClick={() => window.location.reload()}>重新加载</button>
      </div>
    );
    
    export default ErrorPage;
    
  2. 在路由配置中指定错误页面路由:

    import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
    import ErrorPage from './ErrorPage';
    import YourComponent from './YourComponent';
    
    const App = () => (
      <Router>
        <Routes>
          <Route path="/your-route" element={<YourComponent />} />
          <Route path="/error" element={<ErrorPage />} />
          <Route
            path="*"
            element={<Navigate to="/error" replace />}
          />
        </Routes>
      </Router>
    );
    
    export default App;
    

这样,当路由加载失败时,用户将被导航到错误页面,并可以看到友好的提示信息和可能的操作(如重新加载)。

六、错误边界组件在路由加载失败时的工作原理

1、错误捕获阶段

  1. 当路由组件在加载过程中发生 JavaScript 错误时,错误边界组件会尝试捕获这个错误。
    • 无论是在组件的渲染过程、生命周期方法中,还是在异步操作(如数据获取)中发生的错误,都有可能被错误边界捕获。
    • 例如,如果在路由组件的 constructorrender 方法或者 useEffect 钩子(在函数式组件中)中出现了未处理的异常,错误边界组件会介入。

2、状态更新

  1. 一旦错误边界组件捕获到错误,它会通过更新自身的内部状态来反映错误的发生。
    • 通常会有一个布尔类型的状态变量(例如 hasError),当错误发生时,这个状态变量会被设置为 true
    • 这种状态的改变会触发错误边界组件的重新渲染。

3、重新渲染与显示备用 UI

  1. 由于状态的更新,错误边界组件会重新渲染。
    • render 方法中,根据错误状态来决定显示的内容。
    • 如果 hasErrortrue,错误边界组件会显示备用的用户界面,而不是尝试渲染出现错误的子组件(即发生错误的路由组件)。
    • 备用 UI 可以是一个简单的错误消息页面,或者包含一些引导用户进行操作的提示,比如“页面出现错误,请稍后重试”或者提供一个“重新加载”按钮等。

4、错误处理与报告

  1. 除了显示备用 UI,错误边界组件还可以进行其他错误处理操作。
    • componentDidCatch 方法中,可以记录错误信息,例如将错误信息输出到控制台,或者发送到错误报告服务。
    • 这有助于开发人员在开发和生产环境中跟踪和诊断问题,以便后续进行修复和优化。

例如,以下是一个简单的错误边界组件示例:

import React, { Component } from 'react';

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Error caught in error boundary:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <h1>路由加载出现问题。请稍后再试。</h1>;
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

在路由组件外部包裹这个错误边界组件后,当路由组件加载失败时,错误边界组件就会按照上述原理进行处理,以提供更好的用户体验和错误处理机制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值