--- title: Next.js 建置的 pastleo.me - React static-site generator, Tailwindcss thumbnail: https://i.imgur.com/D0FhUpoh.jpg createdAt: 2021/02/28 layout: ../../layouts/wrapPost.js --- [Next.js](https://nextjs.org/) 是一個使用 [React](https://reactjs.org/) 渲染(或是說,生成 html)的 web framework,最大的特色是 server-side rendering,筆者於 2020/3 打掉重練的 [pastleo.me](https://pastleo.me) 選擇使用 Next.js 製作,CSS/styling 部份採用 [tailwindcss](https://tailwindcss.com/) #### [pastleo.me](https://pastleo.me) 原始碼:[https://github.com/pastleo/pastleo.me](https://github.com/pastleo/pastleo.me) 這邊就來介紹一下這些時間在 `pastleo.me` 使用 Next.js 的心得 ## Next.js 基礎專案架構 在撰文時照著[官方 `npx create-next-app`](https://nextjs.org/docs/getting-started)建立的專案看起來像是這樣: ``` ├── 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` 資料夾](https://nextjs.org/docs/basic-features/pages) 視為 Next.js 專案主要內容的進入點,這邊放置各個頁面之 react component,這個資料夾下的目錄/檔案結構相當於網站的 routing,不過在這個生成的專案中只有 `pages/index.js` 符合這樣的規則,也就是首頁作為示範;假設要 `/about` 頁面,就建立 `pages/about.js` 定義該頁的 react component 即可 #### [`Dynamic Routes`](https://nextjs.org/docs/routing/dynamic-routes) 使用目錄結構定義 routing 的話,似乎就沒辦法寫動態 routing 了?其實是可以的,在上面講到的頁面 react component 檔名中加入 `[param]` 來作到,舉例來說,放了一個 `pages/example/[someParam].js` 頁面 react component,`GET /example/hello` 時除了使用此頁面 react component 之外,在頁面中更可以透過 [`next/router`](https://nextjs.org/docs/api-reference/next/router), [`getStaticProps`](https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation), [`getServerSideProps`](https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering) 取得參數 (`query` / `params`): `{ someParam: 'hello' }` #### `pages` 資料夾下的其他東西 * 可以看到在生成的專案中有個 `pages/_app.js`,我們製作網站時常常會需要一個整站共用的 layout,這個 [`pages/_app.js`](https://nextjs.org/docs/advanced-features/custom-app) 就可以讓開發者對網站的所有頁面進行包裝,也可以 import 全站共用 CSS,不過這時還只是 `body` 的內容,如果有需要可以建立 [`pages/_document.js`](https://nextjs.org/docs/advanced-features/custom-document) 覆寫整張 HTML * [`pages/api`](https://nextjs.org/docs/api-routes/introduction) 下放置的想當然爾是 API endpoint 而不是 react component,並且也用跟頁面一樣的 routing 定義方式,在 `pastleo.me` 中沒有需要開 API 因此把這個資料夾刪除了 ## Server-side rendering, static site generator 當使用 [`create-react-app`](https://create-react-app.dev/) 或是說純粹呼叫 [`ReactDOM.render()`](https://reactjs.org/docs/react-dom.html#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` 裡頭有三個指令](https://nextjs.org/docs/deployment#nodejs-server): * `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`](https://nextjs.org/docs/advanced-features/static-html-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](https://webpack.js.org/),並且已經寫好設定檔,雖然可以[客製化](https://nextjs.org/docs/api-reference/next.config.js/custom-webpack-config),不過非必要情況下建議不要去改 * Javascript 部份:[Babel](https://babeljs.io/),預設使用了 Next.js 自己的 preset [`next/babel`](https://nextjs.org/docs/advanced-features/customizing-babel-config),用來 transpile JSX 等語法 * CSS/styling 部份:[PostCSS](https://postcss.org/),[Next.js 內建設定就可以直接在 Javascript 寫 `import` 引入 CSS 檔案](https://nextjs.org/docs/basic-features/built-in-css-support) * 同時已經[設定好 CSS module](https://nextjs.org/docs/basic-features/built-in-css-support#adding-component-level-css),並以 `.module.css` 附檔名慣例來表示該檔案是 CSS module * [CSS module](https://github.com/css-modules/css-modules/blob/9922d6ec376261022fbdc461f3ca06e9623c82b3/README.md) 主要可以避免 `className` 重複的問題,使用上要在 Javascript 裡頭 import `.module.css` 中的 `className` 並寫在 JSX 的 `className={...}` * **值得注意的是,純粹以 `.css` 為附檔名的 CSS 就會被當成 global CSS,而專案內的 global CSS 只允許在 `pages/_app.js` 引入(也就是整站共用的頁面 component 包裝)** 筆者在 `pastleo.me` 主要使用 [TailwindCSS](https://tailwindcss.com/) 作為 CSS 框架,因為 TailwindCSS 的 utility-first 特性使得使用上很靈活,不會被 component style 綁死,而且在需要 component 化避免重複程式碼時 react 本身就提供很好的 component 功能;如果遇到 TailwindCSS 無法或是不好實做特定效果時,使用 [SCSS](https://sass-lang.com) module 實做 首先 CSS module 部份 Next.js 已經內建,而 [Next.js 對於 SCSS/SASS 也有很完整的支援](https://nextjs.org/docs/basic-features/built-in-css-support#sass-support),只要: ```shell npm install --save sass ``` 安裝即可,然後測試了一下發現 Next.js 也完全支援 SCSS module,檔案以 `.module.scss` 作為副檔名就能夠在 Javascript 中 import 使用囉 TailwindCSS 的部份,可以透過其 PostCSS 之 plugin 進行客製化,主要跟著 [TailwindCSS 官方網站對 Next.js 的安裝教學](https://tailwindcss.com/docs/guides/nextjs)即可;一個筆者比較在意的部份是多一個 `tailwind.config.js` 檔案,於是把 TailwindCSS 的設定直接寫在 [`postcss.config.js`](https://github.com/pastleo/pastleo.me/blob/70241243e9d80c335330d0451a2c0e429b9f94ab/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](https://purgecss.com/) 會在 [compiling production 版前端 assets 時去掃描檔案看有用到哪些 `className`,把不必要的都去除](https://tailwindcss.com/docs/optimizing-for-production),需要做的事情就是在 TailwindCSS 設定值裡頭設定 `purge` 選項指定要掃描哪些檔案,在 [TailwindCSS 官方網站對 Next.js 的安裝教學](https://tailwindcss.com/docs/guides/nextjs#configure-tailwind-to-remove-unused-styles-in-production)有寫到 ## Layout 上面提到 Next.js 有提供 `pages/_app.js` 來寫所有頁面共用的『包裝』,除了 import 全站共用 CSS 之外,也可以拿來寫共用的 layout,像是 navbar, footer 之類,不過筆者比較偏好用 HOC 的形式讓各個 page component 各自 import 使用,這樣除了比 `pages/_app.js` 明確之外,也可以給予參數選擇不同的版型,更有不使用任何 layout 的彈性,以 `pastleo.me` 使用起來如下: * 定義 [`withLayout`](https://github.com/pastleo/pastleo.me/blob/70241243e9d80c335330d0451a2c0e429b9f94ab/layouts/index.js#L10) HOC: `(options = {}) => PageComponent => PageComponentWithLayout` * 在首頁 [`pages/index.js` 使用 `withLayout`](https://github.com/pastleo/pastleo.me/blob/70241243e9d80c335330d0451a2c0e429b9f94ab/pages/index.js#L24),因為 `withLayout` 預設使用 [`ContentLayout`](https://github.com/pastleo/pastleo.me/blob/70241243e9d80c335330d0451a2c0e429b9f94ab/layouts/ContentLayout.js),在這邊[設定 `options` 中的 `Layout`](https://github.com/pastleo/pastleo.me/blob/70241243e9d80c335330d0451a2c0e429b9f94ab/pages/index.js#L25) 為 [`HomeLayout`](https://github.com/pastleo/pastleo.me/blob/70241243e9d80c335330d0451a2c0e429b9f94ab/layouts/HomeLayout.js) * 在關於頁 [`pages/about.js` 除了使用 `withLayout`](https://github.com/pastleo/pastleo.me/blob/70241243e9d80c335330d0451a2c0e429b9f94ab/pages/about.js#L112),[設定 Layout](https://github.com/pastleo/pastleo.me/blob/70241243e9d80c335330d0451a2c0e429b9f94ab/pages/about.js#L113) 之外,更[設定了網頁 title](https://github.com/pastleo/pastleo.me/blob/70241243e9d80c335330d0451a2c0e429b9f94ab/pages/about.js#L114) ## 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
` 會在 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`,在頁面中就可以直接寫 `` 來顯示該圖,不過就有可能會遇到更新網站的時候該路徑有原本的 cache 而不會更新,而且也不會經過 assets compiling 做任何處理 [Next.js v10 更新](https://nextjs.org/blog/next-10#built-in-image-component-and-automatic-image-optimization)開始[內建自動 image assets optimization](https://nextjs.org/docs/basic-features/image-optimization),使用上圖檔可以一樣放置在 `public` 資料夾,並透過 Next.js 提供的 `next/image` component `` 來使用,這時瀏覽器請求的 URL 會改成透過圖片轉換服務,例如透過內建轉換服務,`/logo.png` 會變成 `/_next/image?url=%2Flogo.png&w=256&q=75`,圖檔在要取用時 on-the-fly 進行轉換,網站需要一個圖片轉換服務跑著,可以選擇 next server 內建的轉換 endpoint 或是[第三方服務](https://nextjs.org/docs/basic-features/image-optimization#loader) 如果網站採用 static-site generator 的方式佈署,網站服務時不會有 next server,這個 image optimization 就得依靠第三方服務幫忙了,也因此 `next export` 在沒有設定第三方圖片轉換服務下看到 `next/image` 時會直接報錯;筆者偏好讓 `pastleo.me` 網站內非文章內容的圖片在 assets compiling 時期處理,因此尋找了另外一個在 assets compiling 就進行的壓縮/轉換的工具:[`next-optimized-images@canary`](https://github.com/cyrilwanner/next-optimized-images/tree/canary) `next-optimized-images` 讓我們可以用 [`file-loader`](https://webpack.js.org/loaders/file-loader/) 的方式取得圖片網址: ```javascript import imgSrc from './img.jpg' ``` 並且會進行圖片壓縮、path 轉換(避免更新網站後使用原本 path 的 cache),也可以[加入參數](https://github.com/cyrilwanner/next-optimized-images/tree/canary#query-params)進行大小調整、格式轉換,例如轉換成 [webp](https://zh.wikipedia.org/zh-tw/WebP) 並調整大小至寬度 150px: ```javascript import imgSrc from './img.jpg?webp&width=150' ``` 眼尖的朋友應該有發現筆者使用的版本是測試版 `@canary`,因為相較於[目前穩定版](https://github.com/cyrilwanner/next-optimized-images)加入了一個筆者很喜歡的功能:[`` component](https://github.com/cyrilwanner/next-optimized-images/tree/canary#image-components),為什麼呢?使用穩定版如果要寫同時支援 webp 以及向下相容的圖片 [``](https://developer.mozilla.org/zh-TW/docs/Web/HTML/Element/picture) 得這樣寫: ```jsx import webpSrc from '../assets/img.jpg?webp'; import jpgSrc from '../assets/img.jpg'; const Picture = () => ( ) ``` 照著 [`@canary` 的安裝步驟](https://github.com/cyrilwanner/next-optimized-images/tree/canary#installation)做完之後,便可使用其推薦的 [``](https://github.com/cyrilwanner/next-optimized-images/tree/canary#image-components) ,透過 babel plugin `react-optimized-image/plugin` 類似 macro 的魔法,只要這樣寫就可以生成與上面功能相同的 HTML: ```jsx import Img from 'react-optimized-image'; import jpgSrc from '../assets/img.jpg'; const Picture = () => ( ) ``` 把 `pastleo.me` 從 `next-optimized-images` 正式版升級到 `@canary` 的詳細更動請見 git commit: [https://github.com/pastleo/pastleo.me/commit/13bc304650ca6baaab0152d7152eda7c0d9c20e3](https://github.com/pastleo/pastleo.me/commit/13bc304650ca6baaab0152d7152eda7c0d9c20e3) > 可惜的是,`next-optimized-images@canary` 自 2020/8 到撰文期間作者都沒有更新,如果作者有繼續更新,這邊所謂的 `@canary` 很可能已經成為 v3 正式板,詳細狀況可以參考 [`Version 3` issue](https://github.com/cyrilwanner/next-optimized-images/issues/120) ## Deployment 佈署方面,選擇使用的工具大多就只是筆者的偏好,這邊大致列一下: * [GitHub Action](https://github.com/features/actions): CI/CD 自動化 * [`eslint`](https://github.com/pastleo/pastleo.me/blob/70241243e9d80c335330d0451a2c0e429b9f94ab/.eslintrc.js) 檢查、`next export` 產生靜態網站、上傳靜態網站到 GCP bucket * 每當 git push 時會執行 `eslint` 並建置靜態網站,如果 branch 為 `master` 才上傳到 GCP bucket 更新網站 * 設定檔:[`.github/workflows/main.yml`](https://github.com/pastleo/pastleo.me/blob/70241243e9d80c335330d0451a2c0e429b9f94ab/.github/workflows/main.yml) * [GCP bucket](https://cloud.google.com/storage) 放置靜態網站的所有內容,從 `.html`, `.js` 到 `.ico`,同時設定 bucket 為公開讀取 (Member: `allUsers`, Role: `Storage Object Viewer`) * 在 [linode VPS](https://www.linode.com/) 上架設 [Nginx](https://www.nginx.com/), [Lets-encrypt](https://letsencrypt.org/zh-tw/) 並 [proxy_pass](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) 給 GCP bucket * Nginx `proxy_pass` 給 GCP bucket 設定檔範例:[presslabs/gs-proxy 之 nginx.conf](https://github.com/presslabs/gs-proxy/blob/815ac77b26028c4b7d3cbbb935034f8b9f67784b/nginx.conf) * 需要 nginx 幫忙做些 [`rewrite`](http://nginx.org/en/docs/http/ngx_http_rewrite_module.html#rewrite): * static-side generator 產生的網頁檔會是 `xxx.html`,但是瀏覽時網址不會有 `.html`,需要幫忙補上 `.html`:`rewrite "/([^\/]+\.\w{5,}|[^\/.]+)$" ${uri}.html;` * 根目錄 `/` 指的是 `/index.html`,同樣幫忙補上:`rewrite /$ ${uri}index.html;` * 最後使用 [Cloudflare](https://www.cloudflare.com/) 設定 `pastleo.me` [DNS](https://zh.wikipedia.org/wiki/%E5%9F%9F%E5%90%8D%E7%B3%BB%E7%BB%9F) 並啟用 [CDN](https://zh.wikipedia.org/wiki/%E5%85%A7%E5%AE%B9%E5%82%B3%E9%81%9E%E7%B6%B2%E8%B7%AF) ## To be continued: markdown 部落格 本篇介紹到專案架構、工作流程的設定,也聊了一下 Next.js 以及週邊工具筆者的使用心得;上面提到 `pastleo.me` 的主要功能是部落格,這部份等到下篇再來聊聊,大概會是 Next.js 頁面資料取得以及筆者在 `pastleo.me` 實做的 markdown 文章讀取、列出、顯示功能