React Router 完美教程(上)

news2024/11/18 3:26:41

概述

什么叫路由呢,说白了就是如何处理页面的跳转。在传统的网站中,我们都是向服务器请求页面及相应的css和js代码。自从前后端分离的相思提出后,一堆基于js虚拟Dom的框架应运而生。React就是其中优秀的代表作之一。这种方式极大的优化了开发体验。从些,我们前端开发也可以向开发桌面软件一样那么的优雅,这在开发中后端产品中优势特别明显。那么,既然已经分离了,在浏览器中如何处理跳转关系呢,这就离不开路由这个话题了。在React中借助 Router 这个第三方包可以优雅的处理这种操作,让我们的界面实现丝滑的转换。同样,我还是借用官方的示例为大家讲解,应该说话是是比较全面的了。话不多说,进入主题。

安装

进入我们的项目的根目录 my-react-app

cd my-react-app
npm install react-router-dom localforage match-sorter sort-by

## 运行我们的项目
yarn dev

为我们今天的学习创建目录:在我们项目的src目录下创建新的目录 Test08
首先我们引入一个已经定义好的CSS文件,在App.jsx中引用就好了。

/* index.css */
html {
    box-sizing: border-box;
}

*,
*:before,
*:after {
    box-sizing: inherit;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
        "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
        sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

code {
    font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
        monospace;
}

html,
body {
    height: 100%;
    margin: 0;
    line-height: 1.5;
    color: #121212;
}

textarea,
input,
button {
    font-size: 1rem;
    font-family: inherit;
    border: none;
    border-radius: 8px;
    padding: 0.5rem 0.75rem;
    box-shadow: 0 0px 1px hsla(0, 0%, 0%, 0.2), 0 1px 2px hsla(0, 0%, 0%, 0.2);
    background-color: white;
    line-height: 1.5;
    margin: 0;
}

button {
    color: #3992ff;
    font-weight: 500;
}

textarea:hover,
input:hover,
button:hover {
    box-shadow: 0 0px 1px hsla(0, 0%, 0%, 0.6), 0 1px 2px hsla(0, 0%, 0%, 0.2);
}

button:active {
    box-shadow: 0 0px 1px hsla(0, 0%, 0%, 0.4);
    transform: translateY(1px);
}

#contact h1 {
    display: flex;
    align-items: flex-start;
    gap: 1rem;
}

#contact h1 form {
    display: flex;
    align-items: center;
    margin-top: 0.25rem;
}

#contact h1 form button {
    box-shadow: none;
    font-size: 1.5rem;
    font-weight: 400;
    padding: 0;
}

#contact h1 form button[value="true"] {
    color: #a4a4a4;
}

#contact h1 form button[value="true"]:hover,
#contact h1 form button[value="false"] {
    color: #eeb004;
}

form[action$="destroy"] button {
    color: #f44250;
}

.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border-width: 0;
}

#root {
    display: flex;
    height: 100%;
    width: 100%;
}

#sidebar {
    width: 22rem;
    background-color: #f7f7f7;
    border-right: solid 1px #e3e3e3;
    display: flex;
    flex-direction: column;
}

#sidebar>* {
    padding-left: 2rem;
    padding-right: 2rem;
}

#sidebar h1 {
    font-size: 1rem;
    font-weight: 500;
    display: flex;
    align-items: center;
    margin: 0;
    padding: 1rem 2rem;
    border-top: 1px solid #e3e3e3;
    order: 1;
    line-height: 1;
}

