Day07 - 在 Next.js 中使用 pre-rendering (getStaticProps) — Part 1

前言

Next.js 的 pre-rendering 实作是这个框架的一大卖点,在 Next.js 中的 pre-rendering 有两种实作方法,一个是 Server Side Rendering,另一个是 Static Side Generation。

在这篇文章中我们要谈的是 Static Side Generation,Static Side Generation 指的是在打包阶段会将所有渲染所需要的资料都准备好,包括呼叫 API 的资料,最後会将资料都嵌入到 HTML 档案之中,因此使用者在浏览网站时就会直接拿到已经渲染完的 HTML 静态档案。

Next.js 提供了一个 function — getStaticProps ,它可以自动地在程序码打包阶段自动执行执行上述的流程,我们不必做过多的设定就可以撰写 pre-rendering 的程序码。

getStaticProps

它是一个写在 React component 外的 function,必须以 export 的形式定义它,而且同时这个 function 也要是 async 的。加上 async 一个很大的好处是,在 getStaticProps 里面就可以写 await ,在呼叫 API 时就可以利用这个特性撰写。

getStaticProps 会在打包阶段自动执行,并将 props 传入到 component 中,可以用於渲染 React 中的内容。

export async function getStaticProps(context) {
  return {
    props: {},
  };
}

范例

以下这个范例是在 getStaticProps 中呼叫一个 REST API,从 API 拿到贴文 (Post) 的资料後,传入到 component 中,并渲染 titlebody 到画面上。这个范例我们在前面章节深入浅出 CSR、SSR 与 SSG 也有提到过。

import { GetStaticProps } from "next";

interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}

interface HomeProps {
  post: Post;
}

export default function Home({ post }: HomeProps) {
  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </div>
  );
}

export const getStaticProps: GetStaticProps = async () => {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts/1");
  const post: Post = await res.json();

  return {
    props: {
      post,
    },
  };
};

如果上述范例的档案位置在 pages/home.tsx ,Next.js 会在 next build 的阶段为符合的页面产生 .html 档案:

.next/server/pages/home.html

Dynamic routes 的 SSG — getStaticPaths

Dynamic routes 可以匹配近乎是无上限的 pattern,而每一个 pattern 如果在 next build 都要对应到一个页面,这样不是会产生无上限的 HTML 档案吗?

对於 dynamic routes,Next.js 有相对应的解决方案,也就是使用 getStaticPaths 事先定义哪些页面需要产生 HTML 档案。

语法跟 getStaticProps 很像,皆是在 component 外面定义一个 async 的 function,名称即是 getStaticPaths ,回传值包含两个 key,分别是 pathsfallback

export async const getStaticPaths: GetStaticPaths = () => {
  return {
    paths: [
      { params: { ... } }
    ],
    fallback: boolean
  };
}

paths

paths 这个参数将会决定 dynamic routes 有哪些页面将会产生 HTML 档案,例如以在 file-based routing 中我们用到的 pages/products/[id].tsx ,我们可以这样定义:

return {
  paths: [
    { params: { id: '1' } },
    { params: { id: '2' } }
  ],
  fallback: ...
}

以上定义了两个 id ,所以就会针对两个页面 /products/1/products/2 生成 HTML 档案。

而 dynamic routes 还包括很多种的定义方式:

  • 多层次的定义 pages/posts/[year]/[month]/[day].tsx ,所以在每一个 params 中就要包含 yearmonthday 三个 key:

    return {
      paths: [
        { params: { year: '2021', month: '7', day: '24' } },
        { params: { year: '2021', month: '9', day: '28' } }
      ],
      fallback: ...
    }
    
  • catch all routes 的定义方式 pages/posts/[...date].tsx ,而 daterouter.query 会是 array 的型别,所以在 params 中定义 date 也要是 array 的型别:

    return {
      paths: [
        { params: { date: ['2021', '7', '24'] } },
        { params: { date: ['2021', '9', '28'] } }
      ],
      fallback: ...
    }
    
  • 还有一种是 optional catch all rotues,例如 pages/posts/[[...date]].tsx 就可以匹配 /posts/posts/123/posts/2021/7/24 多种的路径,一般情况可以传入 array 定义路径,但是也可以使用 null[]undefinedfalse 多种不同的方式,让 Next.js 打包时只产生 / 的页面:

    return {
      paths: [
        { params: { date: false } },
      ],
      fallback: ...
    }
    

fallback

fallback 允许传入三种值,分别为 truefalse'blocking' ,以下是 Next.js 中的型别定义:

type GetStaticPathsResult<P extends ParsedUrlQuery = ParsedUrlQuery> = {
  paths: Array<string | { params: P; locale?: string }>;
  fallback: boolean | "blocking";
};

fallback: false

fallbackfalse 的行为很单纯,意思是说当使用者浏览没有定义在 getStaticPaths 中的页面时,会回传 404 的页面。

例如在 pages/products/[id].tsx 中的 getStaticPaths 定义以下的回传值,所以 Next.js 只会产生 /products/1/products/2 两个路由相对应的页面。而使用者如果浏览了 /products/3 ,他将会收到 404 的页面。

