iT邦幫忙

2023 iThome 鐵人賽

DAY 24
0
Modern Web

React 走出新手村 系列 第 24

React 走出新手村 — Next SSG

  • 分享至 

  • xImage
  •  

關於渲染的構想

pre-rendering 一直是 Next.js 的核心思想,他們將其分為兩種實作方法,一個是 Rendering,也就是一般的渲染概念,另一個是 Generation,類似預先構成。

了解SSG

Next.js 提供的 SSG(Static Side Generation)(靜態網頁生成)渲染模式目的在解決傳統的 SSR(服務器端渲染)和 CSR(客戶端渲染)中存在的某些問題,SSG的想法是在建構時(而不是每次發請求時)生成靜態 HTML 頁面,代表頁面在第一次訪問之前已經準備好了,可以立即提供給用戶,而不需要等待 server 動態渲染。

比較敏感的朋友可能會發現,預先生成html?這不就和MPA架構一樣了嗎?沒錯!他的精神就是借鏡MPAs的優點來改善SPAs的問題。

想要解決的問題

下面是 SSG 解決的主要問題:

  • 性能優化:
    • 在傳統的 SSR 中,每次用戶請求都需要動態生成 HTML,這可能會導致server端負載增加,尤其在高流量網站上。
    • CSR 則需要用戶等待 JavaScript 下載和執行,然後才能看到頁面。
    • SSG 允許你在應用程式構建時預先生成靜態 HTML,這樣用戶訪問網頁時可以立即看到內容,從而提高性能。
  • SEO(搜索引擎優化):CSR 在 SEO 方面存在挑戰,因為搜索引擎爬蟲難以處理用 JavaScript 動態生成的內容,SSG 可以生成包含所有內容的靜態 HTML 文件,這有助於搜索引擎爬蟲更好地理解和索引網頁內容,提高網站的搜索排名。
  • 用戶體驗:SSG 可以提供更快的初始載入時間(FCP),因為它在構建時預先生成靜態頁面。這意味著用戶不必等待 JavaScript 下載和執行才能看到內容,從而提供更好的用戶體驗。
  • 快取和CDN:由於 SSG 生成的 HTML 是靜態的,它們可以輕鬆地被快取到 CDN 中,這使得內容更容易傳遞到全球的用戶,同時減少伺服器負載。

實作轉換SSG

那麼我們先借用上一篇CSR的範例:

import { useCallback, useEffect, useMemo, useState } from "react"

interface RickandmortyCharacter {
  id: number,
  name: string,
  status: string,
  species: string,
  type: string,
  gender: string,
  origin: {
    name: string,
    url: string
  },
  location: {
    name: string,
    url: string
  },
  image: string,
  episode: string[],
  url: string,
  created: string,
}
interface RickandmortyCharacterRes {
  info: {
    count: number,
    pages: number,
    next: string | null,
    prev: string | null,
  },
  results: RickandmortyCharacter[]
}

interface PageInfo {
  pageUrl: string;
  next: string | null;
  prev: string | null;
  curr: number;
  loading: boolean;
}

