React进阶之React Router&SSR

React Router&SSR

React Router

create-react-router

pnpx create-react-router@latest --template remix-run/react-router/tutorials/address-book

pnpm run dev

目录结构:

tsconfig.json

javascript 复制代码
{
  "include": [
    "**/*", //全局路径
    "**/.server/**/*", //SSR
    "**/.client/**/*", //CSR
    ".react-router/types/**/*"
  ],
  "compilerOptions": {
    "lib": ["DOM", "DOM.Iterable", "ES2022"],
    "types": ["node", "vite/client"],
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "rootDirs": [".", "./.react-router/types"],
    "baseUrl": ".",
    "esModuleInterop": true,
    "verbatimModuleSyntax": true,
    "noEmit": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "strict": true
  }
}

react-router.config.ts

xxx.config.xxx 这种文件是可以从根路径去解析的

typescript 复制代码
import { type Config } from "@react-router/dev/config";

export default {
  ssr: false, //ssr设置为false
} satisfies Config; //Config定义的类型
补充一

添加预加载,这样对后面的静态页面 about 就不会出现先加载后再展示页面了
pre-rendering

借助的是浏览器中的预渲染

通过预加载的方式找到/app/routes/about.tsx这个路径,这个资源因此也可以前置加载了

javascript 复制代码
import { type Config } from "@react-router/dev/config";

export default {
  ssr: false, //ssr设置为false
  prerender: ['/about'] //预加载
} satisfies Config; //Config定义的类型

package.json

scripts
  1. dev:执行的是 react-router-dev,"dev": "react-router dev",
  2. build:传递的是环境的变量,可以看作是服务端渲染时的场景,"build": "cross-env NODE_ENV=production react-router build",

pnpm run build

会先进行打包构建,得到client,如果react-router.config.ts中的ssr为true,则有client和service两个包

这是ssr为false时:

这是ssr为true时:

servser端中的,index.js中是一些路由的配置

state状态,router路由 -> next 在ssr中是有一个同构的概念, 客户端渲染的部分,服务端渲染的部分会有同步的依赖,这部分相同的依赖就有可能是 状态,路由

  1. start:这时是 serve,cross-env NODE_ENV=production react-router-serve ./build/server/index.js
dependencies
  1. @react-router/node
  2. @react-router/servce:react-router-serve的依赖

app

整体的根路径就是:root.tsx

root.tsx
介绍

由三部分构成:

  1. APP 渲染页面的结构,form表单
  2. Layout 布局,整体页面,html是在这里绘制的
  3. ErrorBoundary 目前还没用上
typescript 复制代码
import type { Route } from "./+types/root";

这里 Route的根路径,算是最新版本的特性,指代当前 router的类型,

这里的 ./+types/root 使用的是 tsconfig.json中的 rootDirs[1]的值,完整是 .react-router/types/app/+types 这个路径

用的就是这个文件

javascript 复制代码
export default function App() {
	......
}

是前端应用中常见的绘制的部分

javascript 复制代码
import appStylesHref from "./app.css?url";

这个也就是 app/app.css的部分,算是对应的同一型的,默认型的样式的格式

javascript 复制代码
import {
  Form,
  Scripts,
  ScrollRestoration,
  isRouteErrorResponse,
} from "react-router";
import type { Route } from "./+types/root";

import appStylesHref from "./app.css?url";

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

// The Layout component is a special export for the root route.
// It acts as your document's "app shell" for all route components, HydrateFallback, and ErrorBoundary
// For more information, see https://reactrouter.com/explanation/special-files#layout-export
export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="stylesheet" href={appStylesHref} />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

// The top most error boundary for the app, rendered when your app throws an error
// For more information, see https://reactrouter.com/start/framework/route-module#errorboundary
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
  let message = "Oops!";
  let details = "An unexpected error occurred.";
  let stack: string | undefined;

  if (isRouteErrorResponse(error)) {
    message = error.status === 404 ? "404" : "Error";
    details =
      error.status === 404
        ? "The requested page could not be found."
        : error.statusText || details;
  } else if (import.meta.env.DEV && error && error instanceof Error) {
    details = error.message;
    stack = error.stack;
  }

  return (
    <main id="error-page">
      <h1>{message}</h1>
      <p>{details}</p>
      {stack && (
        <pre>
          <code>{stack}</code>
        </pre>
      )}
    </main>
  );
}
补充一

后面contacts.ts补充后,这里开始增加详情页

  1. 增加Outlet
