WangShuXian6 / blog

FE-BLOG
https://wangshuxian6.github.io/blog/
MIT License
46 stars 10 forks source link

React Router v6 #126

Open WangShuXian6 opened 3 years ago

WangShuXian6 commented 3 years ago

React Router v6

官方文档 [最新版本文档,之后会更新为更高版本]

https://github.com/remix-run/react-router/tree/dev https://github.com/remix-run/react-router/blob/dev/docs/getting-started/installation.md

安装 Installation

npm i  react-router-dom@next -S

npm i @types/react-router-dom -D

npm i history@next -S

基本安装 Basic Installation

npm

$ npm install history@5 react-router-dom@6

Yarn

$ yarn add history@5 react-router-dom@6

pnpm

$ pnpm add history@5 react-router-dom@6

Create React App

按照React 文档中的说明使用 Create React App 设置一个新项目,然后按照上面的安装说明在您的项目中安装 React Router https://reactjs.org/docs/create-a-new-react-app.html#create-react-app

设置好项目并将 React Router 作为依赖项安装后 更新src/index.js 引入 BrowserRouter


import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";

ReactDOM.render(

, document.getElementById("root") ); ``` >之后可以在应用程序的任何地方使用 React Router >`src/App.js` ```tsx import React from "react"; import { Routes, Route, Link } from "react-router-dom"; import "./App.css"; function App() { return (

Welcome to React Router!

} /> } />
); } ``` >在 `src/App.js`中,创建路由组件 ```tsx // App.js function Home() { return ( <>

Welcome to the homepage!

You can do this, I believe in you.

); } function About() { return ( <>

Who are we?

That feels like an existential question, don't you think?

); } ``` >当需要将应用程序部署到生产环境时,请务必按照Create React App 的使用 React Router 部署的说明进行操作,以确保服务器配置正确。 >https://create-react-app.dev/docs/deployment#serving-apps-with-client-side-routing ### Parcel >按照Parcel 文档中的说明设置一个新项目,然后按照上面的安装说明在您的项目中安装 React Router。 >https://parceljs.org/getting_started.html >在项目的package.json,添加一个start脚本,以便在开发过程中在浏览器中打开项目。 ```JSON "scripts": { "start": "parcel index.html" } ``` >设置项目并安装依赖项后,在项目的根目录下创建一个新文件`.babelrc`: ```JSON { "presets": ["@babel/preset-react"] } ``` >编辑 `index.js` ```tsx // index.js import React from "react"; import ReactDOM from "react-dom"; import { BrowserRouter } from "react-router-dom"; import App from "./App.js"; ReactDOM.render( , document.getElementById("root") ); ``` >编辑 `index.html` ```html
``` >创建一个新文件`App.js`并添加一些路由和组件: ```tsx // App.js import React from "react"; import { Routes, Route, Link } from "react-router-dom"; function App() { return (