export default function Home() {
  const [pageInfo, setPageInfo] = useState<PageInfo>({
    pageUrl:'https://rickandmortyapi.com/api/character',
    next: null,
    prev: null,
    loading: true,
    curr: 0
  });
  const [resData, setResData] = useState<RickandmortyCharacterRes|null>(null)
  const chkResData = useMemo(() => resData ,[resData])
  const pageChange = useCallback((status: string) => {
    setPageInfo(pre => ({...pre, loading: true}));
    if (status === 'next') {
      fetch(pageInfo.next!)
      .then((response) => response.json())
      .then((response: RickandmortyCharacterRes) => {
        const info = response.info
        setResData(response)
        setPageInfo(pre => ({
          ...pre, 
          next: info.next, 
          pageUrl: pageInfo.next!,
          prev: info.prev, 
          curr: pre.curr + 1, 
          loading: false
        }))
      });
    }
    if (status === 'prev') {
      fetch(pageInfo.prev!)
      .then((response) => response.json())
      .then((response: RickandmortyCharacterRes) => {
        const info = response.info
        setResData(response)
        setPageInfo(pre => ({
          ...pre, 
          next: info.next, 
          pageUrl: pageInfo.prev!,
          prev: info.prev, 
          curr: pre.curr - 1, 
          loading: false
        }))
      });
    }
  }, [pageInfo])
  
  useEffect(() => {
    if(!chkResData) {
      const controller = new AbortController();
      const signal = controller.signal;
      setPageInfo(pre => ({...pre, loading: true}));
      fetch(pageInfo.pageUrl, {
        signal: signal
      })
      .then((response) => response.json())
      .then((response: RickandmortyCharacterRes) => {
        const info = response.info
        setResData(response)
        setPageInfo(pre => ({...pre, next: info.next, prev: info.prev, loading: false}))
      });
      return () => controller.abort();
    }
  }, [pageInfo.pageUrl, chkResData])
  return (
    <main className="container mx-auto">
      <h2>這裡會是首頁主要內容</h2>
      <div>資料頁面</div>
      <div className="flex justify-center items-center">
        <button 
          className="p-2 mx-2 rounded-lg shadow-md bg-blue-500 hover:bg-blue-300 disabled:bg-gray-100 min-w-40"
          onClick={() => pageChange('prev')}
          disabled={pageInfo.prev===null || pageInfo.loading} >prev</button>
        <div>{pageInfo.curr}</div>
        <button 
          className="p-2 mx-2 rounded-lg shadow-md bg-blue-500 hover:bg-blue-300 min-w-40"
          onClick={() => pageChange('next')}
          disabled={pageInfo.next===null || pageInfo.loading}>next</button>
      </div>
      {resData?.results?.map((character) => (
        <div key={character.id}>
          {character.name}
        </div>
      ))}
    </main>
  )
}

然後來示範怎麼轉換成SSG的吧

import { useCallback, useEffect, useMemo, useState } from "react"

interface RickandmortyCharacter {
  id: number,
  name: string,
  status: string,
  species: string,
  type: string,
  gender: string,
  origin: {
    name: string,
    url: string
  },
  location: {
    name: string,
    url: string
  },
  image: string,
  episode: string[],
  url: string,
  created: string,
}
interface RickandmortyCharacterRes {
  info: {
    count: number,
    pages: number,
    next: string | null,
    prev: string | null,
  },
  results: RickandmortyCharacter[]
}

interface PageInfo {
  pageUrl: string;
  next: string | null;
  prev: string | null;
  curr: number;
  loading: boolean;
}

