Django5+React18前后端分离开发实战14 React-Router6 入门教程

使用nodejs18

首先,将nodejs切换到18版本:

nvm use 18

在这里插入图片描述

创建项目

npm create vite@latest zdpreact_basic_router_dev -- --template react
cd zdpreact_basic_router_dev 
npm install react-router-dom localforage match-sorter sort-by
npm run dev

此时访问:http://localhost:5173/

在这里插入图片描述

使用webstorm打开项目

使用webstorm打开项目,然后配置一个npm启动:
在这里插入图片描述

接着,通过快捷方式启动服务:
在这里插入图片描述

基础环境

修改 src/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%;
}

新增 src/contacts.js

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);});
}

修改 src/main.jsx

import * as React from "react";
import * as ReactDOM from "react-dom/client";
import {createBrowserRouter,RouterProvider,
} from "react-router-dom";
import "./index.css";const router = createBrowserRouter([{path: "/",element: <div>Hello world!</div>,},
]);ReactDOM.createRoot(document.getElementById("root")).render(<React.StrictMode><RouterProvider router={router} /></React.StrictMode>
);

重新预览

http://localhost:5173/

在这里插入图片描述

根路由

新增 src/routes/root.jsx

export default function Root() {return (<><div id="sidebar"><h1>React Router Contacts</h1><div><form id="search-form" role="search"><inputid="q"aria-label="Search contacts"placeholder="Search"type="search"name="q"/><divid="search-spinner"aria-hiddenhidden={true}/><divclassName="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></>);
}

修改 src/main.jsx

import * as React from "react";
import * as ReactDOM from "react-dom/client";
import {createBrowserRouter,RouterProvider,
} from "react-router-dom";
import "./index.css";
import Root from "./routes/root.jsx"const router = createBrowserRouter([{path: "/",element: <Root/>,},
]);ReactDOM.createRoot(document.getElementById("root")).render(<React.StrictMode><RouterProvider router={router}/></React.StrictMode>
);

重新预览

http://localhost:5173/

在这里插入图片描述

处理404错误

新增 src/error-page.jsx

import {useRouteError} from "react-router-dom";export default function ErrorPage() {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>);
}

修改 src/main.jsx

import * as React from "react";
import * as ReactDOM from "react-dom/client";
import {createBrowserRouter,RouterProvider,
} from "react-router-dom";
import "./index.css";
import Root from "./routes/root.jsx"
import ErrorPage from "./error-page.jsx";const router = createBrowserRouter([{path: "/",element: <Root/>,errorElement: <ErrorPage/>,},
]);ReactDOM.createRoot(document.getElementById("root")).render(<React.StrictMode><RouterProvider router={router}/></React.StrictMode>
);

访问不存在页面

此时,访问:http://localhost:5173/404

在这里插入图片描述

联系人页面

新增 src/routes/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><imgkey={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><atarget="_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><Formmethod="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}) {// yes, this is a `let` for laterlet favorite = contact.favorite;return (<Form method="post"><buttonname="favorite"value={favorite ? "false" : "true"}aria-label={favorite? "Remove from favorites": "Add to favorites"}>{favorite ? "★" : "☆"}</button></Form>);
}

修改 src/main.jsx

import * as React from "react";
import * as ReactDOM from "react-dom/client";
import {createBrowserRouter,RouterProvider,
} from "react-router-dom";
import "./index.css";
import Root from "./routes/root.jsx"
import ErrorPage from "./error-page.jsx";
import Contact from "./routes/contact.jsx";const router = createBrowserRouter([{path: "/",element: <Root/>,errorElement: <ErrorPage/>,},{path: "contacts/:contactId",element: <Contact/>,},
]);ReactDOM.createRoot(document.getElementById("root")).render(<React.StrictMode><RouterProvider router={router}/></React.StrictMode>
);

访问联系人页面

浏览器访问:http://localhost:5173/contacts/1

在这里插入图片描述

嵌套路由

修改 src/main.jsx

import * as React from "react";
import * as ReactDOM from "react-dom/client";
import {createBrowserRouter,RouterProvider,
} from "react-router-dom";
import "./index.css";
import Root from "./routes/root.jsx"
import ErrorPage from "./error-page.jsx";
import Contact from "./routes/contact.jsx";const router = createBrowserRouter([{path: "/",element: <Root/>,errorElement: <ErrorPage/>,children: [{path: "contacts/:contactId",element: <Contact/>,},]},
]);ReactDOM.createRoot(document.getElementById("root")).render(<React.StrictMode><RouterProvider router={router}/></React.StrictMode>
);

