前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >React 应用架构实战 0x3:构建和配置页面

React 应用架构实战 0x3:构建和配置页面

作者头像
Cellinlab
发布2023-05-17 20:59:24
发布2023-05-17 20:59:24
83400
代码可运行
举报
文章被收录于专栏:Cellinlab's BlogCellinlab's Blog
运行总次数:0
代码可运行

这一节,将学习 Next.js 中的路由工作原理以及可以使用的渲染方法,以充分利用 Next.js 的特性。然后,我们将学习如何配置每个页面的布局,使应用程序看起来和感觉像一个单页应用程序。

# Next.js 路由

Next.js 有一个基于文件系统的路由机制,其中每个页面文件代表一个页面。页面存放于 pages 文件夹中,其结构如下:

代码语言:javascript
代码运行次数:0
运行
复制
const Page = () => <div>My page</div>;

export default Page;

由于路由是基于文件系统的,路由将由页面文件的命名方式决定。如,指向根路由的页面应该在 src/pages/index.tsx 文件中定义。如果我们想要 About 页面,我们可以在 src/pages/about.tsx 中定义它。

对于任何具有动态数据的复杂应用程序,仅创建预定义页面是不够的。如,假设有一个社交网络应用程序,可以访问用户个人资料,个人资料应该通过用户 ID 加载。由于为每个用户创建页面文件太过重复,因此需要将页面设置为动态页面,如下所示:

代码语言:javascript
代码运行次数:0
运行
复制
// src/pages/users/[userId].tsx
import { useRouter } from "next/router";

const UserProfile = () => {
  const router = useRouter();
  const { userId } = router.query;

  return <div>User Profile: {userId}</div>;
};

export default UserProfile;

为了动态获取 ID 并加载数据,我们可以在 pages/users/[userId].tsx 中定义一个通用的用户个人资料页面,其中 userId 将被动态地注入到页面中。例如,访问 /users/123 将显示用户个人资料页面,并通过 router.query123 作为 userId 传递。

# 渲染策略

Next.js 支持四种渲染策略:

  • 客户端渲染 (CSR)
    • 从服务端上加载初始内容,然后在客户端再获取额外的数据
  • 服务端渲染 (SSR)
    • 在服务端直接获取数据,将其注入到页面上,然后将生成的页面返回到客户端
  • 静态站点生成 (SSG)
    • 静态数据注入到页面中,并将其返回到客户端
  • 增量静态再生 (ISR)
    • 介于服务器端渲染和静态站点生成之间的中间地带
    • 可以静态生成 n 个页面,但如果请求的内容尚未渲染和缓存,Next.js 可以在服务器上渲染并缓存

# 客户端渲染 (CSR)

比如前面用户个人资料页面的例子,可以按以下方式编写页面以执行客户端渲染:

代码语言:javascript
代码运行次数:0
运行
复制
// src/pages/users/[userId].tsx
import { useRouter } from "next/router";
import { useUser } from "./api";

const UserProfile = () => {
  const router = useRouter();
  const { userId } = router.query;
  const { user, isLoading, isError } = useUser(userId);

  if (!user && isLoading) return <div>Loading...</div>;

  if (isError) return <div>Error!</div>;

  if (!user) return <div>User not found</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </div>
  );
};

export default UserProfile;

这种方法在不考虑 SEO 和初始页面加载性能时是可以接受的。在这里,我们必须等待初始页面加载完成,然后再获取用户数据。对于不应该公开的数据(例如管理员看板),这种方法完全有效。

但是,对于公开页面,最好启用服务器返回实际的页面以使搜索引擎更容易爬取和索引我们的页面,可以通过在服务器端呈现页面来实现这一点。

# 服务端渲染 (SSR)

代码语言:javascript
代码运行次数:0
运行
复制
// src/pages/users/[userId].tsx
import { getUser } from "./api";

const UserProfile = ({ user }) => {
  if (!user) return <div>User not found</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </div>
  );
};

export const getServerSideProps = async ({ params }) => {
  const userId = params.userId;
  const user = await getUser(userId);

  return {
    props: {
      user,
    },
  };
};

正如我们在这里看到的,除了页面组件,还导出了 getServerSideProps 函数,该函数在服务端执行。它的返回值可以包含 props 属性,这些将传递给组件的 props

我们需要记住的是,并没有适用于所有情况的完美渲染策略;因此,必须权衡利弊并根据需求选择使用哪种策略。使用 Next.js 的好处在于它允许我们在每个页面上使用不同的渲染策略,因此我们可以组合它们以最佳方式适应应用程序的需求。