javascript 复制代码
import {
  Form,
  //详情页
  Outlet,
  Scripts,
  ScrollRestoration,
  isRouteErrorResponse,
} from "react-router";
  1. Outlet增加到页面
javascript 复制代码
return (
    <>
      <div id="sidebar">
         ....
      </div>
	 <div id="detail">
	    <Outlet />
	 </div>
	 </>
  );

完成上面这两部后,页面上就成这样了:

补充二

目前从 contacts/1 切换到 contacts/2 是重定向的动作,通过刷新页面的方式,利用history导航进行页面跳转,希望不通过刷新页面的方式去做,这样要怎么做?

使用Link

  1. 引入Link
javascript 复制代码
import {
  Form,
  Outlet,
  //增加Link
  Link,
  Scripts,
  ScrollRestoration,
  isRouteErrorResponse,
} from "react-router";
  1. 写入Link
html 复制代码
<nav>
  <ul>
    <li>
      {/* 将这里原本的a标签替换成Link */}
      <Link to={`/contacts/1`}>Your Name</Link>
    </li>
    <li>
      <Link to={`/contacts/2`}>Your Friend</Link>
    </li>
  </ul>
</nav>
补充三
  1. 引入数据
javascript 复制代码
// 通过mock接口请求,返回假数据
import { getContacts } from "./data";
  1. 导出一个方法,客户端请求加载
javascript 复制代码
export async function clientLoader() {
  // 将此方法的loader作为参数透传出去
  const contacts = await getContacts();

  return { contacts };
}
  1. 样式中修改
javascript 复制代码
// 传递不同的加载器,针对不同加载器来加载不同数据
//type ComponentProps = T.CreateComponentProps<Info>,ComponentProps这个类型是客户端下发下来的,Info是路由的节点状态
export default function App({ loaderData }:Route.ComponentProps) {   
	//这里的contacts是自动解析出来的
	const { contacts } = loaderData;
  	...
  		<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>
                    ) : null}
                  </Link>
                </li>
              ))}
            </ul>
          ) : (
            <p>
              <i>No contacts</i>
            </p>
          )}
        </nav>
........        
}
补充四

这里做的是客户端 渲染的事情,客户端渲染会出现一个问题,会有一个白屏渲染的过程,是客户端需要加载资源生成的,优化刷新白屏跳转要怎么做?

优化办法:
HydrateFallback:当页面初始化的时候,渲染之前能够做一个fallback

javascript 复制代码
//router中默认能够解析到的
export function HydrateFallback() {
  return (
    <div id="loading-splash">
      <div id="loading-splash-spinner" />
      <p>Loading, please wait...</p>
    </div>
  );
}
补充五

添加 about 导航

javascript 复制代码
export default function App({ loaderData }:Route.ComponentProps) { 
...
   <div id="sidebar">
        <h1>
          <Link to="about">React Router Contacts</Link>
        </h1>
        ...
   </div>
}
补充六

将sidebar内容提出来,App精简成这样,并且删除clientLoader方法:

javascript 复制代码
export default function App() { 
  return <Outlet />
}

所有代码:

javascript 复制代码
import {
  Outlet,
  Scripts,
  ScrollRestoration,
  isRouteErrorResponse,
} from "react-router";
import type { Route } from "./+types/root";

import { getContacts } from "./data";

import appStylesHref from "./app.css?url";

//router中默认能够解析到的
export function HydrateFallback() {
  return (
    <div id="loading-splash">
      <div id="loading-splash-spinner" />
      <p>Loading, please wait...</p>
    </div>
  );
}


// 传递不同的加载器,针对不同加载器来加载不同数据
//type ComponentProps = T.CreateComponentProps<Info>,ComponentProps这个类型是客户端下发下来的,Info是路由的节点状态
export default function App() { 
  return <Outlet />
}

// The Layout component is a special export for the root route.
// It acts as your document's "app shell" for all route components, HydrateFallback, and ErrorBoundary
// For more information, see https://reactrouter.com/explanation/special-files#layout-export
export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="stylesheet" href={appStylesHref} />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

// The top most error boundary for the app, rendered when your app throws an error
// For more information, see https://reactrouter.com/start/framework/route-module#errorboundary
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
  let message = "Oops!";
  let details = "An unexpected error occurred.";
  let stack: string | undefined;

  if (isRouteErrorResponse(error)) {
    message = error.status === 404 ? "404" : "Error";
    details =
      error.status === 404
        ? "The requested page could not be found."
        : error.statusText || details;
  } else if (import.meta.env.DEV && error && error instanceof Error) {
    details = error.message;
    stack = error.stack;
  }

  return (
    <main id="error-page">
      <h1>{message}</h1>
      <p>{details}</p>
      {stack && (
        <pre>
          <code>{stack}</code>
        </pre>
      )}
    </main>
  );
}
补充七
  1. 引入模拟数据接口
