大家好,大家都叫我西瓜,你現在看到的是 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 部落格
Next.js 基礎專案架構
在撰文時照著官方 npx create-next-app
建立的專案看起來像是這樣:
├── node_modules │ └── ... ├── package.json ├── package-lock.json ├── pages │ ├── api │ │ └── hello.js │ ├── _app.js │ └── index.js ├── public │ ├── favicon.ico │ └── vercel.svg ├── README.md └── styles ├── globals.css └── Home.module.css
基礎上可以把 pages
資料夾 視為 Next.js 專案主要內容的進入點,這邊放置各個頁面之 react component,這個資料夾下的目錄/檔案結構相當於網站的 routing,不過在這個生成的專案中只有 pages/index.js
符合這樣的規則,也就是首頁作為示範;假設要 /about
頁面,就建立 pages/about.js
定義該頁的 react component 即可
Dynamic Routes
使用目錄結構定義 routing 的話,似乎就沒辦法寫動態 routing 了?其實是可以的,在上面講到的頁面 react component 檔名中加入 [param]
來作到,舉例來說,放了一個 pages/example/[someParam].js
頁面 react component,GET /example/hello
時除了使用此頁面 react component 之外,在頁面中更可以透過 next/router
, getStaticProps
, getServerSideProps
取得參數 (query
/ params
): { someParam: 'hello' }
pages
資料夾下的其他東西
- 可以看到在生成的專案中有個
pages/_app.js
,我們製作網站時常常會需要一個整站共用的 layout,這個pages/_app.js
就可以讓開發者對網站的所有頁面進行包裝,也可以 import 全站共用 CSS,不過這時還只是body
的內容,如果有需要可以建立pages/_document.js
覆寫整張 HTML pages/api
下放置的想當然爾是 API endpoint 而不是 react component,並且也用跟頁面一樣的 routing 定義方式,在pastleo.me
中沒有需要開 API 因此把這個資料夾刪除了
Server-side rendering, static site generator
當使用 create-react-app
或是說純粹呼叫 ReactDOM.render()
作為進入點時,我們通常讓伺服器傳回的 HTML 只有 ReactDOM.render()
所需的 root DOM,整個 react app 的內容要等到瀏覽器開始執行 javascript 後才會開始產生,除了對 SEO 不友善之外,對於運算速度較緩慢的裝置來說也會有較長的等待時間
為了要解決這個問題,我們需要在送出 HTML 之前就把 react app 跑起來並且把產生的 HTML 包在裡頭,這就是 server-side rendering;但是要做到這件事情並不容易,首先這個 react app 也會需要在瀏覽器以外的環境(伺服器)上執行,同時要考慮到伺服器 routing、執行 react app 時資料來源等問題,因此遵照 Next.js framework 的規則實作,並透過 Next.js 實做好的 web server (以下稱 next server)把網站跑起來,這個網站就具有 server-side rendering 的功能
Next.js 專案建立時在 package.json
的 scripts
裡頭有三個指令:
dev
(npm run dev
/npx next dev
):顧名思義就是開發模式,會啟動 next server 跟前端 assets compiling(其實就是 webpack)的開發模式build
(npm run build
/npx next build
):進行前端 assets compiling,同時也 compile 一份給 next server 之 production 模式跑start
(npm start
/npx next start
):執行 next server 的 production 模式,在start
之前要先執行build
才能正確執行
如果網站的內容是比較偏靜態的,或是容易列舉出全部頁面,例如 landing page, 部落格這樣的網站就可以選擇使用 static-site generator,如此一來只需要 host 靜態檔案,不僅可以省下伺服器成本,還可以加快網站回應給瀏覽器的速度;Next.js 除了可以做到 server-side rendering,還可以當成 static-site generator 使用,這個功能叫做 next export
,會嘗試列舉所有頁面進行 server-side rendering,把整個網站 "匯出" 成靜態網站,並預設寫入到 out
資料夾
pastleo.me
本身主要的功能是部落格,因此筆者就使用 Next.js 的 static-site generator 佈署網站,在 package.json
寫:
{ "scripts": { "export": "next export", ... }, ... }
以後就可以用 npm run export
來匯出成靜態網站,還有類似文件效果提醒以後的自己
CSS/SCSS module, Tailwindcss + purgecss
Next.js 內建的前端 assets compiling 工具包含:
- Webpack,並且已經寫好設定檔,雖然可以客製化,不過非必要情況下建議不要去改
- Javascript 部份:Babel,預設使用了 Next.js 自己的 preset
next/babel
,用來 transpile JSX 等語法 - CSS/styling 部份:PostCSS,Next.js 內建設定就可以直接在 Javascript 寫
import
引入 CSS 檔案- 同時已經設定好 CSS module,並以
.module.css
附檔名慣例來表示該檔案是 CSS module- CSS module 主要可以避免
className
重複的問題,使用上要在 Javascript 裡頭 import.module.css
中的className
並寫在 JSX 的className={...}
- CSS module 主要可以避免
- 值得注意的是,純粹以
.css
為附檔名的 CSS 就會被當成 global CSS,而專案內的 global CSS 只允許在pages/_app.js
引入(也就是整站共用的頁面 component 包裝)
- 同時已經設定好 CSS module,並以
筆者在 pastleo.me
主要使用 TailwindCSS 作為 CSS 框架,因為 TailwindCSS 的 utility-first 特性使得使用上很靈活,不會被 component style 綁死,而且在需要 component 化避免重複程式碼時 react 本身就提供很好的 component 功能;如果遇到 TailwindCSS 無法或是不好實做特定效果時,使用 SCSS module 實做
首先 CSS module 部份 Next.js 已經內建,而 Next.js 對於 SCSS/SASS 也有很完整的支援,只要:
npm install --save sass
安裝即可,然後測試了一下發現 Next.js 也完全支援 SCSS module,檔案以 .module.scss
作為副檔名就能夠在 Javascript 中 import 使用囉
TailwindCSS 的部份,可以透過其 PostCSS 之 plugin 進行客製化,主要跟著 TailwindCSS 官方網站對 Next.js 的安裝教學即可;一個筆者比較在意的部份是多一個 tailwind.config.js
檔案,於是把 TailwindCSS 的設定直接寫在 postcss.config.js
中
另一個使用 TailwindCSS 的問題是會有一大推沒用到的 CSS rule,因為 TailwindCSS 給的是 utility CSS,然後乘上各種 RWD, :hover
等條件,一個 bg-green-400
可能有 md:bg-green-400
, lg:hover:bg-green-400
等,幸好 TailwindCSS 的 PostCSS plugin 已經內建好 PurgeCSS 會在 compiling production 版前端 assets 時去掃描檔案看有用到哪些 className
,把不必要的都去除,需要做的事情就是在 TailwindCSS 設定值裡頭設定 purge
選項指定要掃描哪些檔案,在 TailwindCSS 官方網站對 Next.js 的安裝教學有寫到
Layout
上面提到 Next.js 有提供 pages/_app.js
來寫所有頁面共用的『包裝』,除了 import 全站共用 CSS 之外,也可以拿來寫共用的 layout,像是 navbar, footer 之類,不過筆者比較偏好用 HOC 的形式讓各個 page component 各自 import 使用,這樣除了比 pages/_app.js
明確之外,也可以給予參數選擇不同的版型,更有不使用任何 layout 的彈性,以 pastleo.me
使用起來如下:
- 定義
withLayout
HOC:(options = {}) => PageComponent => PageComponentWithLayout
- 在首頁
pages/index.js
使用withLayout
,因為withLayout
預設使用ContentLayout
,在這邊設定options
中的Layout
為HomeLayout
- 在關於頁
pages/about.js
除了使用withLayout
,設定 Layout 之外,更設定了網頁 title
React development & life-cycle
網站內容開發大部分就是一般 React app 的開發,React 提供的功能或是別人寫好的 React 套件應該都可以使用,這邊提幾點 Next.js 下寫 React 筆者所觀察到的:
- 因為
pages
是各個頁面的進入點,大多數 Next.js 的範例專案都會開一個components
資料夾來放共用的 component - server-side rendering 就像是把 page component 以及其『呼叫』的 component 當成 function 執行下去,整個 react components tree 展開到每個 node 都 return JSX 完畢,就把獲得的 HTML 作為結果回傳
- 因此,
useState()
,return <div />
會在 server-side 上發生,但是useEffect(fn)
的fn
並不會在 server-side 上執行,用這樣的思維去思考比較容易知道 server-side 會執行到什麼程度,瀏覽器或爬蟲收到的 HTML 會長到哪裡
- 因此,
server-side rendering 把 component 當成 function 執行的部份,如果是 functional component 應該蠻好理解的,而且大家現今新寫的應該都是 functional component 吧
Image Assets
在 Next.js 有個 public
資料夾來放置靜態檔案,例如一張圖放在 public/logo.png
,在頁面中就可以直接寫 <img src='/logo.png' />
來顯示該圖,不過就有可能會遇到更新網站的時候該路徑有原本的 cache 而不會更新,而且也不會經過 assets compiling 做任何處理
Next.js v10 更新開始內建自動 image assets optimization,使用上圖檔可以一樣放置在 public
資料夾,並透過 Next.js 提供的 next/image
component <Image src='/logo.png' />
來使用,這時瀏覽器請求的 URL 會改成透過圖片轉換服務,例如透過內建轉換服務,/logo.png
會變成 /_next/image?url=%2Flogo.png&w=256&q=75
,圖檔在要取用時 on-the-fly 進行轉換,網站需要一個圖片轉換服務跑著,可以選擇 next server 內建的轉換 endpoint 或是第三方服務
如果網站採用 static-site generator 的方式佈署,網站服務時不會有 next server,這個 image optimization 就得依靠第三方服務幫忙了,也因此 next export
在沒有設定第三方圖片轉換服務下看到 next/image
時會直接報錯;筆者偏好讓 pastleo.me
網站內非文章內容的圖片在 assets compiling 時期處理,因此尋找了另外一個在 assets compiling 就進行的壓縮/轉換的工具:next-optimized-images@canary
next-optimized-images
讓我們可以用 file-loader
的方式取得圖片網址:
import imgSrc from './img.jpg'
並且會進行圖片壓縮、path 轉換(避免更新網站後使用原本 path 的 cache),也可以加入參數進行大小調整、格式轉換,例如轉換成 webp 並調整大小至寬度 150px:
import imgSrc from './img.jpg?webp&width=150'
眼尖的朋友應該有發現筆者使用的版本是測試版 @canary
,因為相較於目前穩定版加入了一個筆者很喜歡的功能:<Img />
component,為什麼呢?使用穩定版如果要寫同時支援 webp 以及向下相容的圖片 <picture />
得這樣寫:
import webpSrc from '../assets/img.jpg?webp';import jpgSrc from '../assets/img.jpg';const Picture = () => (<picture><source srcSet={webpSrc} type='image/webp' /><source srcSet={jpgSrc} type='image/jpeg' /><img src={jpgSrc} /></picture>)
照著 @canary
的安裝步驟做完之後,便可使用其推薦的 <Img />
,透過 babel plugin react-optimized-image/plugin
類似 macro 的魔法,只要這樣寫就可以生成與上面功能相同的 HTML:
import Img from 'react-optimized-image';import jpgSrc from '../assets/img.jpg';const Picture = () => (<Img src={jpgSrc} webp />)
把 pastleo.me
從 next-optimized-images
正式版升級到 @canary
的詳細更動請見 git commit: https://github.com/pastleo/pastleo.me/commit/13bc304650ca6baaab0152d7152eda7c0d9c20e3
可惜的是,
next-optimized-images@canary
自 2020/8 到撰文期間作者都沒有更新,如果作者有繼續更新,這邊所謂的@canary
很可能已經成為 v3 正式板,詳細狀況可以參考Version 3
issue
Deployment
佈署方面,選擇使用的工具大多就只是筆者的偏好,這邊大致列一下:
- GitHub Action: CI/CD 自動化
eslint
檢查、next export
產生靜態網站、上傳靜態網站到 GCP bucket- 每當 git push 時會執行
eslint
並建置靜態網站,如果 branch 為master
才上傳到 GCP bucket 更新網站 - 設定檔:
.github/workflows/main.yml
- GCP bucket 放置靜態網站的所有內容,從
.html
,.js
到.ico
,同時設定 bucket 為公開讀取 (Member:allUsers
, Role:Storage Object Viewer
) - 在 linode VPS 上架設 Nginx, Lets-encrypt 並 proxy_pass 給 GCP bucket
- Nginx
proxy_pass
給 GCP bucket 設定檔範例:presslabs/gs-proxy 之 nginx.conf - 需要 nginx 幫忙做些
rewrite
:- static-side generator 產生的網頁檔會是
xxx.html
,但是瀏覽時網址不會有.html
,需要幫忙補上.html
:rewrite "/([^\/]+\.\w{5,}|[^\/.]+)$" ${uri}.html;
- 根目錄
/
指的是/index.html
,同樣幫忙補上:rewrite /$ ${uri}index.html;
- static-side generator 產生的網頁檔會是
- Nginx
- 最後使用 Cloudflare 設定
pastleo.me
DNS 並啟用 CDN
To be continued: markdown 部落格
本篇介紹到專案架構、工作流程的設定,也聊了一下 Next.js 以及週邊工具筆者的使用心得;上面提到 pastleo.me
的主要功能是部落格,這部份等到下篇再來聊聊,大概會是 Next.js 頁面資料取得以及筆者在 pastleo.me
實做的 markdown 文章讀取、列出、顯示功能