# SEO

为了优化页面的 SEO,可以添加一些 meta 标签和页面的标题,并将它们注入到页面中。这可以通过 Next.js 提供的 Head 组件来实现。

对于应用程序,最好有一个专门的组件来添加页面的标题,比如创建 src/components/seo/seo.tsx 文件并添加以下内容:

代码语言:javascript
代码运行次数:0
运行
复制
import Head from "next/head";

interface SeoProps {
  title: string;
  description?: string;
  keywords?: string;
}

export const Seo = ({ title, description, keywords }: SeoProps) => {
  return (
    <Head>
      <title>{title}</title>
      {description && <meta name="description" content={description} />}
      {keywords && <meta name="keywords" content={keywords} />}
    </Head>
  );
};

然后,可以在页面组件中使用它:

代码语言:javascript
代码运行次数:0
运行
复制
// src/pages/users/[userId].tsx
import { Seo } from "@/components/seo/seo";

const UserProfile = ({ user }) => {
  if (!user) return <div>User not found</div>;

  return (
    <div>
      <Seo title={user.name} description={user.bio} />
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </div>
  );
};

# 页面布局

在开发具有多个视图或页面的应用程序时,我们需要考虑布局的可复用性。

# 使用 布局组件

可以创建一个 src/components/layout/layout.tsx 文件,其中包含应用程序的布局:

代码语言:javascript
代码运行次数:0
运行
复制
import { ReactNode } from "react";
import Header from "@/components/header/header";
import Footer from "@/components/footer/footer";

interface LayoutProps {
  children: ReactNode;
}

export const Layout = ({ children }: LayoutProps) => {
  return (
    <div>
      <Header />
      <main>{children}</main>
      <Footer />
    </div>
  );
};

然后,可以在页面组件中使用它:

代码语言:javascript
代码运行次数:0
运行
复制
// src/pages/users/[userId].tsx
import { Layout } from "@/components/layout/layout";

const UserProfile = ({ user }) => {
  return (
    <Layout>
      {user ? (
        <>
          <h1>{user.name}</h1>
          <p>{user.bio}</p>
        </>
      ) : (
        <div>User not found</div>
      )}
    </Layout>
  );
};

这种方式处理 Next.js 应用程序中的布局对于一些简单的情况是可以的。然而,它也有一些缺点:

  • 如果 Layout 组件跟踪一些内部状态,当页面更改时会丢失它
  • 页面会失去滚动位置
  • 任何我们想要在最终返回之前返回的内容,也需要将其包装在 Layout

对于我们的应用程序,我们将使用一种更好的方式来处理每个页面的布局,即将它们附加到所有页面组件(即 page component)上。

# 将布局加在所有页面组件

可以在 src/pages/_app.tsx 实现这一点:

代码语言:javascript
代码运行次数:0
运行
复制
// src/pages/_app.tsx
import { ReactElement, ReactNode } from "react";
import { NextPage } from "next";
import type { AppProps } from "next/app";

import { AppProvider } from "@/providers/app";

type NextPageWithLayout = NextPage & {
  getLayout?: (page: ReactElement) => ReactNode;
};

type AppPropsWithLayout = AppProps & {
  Component: NextPageWithLayout;
};

const App = ({ Component, pageProps }: AppPropsWithLayout) => {
  const getLayout = Component.getLayout ?? ((page) => page);
  const pageCotent = getLayout(<Component {...pageProps} />);

  return <AppProvider>{pageCotent}</AppProvider>;
};

export default App;

页面组件需要附加 getLayout 静态属性,该属性将在 _app.tsx 中渲染组件时用于包装整个组件。得益于 React 的优化,当在具有相同布局的页面之间导航时,所有布局组件状态都将继续保持。

我们已经构建了布局组件,现在只需要将它们添加到我们的页面中即可。

# 构建页面

现在我们已经了解了 Next.js 页面的工作原理,并准备好了 Seo 组件和布局设置,接下来让我们实现应用程序的页面。我们将实现以下页面:

  • 公开组织详情页面
  • 公开职位详情页面
  • 看板中的职位页面
  • 看板中的职位详情页面
  • 创建职位页面
  • 404 页面

# 公开组织详情页面

代码语言:javascript
代码运行次数:0
运行
复制
// src/pages/organizations/[organizationId]/index.tsx
import { ReactElement } from "react";
import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next";
import { Heading, Stack } from "@chakra-ui/react";