return {
  paths: [{ params: { id: "1" } }, { params: { id: "2" } }],
  fallback: false,
};

所以, fallback: false 比较适合用在较为静态的网站,例如部落格、较小的产品型录网页等,只有等网页的管理者新增内容时,重新让 Next.js 打包後,才会有新的页面产生。

fallback: true

使用 fallback: true 的使用比较复杂一点,因为与 fallback: false 不同的点在於,当使用者浏览没有在 getStaticPaths 中定义的页面时,Next.js 并不会回应 404 的页面,而是回应 fallback 的页面给使用者。

这个流程会呼叫 getStaticProps ,在服务器产生资料前,使用者浏览的是 fallback 的页面,在 getStaticProps 执行完後,同样由 props 注入资料到网页中,使用者这时就能看到完整的页面。

而经过这个流程的页面,该页面会被加入到 pre-rendering 页面中,下次如果再有同样页面的请求时,服务器并不会再次的重新呼叫 getServerSideProps ,产生新的页面,而是回应已经产生的页面给使用者。

使用前几个章节用到的产品详细介绍页面,由於在 getStaticPaths 中的 id 只有 '1' ,所以在 next build 阶段只会生成 /products/1 这个页面的 HTML,但是在设定 fallback: true 的情况下,当一位使用者浏览 /products/2 时, Next.js 会做以下几件事情:

  • 即使该页面不存在 Next.js 不会回传 404 页面,Next.js 会开始动态地生成新的页面。
  • 在生成页面时, router.isFallback 会一直为 true ,因次可以用条件式渲染 loading 情况下的页面,而这时从 props 中拿到的 productundefined
  • 而在 HTML 生成完毕後,使用者就会看到完整的商品介绍页面。
import { GetStaticPaths, GetStaticProps } from "next";
import { useRouter } from "next/router";

import { getProductById, Product as ProductType } from "../../fake-data";
import ProductCard from "../../components/ProductCard";
import { PageTitle, ProductContainer } from "./[id].style";

interface ProductProps {
  product: ProductType;
}

const Product = ({ product }: ProductProps) => {
  const router = useRouter();

  if (router.isFallback) {
    return <div>Loading...</div>;
  }

  return (
    <>
      <PageTitle>商品详细页面</PageTitle>
      <ProductContainer>
        <ProductCard product={product} all />
      </ProductContainer>
    </>
  );
};

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const product = getProductById(params?.id as string);

  return {
    props: {
      product,
    },
  };
};

export const getStaticPaths: GetStaticPaths = async () => {
  return {
    paths: [{ params: { id: "1" } }],
    fallback: true,
  };
};

export default Product;

在使用 fallback: true 的时候,使用者第一次浏览网页时,收到的会是在 router.isFallback 底下渲染的资料:

第一次渲染

在第二次浏览同一个页面之後,该页面因为先前已经生成过,所以可以直接回应 HTML 给使用者,在原始码中就可以看到完整的资料。

第二次渲染

各位读者可以注意到,因为是 SSG 的关系, fallback: true 实际上是真的会产生 HTML 在资料夹中,例如使用者如果浏览不存在的 /products/2 ,Next.js 就会动态地生成新的 HTML:

.next/server/pages/prodcuts/2.html

fallback: 'blocking'

getStaticPaths 使用这个设定时,跟 fallback: true 一样,在使用者浏览不存的页面时,服务器不会回传 404 的页面,而是执行 getStaticProps ,走 pre-rendering 的流程。

但是与 fallback: true 不一样的点在於没有 router.isFallback 的状态可以使用,而是让页面卡在 getStaticProps 的阶段,等待执行完後回传结果给使用者。

所以使用者体验会非常像似 getServerSideProps ,但优点是下次使用者再次浏览同一个页面时,服务器可以直接回传已经生成的 HTML 档案,往後甚至可以藉由 CDN 的 cache 提升页面的载入速度。

Why can't I have an option of {fallback: true} and keeping static site generation with front-end fetching? · Issue #22897 · vercel/next.js

Reference


<<:  Day22 - Ajax 加上 Antiforgery Token (二)

>>:  鬼故事 - 天才小钓手

D09 - 打开第一扇窗

现在有资料,只差介面了。 建立 base-window 组件 虽然每个视窗功能都不同,但是视窗外框功...

Day 9. Compare × Final

Conclusion 呼~到今天为止 9 天过去了,Libraries 之间的比较篇章也到今天告一...

Day36 ( 游戏设计 ) 钓鱼游戏

钓鱼游戏 教学原文参考:钓鱼游戏 这篇文章会介绍,如何在 Scratch 3 里使用多个角色、函式、...

PostgreSQL 资料储存与 mybatis

PostgreSQL是一个开源的框架,关联式资料库资管理系统。PostgreSQL 的操作过程相似於...

Excel删除100个空白行,同事都只用5秒钟搞定!

在工作中或多或少都会遇到空白行存在的情况。如果只有几个空白行,那麽手动轻松删除即可,但是遇到100行...