快速掌握 React Router V6.4 的核心内容:包括路由组件使用、嵌套路由、常用 Hooks 及数据路由架构的引入方式和优势。

V6.4 是路由最激动人心的版本,它具有读取、写入和导航钩子的新数据抽象。可以轻松让 UI 与 数据保持同步。数据路由颠覆了传统模式,提高了性能。不再支持 class 类组件,全 hooks 支持。

初始化

npx create-react-app project-name
cd project-name
npm start
npm install react-router-dom

react-router 和 react-router-dom 的区别:

  • 这是两个相关的库。react-router-dom 内部依赖于 react-router
  • react-router 提供了 router 核心的 API。没有有提供有关 dom 操作进行路由跳转的 API
  • react-router-dom 提供了 BrowserRouterRouteLinkAPI,可以通过 dom 操作触发事件控制路由

单页面和多页面的区别:

单页面:
  • 在一个 html 中进行路由跳转,本质是通过 js 来控制的,适合 B 端项目 ,不考虑 SEO
  • 页面跳转不用刷新,用户体验更好,可以实现代码复用。
  • 缺点是 SEO 不友好。
多页面:
  • 多页面实际上就是多个 html,通过 window.location 互相跳转。
  • SEO 友好,更适合 C 端项目,每一个页面都可以是一个独立的项目,隔离性较好。
  • 缺点是每次跳转都要重新加载资源 刷新。体验和性能较差。

路由组件

BrowserRouter

使用简洁的 URL 将当前位置存储在浏览器地址栏中,并使用浏览器内置的历史堆栈进行导航。

http://localhost:3000/page

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
 
import { BrowserRouter, Routes, Route } from 'react-router-dom';
 
const root = ReactDOM.createRoot(document.getElementById('root'));
 
root.render(
  <BrowserRouter basename="/app">
    <Routes>
      <Route path="/" element={<Index />} />
      <Route path="pageA" element={<PageA />} />
      <Route path="pageB" element={<PageB />} />
    </Routes>
  </BrowserRouter>,
);

basename
配置 basename 可以 使其在 URL 中的特定前缀下运行

BrowserRouter 关于 404 的问题:

  • 对于单页面的项目,服务器只需要提供 index.html
  • 服务端可以使用 正则匹配或通配符来返回正确的资源。

HashRouter

http://localhost:3000/#page

<HashRouter> 用于 Web 浏览器,当 URL 因某些原因不应(或不能)发送到服务器时。可以将当前位置存储在当前 URL 的 hash 部分,因此永远不会发送到服务器。

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { HashRouter } from 'react-router-dom';
 
ReactDOM.render(<HashRouter>{/* The rest of your app goes here */}</HashRouter>, root);
  • 不建议使用,改造服务端渲染很麻烦。

MemoryRouter

内存型路。将其位置存储在内部数组中。非常适合需要完全控制历史堆栈的情况。如用于单元测试。

import * as React from 'react';
import { create } from 'react-test-renderer';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
 
describe('My app', () => {
  it('renders correctly', () => {
    let renderer = create(
      <MemoryRouter initialEntries={['/users/mjackson']}>
        <Routes>
          <Route path="users" element={<Users />}>
            <Route path=":id" element={<UserProfile />} />
          </Route>
        </Routes>
      </MemoryRouter>,
    );
 
    expect(renderer.toJSON()).toMatchSnapshot();
  });
});

NativeRouter

是在 React Native 应用程序中运行 React Router 的推荐接口。

import * as React from 'react';
import { NativeRouter } from 'react-router-native';
 
function App() {
  return <NativeRouter>{/* The rest of your app goes here */}</NativeRouter>;
}

StaticRouter

静态路由。一般用于在 node 中渲染 React Router Web 应用程序。node 环境中没有 window 不能使用 BrowserRouter。

import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';
import { StaticRouter } from 'react-router-dom/server';
import http from 'http';
 
function requestHandler(req, res) {
  let html = ReactDOMServer.renderToString(
    <StaticRouter location={req.url}>{/* The rest of your app goes here */}</StaticRouter>,
  );
 
  res.write(html);
  res.end();
}
 
http.createServer(requestHandler).listen(3000);

Outlet 组件

父路由元素中应使用 <Outlet> 来呈现其子路由元素。这样就可以在呈现子路由时显示嵌套用户界面。如果父路由完全匹配,则会呈现子索引路由;如果没有索引路由,则不会呈现任何内容。子页面的占位符。

import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom';
 
const root = ReactDOM.createRoot(document.getElementById('root'));
 
function CommonHeader() {
  return (
    <div>
      <h1>This is common header</h1>
      <Outlet />
    </div>
  );
}
 
root.render(
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<CommonHeader />}>
        <Route path="pageA" element={<PageA />} />
        <Route path="pageB" element={<PageB />} />
      </Route>
    </Routes>
  </BrowserRouter>,
);

常用 Hooks

useNavigate

此钩子会返回一个函数,让您以编程方式导航。

  • 第一个参数 To 值(与 <Link to> 相同)
  • 第二个可选参数 options? 参数(与 Link 可以传递的属性类似)。
  • 也可以在历史堆栈中传递对应值 navigate(-1)