Welcome to React Router!

} /> } />
); } function Home() { return ( <>

Welcome to the homepage!

You can do this, I believe in you.

); } function About() { return ( <>

Who are we?

That feels like an existential question, don't you think?

); } export default App; ``` ### Webpack >按照webpack 文档中的说明设置一个新项目,然后按照上面的安装说明在你的项目中安装 React Router >https://webpack.js.org/guides/getting-started/ ```tsx import { BrowserRouter, Routes, Route } from "react-router-dom"; function App() { return (

Hello, React Router!

} />
); } ``` ### HTML Script Tags >将 React 和 React Router 添加到网站的最快方法之一是使用` ``` >尽管此方法是一种快速启动和运行的好方法,但它确实会加载一些您可能不会在您的应用程序中使用的代码。 >React Router 被设计为许多小组件和函数的集合,允许您根据实际需要使用尽可能少的库。 >为此,您需要使用 JavaScript打包器(如Webpack或Parcel )构建您的网站
WangShuXian6 commented 3 years ago

<BrowserRouter>

使用 React Router 的必要接口

import React from 'react';
import ReactDOM from "react-dom";
import { BrowserRouter as Router } from "react-router-dom";
import App from "./App";

const rootElement = document.getElementById("root");
ReactDOM.render(
    <Router>
      <App />
    </Router>,
  rootElement
);

这意味着应用程序组件及其子组件现在可以访问路由器 API。

<Routes><Route>

Routes

Routes是一个新元素,是对之前Switch组件的升级。

功能

相对路由 链接 自动路由排序

Route

Route 有两个主要的 props

path 是 URL 路径,

element 是当前 URL 匹配路径时渲染的组件。

example

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/menu" element={<Menu />} />
  <Route path="/about" element={<About />} 
</Routes>

<Link><NavLink>

<Link>

为了避免 a 标签导致的网页刷新,React Router 提供了一个Link元素

Link是一个元素,允许用户通过单击或点击它来导航到另一个页面。

props

to,应该导航到的路径

example

import React from 'react';
import { Link } from 'react-router-dom';

export function Navbar() {
  return (
    <nav>
      <Link to="/home"> Home </Link>
      <Link to="/about"> About </Link>
    </nav>
  )
}

<NavLink>

Link和NavLink非常相似 唯一的区别是Navlink知道它是否是active。 当您想对活动链接应用样式时,这很有用。

example

import React from 'react';
import { NavLink } from 'react-router-dom';

export function Navbar() {
  return (
    <nav>
      <NavLink to="/home" activeStyle={{textDecoration:"underline" , color:"red"}}> Home </Link>
      <NavLink to="/about" activeStyle={{textDecoration:"underline" , color:"red"}}> About </Link>
    </nav>
  )
}

useNavigate hook

以编程方式导航。 这在需要进行命令式导航时非常有用,例如在用户提交表单或单击按钮之后。

example

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

function App() {
  let navigate = useNavigate();
  let [error, setError] = React.useState(null);

  async function handleSubmit(event) {
    event.preventDefault();
    let result = await submitForm(event.target);
    if (result.error) {
      setError(result.error);
    } else {
      navigate('success');
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      // ...
    </form>
  );
}

useParams hook

用于访问 URL 参数。

在嵌套路由中使用时,它返回 URL 中所有参数的对象

example

电子商务应用程序中的个人产品页面

<Route path="/products/:id" element={<ProductPage />} />
import React from 'react';
import { useParams } from 'react-router-dom';
import { products } from './data/products';

export function ProductPage() {
  const { id } = useParams()
  const product = products.find(el => el.id === id)
  return (
     <li key={product.id}>
        <h3>{product.name}</h3>
        <p>{product.description}</p>
        <p>{product.price}</p>
     </li>
  );
}
WangShuXian6 commented 2 months ago

import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router-dom";
import { Outlet, useNavigate } from "react-router-dom";
import "./index.css";

export const Dashboard = () => {
  const navigate = useNavigate();

  const goDashboardTasks = () => {
    navigate("/tasks");
  };

  const goDashboardTasks2 = () => {
    navigate("tasks");
  };

  return (
    <>
      <div className="dash-board">
        <div onClick={goDashboardTasks}>
          goDashboardTasks : [navigate("/tasks");]
        </div>
        <div onClick={goDashboardTasks2}>
          goDashboardTasks : [navigate("tasks");]
        </div>
      </div>

      <div>
        <Outlet />
      </div>
    </>
  );
};

navigate("/tasks/1") 表示完整路由【不包含baseUrl,baseUrl默认为空】 navigate("tasks/1") 表示相对父级路由

WangShuXian6 commented 1 month ago

React Router 文章

Remixing React Router

March 23, 2022 Ryan Florence Co-Founder

https://remix.run/blog/remixing-react-router

React Router 的最早版本实际上有一个用于帮助加载数据的异步钩子,叫做 willTransitionTo。当时几乎没有人真正知道如何使用 React,我们也不例外。虽然这个钩子并不完美,但至少它朝着正确的方向发展了。

不论好坏,我们在 React Router v4 中完全专注于组件并移除了这个钩子。willTransitionTo 不再存在,组件成了我们的主要工具,几乎所有的 React Router 应用今天都在组件内加载数据。

我们发现,在组件中加载数据是导致缓慢用户体验的最快途径(还不提由此产生的内容布局的频繁变化)。不仅是用户体验受损,开发者体验也变得复杂,因为需要处理上下文传递、全局状态管理解决方案(通常不过是服务器端状态的客户端缓存),每个带有数据的组件都需要管理自己的加载、错误和成功状态,基本上很少有愉快的开发路径。

在构建 Remix 时,我们获得了大量实践经验,依赖于 React Router 的嵌套路由抽象来一次性解决这些问题。今天我们很高兴地宣布,我们已经开始将这些数据 API 引入 React Router,但这一次它们非常出色。

简而言之,Remix 中几乎所有与数据和异步 UI 管理相关的优秀特性即将引入 React Router:

组件内的数据获取和渲染+获取链
在组件中进行数据获取会导致我们称之为渲染+获取链的问题,这种链式加载会由于顺序数据依赖而导致页面加载和过渡速度变慢,而不是并行加载。

例如,考虑以下路由:

<Routes>
  <Route element={<Root />}>
    <Route path="projects" element={<ProjectsList />}>
      <Route path=":projectId" element={<ProjectPage />} />
    </Route>
  </Route>
</Routes>

再假设这些组件各自加载自己的数据:

function Root() {
  let data = useFetch("/api/user.json");
  if (!data) return <BigSpinner />;
  if (data.error) return <ErrorPage />;
  return (
    <div>
      <Header user={data.user} />
      <Outlet />
      <Footer />
    </div>
  );
}
function ProjectsList() {
  let data = useFetch("/api/projects.json");
  if (!data) return <MediumSpinner />;
  return (
    <div style={{ display: "flex" }}>
      <ProjectsSidebar project={data.projects} />
      <ProjectsContent>
        <Outlet />
      </ProjectsContent>
    </div>
  );
}
function ProjectPage() {
  let params = useParams();
  let data = useFetch(`/api/projects/${params.projectId}.json`);
  if (!data) return <div>Loading...</div>;
  if (data.notFound) return <NotFound />;
  if (data.error) return <ErrorPage />;
  return (
    <div>
      <h3>{project.title}</h3>
      {/* ... */}
    </div>
  );
}

当用户访问 /projects/123 时,

1 <Root> 加载 /api/user.json 并渲染 <BigSpinner />

2 网络响应后 <ProjectsList> 加载 /api/projects.json 并渲染 <MediumSpinner />

3 网络响应后 <ProjectPage> 加载 /api/projects/123.json 并渲染 <div>Loading...</div>

4 网络响应后最终完成渲染。

这种链式获取方式会让应用变得非常缓慢。组件在挂载时启动数据获取,但父组件的等待状态会阻止子组件渲染,从而延迟数据获取!如果每次获取需要一秒钟,那么整个页面至少需要三秒钟才能完全渲染,这就是许多 React 应用加载和过渡缓慢的原因。

将数据获取耦合到组件会导致“渲染+获取链”的出现。 image

解决方案是将获取的启动与读取结果分离。这正是 Remix 当前的 API 所做的事情,也是 React Router 即将实现的功能。通过在嵌套路由边界启动数据获取,请求链变得平坦,速度提升了三倍。

路由级的数据获取可以并行化请求,从而消除缓慢的“渲染+获取链”。 image

不仅如此,新 API 解决的众多问题还极大简化了代码,提高了开发过程中的乐趣。

即将推出的功能
我们还在为几个东西的命名争论不休,但你可以期待以下内容:

import * as React from "react";
import {
  BrowserRouter,
  Routes,
  Route,
  useLoaderData,
  Form,
} from "react-router-dom";

ReactDOM.render(
  <BrowserRouter>
    <Routes
      // 如果没有进行服务器端渲染,这里会管理初始加载状态
      fallbackElement={<BigSpinner />}
      // 所有的渲染或异步加载及变更错误会被自动捕获并渲染在这里,
      // 不再需要跟踪错误状态或编写分支渲染逻辑
      exceptionElement={<GlobalErrorPage />}
    >
      <Route
        // 加载器为路由组件提供数据,并在 URL 更改时启动
        loader={({ signal }) => {
          // React Router 使用 Web Fetch API,因此你可以返回
          // 一个 web fetch 响应,它会自动用 `res.json()` 反序列化。
          // 不再需要每个组件都使用 useFetch 钩子和处理它们的等待状态。
          return fetch("/api/user.json", {
            // 它还会处理导航中断(只要传递了 signal),并取消实际的获取请求。
            signal,
          });
        }}
      >
        <Route
          path="projects"
          element={<Projects />}
          // 异常会冒泡,因此你可以在上下文中处理它们,也可以让它们向上冒泡,
          // 从而有大量的便捷路径!
          exceptionElement={<TasksErrorPage />}
          loader={async ({ signal }) => {
            // 你还可以自行解包 fetch 并编写简单的 `async/await` 代码(试试在 useEffect 里 🥺)。
            // 甚至不必使用 `fetch`,你可以从任何地方获取数据(如 localStorage,indexedDB 等)。
            let res = await fetch("/api/tasks.json", { signal });

            // 如果在加载数据的任何点上无法渲染路由组件,只需 `throw` 一个异常,
            // exceptionElement 就会代替渲染。这样可以确保正常路径正常,
            // 异常路径...呃,例外!
            if (res.status === 404) {
              throw { notFound: true };
            }

            return res.json();
          }}
        >
          <Route
            path=":projectId"
            element={<Projects />}
            // 你加载数据的方式通常会如此简单,React Router 会处理所有的等待状态,
            // 并将其暴露给你,以便构建等待/乐观 UI
            loader={async ({ signal, params }) =>
              fetch(`/api/projects/${params.projectId}`, { signal })
            }
          />
        </Route>
        <Route index element={<Index />} />
      </Route>
    </Routes>
  </BrowserRouter>,
);
function Root() {
  // 组件通过该钩子访问路由数据,数据在这里是保证无错误且无等待状态的,
  // 每个依赖数据的组件都无需处理等待状态(这也有助于消除内容布局的偏移)。
  let data = useLoaderData();

  // transition 提供了构建等待指示器、忙碌的加载动画、乐观 UI 和副作用所需的所有信息。
  let transition = useTransition();

  return (
    <div>
      {/* 你可以在根组件放置全局导航指示器,
          从此不再需要在组件中担心加载状态,
          或者可以在 Outlet 周围更精细地构建骨架 UI,
          以便用户点击链接时立即获得反馈(我们稍后会展示如何实现) */}
      <GlobalNavSpinner show={transition.state === "loading"} />
      <Header user={data.user} />
      <Outlet />
      <Footer />
    </div>
  );
}

在 React Router 中,我们很快会将数据加载和变更 API 带给你。这些 API 会加快你的应用速度,并提供数据读取和写入的完整解决方案。

数据变更也来了!
不仅我们加速了你的应用,还带来了数据变更 API。通过这种加载和变更的组合,许多问题迎刃而解。

例如,新建项目表单:

function NewProjectForm() {
  return (
    <Form method="post" action="/projects">
      <label>
        New Project: <input name="title" />
      </label>
      <button type="submit">Create</button>
    </Form>
  );
}

一旦你完成了 UI,唯一还需要的就是路由中的 action,该 action 对应表单的 action 属性指向的路由:

<Route
  path="projects"
  element={<Projects />}
  // 当表单提交时会调用此 action,因为它匹配表单的 action 属性,
  // 路由现在可以处理所有数据需求:读取和写入。
  action={async ({ request, signal }) => {
    let values = Object.fromEntries(
      // React Router 拦截了普通的浏览器 POST 请求,
      // 并将其作为标准 Web Fetch Request 提供给你。
      // 表单数据已被 React Router 序列化,可通过 request 获取。
      // 使用标准 HTML 和 DOM API,无需任何新内容。
      await request.formData(),
    );

    // 你已经熟悉 Web Fetch API,因为你多年来一直这样使用它:
    let res = await fetch("/api/projects.json", {
      signal,
      method: "post",
      body: JSON.stringify(values),
      headers: { "Content-Type": "application/json; utf-8" },
    });

    let project = await res.json();

    // 如果出现问题,只需抛出一个异常,异常元素会渲染,
    // 保持正常路径顺畅。(如果继续阅读,会有比抛出错误更好的选择)
    if (project.error) throw new Error(project.error);

    // 现在你可以返回该路由的渲染结果,或者返回一个重定向(实际是一个 Web Fetch Response),
    // 导向其他位置,比如新创建的项目页面!
    return redirect(`/projects/${project.id}`);
  }}
/>

就是这样。你只需编写 UI 和与应用相关的变更代码在一个简单的异步函数中。

不需要处理错误或成功状态,不用担心 useEffect 的依赖项,无需返回清理函数,也无需缓存键过期。你的唯一关注点是执行变更,如果出错,就抛出异常。异步 UI、变更处理和异常渲染路径已完全解耦。

从此,React Router 会为你处理所有这些问题:

因为 React Router 会为你处理这些问题,它能通过一个简单的钩子暴露所有信息:useTransition。这让你能够为用户提供反馈,让你的应用感觉坚如磐石(这也是我们将 React 放在页面上的原因!)。

function NewProjectForm() {
  let transition = useTransition();

  let busy = transition.state === "submitting";

  // 该钩子提供了所有信息——当前的状态("idle", "submitting", "loading"),
  // 以及提交给服务器的表单数据等,为乐观 UI 提供支持。

  // 你可以构建设计师梦寐以求的精美单页应用...
  return (
    <Form method="post" action="/projects">
      <label>
        New Project: <input name="title" />
      </label>
      {/* ...或者干脆禁用按钮 😂 */}
      <button type="submit" disabled={busy}>
        Create
      </button>
    </Form>
  );
}

如果你的应用主要处理与 API 路由的获取和提交操作,那么当这一功能发布时,准备好删除大量代码吧。

为抽象而生
许多开发者可能会认为在路由配置中写这么多代码有点过于冗长。Remix 能够将加载器和操作与路由模块放在一起,并从文件系统自动构建路由配置。我们希望大家能够为自己的应用创建类似的模式。

以下是一个简单的示例,展示了如何轻松地将这些内容放在一起。创建一个“路由模块”并动态导入实际的模块。这不仅实现了代码分割,还简化了路由配置。

export async function loader(args) {
  let actualLoader = await import("./actualModule").loader;
  return actualLoader(args);
}

export async function action(args) {
  let actualAction = await import("./actualModule").action;
  return actualAction(args);
}

export const Component = React.lazy(() => import("./actualModule").default);
import * as Tasks from "./tasks.route";

// ...
<Route
  path="/tasks"
  element={<Tasks.Component />}
  loader={Tasks.loader}
  action={Tasks.action}
/>;

Suspense + React Router = ❤️
React Server Components、Suspense 和 Streaming 这些即将发布的特性正为 React 发展注入新的活力。在 React Router 的开发中,我们充分考虑了这些 API。

这些 React API 设计用于在渲染之前启动数据加载的系统。它们并不决定你在何处发起数据请求,而是在哪里访问数据结果。

这些 API 并不发起数据加载,而是决定数据可用时在何处渲染。如果在 Suspense 边界内启动请求,依然会面临当前 React Router 应用中的性能问题。

React Router 的新数据加载 API 正是 Suspense 所期望的!当 URL 发生变化时,React Router 为每个匹配的路由启动请求并在渲染前完成,这为这些新特性提供了发光的机会 ✨。

存储库合并
在开发这些特性时,我们的工作涉及三个存储库:History、React Router 和 Remix。这种分散的方式严重影响 DX(开发者体验),难以维护工具、问题和 PR,且影响社区贡献。

我们一直视 Remix 为 “React Router 的编译器和服务器”。是时候将它们整合到一起了。

操作上,这意味着我们将:

这需要做很多整理工作,因此请期待我们开始合并工作时,存储库中的问题/PR 将被移动、合并或关闭。我们将尽全力保留每个存储库的提交历史,因为我们相信每个贡献者都应该得到他们应有的认可!

如果你对此有任何疑问或感到兴奋,欢迎在 DiscordTwitter 上联系我们 :)

When To Fetch: Remixing React Router - Ryan Florence

何时获取数据:重混 React Router - Ryan Florence
https://www.youtube.com/watch?v=95B8mnhzoCM&ab_channel=RealWorldReact

在 Reactathon 2022 上的现场录制。了解更多信息请访问 https://reactathon.com

何时获取数据:重混 React Router
我们了解到,在组件中获取数据是实现最差用户体验的最快方式。但不仅仅是用户体验受到影响,在组件中获取数据给开发者体验也带来了很多偶然的复杂性:数据获取、数据变更、繁忙指示器、乐观 UI、错误处理、表单状态、网络竞态条件、用户事件中断,以及所有维持它们的代码都变得相当困难!在构建 Remix 的过程中,我们多次利用 React Router 的嵌套路由抽象来一次性解决所有这些问题。现在,数百万个正在生产的 React Router 应用程序也可以获得同样的好处,因为我们已经将何时获取数据的责任转移到了 React Router 本身!

关于 Ryan Florence
Ryan 是 Remix 的联合创始人、React Router 的共同创作者以及 React Training 的联合创始人。

活动制作和后期制作由 EventLoop 完成。
让我们一起构建您的会议品牌,或将其提升到新的水平。info@eventloop.app

WangShuXian6 commented 1 month ago

路由权限

认证示例 (使用 RouterProvider)

本示例演示了如何在使用 <RouterProvider> 时限制访问路由,仅允许已认证用户访问。

BrowserRouter 处理认证的方式相比,主要的区别在于,由于 RouterProvider 将数据获取与渲染解耦,我们不能再依赖 React 上下文和/或钩子来获取用户的认证状态。我们需要在 React 树外部访问这些信息,以便在路由的 loaderaction 函数中使用它。

关于这种设计选择的一些背景信息,请查看 Remixing React Router 博客文章和 Ryan 在 Reactathon 上的 When to Fetch 演讲。

在本示例中,请特别注意以下功能:

预览

StackBlitz 上打开本示例:

在 StackBlitz 上打开