這篇雖然是講解 Next.js
的各種用法
- 但也順便討論了 SSR vs SSG 的優缺點,是很好的思考文章
- 另外也凸顯了
Next.js
能夠同時混用兩種模式的優點(超強大)
Next.js
提供兩種 pre-rendered HTML 的方法
SSR 時,Next.js
對 每一個 request 進來時,會將 page 轉為 HTML 來 pre-render
- TTFB (Time to first byte) 會比較慢
- 但 data 永遠是最新的
SSG 時,Next.js
會在 request 前 (通常是在 build time) 就把 page 轉為 HTML 來 pre-render
- HTML 就能夠被 cached、能靠 CDN 支援
- SSG 的 performance 比較好
- 但因為是 request 前就先 build 好,所以 data 可能是舊的(比起 request 來產生 data)
兩種結合的方法
- 先把大部分內容用 SSG 產生好,而 data 部份留在 client 時再 fetch
這邊用電商網站做舉例,假如有四大頁面
- About Us: 公司介紹、No need to fetch data
- 所有產品: 產品的 List 清單、需要從 DB 取資料、所有 User 看到此頁面的內容是一樣的
- 單一產品: 產品詳細頁面、需要從 DB 取資料、所有 User 看到此頁面的內容是一樣的
- 購物車 : User 購物車、需要從 DB 取資料、每位 User 看到的不一樣,是自己的 data
Next.js
最優秀的地方之一就是可以針對每一個 page 來規劃它的 data fetch 策略
- About Us: SSG without data
- 所有產品/單一產品: SSG with data,然後用 Incremental SSG 強化
- 購物車 : SSG without data,並結合 Client-side Fetching
當沒有 data 需要 fetch 時,Next.js
default 行為就是在 build time 時,轉成 HTML
在 pages
folder 底下建立 Component、export
// pages/about.js
// This page can can be pre-rendered without
// external data: It will be pre-rendered
// into a HTML file at build time.
export default function About() {
return <div>
<h1>About Us</h1>
{/* ... */}
</div>
}
所有產品
- 我們打算在 build time 時去 db 查資料
建立 page 的 component,並 export 一個 getStaticProps
function
getStaticProps
會在 build time 時被呼叫,去 db 取資料來產生 pre-render 的 page component- (
getStaticProps
並不會被 bundle 進去 JS bundle、不會給 client side 看到,可用來直接存取 db)
// This function runs at build time on the build server
export async function getStaticProps() {
return {
props: {
products: await getProductsFromDatabase()
}
}
}
// The page component receives products prop
// from getStaticProps at build time
export default function Products({ products }) {
return (
<>
<h1>Products</h1>
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</>
)
}
- 需要為每一個產品產生一個 page,用 product id 來當作 route
- (for example, /products/[id])
這可以在 build time 時使用 Next.js
的 dynamic routes 和 getStaticPaths.
- 建立一個檔案叫做
products/[id].js
- 裡面使用
getStaticPaths
來 return 所有 id- 這樣就會在 build time 時產生所有頁面
- 接著用
getStaticProps
來依 id 來取得產品詳細 data,在 build time 完成這些
// pages/products/[id].js
// In getStaticPaths(), you need to return the list of
// ids of product pages (/products/[id]) that you’d
// like to pre-render at build time. To do so,
// you can fetch all products from a database.
export async function getStaticPaths() {
const products = await getProductsFromDatabase()
const paths = products.map((product) => ({
params: { id: product.id }
}))
// fallback: false means pages that don’t have the
// correct id will 404.
return { paths, fallback: false }
}
// params will contain the id for each generated page.
export async function getStaticProps({ params }) {
return {
props: {
product: await getProductFromDatabase(params.id)
}
}
}
export default function Product({ product }) {
// Render product
}
假如,電商成長很快。原本 100 個產品,已經成長到 10萬 個產品,這時候有兩個問題
- 在 build time 時 Pre-rendering 10萬 個 page 非常慢
- 某一產品資料更新時,我們只想 update 那一個產品,並不想整個 App 重 build 一次啊
這就靠 Incremental Static Generation 來處理
- Incremental Static Generation 讓我們在 build time 之後 能 pre-render pages 的 subset
- 這能用在 1. add new pages 2. update exist page
10萬 個產品,並且在 build time pre-render,這會非常慢
- 可以利用 lazily 來 pre-render page
- 當 user request 某產品 X 時,我們在 pre-render
這方法要在 getStaticPaths
裡面把 fallback
set true
然後在 page 裡面就能用 router.isFallback
判斷,來顯示一個 loading 的樣式
這個細節很重要,多看看官網的說明
// pages/products/[id].js
export async function getStaticProps({ params }) {
// ...
}
export async function getStaticPaths() {
// ...
// fallback: true means that the missing pages
// will not 404, and instead can render a fallback.
return { paths, fallback: true }
}
export default function Product({ product }) {
const router = useRouter()
if (router.isFallback) {
return <div>Loading...</div>
}
// Render product...
}
當某產品 data update 時,不需要整個 App 重 build
- 透過 Incremental Static Regeneration 來每隔一些 interval 後來 pre-rendered the page
在 getStaticProps
中把 unstable_revalidate
設為 60
unstable_
表示此功能還在 beta 階段- (Next.js v9.5 Stable,使用時確認一下 document)
// pages/products/[id].js
export async function getStaticProps({ params }) {
return {
props: {
product: await getProductFromDatabase(params.id)
},
unstable_revalidate: 60
}
}
- 購物車我們只能部分 pre-render,User 相關的一定需要等 request
- 這個情境,也不需要 Server-side Rendering,而 Client-side Fetching 的效能會更好
- 這邊推薦 SWR 來搭配
import useSWR from 'swr'
function ShoppingCart() {
// fetchAPI is the function to do data fetching
const { data, error } = useSWR('/api/cart', fetchAPI)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return <div>Items in Cart: {data.products.length}</div>
}
- fallback
- 當 user request 某一(還沒有 build pre-render的)產品
- 用 fallback,代替 404
- 先是顯示 loading,當
Next.js
build 完產品頁面時,就會改顯示產品頁面 - 後續有其他 User 來此頁面時,就會提供這個已經產生好的頁面
- update Page
- 當需要 update 已經存在的 page 時,利用 timeout 來每隔 60 秒 update page
- 60 秒內,所有的 user 的 request 只會看到舊的 page
- 等
Next.js
update 過後,新的頁面就 ready 了
- Static is fast: Pre-rendered HTML files can be cached and served by a global CDN
- Static is always online: backend or db 死掉了也沒關係
- Static minimizes backend load: 不需要 request,所以 backend, db 也就沒有 loading
- 要採用 SSR 時,可以用
getServerSideProps
- 但採用 SSR,相對就沒有上面 (static) 的好處
- 所以
Next.js
如果符合需求,官方推薦採用Incremental Static Generation
orClient-side Fetching
- 所以
fetch data 還不夠,App 一定還需要寫資料進去,下面用 新增產品 來舉例
- 在
pages/api
裡面建立檔案,這會建立一個 API endpoint- 例如
pages/api/cart.js
來接收 production id,把產品加到購物車
- 例如
- more info https://nextjs.org/docs/api-routes/introduction
export default async (req, res) => {
const response = await fetch(`https://.../cart`, {
body: JSON.stringify({
productId: req.query.productId
}),
headers: {
Authorization: `Token ${process.env.YOUR_API_KEY}`,
'Content-Type': 'application/json'
},
method: 'POST'
})
const { products } = await response.json()
return res.status(200).json({ products })
};
API routes
- 允許讀取外部資料。利用環境變數來存 key or token
- API routes 可以部署成 serverless (如果是部署在 Vercel,serverless 是 defualt 的設定)