無聊聊Blog

使用 GitHub Pages 架設靜態 Single Page Application 網頁

2023-01-30
使用 GitHub Pages 架設靜態 Single Page Application 網頁

Github Pages 讓我們可以快速的發布靜態網頁,如果是 Vue, React 這種 SPA (Single Page Application) 要如何使用 Github Pages 發布,以及需要注意的地方?

專案結構

我們要 deploy 的內容是 build 完的靜態檔案,而不是 source code。 因此通常會將 source code 放在 mastermain 或其他 branch,然後將 GitHub Pages 設定在 gh-pages 分支。

github-pages-spa-1

這樣我們只要將 build 好的檔案丟到 gh-pages 分支就可以囉。

gh-pages

你可能會想:每次發布都要手動 build 、把檔案搬到 gh-pages branch 、並且 push 到 github,也太麻煩了吧!

幸好有一個 npm package gh-pages 可以使用,幫助你進行相關操作。 https://www.npmjs.com/package/gh-pages#command-line-utility

依照文件說明,你可以使用 npm script,例如:

"scripts": {
  "deploy": "gh-pages -d dist"
}

運行 npm run deploy 就可以在 build 完之後,將打包的資料夾(上例為 /dist) checkin 到 gh-pages branch 並且上傳 (git push)。

也可以結合 build 一起使用

"scripts": {
  "deploy": "npm run build && gh-pages -d dist"
}

不過這樣每次要發布時都需要下一次指令, 再更進階一點的話,可以搭配 CI/CD 工具,例如 Github Actions 來達到每次 push 某個 branch 就自動幫你執行 deploy 的指令!

靜態檔案路徑

如果沒有特別去設定,上述步驟完成後瀏覽 Github Pages 網頁,可能會發生 js, css 檔案找不到的問題。 因為通常 SPA 打包通常預設 host 在網站根目錄,假設打包出的檔案長這樣:

dist_folder/
|-- index.html
|-- index.js
|-- index.css

index.html 會去引用 /index.js/index.css

但是在 GitHub Pages host 的網址會是 https://<user>.github.io/<repo_name>/ 引用 /index.js 會索引到 https://<user>.github.io/index.js => 404 NOT FOUND。 正確的網址要加上 sub path 才對: https://<user>.github.io/<repo_name>/index.js

所以我們需要設定打包檔案引用的路徑為 /<repo_name>/,或者使用完整的網址 https://<user>.github.io/<repo_name>/

每個框架/工具設定方式不同,這邊舉幾個例子:

Vue Cli

vue-cli 創建的 Vue2 專案可以在 vue.config.js 裡面設定 publicPath

// vue.config.js
module.exports = {
  publicPath:
    process.env.NODE_ENV === "production"
      ? "/<repo_name>"
      : "/",
};

Create-React-App

可以參考 官方文件,在 package.json 設定 homepage

HtmlWebpackPlugin

如果是自己用 webpack 打包,使用 HtmlWebpackPlugin,可以設定 option.publicPath (文件):

前端 Router Basename

和上面的問題發生原因一樣是路徑問題,前端 routing 的 path 無法正確處理。

這裡用 react-router-dom 官網的說明來看就很清楚了 需要設定 basename

The base URL for all locations. If your app is served from a sub-directory on your server, you’ll want to set this to the sub-directory. A properly formatted basename should have a leading slash, but no trailing slash.

<BrowserRouter basename="/calendar">
    <Link to="/today"/> // renders <a href="/calendar/today">
    <Link to="/tomorrow"/> // renders <a href="/calendar/tomorrow">
    ...
</BrowserRouter>

今天部署到 GitHub Pages 上是在 sub-directory: /<repo_name>,因此需要正確設定。

可以加上環境變數來區分本地開發和 production 版本

Create-React-App + react-router-dom

const basename = process.env.REACT_APP_BASENAME; // 可以設定在 .env 相關檔案

ReactDOM.render(
  <BrowserRouter basename={basename}>
    <App />
  </BrowserRouter>,
  document.getElementById("root"),
);

vue-router

export default new Router({
  mode: "history",
  base: process.env.BASE_URL, // 見 註1
  // ...
});

註 1: Vue 的官網說明 環境變數

BASE_URL - 会和 vue.config.js 中的 publicPath 选项相符,即你的应用会部署到的基础路径。

Fallback to index.html

假設你的 SPA 有兩個 route /, /about,而且你的前端 routing 是使用 history mode

到了這一步,你打開網頁,已經可以正常運作了,點擊連結也可以藉由前端 routing 渲染訪問 /about

但是如果你直接訪問 /about 或是在非首頁重新整理,那麼頁面又會 404 NOT FOUND 了 因為 https://<user>.github.io/<repo_name>/ 會返回 index.html 但是 https://<user>.github.io/<repo_name>/about 並沒有 about.html 這個檔案。

這是為什麼 SPA 部署都需要做 fallback 也就是所有找不到資源的路徑,都要回傳 index.html,然後再交由前端 router 來決定要渲染甚麼內容。

通常部署使用 nginx/Apache 等 web server 就是在上面設定 (可以參考 create-react-app 文件)

那 GitHub Pages 要怎麼做 fallback 就比較 tricky,

spa-github-pages

詳細方法在這個 repository

https://github.com/rafgraph/spa-github-pages

簡單來說

  1. 當 Github Pages 找不到資源,會返回 404.html
  2. 你可以使用自己的 404.html,導轉到 index.html 並帶上一些參數 (訪問的路徑、 query string 等等)
  3. index.html 使用這些參數來替換正確的網址,渲染前端頁面。

以上完成後,就解決 SPA 在 GitHub Pages 上的問題了!

總結

雖然是用 GitHub Pages 來示範,但其實以上的大方向都是 SPA Deploy 要注意、解決的問題,只是細節上實作不同而已。 主要就是

  • 靜態檔案引用的相對路徑
  • 前端 Router 的 Basename
  • web server 要做 fallback

不過最近發現了更厲害可以用來放前端專案的服務,那就是 Vercel,之後有空會再介紹。

https://vercel.com/home