修改 src/routes/root.jsx

import {Outlet} from "react-router-dom";export default function Root() {return (<><div id="sidebar"><h1>React Router Contacts</h1><div><form id="search-form" role="search"><inputid="q"aria-label="Search contacts"placeholder="Search"type="search"name="q"/><divid="search-spinner"aria-hiddenhidden={true}/><divclassName="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">{/*嵌套路由*/}<Outlet/></div></>);
}

继续修改 src/routes/root.jx

修改左侧菜单链接:

import {Link, Outlet} from "react-router-dom";export default function Root() {return (<><div id="sidebar"><h1>React Router Contacts</h1><div><form id="search-form" role="search"><inputid="q"aria-label="Search contacts"placeholder="Search"type="search"name="q"/><divid="search-spinner"aria-hiddenhidden={true}/><divclassName="sr-only"aria-live="polite"></div></form><form method="post"><button type="submit">New</button></form></div>{/*左侧菜单*/}<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></>);
}

效果预览

先访问首页,在点击左侧菜单的链接,会自动跳转:http://localhost:5173/contacts/1

在这里插入图片描述

加载数据

修改 src/routes/root.jsx

核心代码:

import {getContacts} from "../contacts";// 加载联系人信息
export async function loader() {const contacts = await getContacts();return {contacts};
}

完整代码:

import {Link, Outlet} from "react-router-dom";
import {getContacts} from "../contacts";// 加载联系人信息
export async function loader() {const contacts = await getContacts();return {contacts};
}export default function Root() {return (<><div id="sidebar"><h1>React Router Contacts</h1><div><form id="search-form" role="search"><inputid="q"aria-label="Search contacts"placeholder="Search"type="search"name="q"/><divid="search-spinner"aria-hiddenhidden={true}/><divclassName="sr-only"aria-live="polite"></div></form><form method="post"><button type="submit">New</button></form></div>{/*左侧菜单*/}<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></>);
}

修改 src/main.jsx

核心代码:

import Root, {loader as rootLoader} from "./routes/root.jsx"const router = createBrowserRouter([{path: "/",element: <Root/>,errorElement: <ErrorPage/>,loader: rootLoader,children: [{path: "contacts/:contactId",element: <Contact/>,},]},
]);

完整代码:

import * as React from "react";
import * as ReactDOM from "react-dom/client";
import {createBrowserRouter,RouterProvider,
} from "react-router-dom";
import "./index.css";
import Root, {loader as rootLoader} from "./routes/root.jsx"
import ErrorPage from "./error-page.jsx";
import Contact from "./routes/contact.jsx";const router = createBrowserRouter([{path: "/",element: <Root/>,errorElement: <ErrorPage/>,loader: rootLoader,children: [{path: "contacts/:contactId",element: <Contact/>,},]},
]);ReactDOM.createRoot(document.getElementById("root")).render(<React.StrictMode><RouterProvider router={router}/></React.StrictMode>
);

再次修改 src/routes/root.jsx

import {Link, Outlet, useLoaderData} from "react-router-dom";
import {getContacts} from "../contacts";// 加载联系人信息
export async function loader() {const contacts = await getContacts();return {contacts};
}export default function Root() {const {contacts} = useLoaderData();return (<><div id="sidebar"><h1>React Router Contacts</h1><div><form id="search-form" role="search"><inputid="q"aria-label="Search contacts"placeholder="Search"type="search"name="q"/><divid="search-spinner"aria-hiddenhidden={true}/><divclassName="sr-only"aria-live="polite"></div></form><form method="post"><button type="submit">New</button></form></div>{/*左侧菜单*/}<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></>);
}

新增联系人功能

修改 src/routes/root.jsx

核心代码:

export async function action() {const contact = await createContact();return {contact};
}

完整代码:

