大家好,大家都叫我西瓜,你現在看到的是 Next.js 系列文章的第二篇。Next.js 是一個使用 React 渲染的 web framework,並使網頁在從伺服器送出時就先渲染好,可選擇使用 server-side rendering 或是 static-site generating。筆者使用 React 開發多年,本部落格選擇使用 Next.js 製作,這邊把使用的心得寫下來,如果你對 React 有基本的了解,希望這系列文章能讓你了解 Next.js 的機制以及使用方式
pastleo.me 原始碼:https://github.com/pastleo/pastleo.me
本系列文章分成兩篇,章節架構如下:
- 第一篇:React static-site generator, Tailwindcss
- 第二篇(本篇):Data Fetching & Markdown 部落格
Data fetching
放置在 pages/ 下的頁面 react component 在不需要動態內容的狀況下,寫法與一般的 react component 是一樣的,假設有個頁面 component pages/index.js:
import { useState } from 'react';export default function Home() {const [count, setCount] = useState(0);console.log({ count })return (<div><p>{ count }</p><button onClick={() => setCount(c => c+1)}>INC</button></div>)}
因為 Next.js 是 server-side rendering 的,這邊放了一個 console.log,開啟網頁時除了在瀏覽器開發工具的 Console 上可以看到 { count: 0 },在終端機上看到也可以看到 Next.js 伺服器印出 { count: 0 }。React component/function 內寫的程式會在伺服器上執行產生 HTML 送到瀏覽器,瀏覽器收到 HTML 確實可以先把畫面顯示出來,不過還是得 mount react component 上去,這時在該跑的 Javascript 還是得跑;因此,Next.js 的 React App 在 伺服器、瀏覽器 兩邊都必須要可以執行
動態內容的來源
要做的功能需求動態內容時,通常會有動態的來源,data fetching 就是去抓取這個來源,像是去 資料庫 撈文章列表、又或者透過物流系統 API 取得運送狀態,這些動作有可能沒辦法在瀏覽器上進行,而且我們已經在進行 server-side rendering,就算可以也會希望不要發生在瀏覽器上,因此需要一種寫法使這些動作的程式只在伺服器上執行
Next.js 的 data fetching 功能就是來解決這個問題,讓頁面 component 除了預設 export react component 之外,可以實做並 export 以下 function:
async function getServerSideProps(context): Next.js server 每次收到請求時執行,這邊寫的東西就很接近一般 Rails, Django, Laravel 等後端框架之 controllerasync function getStaticProps(context): 原則上在next build時執行
這兩個 function 用來準備資料提供 props 給頁面 component 使用,會讓筆者覺得很像是 MVC 架構的 controller 部份:
同時,在前端 assets compiling 時也會把這兩個 server 才需要的 function、下面將提到之 getStaticPaths() 排除在外,減少瀏覽器需要下載的資料量
另外還有一個東西叫做
getInitialProps,這是 Next.js 先前data fetching的方法,會在 Next.js server 每次收到請求 以及next export時執行,筆者測試了一下,可能是因為其宣告的方式導致無法排除在前端 assets compiling,而且現在 Next.js 官方也不推薦使用
頁面運作方式與 data fetching
Next.js 的每個頁面可以有不同的運作方式,根據頁面 component 有 export 什麼 function 來決定,在 next build 時可以看到各個頁面前面可能有 ○, ●, λ 的符號,就是表示運作方式:
- Static (
○): 這個頁面 component 沒有export任何data fetching相關 function,內容是靜態的 - Static-site generator (
●): 簡稱 SSG,表示該頁面寫了getStaticProps,每次next build出來的內容可以因為取得的資料不同而有所改變 - Server-side rendering (
λ): 簡稱 SSR/Server,表示該頁面寫了getServerSideProps,每次伺服器回傳的內容可以不同
原先筆者認為 SSG 的 getStaticProps 是在 next export 執行,使用並研究之後發現這個 SSG 運作方式有些很有趣的設計:
getStaticProps在next build執行,不僅可以滿足一般 SSG 使用情境之外,也可以變成一種在 build time 為next server做的最佳化,可以先算的就先運算,next server就不需要重複運算- 所以
next export也只是把next build的結果整理複製出來而已
- 所以
- 如果 SSG 的頁面有 dynamic routing,Next.js 會要求開發者實做並
exportasync function getStaticPaths()來列舉dynamic routing的路徑- 並且透過
getStaticPaths設定 fallback,讓next server被請求沒有列舉到的路徑時執行getStaticProps
- 並且透過
把各種情況整理下來大概看起來像是這樣:
Next.js 會做一定的檢查,例如 getStaticProps 以及 getServerSideProps 不能同時寫,next export 時也不能有用到需要 next server 的功能
範例:計算訪問次數
綜合以上,筆者製作了一個簡單的範例,lib/increment-data-source.js 實做 incrementAndGet 假裝是 persist 資料庫,一開始數值為 0,呼叫會讓數值加一並回傳數值
import { incrementAndGet } from '../lib/increment-data-source.js';export default function Home({ visitedCnt }) {console.log('Home page component', { visitedCnt });return (<div><h1>Next.js Data Fetching DEMO</h1><hr /><h2>visited { visitedCnt } times.</h2></div>);}export async function getServerSideProps(context) {const visitedCnt = incrementAndGet();console.log('Home getServerSideProps', { visitedCnt });return {props: { visitedCnt }, // will be passed to the page component as props};}
瀏覽器開啟此網頁時,getServerSideProps 會執行並呼叫 incrementAndGet 把 visitedCnt 準備好送到頁面 component 的 props,最後可以在瀏覽器上看到 visited 1 times.,再重整一次看到 visited 2 times.、visited 3 times....雖然上方 import 了 incrementAndGet,在 incrementAndGet 只有在 getServerSideProps(context) 內使用的狀況下,lib/increment-data-source.js 是不會被 assets compile 進去的,減少瀏覽器所需要下載的資料量
完整的範例以及 SSG 請參考 repository:
https://github.com/pastleo/next-data-fetching-demo
主要是 pages/index.js,SSG/next export 以及 dynamic routing 部份請參考 pages/counter/[n].js
題外話:如果動態的來源其中有 client 身上的資料
這段是上一篇分享至 Facebook 社團 ReactJS.tw 有人留言問的,筆者覺得很不錯所以也整理起來
如果需要每個 client 有不同的 persist state 又要 SSR 的話(SSG 就不可能了),就得用到 cookie 了,有人已經有做了類似的嘗試;在 getServerSideProps(context) 可以取得 req,也就可以寫一般伺服器端程式取用 cookie、甚至做成 session 來使用
因為在這個情況下 SEO 也不重要了,可以放棄部份 SSR/SSG,讓針對每個 client 顯示不同內容的區塊在伺服器端不要產生,到 client 手上開始執行時透過 useEffect 觸發把該區內容顯示出來(應該也需要打具有身份狀態的 API 取回對應資料),在 pastleo.me 有一個 PostBanner component 有類似手法,讓每個文章 Banner 的背景圖等到網站主要文字內容先顯示之後再開始讀取
Markdown 部落格
Markdown 是一種軟體工程文件或文章常用的格式,pastleo.me 部落格內容就以一篇文章為一個 markdown 檔案的方式放置在 pages/post,接下來介紹要如何讓 Next.js 使用 markdown 作為文章格式建構部落格功能
在 Next.js 官方部落格上有一篇文章也是在介紹 Next.js markdown 部落格,大概有三種方法:
- 把專案內的
.md檔案當作getStaticPaths()的資料來源,並使用 remark 轉換成 HTML ,最後用dangerouslySetInnerHTML={{ __html: content }}方式顯示。官方完整範例:blog-starter - 使用 MDX,一種把 Markdown & JSX 混在一起的語法,文章以
.mdx格式直接放置在pages資料夾內,並透過@next/mdx,@mdx-js/loader轉換成 JSX 格式使得 Next.js 可以當成一般頁面 component 使用。官方範例:with-mdx - 像是第二點一樣使用 MDX,不過不把
.mdx當成頁面 component,而是作為getStaticPaths()的資料來源,使用next-mdx-remote使得 MDX/Markdown 轉換的結果可以 serialize 用 props 傳送給 react component,算是第一點與第二點的結合。官方完整範例:with-mdx-remote
pastleo.me 在撰文時採用的是這邊的第二種,並且加上 Layout, components 客製化,同時也是在首頁、文章列表頁面使用了 getStaticPaths() 來蒐集 .mdx 以及標題、縮圖等 metadata
安裝設定 MDX
這部份 MDX 官方有與 Next.js 整合的教學:https://mdxjs.com/getting-started/next
只不過這邊可能會遇到 next.config.js 已經有寫好其他 plugin,因此筆者使用了 next-compose-plugins 來跟其他 plugin 組合
不使用
next-compose-plugins也行,不過就會變成withMDX({ ... })(withXXX({ ... }))
Layout, Component 客製化
筆者一開始選擇使用 MDX 是因為可以提供比較好的客製化,像是直接針對 markdown 的各個元件進行 react component 的覆寫,除了 CSS style 之外,甚至可以用 react 實做互動功能,像是 component 客製化官方文件使用 components 的 props 直接指定 markdown 內容各個元件所應使用的 react component:
<Hellocomponents={{h1: Heading,p: Text,code: Code,inlineCode: InlineCode}}/>
關於 layout 部份,從 MDX 官方文件看起來作者預期的使用方式是由上層 component 作為 layout,並且 import .mdx 檔案使用,但是依照本文的使用方式每個 .mdx 就是一個個的頁面 component,這些 .mdx 幾乎就是就是最上層的 component,幸好 MDX 也有提供方法覆寫 wrapper 讓我們可以客製化 layout:在 .mdx 檔案中 export default MdxLayout 就會讓 MdxLayout 覆寫掉預設的 wrapper
接著就是設計、刻板、實做出這個 MdxLayout,有興趣可以參考筆者的 wrapPost.js
front-matter: 標題、縮圖等 metadata
有時候我們可以在一個 markdown 檔案內容最上方看到這樣的語法:
--- title: Next.js 建置的 pastleo.me - React static-site generator, Tailwindcss thumbnail: https://i.imgur.com/D0FhUpoh.jpg ---
一個檔案內容最上方的 --- 包著 YAML,這樣的語法似乎叫做 front-matter,常常拿來寫標題、縮圖等 metadata;在部落格系統的每篇 .mdx 文章當然也會有 metadata 的需求,根據 MDX 官方文件的做法,我們要在 .mdx 裡頭寫 export const metadata = { ... },然後讓 MdxLayout 或其他 component 使用,雖然就達成所需要的目標了,不過加上上面的 layout, component 覆寫,每個 .mdx 就有不少 javascript 混在裡面:
import MDdxLayout from '../layouts/wrapPost.js'
export const metadata = {
title: 'Page Title',
description: 'This is a page description',
}
Lorem Ipsum
===
Lorem Ipsum is simply dummy text of the printing and typesetting industry.
export default MDdxLayout;
為了讓這邊文章的 .mdx 可以用比較常見的 frontmatter markdown 寫法,筆者研究了一下 MDX 進階使用方式,發現官方的 compiler 客製化範例就是透過 remark-frontmatter 裝上 frontmatter 支援,再利用 AST explorer 去觀察其 輸入 以及 輸出,筆者寫了自己的 extractFrontmatter.js,最後整合到 next.config.js 的 withMDX;這樣一來在 .mdx 裡頭不僅可以寫 frontmatter,甚至不用寫 import/export MDdxLayout,在 frontmatter 裡面指定 layout 即可,像是這樣
文章列表功能
pastleo.me 文章顯示所使用的 MDX 方式,是在 asset compile 下手,使 markdown 成為 react 程式碼的一部分;不過我們還是需要在首頁列出文章,這邊就用本文上半部 data fetching 的 getStaticProps(context),在 next build 時蒐集 pages/post/ 內的 .mdx 檔案,並把 frontmatter 記載的 metadata 拉出來,整理成 props 給頁面 component 使用
比較值得注意的是,除了列出文章之外,還需要分頁功能,因此寫了 pages/posts/[q].js,這頁除了 getStaticProps(context) 之外還有 getStaticPaths() 列舉第一頁、第二頁、第三頁...最後把文章資料功能整理成 lib/node/posts.js
總結心得
在筆者實做文章功能時,next-mdx-remote (上方 Markdown 部落格的第三個方法)還沒有浮上檯面,採用的整套 MDX setup 在最後加了不少 hack,雖然結果還算滿意,不過在寫這篇文章找資料的過程中才發現 next-mdx-remote,確實應該把文章當成一種資料,這個方式理當也可以避免對 asset compile 做修改、hack MDX 的 AST,未來筆者應該會朝這個方向做修改
本篇有不少篇幅在介紹 Next.js SSR,Next.js 也是以 SSR 為主進行設計的,但是 pastleo.me 卻是以 SSG 的方式佈署,只看專門做 React SSG 的話,目前比較熱門的框架有 Gatsby, react-static,當時筆者有考慮使用 Gatsby,不過看到 Next.js 這種兩種皆可的 data fetching 機制時,確實覺得 SSG 與 SSR 的組成有許多是重疊可以共用的,而且也不至於把事情搞的非常複雜,於是就選擇 Next.js 進行開發;在 2020/12 React 官方也發布了 React server component 的 RFC,大致可以理解成 React 官方的 SSR,現在看來與 Next.js 的使用方式相當不同,等正式發布的時候再來看怎麼發展吧
使用 Next.js 的心得就寫到這邊,感謝讀者的閱讀,希望這些心得對大家有幫助!