import { NotFound } from "@/components/not-found";
import { Seo } from "@/components/seo/seo";
import { JobsList } from "@/features/jobs";
import type { Job } from "@/features/jobs";
import { OrganizationInfo } from "@/features/organizations";
import { PublicLayout } from "@/layouts/public-layout";

import { getJobs, getOrganization } from "@/testing/test-data";

type PublicOrganizationPageProps = InferGetServerSidePropsType<typeof getServerSideProps>;

const PublicOrganizationPage = ({ organization, jobs }: PublicOrganizationPageProps) => {
  if (!organization) {
    return <NotFound />;
  }

  return (
    <>
      <Seo title={organization.name} />
      <Stack spacing="4" w="full" maxW="container.lg" mx="auto" mt="12" p="4">
        <OrganizationInfo organization={organization} />
        <Heading size="md" my="6">
          Open Jobs
        </Heading>
        <JobsList jobs={jobs} organizationId={organization.id} type="public" />
      </Stack>
    </>
  );
};

PublicOrganizationPage.getLayout = function getLayout(page: ReactElement) {
  return <PublicLayout>{page}</PublicLayout>;
};

export default PublicOrganizationPage;

export const getServerSideProps = async ({ params }: GetServerSidePropsContext) => {
  const organizationId = params?.organizationId as string;

  const [organization, jobs] = await Promise.all([
    getOrganization(organizationId).catch(() => null),
    getJobs(organizationId).catch(() => [] as Job[]),
  ]);

  return {
    props: {
      organization,
      jobs,
    },
  };
};

其他代码细节可以在 GitHub (opens new window) 提交查看。

启动服务后,访问 http://localhost:3000/organizations/amYXmIyT9mD9GyO6CCr,可以看到如下页面:

# 公开职位详情页面

代码语言:javascript
代码运行次数:0
运行
复制
import { ReactElement } from "react";
import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next";
import { Stack, Button } from "@chakra-ui/react";

import { NotFound } from "@/components/not-found";
import { Seo } from "@/components/seo";
import { PublicJobInfo } from "@/features/jobs";
import { PublicLayout } from "@/layouts/public-layout";

import { getJob, getOrganization } from "@/testing/test-data";

type PublicJobPageProps = InferGetServerSidePropsType<typeof getServerSideProps>;

export const PublicJobPage = ({ organization, job }: PublicJobPageProps) => {
  const isInvalid = !job || !organization || job.organizationId !== organization.id;

  if (isInvalid) {
    return <NotFound />;
  }

  return (
    <>
      <Seo title={`${job.position} | ${job.location}`} />
      <Stack w="full">
        <PublicJobInfo job={job} />
        <Button
          bg="primary"
          color="primaryAccent"
          _hover={{
            opacity: "0.9",
          }}
          as="a"
          href={`mailto:${organization.email}?subject=Application for ${job.position} position`}
          target="_blank"
        >
          Apply
        </Button>
      </Stack>
    </>
  );
};

PublicJobPage.getLayout = (page: ReactElement) => <PublicLayout>{page}</PublicLayout>;

export default PublicJobPage;

export const getServerSideProps = async ({ params }: GetServerSidePropsContext) => {
  const organizationId = params?.organizationId as string;
  const jobId = params?.jobId as string;

  const [organization, job] = await Promise.all([
    getOrganization(organizationId).catch(() => null),
    getJob(jobId).catch(() => null),
  ]);

  return {
    props: {
      organization,
      job,
    },
  };
};

访问 http://localhost:3000/organizations/amYXmIyT9mD9GyO6CCr/jobs/wS6UeppUQoiXGTzAI6XrM,可以看到如下页面:

更多代码细节可以通过 GitHub (opens new window) 提交查看。

一些 SSR 的缺点,主要包括:

  • 需要更多的计算资源,这可能会影响服务器成本
  • 较长的 getServerSideProps 执行时间可能会阻塞整个应用程序

因此,我们只希望在合适的情况下使用 SSR,比如需要对 SEO 友好的公开页面,以及它们的内容可能更频繁变化。

# 管理看板中的职位页面

代码语言:javascript
代码运行次数:0
运行
复制
// pages/dashboard/jobs/index.tsx
import { ReactElement } from "react";
import { Heading, HStack } from "@chakra-ui/react";
import { PlusSquareIcon } from "@chakra-ui/icons";

import { Link } from "@/components/link";
import { Loading } from "@/components/loading";
import { Seo } from "@/components/seo";
import { JobsList } from "@/features/jobs";
import { DashboardLayout } from "@/layouts/dashboard-layout";

import { useJobs, useUser } from "@/testing/test-data";