import {Link, Outlet, useLoaderData} from "react-router-dom";
import {createContact, getContacts} from "../contacts";// 加载联系人信息
export async function loader() {const contacts = await getContacts();return {contacts};
}// 创建联系人
export async function action() {const contact = await createContact();return {contact};
}export default function Root() {const {contacts} = useLoaderData();return (<><div id="sidebar"><h1>React Router Contacts</h1><div><form id="search-form" role="search"><inputid="q"aria-label="Search contacts"placeholder="Search"type="search"name="q"/><divid="search-spinner"aria-hiddenhidden={true}/><divclassName="sr-only"aria-live="polite"></div></form><form method="post"><button type="submit">New</button></form></div>{/*左侧菜单*/}<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></>);
}

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

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

相关文章

kafka跨地区跨集群同步工具MirrorMaker2 —— 筑梦之路

MM2简介 KIP-382: MirrorMaker 2.0 - Apache Kafka - Apache Software Foundation 有四种运行MM2的方法&#xff1a; As a dedicated MirrorMaker cluster.&#xff08;作为专用的MirrorMaker群集&#xff09; As a Connector in a distributed Connect cluster.&#xff08…

使用IDEA远程debug调试

文章目录 应用背景开启方式IDEA设置启动脚本改造 参考资料 应用背景 springboot项目&#xff0c;部署到服务器上&#xff0c;需要开启远程debug跟踪代码。 使用idea开启远程debug。 开启方式 IDEA设置 选择 Edit Configuration 如图&#xff0c;点击加号&#xff0c;选择Re…

【机器学习】利用机器学习优化陆军战术决策与战场态势感知

&#x1f512;文章目录&#xff1a; &#x1f4a5;1.引言 &#x1f6f4;2.机器学习在陆军战术决策中的应用 &#x1f6e3;️2.1数据收集与预处理 &#x1f304;2.2模型构建与训练&#xff1a; &#x1f305;2.3实时决策支持&#xff1a; &#x1f305;2.4代码实现 &…

排序算法——上

一、冒泡排序&#xff1a; 1、冒泡排序算法的思想 我们从左边开始把相邻的两个数两两做比较&#xff0c;当一个元素大于右侧与它相邻的元素时&#xff0c;交换它们之间位置&#xff1b;反之&#xff0c;它们之间的位置不发生变化。冒泡排序是一种稳定的排序算法。 2、代码实现…

5月20日分割等和子集+最后一块石头的重量Ⅱ

416.分割等和子集 给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集&#xff0c;使得两个子集的元素和相等。 示例 1&#xff1a; 输入&#xff1a;nums [1,5,11,5] 输出&#xff1a;true 解释&#xff1a;数组可以分割成 [1, 5, 5] 和…

【ai】LiveKit Agent 的example及python本地开发模式工程实例

title: ‘LiveKit Agent Playground’ playgroundLiveKit Community playground的环境变量&#xff1a;LiveKit API # LiveKit API Configuration LIVEKIT_API_KEYYOUR_API_KEY LIVEKIT_API_SECRETYOUR_API_SECRET# Public configuration NEXT_PUBLIC_LIVEKIT_URLwss://YOUR_…

JAVA智慧工厂制造生产管理MES系统,全套源码,多端展示(MES与ERP系统的区别和联系)

MES与ERP系统的区别和联系 MES制造执行系统&#xff0c;是一套面向制造公司车间执行层的生产信息化管理系统。MES 可觉得公司提供涉及制造数据管理、计划排产管理、生产调度管理、库存管理、质量管理、人力资源管理、工作中心、设备管理、工具工装管理、采购管理、成本管理、项…

为什么推荐前端用WebStorm软件编程?

一、介绍 WebStorm是由JetBrains公司开发的一款JavaScript开发工具&#xff0c;被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。它支持JavaScript、ECMAScript 6、TypeScript、CoffeeScript、Dart和Flow等多种语言的代码…

大学搜题软件音乐类?分享三个支持答案和解析的工具 #微信#媒体

高效的学习工具可以帮助我们提高记忆力和理解能力&#xff0c;使知识更加深入人心。 1.彩虹搜题 这是个微信公众号 一款专门供全国大学生使用的查题神器!致力于帮助大学生解决学习上的难题,涵盖了大学生学习所需的学习资料。 下方附上一些测试的试题及答案 1、甲、乙合伙开…

goimghdr,一个有趣的 Python 库!

更多Python学习内容&#xff1a;ipengtao.com 大家好&#xff0c;今天为大家分享一个有趣的 Python 库 - goimghdr。 Github地址&#xff1a;https://github.com/corona10/goimghdr 在图像处理和分析过程中&#xff0c;识别图像文件的类型是一个常见的需求。Python自带的imghdr…