#sidebar h1::before {
    content: url("data:image/svg+xml,%3Csvg width='25' height='18' viewBox='0 0 25 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M19.4127 6.4904C18.6984 6.26581 18.3295 6.34153 17.5802 6.25965C16.4219 6.13331 15.9604 5.68062 15.7646 4.51554C15.6551 3.86516 15.7844 2.9129 15.5048 2.32334C14.9699 1.19921 13.7183 0.695046 12.461 0.982805C11.3994 1.22611 10.516 2.28708 10.4671 3.37612C10.4112 4.61957 11.1197 5.68054 12.3363 6.04667C12.9143 6.22097 13.5284 6.3087 14.132 6.35315C15.2391 6.43386 15.3241 7.04923 15.6236 7.55574C15.8124 7.87508 15.9954 8.18975 15.9954 9.14193C15.9954 10.0941 15.8112 10.4088 15.6236 10.7281C15.3241 11.2334 14.9547 11.5645 13.8477 11.6464C13.244 11.6908 12.6288 11.7786 12.0519 11.9528C10.8353 12.3201 10.1268 13.3799 10.1828 14.6234C10.2317 15.7124 11.115 16.7734 12.1766 17.0167C13.434 17.3056 14.6855 16.8003 15.2204 15.6762C15.5013 15.0866 15.6551 14.4187 15.7646 13.7683C15.9616 12.6032 16.423 12.1505 17.5802 12.0242C18.3295 11.9423 19.1049 12.0242 19.8071 11.6253C20.5491 11.0832 21.212 10.2696 21.212 9.14192C21.212 8.01428 20.4976 6.83197 19.4127 6.4904Z' fill='%23F44250'/%3E%3Cpath d='M7.59953 11.7459C6.12615 11.7459 4.92432 10.5547 4.92432 9.09441C4.92432 7.63407 6.12615 6.44287 7.59953 6.44287C9.0729 6.44287 10.2747 7.63407 10.2747 9.09441C10.2747 10.5536 9.07172 11.7459 7.59953 11.7459Z' fill='black'/%3E%3Cpath d='M2.64217 17.0965C1.18419 17.093 -0.0034949 15.8971 7.72743e-06 14.4356C0.00352588 12.9765 1.1994 11.7888 2.66089 11.7935C4.12004 11.797 5.30772 12.9929 5.30306 14.4544C5.29953 15.9123 4.10366 17.1 2.64217 17.0965Z' fill='black'/%3E%3Cpath d='M22.3677 17.0965C20.9051 17.1046 19.7046 15.9217 19.6963 14.4649C19.6882 13.0023 20.8712 11.8017 22.3279 11.7935C23.7906 11.7854 24.9911 12.9683 24.9993 14.4251C25.0075 15.8866 23.8245 17.0883 22.3677 17.0965Z' fill='black'/%3E%3C/svg%3E%0A");
    margin-right: 0.5rem;
    position: relative;
    top: 1px;
}

#sidebar>div {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding-top: 1rem;
    padding-bottom: 1rem;
    border-bottom: 1px solid #e3e3e3;
}

#sidebar>div form {
    position: relative;
}

#sidebar>div form input[type="search"] {
    width: 100%;
    padding-left: 2rem;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='h-6 w-6' fill='none' viewBox='0 0 24 24' stroke='%23999' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z' /%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: 0.625rem 0.75rem;
    background-size: 1rem;
    position: relative;
}

#sidebar>div form input[type="search"].loading {
    background-image: none;
}

#search-spinner {
    width: 1rem;
    height: 1rem;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'%3E%3Cpath stroke='%23000' strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M20 4v5h-.582m0 0a8.001 8.001 0 00-15.356 2m15.356-2H15M4 20v-5h.581m0 0a8.003 8.003 0 0015.357-2M4.581 15H9' /%3E%3C/svg%3E");
    animation: spin 1s infinite linear;
    position: absolute;
    left: 0.625rem;
    top: 0.75rem;
}

@keyframes spin {
    from {
        transform: rotate(0deg);
    }

    to {
        transform: rotate(360deg);
    }
}

#sidebar nav {
    flex: 1;
    overflow: auto;
    padding-top: 1rem;
}

#sidebar nav a span {
    float: right;
    color: #eeb004;
}

#sidebar nav a.active span {
    color: inherit;
}

i {
    color: #818181;
}

#sidebar nav .active i {
    color: inherit;
}

#sidebar ul {
    padding: 0;
    margin: 0;
    list-style: none;
}

#sidebar li {
    margin: 0.25rem 0;
}

#sidebar nav a {
    display: flex;
    align-items: center;
    justify-content: space-between;
    overflow: hidden;

    white-space: pre;
    padding: 0.5rem;
    border-radius: 8px;
    color: inherit;
    text-decoration: none;
    gap: 1rem;
}

#sidebar nav a:hover {
    background: #e3e3e3;
}

#sidebar nav a.active {
    background: hsl(224, 98%, 58%);
    color: white;
}

#sidebar nav a.pending {
    color: hsl(224, 98%, 58%);
}

#detail {
    flex: 1;
    padding: 2rem 4rem;
    width: 100%;
}

#detail.loading {
    opacity: 0.25;
    transition: opacity 200ms;
    transition-delay: 200ms;
}

#contact {
    max-width: 40rem;
    display: flex;
}

#contact h1 {
    font-size: 2rem;
    font-weight: 700;
    margin: 0;
    line-height: 1.2;
}

#contact h1+p {
    margin: 0;
}

#contact h1+p+p {
    white-space: break-spaces;
}

#contact h1:focus {
    outline: none;
    color: hsl(224, 98%, 58%);
}