const DashboardJobsPage = () => {
  const user = useUser();
  const jobs = useJobs(user.data?.organizationId ?? "");

  if (jobs.isLoading) {
    return <Loading />;
  }

  if (!user.data) return null;

  return (
    <>
      <Seo title="Jobs" />
      <HStack mb="8" justify="space-between" align="center">
        <Heading>Jobs</Heading>
        <Link href={`/dashboard/jobs/create`} icon={<PlusSquareIcon />} variant="solid">
          Create Job
        </Link>
      </HStack>
      <JobsList
        jobs={jobs.data || []}
        isLoading={jobs.isLoading}
        organizationId={user.data.organizationId}
        type="dashboard"
      />
    </>
  );
};

DashboardJobsPage.getLayout = (page: ReactElement) => <DashboardLayout>{page}</DashboardLayout>;

export default DashboardJobsPage;

访问 http://localhost:3000/dashboard/jobs,可以看到如下页面:

更多代码细节可以通过 GitHub (opens new window) 提交查看。

# 看板中的职位详情页面

代码语言:javascript
代码运行次数:0
运行
复制
// pages/dashboard/jobs/[jobId].tsx
import { ReactElement } from "react";
import { useRouter } from "next/router";

import { Loading } from "@/components/loading";
import { NotFound } from "@/components/not-found";
import { Seo } from "@/components/seo";
import { DashboardJobInfo } from "@/features/jobs";
import { DashboardLayout } from "@/layouts/dashboard-layout";

import { useJob } from "@/testing/test-data";

const DashboardJobPage = () => {
  const router = useRouter();
  const jobId = router.query.jobId as string;

  const job = useJob(jobId);

  if (job.isLoading) {
    return <Loading />;
  }

  if (!job.data) {
    return <NotFound />;
  }

  return (
    <>
      <Seo title={`${job.data.position} | ${job.data.location}`} />
      <DashboardJobInfo job={job.data} />
    </>
  );
};

DashboardJobPage.getLayout = (page: ReactElement) => <DashboardLayout>{page}</DashboardLayout>;

export default DashboardJobPage;

访问 http://localhost:3000/dashboard/jobs/wS6UeppUQoiXGTzAI6XrM,可以看到如下页面:

更多代码细节可以通过 GitHub (opens new window) 提交查看。

# 新增职位页面

代码语言:javascript
代码运行次数:0
运行
复制
// pages/dashboard/jobs/create.tsx
import { ReactElement } from "react";
import { useRouter } from "next/router";
import { Heading } from "@chakra-ui/react";

import { Seo } from "@/components/seo";
import { CreateJobForm } from "@/features/jobs";
import { DashboardLayout } from "@/layouts/dashboard-layout";

const DashboardCreateJobPage = () => {
  const router = useRouter();

  const onSuccess = () => {
    router.push("/dashboard/jobs");
  };

  return (
    <>
      <Seo title="Create Job" />
      <Heading mb="8">Create Job</Heading>
      <CreateJobForm onSuccess={onSuccess} />
    </>
  );
};

DashboardCreateJobPage.getLayout = (page: ReactElement) => (
  <DashboardLayout>{page}</DashboardLayout>
);

export default DashboardCreateJobPage;

访问 http://localhost:3000/dashboard/jobs/create,可以看到如下页面:

更多代码细节可以通过 GitHub (opens new window) 提交查看。

# 404 页面

代码语言:javascript
代码运行次数:0
运行
复制
// pages/404.tsx
import { Center } from "@chakra-ui/react";

import { Link } from "@/components/link";
import { NotFound } from "@/components/not-found";

const NotFoundPage = () => {
  return (
    <>
      <NotFound />
      <Center>
        <Link href="/">Go back home</Link>
      </Center>
    </>
  );
};

export default NotFoundPage;

pages 文件夹中的 404.tsx 文件是一个特殊的页面,每当用户访问未知页面时,它就会显示出来。

访问 http://localhost:3000/somepage,可以看到如下页面:

更多代码细节可以通过 GitHub (opens new window) 提交查看。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023/1/14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • # Next.js 路由
  • # 渲染策略
    • # 客户端渲染 (CSR)
    • # 服务端渲染 (SSR)
  • # SEO
  • # 页面布局
    • # 使用 布局组件
    • # 将布局加在所有页面组件
  • # 构建页面
    • # 公开组织详情页面
    • # 公开职位详情页面
    • # 管理看板中的职位页面
    • # 看板中的职位详情页面
    • # 新增职位页面
    • # 404 页面
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档