pre-rendering 一直是 Next.js 的核心思想,他們將其分為兩種實作方法,一個是 Rendering,也就是一般的渲染概念,另一個是 Generation,類似預先構成。
Next.js 提供的 SSG(Static Side Generation)(靜態網頁生成)渲染模式目的在解決傳統的 SSR(服務器端渲染)和 CSR(客戶端渲染)中存在的某些問題,SSG的想法是在建構時(而不是每次發請求時)生成靜態 HTML 頁面,代表頁面在第一次訪問之前已經準備好了,可以立即提供給用戶,而不需要等待 server 動態渲染。
比較敏感的朋友可能會發現,預先生成html?這不就和MPA架構一樣了嗎?沒錯!他的精神就是借鏡MPAs的優點來改善SPAs的問題。
下面是 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(靜態站點生成)的流程:
getStaticProps
,讓它在構建時運行,以獲取數據。.next/server/pages
中。SSG vs. CSR比較:
可是如果我想要更新原本的畫面必須怎麼處理呢?下一篇我們會講解到他的改進方案。