#contact a[href*="twitter"] {
    display: flex;
    font-size: 1.5rem;
    color: #3992ff;
    text-decoration: none;
}

#contact a[href*="twitter"]:hover {
    text-decoration: underline;
}

#contact img {
    width: 12rem;
    height: 12rem;
    background: #c8c8c8;
    margin-right: 2rem;
    border-radius: 1.5rem;
    object-fit: cover;
}

#contact h1~div {
    display: flex;
    gap: 0.5rem;
    margin: 1rem 0;
}

#contact-form {
    display: flex;
    max-width: 40rem;
    flex-direction: column;
    gap: 1rem;
}

#contact-form>p:first-child {
    margin: 0;
    padding: 0;
}

#contact-form>p:first-child> :nth-child(2) {
    margin-right: 1rem;
}

#contact-form>p:first-child,
#contact-form label {
    display: flex;
}

#contact-form p:first-child span,
#contact-form label span {
    width: 8rem;
}

#contact-form p:first-child input,
#contact-form label input,
#contact-form label textarea {
    flex-grow: 2;
}

#contact-form-avatar {
    margin-right: 2rem;
}

#contact-form-avatar img {
    width: 12rem;
    height: 12rem;
    background: hsla(0, 0%, 0%, 0.2);
    border-radius: 1rem;
}

#contact-form-avatar input {
    box-sizing: border-box;
    width: 100%;
}

#contact-form p:last-child {
    display: flex;
    gap: 0.5rem;
    margin: 0 0 0 8rem;
}

#contact-form p:last-child button[type="button"] {
    color: inherit;
}

#zero-state {
    margin: 2rem auto;
    text-align: center;
    color: #818181;
}

#zero-state a {
    color: inherit;
}

#zero-state a:hover {
    color: #121212;
}

#zero-state:before {
    display: block;
    margin-bottom: 0.5rem;
    content: url("data:image/svg+xml,%3Csvg width='50' height='33' viewBox='0 0 50 33' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M38.8262 11.1744C37.3975 10.7252 36.6597 10.8766 35.1611 10.7128C32.8444 10.4602 31.9215 9.55475 31.5299 7.22456C31.3108 5.92377 31.5695 4.01923 31.0102 2.8401C29.9404 0.591789 27.4373 -0.416556 24.9225 0.158973C22.7992 0.645599 21.0326 2.76757 20.9347 4.94569C20.8228 7.43263 22.2399 9.5546 24.6731 10.2869C25.8291 10.6355 27.0574 10.8109 28.2646 10.8998C30.4788 11.0613 30.6489 12.292 31.2479 13.3051C31.6255 13.9438 31.9914 14.5731 31.9914 16.4775C31.9914 18.3819 31.6231 19.0112 31.2479 19.6499C30.6489 20.6606 29.9101 21.3227 27.696 21.4865C26.4887 21.5754 25.2581 21.7508 24.1044 22.0994C21.6712 22.834 20.2542 24.9537 20.366 27.4406C20.4639 29.6187 22.2306 31.7407 24.3538 32.2273C26.8686 32.8052 29.3717 31.7945 30.4415 29.5462C31.0032 28.3671 31.3108 27.0312 31.5299 25.7304C31.9238 23.4002 32.8467 22.4948 35.1611 22.2421C36.6597 22.0784 38.2107 22.2421 39.615 21.4443C41.099 20.36 42.4248 18.7328 42.4248 16.4775C42.4248 14.2222 40.9961 11.8575 38.8262 11.1744Z' fill='%23E3E3E3'/%3E%3Cpath d='M15.1991 21.6854C12.2523 21.6854 9.84863 19.303 9.84863 16.3823C9.84863 13.4615 12.2523 11.0791 15.1991 11.0791C18.1459 11.0791 20.5497 13.4615 20.5497 16.3823C20.5497 19.3006 18.1436 21.6854 15.1991 21.6854Z' fill='%23E3E3E3'/%3E%3Cpath d='M5.28442 32.3871C2.36841 32.38 -0.00698992 29.9882 1.54551e-05 27.0652C0.00705187 24.1469 2.39884 21.7715 5.32187 21.7808C8.24022 21.7878 10.6156 24.1796 10.6063 27.1027C10.5992 30.0187 8.20746 32.3941 5.28442 32.3871Z' fill='%23E3E3E3'/%3E%3Cpath d='M44.736 32.387C41.8107 32.4033 39.4096 30.0373 39.3932 27.1237C39.3769 24.1984 41.7428 21.7973 44.6564 21.7808C47.5817 21.7645 49.9828 24.1305 49.9993 27.0441C50.0156 29.9671 47.6496 32.3705 44.736 32.387Z' fill='%23E3E3E3'/%3E%3C/svg%3E%0A");
}