javascript 复制代码
import { createEmptyContact } from "./data";
  1. 导出数据
javascript 复制代码
export async function action() {
  const contact = await createEmptyContact()
  
  return {
    contact 
  }
}
routes.ts

安装 @react-router/dev 和 @types/react-router 两个插件

typescript 复制代码
import type { RouteConfig } from "@react-router/dev/routes";

//后补充的代码
import {route} from '@react-router/dev/routes' 

//export default [] satisfies RouteConfig;
// 针对 contacts/1,contacts/2 这种格式,匹配到 routes/contacts.tsx这个文件
export default [
   route('contacts/:contactId','routes/contacts.tsx')
] satisfies RouteConfig;
补充一

引入home.tsx

  1. 引入index
javascript 复制代码
import {index,route} from '@react-router/dev/routes' 
  1. 导出home.tsx
javascript 复制代码
export default [
   index('routes/home.tsx'),
   route('contacts/:contactId','routes/contacts.tsx')
] satisfies RouteConfig;
补充二

添加about路由

javascript 复制代码
import type { RouteConfig } from "@react-router/dev/routes";

import {index,route} from '@react-router/dev/routes' 

export default [
   index('routes/home.tsx'),
   route('contacts/:contactId', 'routes/contacts.tsx'),
   //about路由添加
   route('about','routes/about.tsx')
] satisfies RouteConfig;
补充三

额外增加新定义的想表达的页面

layout页面=左侧sidebar+右侧home/contacts/about

about页面=并列的关系,需要layout布局改变

  1. 引入layout
javascript 复制代码
import {index,layout,route} from '@react-router/dev/routes' 
  1. 修改路由
javascript 复制代码
export default [
   layout('layouts/sidebar.tsx', [
      index('routes/home.tsx'),
      route('contacts/:contactId', 'routes/contacts.tsx'),
   ]),
   route('about','routes/about.tsx')
] satisfies RouteConfig;
补充四
javascript 复制代码
export default [
   layout('layouts/sidebar.tsx', [
      index('routes/home.tsx'),
      route('contacts/:contactId', 'routes/contacts.tsx'),
   ]),
   route('about', 'routes/about.tsx'),
   route('contacts/:contactId/edit','routes/edit-contact.tsx')
] satisfies RouteConfig;

增加 edit 路由

routes
contacts.tsx

创建 routes/contacts.tsx 文件

从router 6开始,就开始刻意将路由视图绑定,由于 路由在客户端和服务端渲染都是能够共用的,除此之外,jsx的部分大多数也是能够共用的,因此认定 客户端渲染 和 服务端渲染,只是一种渲染模式,渲染内容是完全一致的。

javascript 复制代码
import { Form } from "react-router";

import type { ContactRecord } from "../data";

export default function Contact() {
  //初始的值,针对于输入框内补充对应用户的基本的信息
  const contact = {
    first: "Your",
    last: "Name",
    avatar: "https://placecats.com/200/200",
    twitter: "your_handle",
    notes: "Some notes",
    favorite: true,
  };

    return (
    //   布局的返回,使用jsx的方式,有数据则返回,没有数据则做一个兜底
    <div id="contact">
      <div>
        <img
          alt={`${contact.first} ${contact.last} avatar`}
          key={contact.avatar}
          src={contact.avatar}
        />
      </div>

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

        {contact.twitter ? (
          <p>
            <a
              href={`https://twitter.com/${contact.twitter}`}
            >
              {contact.twitter}
            </a>
          </p>
        ) : null}

        {contact.notes ? <p>{contact.notes}</p> : null}

        <div>
          <Form action="edit">
            <button type="submit">Edit</button>
          </Form>
          {/*  form表单,是否要删除的操作 */}
          <Form
            action="destroy"
            method="post"
            onSubmit={(event) => {
              const response = confirm(
                "Please confirm you want to delete this record."
              );
              if (!response) {
                event.preventDefault();
              }
            }}
          >
            <button type="submit">Delete</button>
          </Form>
        </div>
      </div>
    </div>
  );
}