function CommonHeader() {
  const navigate = useNavigate();
  return (
    <div>
      <h1>This is common header</h1>
      <button onClick={() => navigate('/pageA?id=1&name=zhangsan')}>To PageA</button>
      <button onClick={() => navigate('/pageB', { state: { id: 1, name: 'zhangsan' } })}>To PageB</button>
      <button onClick={() => navigate(-1)}> Back</button>
      <Outlet />
    </div>
  );
}

useLocation

此钩子返回当前 location 对象。 可以获取一些 navigate 路由过来的状态。

function PageB() {
  const location = useLocation();
  console.log(location);
  return <h2>This is PageB</h2>;
}

useSearchParams

此钩子用于读取和修改当前位置 URL 中的查询字符串。

  • 返回一个包含两个值的数组:当前位置的搜索参数和一个可用于更新这些参数的函数。
function PageA() {
  const [searchParams, setSearchParams] = useSearchParams();
 
  const name = searchParams.get('name');
  const id = searchParams.get('id');
  console.log(id, name);
 
  return (
    <div>
      <h2>This is PageA</h2>
      <button onClick={() => setSearchParams({ id: 2, name: 'lisi', age: 18 })}>change params</button>
    </div>
  );
}

useMatch

返回给定路径上的路由相对于当前位置的匹配数据

  • 将路由路径模式与 URL 路径名进行匹配,并返回匹配信息。
  • 当您需要手动运行路由器的匹配算法来确定路由路径是否匹配时,这个功能就非常有用。
  • 如果模式与给定路径名不匹配,则返回 null。
const match1 = useMatch('/pageA');
const match2 = useMatch('/pageB');
console.log(match1);
console.log(match2);

数据路由架构

  • 传统路由: 路由与 UI 渲染之间的关系。
  • 数据路由: 路由与 UI 渲染和数据之间的关系,多了一个数据状态的抽象。

Router V6.4 解决的核心问题就是 可以实现并行加载资源,解决了单页面瀑布流的问题。

Before: 假设 CommonHeader 组件渲染了两秒,PageA 中接口请求了两秒 那 username 的显示,至少大于四秒。

root.render(
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<CommonHeader />}>
        <Route path="pageA" element={<PageA />} />
        <Route path="pageB" element={<PageB />} />
      </Route>
    </Routes>
  </BrowserRouter>,
);
function PageA() {
  const [userName, setUserName] = useState('');
 
  // 用户信息
  useEffect(() => {
    axios.get('/api/xxx').then((res) => {
      setUserName(res.data.userName);
    });
  }, []);
 
  return (
    <div>
      <h1>This is PageB</h1>
      <p>{userName}</p>
    </div>
  );
}

After: 假设 CommonHeader 组件渲染了两秒,PageA 接口请求了两秒,那么获取到的时间只接近于接口的时间。

const router = createBrowserRouter(
  createRoutesFromElements(
    <Route path="/" element={<CommonHeader />}>
      <Route
        path="pageA"
        element={<PageA />}
        // 支持配置loader 使用返回值
        loader={async ({ request }) => {
          const res = await axios.get('/api/xxx');
          return res.data;
        }}
      />
      <Route path="pageB" element={<PageB />} />
    </Route>,
  ),
);
 
root.render(<RouterProvider router={router} />);
function PageA() {
  const data = useLoaderData();
  return (
    <div>
      <h1>This is PageA</h1>
      <p>{data.userName}</p>
    </div>
  );
}

关于分包

React.lazy 是 React 16.6 版本引入的一个特性,它可以让你以动态的方式进行代码拆分(code splitting)。通过 React.lazy,你可以延迟加载(lazy load)一个组件,只有在需要时才会加载该组件,从而提高应用程序的性能。

  • Before: 依然假设 CommonHeader 组件渲染了两秒,PageA 做了分包,从至少两秒之后开始加载?
const PageA = React.lazy(() => import('./PageA'))
 
<Route path="/" element={<CommonHeader />}>
  <Route
    path="pageA"
    element={<PageA />}
    loader={async ({ request }) => {
      const res = await axios.get("/api/xxx");
      return res.data;
    }}
  />
  <Route path="pageB" element={<PageB />} />
</Route>
  • After: 依然假设 CommonHeader 组件渲染了两秒,做了数据路由的 lazy,什么时候开始加载?
<Route path="/" element={<CommonHeader />}>
  <Route
    path="pageA"
    loader={async ({ request }) => {
      const res = await axios.get('/api/xxx');
      return res.data;
    }}
    lazy={async () => {
      const PageAModule = await import('./PageA');
      const PageA = PageAModule.default;
      return { element: <PageA /> };
    }}
  />
  <Route path="pageB" element={<PageB />} />
</Route>

没有银弹

数据路由 带来了直观的性能提升。但是他的缺点是讲数据耦合到了 UI 中,增加了项目耦合性,维护成本提高。

然而世界上没有银弹。世界上没有完美的解决方案。

我们可以在性能没有问题的组件中使用传统模式。
对性能有要求可以考虑使用数据路由并行方式进行优化。

传统路由结合数据路由模式。