// SSG透過props的方式將處理玩的data塞回component來使用
export default function Home({ apiData }: { apiData: RickandmortyCharacterRes }) {
	// 改由apiData的資料作為default data
  const [pageInfo, setPageInfo] = useState<PageInfo>({
    pageUrl:'https://rickandmortyapi.com/api/character',
    next: apiData.info.next,
    prev: apiData.info.prev,
    loading: false,
    curr: 0
  });
  const [resData, setResData] = useState<RickandmortyCharacterRes>(apiData)
  // const chkResData = useMemo(() => resData ,[resData])
  const pageChange = useCallback((status: string) => {
    setPageInfo(pre => ({...pre, loading: true}));
    if (status === 'next') {
      // console.log(pageInfo);
      fetch(pageInfo.next!)
      .then((response) => response.json())
      .then((response: RickandmortyCharacterRes) => {
        const info = response.info
        setResData(response)
        setPageInfo(pre => ({
          ...pre, 
          next: info.next, 
          pageUrl: pageInfo.next!,
          prev: info.prev, 
          curr: pre.curr + 1, 
          loading: false
        }))
      });
    }
    if (status === 'prev') {
      fetch(pageInfo.prev!)
      .then((response) => response.json())
      .then((response: RickandmortyCharacterRes) => {
        const info = response.info
        setResData(response)
        setPageInfo(pre => ({
          ...pre, 
          next: info.next, 
          pageUrl: pageInfo.prev!,
          prev: info.prev, 
          curr: pre.curr - 1, 
          loading: false
        }))
      });
    }
  }, [pageInfo])
  // 透過SSG處理fetching data的動作可以降低useFootGun的問題
  // useEffect(() => {
  //   if(!chkResData) {
  //     const controller = new AbortController();
  //     const signal = controller.signal;
  //     setPageInfo(pre => ({...pre, loading: true}));
  //     fetch(pageInfo.pageUrl, {
  //       signal: signal
  //     })
  //     .then((response) => response.json())
  //     .then((response: RickandmortyCharacterRes) => {
  //       const info = response.info
  //       // console.log(info);
  //       setResData(response)
  //       setPageInfo(pre => ({...pre, next: info.next, prev: info.prev, loading: false}))
  //     });
  //     return () => controller.abort();
  //   }
  // }, [pageInfo.pageUrl, chkResData])
  return (
    <main className="container mx-auto">
      <h2>這裡會是首頁主要內容</h2>
      <div>資料頁面</div>
      <div className="flex justify-center items-center">
        <button 
          className="p-2 mx-2 rounded-lg shadow-md bg-blue-500 hover:bg-blue-300 disabled:bg-gray-100 min-w-40"
          onClick={() => pageChange('prev')}
          disabled={pageInfo.prev===null || pageInfo.loading} >prev</button>
        <div>{pageInfo.curr}</div>
        <button 
          className="p-2 mx-2 rounded-lg shadow-md bg-blue-500 hover:bg-blue-300 min-w-40"
          onClick={() => pageChange('next')}
          disabled={pageInfo.next===null || pageInfo.loading}>next</button>
      </div>
      {resData?.results?.map((character) => (
        <div key={character.id}>
          {character.name}
        </div>
      ))}
        
    </main>
  )
}

// 下面是改用SSG的操作
// 透過getStaticProps來處理fetching data的問題
export async function getStaticProps() {
  // 這裡已經處理default值
  try{
    const res = await fetch("https://rickandmortyapi.com/api/character");
    // 我懶得處理error page原諒我先用next原生的頁面
    if (!res.ok) {
      return { notFound: true };
    }
    const apiData: RickandmortyCharacterRes = await res.json();
    return {
      props: {
        apiData,
      },
    };
  } catch(err) {
    console.log('err',err);
  }
}

透過 getStaticProps() 的方式將外部資料透過 async function 的方式處理,再透過 props 的方式傳遞到 component 當中使用,最顯著的好處就是能避免 useEffect 的使用,同時生成後的檔案會存放在cdn裏面,如果資料沒有異動他就不會重新生成,也能降低 server 的負擔。

總結

SSG(靜態站點生成)的流程:

  1. 在頁面組件中,使用 getStaticProps ,讓它在構建時運行,以獲取數據。
  2. Next.js編譯這些頁面,並在構建時生成靜態HTML + JSON文件。
  3. 這些HTML文件被存儲在網站的 .next/server/pages 中。
  4. 當用戶訪問頁面時,他們將收到預先渲染的HTML文件,而不是等待服務器處理。

SSG vs. CSR比較:

  • SSG 在構建時預先生成HTML,因此訪問者立即看到內容,但數據可能過時,比較適用於內容不經常更改的網站。
  • CSR 在每次訪問時動態生成內容,因此數據是最新的,但首次載入可能需要更長時間,比較適用於需要即時數據的應用程序。

可是如果我想要更新原本的畫面必須怎麼處理呢?下一篇我們會講解到他的改進方案。

給全新手的大禮包

React基本Hook教學


上一篇
React 走出新手村 — 認識Next
下一篇
React 走出新手村 — Next ISR
系列文
React 走出新手村 31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言