function Favorite({
  contact,
}: {
  contact: Pick<ContactRecord, "favorite">;
}) {
  const favorite = contact.favorite; //favorite的传参

  return (
    // 是否喜爱的一个操作
    <Form method="post">
      <button
        aria-label={
          favorite
            ? "Remove from favorites"
            : "Add to favorites"
        }
        name="favorite"
        value={favorite ? "false" : "true"}
      >
        {favorite ? "★" : "☆"}
      </button>
    </Form>
  );
}

这样子以后,路径 contacts/1 就不会报404了

补充一
  1. 请求数据
javascript 复制代码
import { getContact, type ContactRecord } from "../data";

// params 就是 id的入参
export async function loader({params}):Route.LoaderArgs {
  const contact = await getContact(params.contactId)

  // 兜底,找不到名字的话就返回404
  if (!contact) {
    throw new Response('Could not find contact',{status:404})
  }

  return { contact };
}
  1. 获取数据,显示数据
javascript 复制代码
export default function Contact({ loaderData }:Route.ComponentProps) {
  //初始的值,针对于输入框内补充对应用户的基本的信息
  /* const contact = {
    first: "Your",
    last: "Name",
    avatar: "https://placecats.com/200/200",
    twitter: "your_handle",
    notes: "Some notes",
    favorite: true,
  }; */
   const {contact}=loaderData

    return (
    ...)
}
home.tsx

创建 routes/home.tsx 文件

javascript 复制代码
export default function Home() {
  return (
    <p id="index-page">
      This is a demo for React Router.
      <br />
      Check out{" "}
      <a href="https://reactrouter.com">
        the docs at reactrouter.com
      </a>
      .
    </p>
  );
}
about.tsx

创建 routes/about.tsx 文件

做静态页面,没有包含动态数据展示和请求处理

javascript 复制代码
import { Link } from "react-router";

export default function About() {
  return (
    <div id="about">
      <Link to="/">← Go to demo</Link>
      <h1>About React Router Contacts</h1>

      <div>
        <p>
          This is a demo application showing off some of the
          powerful features of React Router, including
          dynamic routing, nested routes, loaders, actions,
          and more.
        </p>

        <h2>Features</h2>
        <p>
          Explore the demo to see how React Router handles:
        </p>
        <ul>
          <li>
            Data loading and mutations with loaders and
            actions
          </li>
          <li>
            Nested routing with parent/child relationships
          </li>
          <li>URL-based routing with dynamic segments</li>
          <li>Pending and optimistic UI</li>
        </ul>

        <h2>Learn More</h2>
        <p>
          Check out the official documentation at{" "}
          <a href="https://reactrouter.com">
            reactrouter.com
          </a>{" "}
          to learn more about building great web
          applications with React Router.
        </p>
      </div>
    </div>
  );
}
edit-contact.tsx

在routes目录中新增edit-contact.tsx文件,也要注册到routes中

javascript 复制代码
import { Form,redirect } from "react-router";
import type { Route } from "./+types/edit-contact";

import { getContact,updateContact } from "../data";

export async function action({
  params,
  request,
}: Route.ActionArgs) {
    const formData = await request.formData();
    // React内置的绑定
    const updates = Object.fromEntries(formData);
    await updateContact(params.contactId, updates);
    // 重定向到当前的数据
    return redirect(`/contacts/${params.contactId}`);
}

export async function loader({ params }: Route.LoaderArgs) {
  const contact = await getContact(params.contactId);
  if (!contact) {
    throw new Response("Not Found", { status: 404 });
  }
  return { contact };
}

export default function EditContact({
  loaderData,
}: Route.ComponentProps) {
  const { contact } = loaderData;

  return (
    <Form key={contact.id} id="contact-form" method="post">
      <p>
        <span>Name</span>
        <input
          aria-label="First name"
          defaultValue={contact.first}
          name="first"
          placeholder="First"
          type="text"
        />
        <input
          aria-label="Last name"
          defaultValue={contact.last}
          name="last"
          placeholder="Last"
          type="text"
        />
      </p>
      <label>
        <span>Twitter</span>
        <input
          defaultValue={contact.twitter}
          name="twitter"
          placeholder="@jack"
          type="text"
        />
      </label>
      <label>
        <span>Avatar URL</span>
        <input
          aria-label="Avatar URL"
          defaultValue={contact.avatar}
          name="avatar"
          placeholder="https://example.com/avatar.jpg"
          type="text"
        />
      </label>
      <label>
        <span>Notes</span>
        <textarea
          defaultValue={contact.notes}
          name="notes"
          rows={6}
        />
      </label>
      <p>
        <button type="submit">Save</button>
        <button type="button">Cancel</button>
      </p>
    </Form>
  );
}

增加action方法后:

