iT邦幫忙

2021 iThome 鐵人賽

DAY 7
0
Modern Web

從零開始學習 Next.js系列 第 7

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


上一篇
Day06 - 用 Next.js 做一個簡易產品介紹頁,使用 file-based routing
下一篇
Day08 - 在 Next.js 中使用 pre-rendering (getStaticProps) — Part 2
系列文
從零開始學習 Next.js30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
AndrewYEE
iT邦新手 3 級 ‧ 2023-02-13 20:01:10

您好,在實作您的範例的時候我遇到以下錯誤:

yarn build

Error occurred prerendering page "/product/Product". Read more: https://nextjs.org/docs/messages/prerender-error
TypeError: Cannot destructure property 'id' of 'product' as it is undefined.

product/[id].tsx:

import React from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import { getProductById, Product as ProductType } from "../../data/fake-data";
import { PageTitle, ProductContainer, BackLink } from "../../styles/index.style";
import { GetStaticPaths } from "next";
import { GetStaticProps } from "next";
import ProductCard from "./Product";

interface ProductProps {
  product: ProductType;
}

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

  if (router.isFallback){
    return <>Loading...</>; //防止因為第一次渲染沒拿到id而出問題
  }

  return (
    <div>
      <PageTitle>商品詳細頁面</PageTitle>
      <BackLink>
        <Link href="/product">
          回產品列表
        </Link>
      </BackLink>
      <ProductContainer>
        <ProductCard product={product} all />
      </ProductContainer>
    </div>
  );
};

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

//Step1 
export const getStaticPaths: GetStaticPaths = async () => {
  return {
    paths: [
      { params: { id: '1' } },
      { params: { id: '2' } }
    ], //預先產生id=1與id=2的頁面
    fallback: false //非id=1, id=2的頁面一律404
  }
}

export default Product;

product/Product.tsx

import Image from "next/image";
import Link from "next/link";

import { Product as ProductType } from "../../data/fake-data";
import {
  Product,
  ImageWrapper,
  ProductDetail,
  ProductTitle,
  ProductDescription,
  ProductPrice,
} from "../../styles/ProductCard.style";

interface ProductCardProps {
  product: ProductType;
  all?: boolean;
}

const ProductCard = ({ product, all }: ProductCardProps) => {
  const { id, image, title, description, price } = product;
  return (
    <Product key={id}>
      <ImageWrapper>
        <Image src={image} alt="product" style={{ objectFit:"cover"}} fill/>
      </ImageWrapper>
      <ProductDetail>
        <Link href={`/product/${id}`} passHref>
          <ProductTitle>{title}</ProductTitle>
        </Link>
        <ProductDescription $all={all}>{description}</ProductDescription>
        <ProductPrice>${price}</ProductPrice>
      </ProductDetail>
    </Product>
  );
};

export default ProductCard;

請問是什麼問題導致ProductCard拿不到id呢?

我要留言

立即登入留言