#error-page {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 100%;
}

准备根组件 Root

所谓根组件就是App的主体界面组件。

export default function Root() {
    return (
        <>
            <div id="sidebar">
                <h1>React Router Contacts</h1>
                <div>
                    <form id="search-form" role="search">
                        <input
                            id="q"
                            aria-label="Search contacts"
                            placeholder="Search"
                            type="search"
                            name="q"
                        />
                        <div
                            id="search-spinner"
                            aria-hidden
                            hidden={true}
                        />
                        <div
                            className="sr-only"
                            aria-live="polite"
                        ></div>
                    </form>
                    <form method="post">
                        <button type="submit">New</button>
                    </form>
                </div>
                <nav>
                    <ul>
                        <li>
                            <a href={`/contacts/1`}>Your Name</a>
                        </li>
                        <li>
                            <a href={`/contacts/2`}>Your Friend</a>
                        </li>
                    </ul>
                </nav>
            </div>
            <div id="detail"></div>
        </>
    );
}

我们先在App中引入,查看一下基本的界面

import './Test08/index.css';
import Root from './Test08/Root';

function App() {
  return <Root />
}

export default App

在这里插入图片描述

创建路由表文件

路由表文件是指示路由走向的文件。创建路由有几种方式,我们这里以最通用的哈希路由为例,创建路由文件:

// routerConfig.jsx

import { createBrowserRouter } from "react-router-dom";

import Root from "./Root";

const router = createBrowserRouter([
    {
        path: "/",
        element: <Root />,
    },
]);

export default router;

createBrowserRouter()函数内是一个json数组。注意,这是一个数组,每个成员都是一个路由对象。

  • path: 跳转的路径
  • element: 当跳转到这个路径时要显示的页面组件。
    当然还有很多成员属性,我们一步一步来。
    现在我们有了路由,还要把它连接到App才能起作用。再次对App.jsx进行修改
import './Test08/index.css';
import { RouterProvider } from "react-router-dom";
import router from './Test08/routerConfig';

function App() {
  return <RouterProvider router={router} />
}

export default App

你看,这里也有一个Provider, 跟我们之前文章中讲的状态管理器很像是不是,其实它就众多组件状态管理器中的一种,是二次包装的 Context

这时我们点击 Your Name 或其它链接时,会出现一个 404 的错误页面。因为目录针对于其它的地址链接我们还没有与之对应的页面显示,所以出现 404 是意料之中的事。
在这里插入图片描述

这样突兀的显示自然与我们的设计风格有点格格不入。这当然不是我们的设想。当然我们可以重新设计一个404页面并在导航出错时显示。

// Error404.jsx

import { useRouteError } from "react-router-dom";

export default function Error404() {
    const error = useRouteError();
    console.error(error);

    return (
        <div id="error-page">
            <h1>Oops!</h1>
            <p>Sorry, an unexpected error has occurred.</p>
            <p>
                <i>{error.statusText || error.message}</i>
            </p>
        </div>
    );
}

在路由中指定:修改路由配置文件routerConfig.jsx

import { createBrowserRouter } from "react-router-dom";

import Root from "./Root";
import Error404 from "./Error404";

const router = createBrowserRouter([
    {
        path: "/",
        element: <Root />,
        errorElement: <Error404 />
    },
]);

增加 errorElement 属性,作用是当当前路径下的子路径没有对应的页面显示时就显示errorElement 页面。 现在我们点击 联系人后 就会立刻跳转到我们指定的404页面了。

在这里插入图片描述

现在我们要创建联系的组件。

// Contact.jsx

import { Form } from "react-router-dom";

export default function Contact() {
  const contact = {
    first: "Your",
    last: "Name",
    avatar: "https://placekitten.com/g/200/200",
    twitter: "your_handle",
    notes: "Some notes",
    favorite: true,
  };

  return (
    <div id="contact">
      <div>
        <img
          key={contact.avatar}
          src={contact.avatar || null}
        />
      </div>

      <div>
        <h1>
          {contact.first || contact.last ? (
            <>
              {contact.first} {contact.last}
            </>
          ) : (
            <i>No Name</i>
          )}{" "}
          <Favorite contact={contact} />
        </h1>

        {contact.twitter && (
          <p>
            <a
              target="_blank"
              href={`https://twitter.com/${contact.twitter}`}
            >
              {contact.twitter}
            </a>
          </p>
        )}

        {contact.notes && <p>{contact.notes}</p>}

        <div>
          <Form action="edit">
            <button type="submit">Edit</button>
          </Form>
          <Form
            method="post"
            action="destroy"
            onSubmit={(event) => {
              if (
                !confirm(
                  "Please confirm you want to delete this record."
                )
              ) {
                event.preventDefault();
              }
            }}
          >
            <button type="submit">Delete</button>
          </Form>
        </div>
      </div>
    </div>
  );
}