data.ts

默认的数据,后面还有许多测试数据

layouts
sidebar.tsx

创建 app/layouts目录

创建 layouts/sidebar.tsx文件

javascript 复制代码
import { Outlet } from "react-router";

export default function SidebarLayout() {
  return <Outlet />;
}
补充一

将 root.tsx 文件的 App 内容复制到这里

全部内容:

javascript 复制代码
import { Outlet } from "react-router";
import { Link,Form } from "react-router";

import { getContacts } from "../data";
import type { Route } from "../+types/root";

export async function clientLoader() {
  // 将此方法的loader作为参数透传出去
  const contacts = await getContacts();

  return { contacts };
}
export default function SidebarLayout({ loaderData }:Route.ComponentProps) {
    const { contacts } = loaderData;

    return (
     <>
      <div id="sidebar">
        <h1>React Router Contacts</h1>
        <div>
          <Form id="search-form" role="search">
            <input
              aria-label="Search contacts"
              id="q"
              name="q"
              placeholder="Search"
              type="search"
            />
            <div aria-hidden hidden={true} id="search-spinner" />
          </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>
                    ) : null}
                  </Link>
                </li>
              ))}
            </ul>
          ) : (
            <p>
              <i>No contacts</i>
            </p>
          )}
        </nav>
      </div>
      <div id="detail">
        <Outlet />
      </div>
    </>
  )
}

about 页面就不在 sidebar 里了

补充二

数据添加,结合roots补充七

javascript 复制代码
import { createEmptyContact } from "../data";

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

SSR端启动

  1. 开放 ssr:
    react-router.config.ts:
typescript 复制代码
import { type Config } from "@react-router/dev/config";

export default {
  ssr: true, //ssr设置为false
  prerender: ['/about'] //预加载
} satisfies Config; //Config定义的类型
  1. 打包构建

pnpm run build

  1. 启动

pnpm run start

  1. 打开链接

V6版本的React Router

最新版本的router,如果没有SSR的场景,只是为了渲染,只是使用路由基本的显示声明的话,其实完全没必要升级的

除了 hashRouter,memoryRouter外,还需要熟悉的几个hooks:

useNavigate:重定向

useLocation:location.href 获取可以通过这个方法获取

useMatch:像contacts/:id 可以通过useMatch的方式去获取
React Router for API

在V7之前,VueRouter 和 React Router 是很相像的

这里看 react-router-demo的这个例子:

pnpm i

pnpm run start

App.test.tsx报错则删除这个文件

  1. index.tsx文件的路由这里使用的是hashRouter,你可以使用createBrowserRouter等

  2. 路由使用声明式的方式定义的

  3. memoryRouter是从内存中读取的,不会关联到这边url上

  4. browserRouter是通过一个非hash值,是比较符合直觉的

    这里使用browserRouter

    在hello中,使用的是match能够获取数据

  1. 要使用memoryRouter的话,需要进行一些配置
    createMemoryRouter

    createMemoryRouter github

    hashRouter也是调用的createRouter,唯一的区别是,它传递window参数的时候,使用window中的hash值传递。createBrowserRouter也是一样,调用的createRouter函数
相关推荐
GDAL23 分钟前
HTML 中的 Canvas 样式设置全解
javascript
m0_5287238129 分钟前
HTML中,title和h1标签的区别是什么?
前端·html
Dark_programmer29 分钟前
html - - - - - modal弹窗出现时,页面怎么能限制滚动
前端·html
GDAL35 分钟前
HTML Canvas clip 深入全面讲解
前端·javascript·canvas
禾苗种树36 分钟前
在 Vue 3 中使用 ECharts 制作多 Y 轴折线图时,若希望 **Y 轴颜色自动匹配折线颜色**且无需手动干预,可以通过以下步骤实现:
前端·vue.js·echarts
GISer_Jing41 分钟前
Javascript排序算法(冒泡排序、快速排序、选择排序、堆排序、插入排序、希尔排序)详解
javascript·算法·排序算法
贵州数擎科技有限公司1 小时前
使用 Three.js 实现流光特效
前端·webgl
JustHappy1 小时前
「我们一起做组件库🌻」做个面包屑🥖,Vue的依赖注入实战💉(VersakitUI开发实录)
前端·javascript·github
拉不动的猪1 小时前
刷刷题16
前端·javascript·面试
kiramario1 小时前
【结束】JS如何不通过input的onInputFileChange使用本地mp4文件并播放,nextjs下放入public文件的视频用video标签无法打开
开发语言·javascript·音视频