开源与闭源:AI模型发展的两条路径

目录 前言1 数据隐私保护与用户数据安全1.1 开源大模型的透明性与挑战1.2 闭源大模型的控制与责任 2 商业应用的优劣比较2.1 开源大模型的灵活性与创新2.2 闭源大模型的可靠性与服务质量 3 社区参与与合作的差异3.1 开源大模型的社区驱动与协作3.2 闭源大模型的企业主导与保密性…

【openlayers系统学习】3.3假彩色图像合成(三个波段合成假彩色图像)

三、假彩色图像合成 在上一步中&#xff0c;我们使用 ol/source/GeoTIFF​ 源从单个多波段源&#xff08;具有红色、绿色、蓝色和Alpha波段&#xff09;渲染真彩色图像。在下面这个例子中&#xff0c;我们将从可见光谱之外提取数据&#xff0c;并使用它来呈现假彩色合成。 我…

【基于 PyTorch 的 Python 深度学习】9 目标检测与语义分割(2)

前言 文章性质&#xff1a;学习笔记 &#x1f4d6; 学习资料&#xff1a;吴茂贵《 Python 深度学习基于 PyTorch ( 第 2 版 ) 》【ISBN】978-7-111-71880-2 主要内容&#xff1a;根据学习资料撰写的学习笔记&#xff0c;该篇主要介绍了优化候选框的几种方法。 一、优化候选框的…

抖店怎么选品?抖店爆款选品思路技巧,新手直接用!

大家好&#xff0c;我是电商花花。 抖店选品永远是我们做抖店&#xff0c;做电商的核心&#xff0c;店铺想要出单&#xff0c;想要赚钱&#xff0c;我们就一定要学会怎么选品&#xff0c;怎么筛选商品。 而我们绝大多数新手并没有办法保证持续选爆款的能力&#xff0c;如果店…

【ARM+Codesys案例】T3/RK3568/树莓派+Codesys锂电池测试设备控制解决方案

锂电池诞生于上世纪60年代&#xff0c;90年代开始由日本索尼公司实现商业化。锂离子电池凭借快速充放电、长循环寿命、无记忆效应等众多优点&#xff0c;成为当今数码产品及电动汽车大规模应用的第一选择。与镍氢电池、铅酸电池相比&#xff0c;锂电池可以存储更多电能。现在&a…

Visual Studio 智能代码插件:CodeGeeX

前言 在软件开发领域&#xff0c;高效的编程助手一直是提升开发者效率和质量的关键。 随着人工智能技术的不断发展&#xff0c;智能编程助手逐渐成为开发者们不可或缺的工具。其中&#xff0c;CodeGeeX作为一款专为Visual Studio设计的免费智能编程助手&#xff0c;凭借其强大…

让大模型更聪明——复杂而艰巨的任务

一、引言 在人工智能领域&#xff0c;大模型因其强大的数据处理能力和复杂的结构&#xff0c;成为了推动技术进步的重要力量。然而&#xff0c;要让大模型真正展现出“聪明”的特质&#xff0c;即具备高度的人类智能水平&#xff0c;仍是一项极具挑战性的任务。本文将从数据质…

Java-Stream流-概述、创建、使用:遍历/匹配、筛选、聚合、映射、归约、排序、提取/组合

Java8-Stream&#xff1a; 一、Stream流概述1.Stream流的特点&#xff1a;2.使用步骤&#xff1a;3.常用方法示例&#xff1a; 二、Stream流创建1.常见的创建Stream的方法2. stream()或parallelStream()方法的使用和选择 三、Stream流使用Optional案例中使用的实体类1.遍历/匹配…

MYSQL之安装

一&#xff0c;下载仓库包 wget -i -c https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm二&#xff0c;安装仓库 yum -y install mysql80-community-release-el7-3.noarch.rpmsed -i s/gpgcheck1/gpgcheck0/g mysql-community.repo三&#xff0c;安装MY…

JS——对象

1.什么是对象 对象是什么&#xff1f; 对象是一种数据类型 无序的数据的集合&#xff08; 数组是有序的数据集合 &#xff09; 对象有什么特点&#xff1f; 无序的数据的集合 可以详细地描述某个事物 静态特征 (姓名, 年龄, 身高, 性别, 爱好) > 可以使用数字, 字符串…