function Favorite({ contact }) {
  let favorite = contact.favorite;
  return (
    <Form method="post">
      <button
        name="favorite"
        value={favorite ? "false" : "true"}
        aria-label={
          favorite
            ? "Remove from favorites"
            : "Add to favorites"
        }
      >
        {favorite ? "★" : "☆"}
      </button>
    </Form>
  );
}

react-router 是提供了一些与路由操作有关的组件,它们与HTML中的DOM元素功能类似,但做了二次封装,方便在路由中获取数据。比如 Form 组件。

现在我们再把这个页面添加到路由中:

import { createBrowserRouter } from "react-router-dom";

import Root from "./Root";
import Error404 from "./Error404";
import Contact from "./Contact";

const router = createBrowserRouter([
    {
        path: "/",
        element: <Root />,
        errorElement: <Error404 />
    },

    {
        path: "contacts/:contactId",
        element: <Contact />,
    }
]);

现在路由中我们有了两个平级的页面。 也就是说这两个页面的切换会卸载前一个页面,所以叫平级的。

我们发现路径contacts/:contactId 有所不同,有时候我们有这样的需求,通过路径向组件传递某种信息,比如:当我们点击联系人后想把联系的ID号传递给组件 <Contact />, 以往我们都是通过Propsredux 等方式进行传递,现在我们可以通过路由传递信息到相应的路由组件中。
注意分析 Root 组件,我们可以把联系人的 ID 信息写在跳转路径中。如 contacts/ID形式。本例中为 contacts/1contacts/2, 现在路径中有了我们联系的ID信息了("1 "和 “2”),那么如何传递到路径组件中呢。
我们可以通过路径变量的形式把对应的部分路径信息传递给组件。路径变量的格式为: :变量名, 如上面的 <Contact />的路径信息中为:contacts/:contactId, 就可以通过 变量contactId 获取到联系人的 ID 号了。
相同的原理,我还可以通过这种方法获取更多路由的信息,如 contacts/:contactId/:contactName等等。

回过话题,我们经过这样的配置后看看效果。这当然不是我们所期望的。我希望在联系人的右侧显示相关联系人的详情信息。就像下面这样。
在这里插入图片描述

如何实现呢,我们再次对路由的配置文件做个修改:

...

const router = createBrowserRouter([
    {
        path: "/",
        element: <Root />,
        errorElement: <Error404 />,
        children: [
            {
                path: "contacts/:contactId",
                element: <Contact />,
            },
        ]
    }
]);
...

我们把第二个页面的路由配置信息移到了第一个的 children 子页面列表里了。这个时候我们再次点击联系人时,会发现路径虽然变化了,但页面没其它变化, 联系人的详细信息没有显示出来。也就是说 <Contact />没有显示出来。 这是因为我们没有在 Root 中给 <Contact /> 组件预留显示位置, 自然也就没有显示。

渲染占位

现在修改 Root.jsx文件, 添加 组件。 它就是一个占位符,为子组件提供显示空间。

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

export default function Root() {
    return (
        <>
            { ... 其它元素信息 }
            <div id="detail"> <Outlet /> </div>
        </>
    );
}

react-router 提供了很多很实用的组件,搭配使用效果更好。我们把 Root 中的 <a></a> 换成 Link 组件要更好,因为 Link 只改变路由,不从服务器请求文档。

import { Outlet, Link } from 'react-router-dom';

export default function Root() {
    return (
        <>
            <div id="sidebar">
                { ... }
                <nav>
                    <ul>
                        <li>
                            <Link to={`/contacts/1`}>Your Name</Link>
                        </li>
                        <li>
                            <Link to ={`/contacts/2`}>Your Friend</Link>
                        </li>
                    </ul>
                </nav>
            </div>
            <div id="detail"> <Outlet /> </div>
        </>
    );
}

加载数据

有时我们想在页面加载的过程中就把数据也准备好,这样各组件不用各自向服务器请求数据了,大大节省了网络资源。
我们将使用 loaderuseLoaderData 来加载数据。
我们在Test09目录下创建一个工具函数库文件contacts.jsx,直接把复制进去就行了,这些工具函数只是为了示例而创建,没有其它的实用价值,我们只要知道它们的作用就行了。

// cantacts.jsx

import localforage from "localforage";
import { matchSorter } from "match-sorter";
import sortBy from "sort-by";

export async function getContacts(query) {
    await fakeNetwork(`getContacts:${query}`);
    let contacts = await localforage.getItem("contacts");
    if (!contacts) contacts = [];
    if (query) {
        contacts = matchSorter(contacts, query, { keys: ["first", "last"] });
    }
    return contacts.sort(sortBy("last", "createdAt"));
}

export async function createContact() {
    await fakeNetwork();
    let id = Math.random().toString(36).substring(2, 9);
    let contact = { id, createdAt: Date.now() };
    let contacts = await getContacts();
    contacts.unshift(contact);
    await set(contacts);
    return contact;
}

export async function getContact(id) {
    await fakeNetwork(`contact:${id}`);
    let contacts = await localforage.getItem("contacts");
    let contact = contacts.find(contact => contact.id === id);
    return contact ?? null;
}

export async function updateContact(id, updates) {
    await fakeNetwork();
    let contacts = await localforage.getItem("contacts");
    let contact = contacts.find(contact => contact.id === id);
    if (!contact) throw new Error("No contact found for", id);
    Object.assign(contact, updates);
    await set(contacts);
    return contact;
}

export async function deleteContact(id) {
    let contacts = await localforage.getItem("contacts");
    let index = contacts.findIndex(contact => contact.id === id);
    if (index > -1) {
        contacts.splice(index, 1);
        await set(contacts);
        return true;
    }
    return false;
}

function set(contacts) {
    return localforage.setItem("contacts", contacts);
}

// fake a cache so we don't slow down stuff we've already seen
let fakeCache = {};

async function fakeNetwork(key) {
    if (!key) {
        fakeCache = {};
    }

    if (fakeCache[key]) {
        return;
    }

    fakeCache[key] = true;
    return new Promise(res => {
        setTimeout(res, Math.random() * 800);
    });
}

我们在再创建一个工具函数库文件, 创建一个 loader 函数工具并导出加载程序:

// utils.jsx

import { getContacts } from "./contacts";

// 模拟网络请求,获取数据
export async function rootLoader () {
    const contacts = await getContacts();
    return { contacts };
}

修改路由配置文件 routerConfig , 加入数据加载的配置

import { createBrowserRouter } from "react-router-dom";

import Root from "./Root";
import Error404 from "./Error404";
import Contact from "./Contact";
import { rootLoader } from "./utils";

const router = createBrowserRouter([
    {
        path: "/",
        element: <Root />,
        errorElement: <Error404 />,
        loader: rootLoader,
        children: [
            {
                path: "contacts/:contactId",
                element: <Contact />,
            },
        ]
    }
]);

export default router;

关于网络请求,我后面会写一个 Axios 的专门技术文章。这里请忽略网络请求部分。
现在当Root组件呈现后,loader 中就有了要使用的数据了。下面我们来把这个数据呈现出来:

// Root.jsx

import {
    Outlet,
    Link,
    useLoaderData,
} from 'react-router-dom';

import { getContact } from './contacts';

export default function Root() {
    const { contacts } = useLoaderData();
    return (
        <>
            <div id="sidebar">
                { ... }
                <nav>
                    {contacts.length ? (
                        <ul>
                            {contacts.map((contact) => (
                                <li key={contact.id}>
                                    <Link to={`contacts/${contact.id}`}>
                                        {contact.first || contact.last ? (
                                            <>
                                                {contact.first} {contact.last}
                                            </>
                                        ) : (
                                            <i>No Name</i>
                                        )}{" "}
                                        {contact.favorite && <span></span>}
                                    </Link>
                                </li>
                            ))}
                        </ul>
                    ) : (
                        <p>
                            <i>No contacts</i>
                        </p>
                    )}
                </nav>
            </div>
            <div id="detail"> <Outlet /> </div>
        </>
    );
}

使用 useLoaderData() 就可以获取 loader 中的数据。是不是很简单。
现在是一个空的联系人列表。我们继续:

我们在工具函数库 utils.jsx中增加函数 createContact(),

import { getContacts, createContact } from "./contacts";

... 

// 创建联系人
export async function action() {
    const contact = await createContact();
    return { contact };
}
添加联系人

继续修改Root.jsx组件:
一般的网页中我们提交表单都是通过 <form></form> 元素中写处理表单的服务器接口, 本示例的做法是提交表单是向路由提交,至于提交后的如何处理则由路由的action来做决定。总之最终的数据是由action来负责请求返回的。要想向路由提交表单,必须要用到 React Router Dom中的 Form 组件功能。下面我们把 form 改用成 Form,如例如示。

// Root.jsx

import {
  Outlet,
  Link,
  useLoaderData,
  Form,
} from "react-router-dom";
import { getContacts, createContact } from "./contacts";

/* ... */

export default function Root() {
  const { contacts } = useLoaderData();
  return (
    <>
      <div id="sidebar">
        <h1>React Router Contacts</h1>
        <div>
          {/* ... */}
          <Form method="post">
            <button type="submit">New</button>
          </Form>
        </div>

        {/* ...  */}
      </div>
    </>
  );
}

那么我们还要向路由添加 action 功能。
修改 routerConfig.jsx 配置文件,增加 action :

import { createBrowserRouter } from "react-router-dom";

import Root from "./Root";
import Error404 from "./Error404";
import Contact from "./Contact";
import {
    rootLoader,
    action as rootAction,
} from "./utils";

const router = createBrowserRouter([
    {
        path: "/",
        element: <Root />,
        errorElement: <Error404 />,
        loader: rootLoader,
        action: rootAction,
        children: [
            {
                path: "contacts/:contactId",
                element: <Contact />,
            },
        ]
    }
]);

export default router;

现在我们单击新建联系人按钮只是增加了一个空的联系人。不过没有关系,后面我们还要继续讲解,我们下回见分晓。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1423187.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

怎么判断一个链表是否成环?怎么找到成环的起点

问题引入 给定一个单向链表&#xff0c;怎么判断这个链表是否成环&#xff1f;如果这个链表是环形的&#xff0c;找到这个环形的起点。 Getter Setter public class ListNode {public Integer val;public ListNode next;public ListNode(Integer val) {this.val val;}public…

嵌入式学习第十四天

1.结构体&#xff08;2&#xff09;: &#xff08;1&#xff09;结构体类型定义 &#xff08;2&#xff09;结构体变量的定义 &#xff08;3&#xff09;结构体元素的访问 &#xff08;4&#xff09;结构体的存储: 内存对齐: char 按照1字节对齐 …

DDD学习使用

简介 DDD(Domain-Driven Design)&#xff1a;领域驱动设计。 Eric Evans “领域驱动设计之父” DDD不是架构&#xff0c;而是一种方法论&#xff08;Methodology&#xff09;微服务架构从一出来就没有很好的理论支撑如何合理的划分服务边界&#xff0c;人们常常为服务要划分多…

java面向对象基础(面试)

一、面向对象基础 1. 面向对象和面向过程的区别 面向过程把解决问题的过程拆成一个个方法&#xff0c;通过一个个方法的执行解决问题。面向对象会先抽象出对象&#xff0c;然后用对象执行方法的方式解决问题。 2.创建一个对象用什么运算符?对象实体与对象引用有何不同? n…

坚持刷题 | 完全二叉树的节点个数

Hello&#xff0c;大家好&#xff0c;我是阿月&#xff01;坚持刷题&#xff0c;老年痴呆追不上我&#xff0c;今天刷&#xff1a;完全二叉树的节点个数 题目 222.完全二叉树的节点个数 代码实现 class TreeNode {int val;TreeNode left, right;public TreeNode(int val) …

1990-2021年各省绿色金融指数数据(含原始数据+测算结果)

1990-2021年全国各省绿色金融指数数据&#xff08;含原始数据结果&#xff09; 1、时间&#xff1a;1990-2021年 2、指标&#xff1a;地区、年份、该省环保项目信贷总额&#xff08;亿元&#xff09;、全省信贷总额&#xff08;亿元&#xff09;、绿色信贷、环境污染治理投资…

记录 arm 开发板上 nginx 配置 http 服务注意事项

1. 自定义项目&#xff0c;需要在 conf.d 目录中增加一个 .conf 配置文件&#xff1a; server {listen 9200; # 端口号server_name localhost; # 服务名称location / {root /home/imx6q/media; # 项目根目录&#xff08;需要修改 n…

41、WEB攻防——通用漏洞XMLXXE无回显DTD实体伪协议代码审计

文章目录 XXE原理&探针&利用XXE读取文件XXE带外测试XXE实体引用XXE挖掘XXE修复 参考资料&#xff1a;CTF XXE XXE原理&探针&利用 XXE用到的重点知识是XML&#xff0c;XML被设计为传输和存储数据&#xff0c;XML文档结构包括XML声明、DTD文档类型定义&#xf…

Elasticsearch Windows版安装配置

Elasticsearch简介 Elasticsearch是一个开源的搜索文献的引擎&#xff0c;大概含义就是你通过Rest请求告诉它关键字&#xff0c;他给你返回对应的内容&#xff0c;就这么简单。 Elasticsearch封装了Lucene&#xff0c;Lucene是apache软件基金会一个开放源代码的全文检索引擎工…

OpenAI API 的最新动态:新一代的嵌入模型,更新 GPT-4 Turbo,更新 GPT-3.5 Turbo 以及降低 API 价格

文章目录 一、前言二、主要内容三、总结 &#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ 一、前言 OpenAI 正在推出新一代嵌入模型、新的 GPT-4 Turbo 和审查模型、新的 API 使用管理工具&#xff0c;而且很快就会降低 GPT-3.5 Turbo 的价格。 OpenAI…

latex中的一些小要点

表格 1.生成双直线表格 \begin{tabular}{| l | l |}⬇ ⬇ ⬇ ⬇ ⬇ ⬇\begin{tabular}{| l || l |}\hline ⬇ ⬇ ⬇\hline \hline →

Java基于SpringBoot+Vue的电影影城管理系统,附源码,文档

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

【PyQt】04-Designer

文章目录 前言一、初级 Designer1.1 拖拽设计界面1.2 搞定之后记得保存ui文件1.3 载入代码1.4 运行结果 二、登入界面代码效果展示账号密码错误时账号和密码正确 总结 前言 自然还是跟着王铭东老师学的 一、初级 Designer 1.1 拖拽设计界面 进度条是这个 1.2 搞定之后记得保…

布局技巧及CSS初始化

一&#xff0c;margin负值巧妙应用 二&#xff0c;文字围绕浮动元素 三&#xff0c;行内块 四&#xff0c;CSS三角强化 五&#xff0c;CSS初始化 一&#xff0c;margin负值巧妙应用 制作盒子的细线边框&#xff1a; 鼠标经过li后变色&#xff1a; 二&#xff0c;文字围绕…

3D 转换

1&#xff0c;3D的特点&#xff1a; 近小远大 物体后面遮挡不可见 2&#xff0c;3D移动 translate3d 3D移动在2D移动的基础上多加了一个可以移动的方向&#xff0c;就是z轴方向 transform&#xff1a;translateX&#xff08;100px&#xff09;&#xff1a;仅仅是在x轴上移动…

Redis冲冲冲——缓存三兄弟:缓存击穿、穿透、雪崩

目录 引出缓存击穿缓存穿透缓存雪崩 总结 引出 谈谈redis的击穿、穿透、雪崩。 缓存击穿 缓存击穿&#xff1a;redis中没有&#xff0c;但是数据库有 顺序&#xff1a;先查缓存&#xff0c;判断缓存是否存在&#xff1b;如果缓存存在&#xff0c;直接返回数据&#xff1b;如果…

考研高数(共轭根式)

1.定义 共轭根式&#xff1a;是指两个不等于零的根式A、B&#xff0c;若它们的积AB不含根式&#xff0c;则称A、B互为共轭根式。 共轭根式的一个显著特点是通过相乘能把根号去掉&#xff0c;这是很有帮助的 2.常用的共轭根式 3.例题 1&#xff09;求极限 2&#xff09;证明…

第九节HarmonyOS 常用基础组件21-ImageAnimator

1、描述 提供帧动画组件来实现逐帧播放图片的能力&#xff0c;可以配置需要播放的图片列表&#xff0c;每张图片可以配置时长。 2、接口 ImageAnimator() 3、属性 参数名 参数类型 描述 images Array<ImageFrameInfo> 设置图片帧信息集合&#xff0c;每一帧的帧…

#pycharm|插件#pycharm提高生产力的工具,自用顺手,推荐一波

aiXcoder Code Completer aiXcoder 结合上下文为用户生成完整且更符合实际场景的代码行或者代码块&#xff0c;同时提供生成代码、自动生成单元测试、Bug自动修复、代码解释、生成注释等功能。 CodeGeeX 代码极X CodeGeeX是一款基于LLMs的强大智能编程助手。提供代码生成/补全…

bluecms 代码审计

一 环境准备 BlueCMS v1.6 sp1 phpstudy php5.6.9apachemysql 二 环境搭建 phpstudy 1.把下好的BlueCMS源码文件bluecms_src放到phpStudy的WWW目录下 2.访问本地&#xff1a;http://localhost/bluecms_src/&#xff0c; 能看到项目文件 3.访问地址&#xff1a;http://loca…