TRANCE STACK Storybook

警告 這個技術棧目前僅支援 TypeScript 和 NPM。

NPM 的要求來自於 GitHub Actions 腳本。我很快會讓它支援 pnpm 和 yarn,但這需要更多時間,在此之前我希望得到有關此技術棧的回饋。

包含哪些內容

這是一個 Remix 技術棧,提供了一種發布可立即用於生產環境的 Remix 應用程式的*方法*。它以一個有主見的方式建構,旨在作為您自己的 Remix 專案的起點。您可以根據自己的喜好修改它,並將其用作您自己的 Remix 專案的基礎。

📦 點擊以查看包含的技術列表

使用此技術棧

使用此技術棧建立您的專案

npx create-remix@latest --template meza/trance-stack my-app

設定過程會要求您輸入 GitHub 儲存庫名稱。如果您沒有儲存庫,別擔心,您可以在設定過程後建立。

警告

從現在開始,請在您自己的專案目錄中閱讀此文件。它將包含與您相關的連結,因為初始化腳本會將此 README 中的連結替換為針對您的專案自訂的連結。

現在啟動開發伺服器

npm run dev

這已為您設定了一個預設的 Remix 應用程式。在您完成設定過程之前,它不會正常運作。您可以在這裡找到相關說明


快速開始

  1. 安裝依賴套件
npm install
  1. 啟動開發伺服器
npm run dev
  1. 請閱讀開始使用章節,以設定本地和部署環境

值得注意的 npm 指令稿

  • npm run ci - 執行與 CI 中相同的驗證指令稿
  • npm run clean - 移除所有產生的檔案
  • npm run clean:all - 移除所有產生的檔案和所有 node_modules 目錄
  • npm run dev - 啟動開發伺服器
  • npm run deploy:dev - 將應用程式部署到臨時環境
  • npm run deploy:prod - 將應用程式部署到正式環境 (您可能永遠不應該在本地使用此指令)
  • npm run int - 執行 Playwright 整合測試
  • npm run report - 執行所有為您產生報告的動作 (覆蓋率、cpd、loc 等)
  • npm run storybook - 啟動 Storybook 伺服器
  • npm run validate - 執行 CI 測試和整合測試

目錄

開始使用

為了使此專案正常運作,您需要先設定一些東西。

此技術棧的設計方式使其可以相對簡單地移除您不需要的部分。您可以在每個步驟找到移除說明,因此如果您不喜歡特定服務,請不用擔心。

但是... 為什麼?

注意 我們在整個專案開發過程中使用了架構決策記錄,因此如果您想知道我們為什麼選擇特定的服務或實作方式,您可以查看 ADR 頁面以獲取更多資訊。

我們強烈建議您繼續新增自己的決策。這是記錄專案歷史背景的好方法,也是與團隊其他成員分享知識的好方法。

我們使用 adr-tools 來管理我們的 ADR。它已作為依賴套件的一部分安裝,因此您應該可以立即使用它。

環境

檢查專案根目錄是否有 .env 檔案。如果沒有,請將 .env.example 檔案複製到 .env

cp .env.example .env

此檔案包含您需要設定的所有變數,以使專案按原樣運作。

APP_DOMAIN 通常應該保持不變。它是您的應用程式將從中提供服務的網域。此變數也會由部署指令稿設定,因此您無需擔心。在本地開發期間,它將設定為 https://127.0.0.1:3000

NODE_ENV 變數用於判斷您正在哪個環境中執行應用程式。ARC 似乎很難自行判斷,因此我們已將其設定為手動設定。如果一切順利,則不需要很長時間。

SESSION_SECRET 變數用於加密 Session Cookie。它應該是一個長而隨機的字串。

GitHub 設定

注意 此專案使用 GitHub Actions。如果您不熟悉 GitHub Actions,可以在這裡閱讀更多相關資訊。

您需要執行一些操作,以確保 GitHub Actions 可以與您的專案一起運作。

工作流程權限

首先,前往https://github.com/meza/trance-stack/settings/actions,在 Workflow permissions 部分下,請確保它已設定為 Read and write permissions 選項。

如果沒有此權限,部署指令稿將無法建立必要的 GitHub 發布版本。

分支保護

接下來,前往https://github.com/meza/trance-stack/settings/branches並新增一些分支保護規則。

  • main
  • alpha
  • beta

這些是將用於應用程式不同階段的分支。您可以根據自己的喜好設定這些分支的設定,但有一個設定需要確保取消勾選:Allow deletions

Branch Protection

我們稍後將在部署章節中使用此設定,以防止已命名的環境被刪除。

頁面

接下來,前往 https://github.com/meza/trance-stack/settings/pages,並確認 Source 設定為 GitHub Actions。這將允許我們將專案的 Storybook 部署到 GitHub Pages。

環境

注意 我們使用 GitHub 環境來管理應用程式的不同階段。您可以在這裡閱讀更多相關資訊。

GitHub 環境非常適合用來控制您的工作流程中使用的環境變數。

目前,請前往 https://github.com/meza/trance-stack/settings/environments 並建立以下環境

  • 正式環境
  • Staging(預備環境)
  • Ephemeral(臨時環境)

這些環境在 部署工作流程中,例如使用 environment 鍵值來引用。Ephemeral 環境用於功能分支和 Pull Request,並在 臨時工作流程中引用。

變數 vs. 機密

有些設定值是敏感的,而有些則不是。例如,COOKIEYES_TOKEN 不敏感,但 AUTH0_CLIENT_SECRET 是。這主要來自於一些值會被嵌入到應用程式的 HTML 中,並對所有人可見。

警告 請仔細檢查服務的文件,以確保您正確設定它們。

如果您將機密設定為變數,或將變數設定為機密,應用程式將無法正常運作。

GitHub Token - 先做這件事!

為了讓發布正常運作,您需要建立個人存取權杖。它需要以下設定

  • Expiration(到期時間):never(永不)
  • Scopes(權限範圍)

建立權杖後,前往機密設定,並將其以 GH_TOKEN 加入。

持續部署設定

部署流程在 部署 章節中描述,但為了讓您開始,請建立 環境變數 章節中定義的環境變數和機密。

使用 Auth0 進行身份驗證

我們使用 Auth0 進行身份驗證。您需要建立一個 Auth0 帳戶並設定一個應用程式

在建立新應用程式時,請確保設定以下設定

  1. 應用程式類型應為 Regular Web Applications (常規 Web 應用程式)
  2. 忽略快速入門部分
  3. 前往 Settings (設定) 並複製 Domain (網域)、Client ID (客戶端 ID) 和 Client Secret (客戶端密鑰) 並將它們貼到 .env 檔案中
  4. 將 Token Endpoint Authentication Method (權杖端點身份驗證方法) 設定為 Post
  5. 前往 Allowed Callback URLs (允許的回調 URL) 部分,並加入 https://127.0.0.1:3000/auth/callback
  6. 前往 Allowed Logout URLs (允許的登出 URL) 部分,並加入 https://127.0.0.1:3000
  7. 前往 Allowed Web Origins (允許的 Web 來源) 部分,並加入 https://127.0.0.1:3000
  8. 前往 Allowed Origins (CORS) (允許的來源 (CORS)) 部分,並加入 https://127.0.0.1:3000
  9. 前往 Refresh Token Rotation (刷新權杖輪換) 部分並啟用它,同時您還必須啟用 Absolute Expiration (絕對到期) 選項。

將 Auth0 變數新增至 GitHub

現在您有了 Auth0 變數,您需要將它們新增至您上面建立的 GitHub 環境中。

前往機密設定,並使用與 .env 檔案中變數相同的名稱新增 Auth0 機密。

如果您願意,可以為每個環境設定自訂值。例如,您可以將 AUTH0_DOMAIN 設定為 dev-123456.eu.auth0.comStaging 環境,而將 prod-123456.eu.auth0.comProduction 環境。

但為了簡單起見,您只需在主要的 Actions 機密頁面上設定相同的值一次,它將用於所有環境。

為功能分支/PR 部署啟用 Auth0 整合

如果您想為功能分支/PR 部署啟用 Auth0 整合,您需要執行一些額外的步驟。由於功能分支/PR 部署是臨時性的,它們每次部署都會有不同的網域名稱。這表示您需要將網域名稱新增至 Allowed Callback URLsAllowed Logout URLs 中。

為了讓這個過程無痛,我們可以在網域名稱中使用 * 通配符。這將允許使用任何網域名稱。

在上述初始設定期間,您在幾個地方新增了 https://127.0.0.1:3000。您需要將 ,https://*.execute-api.us-east-1.amazonaws.com 加入相同的地方。(注意開頭的逗號。網域需要用逗號分隔)

注意 您需要將 us-east-1 部分替換為您正在使用的區域。

例如,Allowed Callback URLs 部分應如下所示

https://127.0.0.1:3000/auth/callback,https://*.execute-api.us-east-1.amazonaws.com/auth/callback

警告

* 通配符將允許您使用您想要的任何網域名稱。然而,這樣做的代價是安全性。我們強烈建議您在 Auth0 上為您的功能分支/PR 部署建立一個替代租戶。

從應用程式中移除 Auth0 整合

  1. .env 檔案和 GitHub 機密中刪除 AUTH0_DOMAINAUTH0_CLIENT_IDAUTH0_CLIENT_SECRET 變數。
  2. 刪除 src/auth.server.tssrc/auth.server.test.ts 檔案。
  3. package.json 檔案中刪除 auth0-remix-server 相依性。
  4. 按照編譯和測試錯誤來移除所有使用 auth0-remix-server 相依性的程式碼。

Google Analytics 4 整合

我們使用 Google Analytics v4 進行分析。您需要建立一個 Google Analytics 帳戶並設定一個資源

當您完成資源設定後,您需要複製資料串流的 Measurement ID (評估 ID),並將 GOOGLE_ANALYTICS_ID 變數貼到 .env 檔案中。

您還必須前往 變數設定,並新增與 .env 檔案中相同的變數名稱。

警告 GOOGLE_ANALYTICS_ID 是為 actions 設定為變數

從應用程式中移除 Google Analytics 4 整合

  1. .env 檔案和 GitHub 變數中刪除 GOOGLE_ANALYTICS_ID 變數。
  2. 刪除 src/components/GoogleAnalytics 目錄。
  3. src/types/global.d.ts 檔案中的 appConfig 類型中刪除相關類型。
  4. src/root.tsx 檔案中刪除 <GoogleAnalytics ... /> 元件及其 import。
  5. 執行 vitest --run --update 來更新快照。

Hotjar 整合

我們使用 Hotjar 進行熱圖和使用者錄製。您需要建立一個 Hotjar 帳戶並設定一個新網站。

當您設定好網站後,前往 https://insights.hotjar.com/site/list 並複製您網站的 ID,並將 HOTJAR_ID 變數貼到 .env 檔案中。

您還必須前往 變數設定,並新增與 .env 檔案中相同的變數名稱。

警告 HOTJAR_ID 是為 actions 設定為變數

從應用程式中移除 Hotjar 整合

  1. .env 檔案和 GitHub 變數中刪除 HOTJAR_ID 變數。
  2. 刪除 src/components/Hotjar 目錄。
  3. src/types/global.d.ts 檔案中的 appConfig 類型中刪除相關類型。
  4. src/root.tsx 檔案中刪除 <Hotjar ... /> 元件及其 import。
  5. 執行 vitest --run --update 來更新快照。

PostHog 整合

我們使用 PostHog 進行分析。您需要建立一個 PostHog 帳戶並設定一個新專案。

當您設定好專案後,前往 https://posthog.com/project/settings 並複製您專案的 API 金鑰,並將 POSTHOG_TOKEN 變數貼到 .env 檔案中。您還需要根據您的資料駐留偏好將 POSTHOG_API 變數設定為 https://eu.posthog.comhttps://posthog.com

您還必須前往 變數設定,並新增與 .env 檔案中相同的變數名稱。

區分不同環境

在 PostHog 中,您的主要單位稱為「組織」。一個組織可以擁有多個「專案」,這些專案本質上是環境。例如,您可以擁有一個 production 專案和一個 staging 專案。

這允許您為每個環境擁有不同的功能標誌、使用者和資料。請隨意為每個環境建立一個新專案,然後設定適當的環境變數。

從應用程式中移除 PostHog 整合

  1. .env 檔案和 GitHub 變數中刪除 POSTHOG_TOKENPOSTHOG_API 變數。
  2. 刪除 src/components/Posthog 目錄。
  3. src/types/global.d.ts 檔案中的 appConfig 類型中刪除相關類型。
  4. src/root.tsx 檔案中刪除 <Posthog ... /> 元件及其 import。
  5. 執行 vitest --run --update 來更新快照。
  6. package.json 檔案中刪除 posthog 相依性。
  7. 按照編譯和測試錯誤來移除所有使用 posthog 相依性的程式碼。

Renovate 機器人設定

我們使用 Renovate 來管理相依性更新。若要使用它,您需要安裝 Renovate GitHub App

首先,前往 https://github.com/apps/renovate 並點擊 Install (安裝) 按鈕。

Renovate GitHub App install button

在下一個畫面中,我們建議您選擇「All repositories」(所有儲存庫) 以方便使用,但您可以將其設定為僅在您目前所在的儲存庫中運作。

Select which repositories to use Renovate on

Sentry 整合

注意:由於與 Architect 的相容性問題,Sentry 的伺服器端儀器目前無法運作。請密切關注 此問題 以取得更新。相關程式碼已在 entry.server.tsx 檔案中註解。

我們使用 Sentry 進行錯誤報告。您需要向他們建立一個帳號並設定一個新專案。

當您設定好專案後,請前往專案設定並複製 DSN,並將其貼到 .env 檔案中設定 SENTRY_DSN 變數。

您還必須前往 變數設定,並新增與 .env 檔案中相同的變數名稱。

接下來,前往 https://sentry.io/settings/account/api/auth-tokens/ 並建立一個新的權杖。您需要 project:releasesproject:read 權限。

取得權杖後,前往 密鑰設定 並新增

  • SENTRY_AUTH_TOKEN - 您剛建立的權杖
  • SENTRY_ORG - 組織 slug
  • SENTRY_PROJECT - 專案 slug

我們將使用這些來將原始碼對應檔傳送到 Sentry,以便將錯誤正確對應到原始碼。

部署腳本會自動將原始碼對應檔上傳到 Sentry,然後在本機將其移除,這樣它們就不會上傳到環境中。

如何找到 DSN

首先,前往專案設定

Sentry Settings Icon

然後在側邊欄中,點擊 Client Keys (DSN)

Sentry Client Keys Icon

最後,複製 DSN

從應用程式中移除 Sentry 整合

  1. .env 檔案和 GitHub 變數中刪除 SENTRY_DSN 變數。
  2. 執行 npm remove @sentry/* 以移除所有 Sentry 套件。
  3. appConfig 中移除 sentryDsn,並從 src/types/global.d.ts 檔案中的 ProcessEnv 類型中移除 SENTRY_DSN
  4. src/root.tsx 檔案的最底部,將 withSentry(App) 取代為 App
  5. src/entry.client.tsxsrc/entry.server.tsx 檔案中移除 Sentry.init 呼叫。
  6. 依照編譯和測試錯誤來移除所有使用 Sentry 的程式碼。
  7. 開啟 .github/workflows/deploy.yml.github/workflows/ephemeralDeply.yml 檔案,並移除 Sentry Sourcemaps 步驟。

如何使用...?

本節將深入探討堆疊中存在的概念。

身分驗證

身分驗證是透過 auth0-remix-server 套件完成的。該套件中的 README 檔案包含您需要了解其運作方式的所有資訊。

自動語義版本控制

我們使用 Conventional Commits 來自動判斷套件的下一個版本。它使用 semantic-release 套件來自動化版本控制和發布流程。

此功能由 .releaserc.json 檔案控制。由於從此堆疊建立的專案很可能不是 npm 程式庫,因此設定中未包含 npm 發布外掛程式。

若要有效使用 Conventional Commits,您需要了解以下基本原則

您的 commit 訊息決定是否將新的部署發布到生產環境。

觸發建置的訊息為

  • fix: ... - 修正錯誤
  • feat: ... - 新增新功能

不會觸發新版本(因此不會建置)的訊息為

  • docs: ... - 變更文件
  • chore: ... - 變更建置流程或輔助工具和程式庫,例如文件產生
  • refactor: ... - 既不修正錯誤也不新增功能的程式碼變更
  • style: ... - 不影響程式碼意義的變更(空白、格式、缺少分號等)
  • test: ... - 新增遺失的測試或修正現有測試
  • ci: ... - 變更 CI 設定檔和腳本
  • perf: ... - 改善效能的程式碼變更

使用語義版本控制的分支策略

我們將在 部署 章節中討論部署如何運作。現在,我們先來看看分支策略如何與版本控制搭配運作。

有 3 個主要分支

  • main - 這是主要分支。這是部署到生產環境的分支。
  • beta - 這是部署到 beta(預備)環境的分支。
  • alpha - 這是部署到 alpha(預備)環境的分支。

當您推送至 main 分支時,會將新版本發布到生產環境。版本由 commit 訊息決定,而推送至 main 分支的每個 commit 都會觸發新版本。

當您推送至 alphabeta 分支時,會建立新的 預先發布 版本。這可讓您反覆處理即將發布的功能,而無需擔心每次推送引入新功能或修正的 commit 時都會增加版本號碼。

例如,如果您在生產環境中有 1.0.0 版本,並且將 commit 推送至 alpha 分支,則版本將為 1.1.0-alpha.0。如果您將另一個 commit 推送至 alpha 分支,則版本將為 1.1.0-alpha.1,依此類推。

當您將來自 alphabeta 分支的提取請求合併至 main 分支時,這些分支中的所有變更都會被收集並捆綁到單一版本中。若要遵循上述範例,如果您在生產環境中有 1.0.0 版本,並合併具有 1.1.0-alpha.1 版本的 alpha 分支,則您在生產環境中新建立的版本將為 1.1.0

---
title: Branching & Versioning
---
%%{title: '', init: {'theme': 'base', 'gitGraph': {'rotateCommitLabel': true}} }%%
gitGraph
    commit id: "v1.0.0"
    branch feature order: 2
    branch alpha order: 1
    checkout feature
    commit id: "fix: x"
    commit id: "fix: y"
    checkout alpha
    merge feature id: "v1.0.1-alpha.1"
    checkout feature
    commit id: "fix: z"
    checkout alpha
    merge feature id: "v1.0.1-alpha.2"
    checkout feature
    commit id: "feat: added something cool"
    commit id: "fix: fixed a mistake"
    commit id: "refactor: refactored the tests"
    checkout alpha
    merge feature id: "v1.1.0-alpha.1"
    checkout main
    merge alpha id: "v1.1.0"

程式碼檢查

我們使用 commitlint 來檢查 commit 訊息。設定位於 package.json 檔案中。每當您進行 commit 時都會進行程式碼檢查。如果 commit 訊息不符合 Conventional Commits 格式,commit 將會失敗。

程式碼檢查本身是由 lefthook 觸發的

我執行的是哪個版本?

應用程式的版本會傳送到 <html data-version="..."> 屬性中。您可以使用此屬性來判斷任何給定環境中正在執行的應用程式版本。

我們已經建置了一個自訂的 Cookie 同意解決方案,該解決方案與安全的 XSS 保護實務以及歐盟 Cookie 法相容。

注意:您可以在 Cookie 同意 ADR 中閱讀更多相關資訊

該解決方案位於 src/components/CookieConsent 資料夾中,它旨在根據您的需求進行修改。

當您開啟 _index.tsx 檔案時,您可以看到以下介面

interface ConsentData {
  analytics?: boolean | undefined;
  //add your own if you need more
  // marketing?: boolean | undefined;
  // tracking?: boolean | undefined;
}

interface CookieConsentContextProps {
  analytics?: boolean | undefined;
  setAnalytics: (enabled: boolean) => void;
  //add your own if you need more
  // marketing?: boolean | undefined;
  // setMarketing: (enabled: boolean) => void;
  // tracking?: boolean | undefined;
  // setTracking: (enabled: boolean) => void;
}

您需要修改這些介面,以便新增您的特定 Cookie 類型。例如,如果您想要新增 marketing Cookie,您需要新增以下內容

interface ConsentData {
  analytics?: boolean | undefined;
  marketing?: boolean | undefined;
}

interface CookieConsentContextProps {
  analytics?: boolean | undefined;
  setAnalytics: (enabled: boolean) => void;
  marketing?: boolean | undefined;
  setMarketing: (enabled: boolean) => void;
}

為了遵守 Cookie 同意,您需要識別專案中新增特定 Cookie 類型的元素。

此堆疊中的一個好範例是 GoogleAnalytics 元件。它位於 src/components/GoogleAnalytics

Cookie 同意提供者會在 root.tsx 檔案中使用,因此所有元件都可以使用。若要使用它,您只需要

const { analytics } = useContext(CookieConsentContext);

if (analytics) {
  //add your analytics code here
}

相依性版本更新

我們使用 Renovate 來自動更新相依性。設定位於 .github/renovate.json 檔案中。

依預設,它已設定為根據一些基本規則更新相依性

執行階段相依性

執行階段相依性是 package.json 檔案中的 dependencies 區段

執行階段相依性是我們用來執行應用程式的程式庫。這也表示安全性與錯誤修正對於這些相依性非常重要。

我們希望盡快更新這些相依性,因此我們有以下設定

  • 次要版本和修補程式版本 - 建立一個在 commit 訊息中具有 fix: 前置詞的提取請求,並在可能的情況下自動合併
  • 主要版本 - 建立一個在 commit 訊息中具有 fix: 前置詞的提取請求,並且「不」自動合併

開發相依性

開發相依性是 package.json 檔案中的 devDependencies 區段

開發相依性是我們用來開發應用程式的程式庫。這表示當我們更新這些相依性時,我們不需要發布應用程式的新版本。

我們仍然希望盡快更新這些相依性,因此我們有以下設定

  • 次要版本和修補程式版本 - 建立一個在 commit 訊息中具有 chore: 前置詞的提取請求,並在可能的情況下自動合併
  • 主要版本 - 建立一個在 commit 訊息中具有 chore: 前置詞的提取請求,並且「不」自動合併

部署

此堆疊的主要重點之一是建立一個部署策略,這個策略對任何從此堆疊建立專案的人來說都是一個很好的起點。

我們結合使用 GitHub ActionsAWS CDK,將應用程式部署到類似生產環境和臨時環境。

臨時環境

臨時環境是根據需求建立並在不再需要時銷毀的環境。我們將這些用於功能分支和提取請求。

它們會為提取請求自動建立,但如果您只是想要部署功能分支,則必須手動觸發一個。

手動臨時部署

導覽至 https://github.com/meza/trance-stack/actions/workflows/ephemeralDeploy.yml,然後按一下「執行工作流程」按鈕。

Run workflow button

選擇分支後,它將開始建置應用程式並將其部署到臨時環境。

當流程完成時,它會在執行的摘要儀表板上發布摘要,並提供已部署應用程式的連結。它看起來會像這樣

Run workflow summary

提取請求臨時部署

當您建立提取請求時,GitHub Actions 會自動為您建立臨時環境,並且部署連結將以註解的形式新增到提取請求中。

類似生產環境

類似生產環境是一次建立,然後在應用程式更新時更新的環境。

main 分支被視為生產環境分支,而 alphabeta 則被視為預備環境。

這是由 deploy.yml 檔案決定的。

  build:
    environment: ${{ github.ref_name == 'main' && 'Production' || 'Staging' }}

此處的 ProductionStaging 字詞直接參考我們已設定的 GitHub 環境

警告 這表示 alphabeta 分支都會被部署到 Staging 環境。

這樣做是為了堆疊的方便性,但我們強烈建議您根據自身需求更改此設定。或許可以新增一個獨立的 alpha 環境?

注意 請記住,GitHub 環境會保存用於該特定工作流程的環境變數。這表示您可以為每個環境設定不同的 APP_URL,以及其他設定,例如獨立的 Auth0 租戶。

GitHub Actions

GitHub Actions 會回應儲存庫生命週期中的各種事件。下圖顯示了部署流程。

flowchart TD
    F1 -.->|Manual Trigger| F

    subgraph Push
        A[Push] --> D{Is Protected Branch?}

        D -->|Yes| H{Is it the 'main' branch?}
        D -->|No| F1[Offer Manual Ephemeral Deployment]
        H -->|Yes| I1{{Deploy to Production}}
        H -->|No| I2{{Deploy to Staging}}

        I1 --> J1[Create GitHub Release]
        H -->|Yes| J2[Deploy Storybook]
        I2 --> J1
    end
    subgraph Pull Request
        B[Pull Request] --> F{{Ephemeral Deployment}}
    end

    subgraph Cleanup
        C[Delete Branch] --> X{{Destroy Deployment Stack}}
    end

六邊形節點是由 CDK 執行的流程,而其他節點則由 GitHub Actions 處理。

CDK

AWS Cloud Development Kit (CDK) 是一個開源軟體開發框架,用於以程式碼定義雲端基礎架構,並透過 AWS CloudFormation 佈建。

注意 如果您想了解我們為什麼選擇 CDK,請查看相關的 ADR

大多數基礎架構都定義在 deployment 目錄中。 deployment/lib 目錄包含用於建構基礎架構的自訂 Constructs

環境變數

為了部署應用程式,您需要設定以下環境變數

變數 密鑰 描述
AWS_ACCESS_KEY_ID 用於部署應用程式的 AWS 存取金鑰 ID。
AWS_CERT_ARN 用於網域的憑證 ARN。
AWS_SECRET_ACCESS_KEY 用於部署應用程式的 AWS 秘密存取金鑰。
AWS_DOMAIN_NAME 應用程式的最終網域名稱。
AWS_HOSTED_ZONE_NAME Route53 中託管區域的名稱。

如果您是從文件頂部來到這裡,請回到您之前的位置,然後從那裡繼續。

本機環境

如果您想在本機部署應用程式,則只需要設定 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY 環境變數即可。

deployment 目錄

deployment/stacks 目錄包含實際部署到 AWS 的堆疊。它們的命名應該是不言自明的。我們有一個用於 Ephemeral 環境,另一個用於 Production 環境。

如果您檢查任一部署檔案,您會注意到部署基本上是一個單一命令

 npx cdk deploy remix-trance-stack-ephemeral -O /tmp/deployment.result.json \
 --require-approval never \
 --context environmentName=${{ env.REF_NAME }} \
 --context domainName=${{ vars.AWS_DOMAIN_NAME }} \
 --context certificateArn=${{ secrets.AWS_CERT_ARN }} \
 --context hostedZoneName=${{ vars.AWS_HOSTED_ZONE_NAME }}

暫時性和生產部署之間的差異在於堆疊的名稱。它可以是 remix-trance-stack-ephemeralremix-trance-stack-production

上下文變數

上下文變數用於將資訊傳遞到 CDK 堆疊。

變數 描述 範例
environmentName 環境的名稱。這用於建立在 AWS 上建立的每個單一資源的名稱。 feature1
domainName 應用程式的網域名稱。 trance-stack.vsbmeza.com
certificateArn 用於應用程式的憑證 ARN。 arn:aws:acm:region:123456789012:certificate/12345678-1234-1234-1234-123456789012
hostedZoneName 用於應用程式的託管區域的名稱。 vsbmeza.com

domainNamecertificateArnhostedZoneName 僅用於生產環境部署。

注意 儘管某些上下文變數僅用於生產環境部署,但它們仍會傳遞到暫時性部署。這是因為 CDK 堆疊對於兩個環境都是相同的,而上下文變數的評估是在執行階段完成的。對於暫時性部署,您可以為 domainNamecertificateArnhostedZoneName 使用空字串。

從本機部署

我們建議您使用 GitHub Actions 來部署應用程式。但是,如果您想從本機部署,您可以執行與部署腳本相同的命令來進行部署。

警告 別忘了在部署之前執行 npm run build

您可以在命令列上定義上下文變數,也可以使用 cdk.context.json 檔案。

{
  "environmentName": "localdev",
  "domainName": "trance-stack.example.com",
  "hostedZoneName": "example.com",
  "certificateArn": "arn:aws:acm:region:123456789012:certificate/12345678-1234-1234-1234-123456789012"
}
githubActionSupport.ts 檔案

讓我們來談談 githubActionSupport.ts 檔案。

此檔案使用 GitHub Actions 工具組,讓我們可以使用部署 URL 回報給 GitHub Actions/Pull Request。

它之所以比它需要來得複雜一些,是因為我們不想每次部署相同分支時都發布 PR 評論。由於 URL 對於已部署的分支不會變更,因此沒有必要濫發 PR。

這產生了尋找現有部署評論並更新它而不是建立新評論的挑戰。

在本機測試 GitHub 支援

如果因為任何原因您想要取得 GitHub Actions 支援的本機輸出,您可以執行以下命令來執行

npx ts-node --prefer-ts-exts deployment/githubActionSupport.ts /tmp/deployment.result.json

這需要您在 /tmp 目錄中擁有 deployment.result.json 檔案。您可以執行本機部署命令來取得此檔案。

結果將會新增至 deploymentSummary.md 檔案中。

環境變數

環境變數可能是維護此專案時最大的痛點。您必須將它們新增至 GitHub、新增至部署腳本並新增至 .env 檔案。

我們正在努力解決這個問題,但現在,您必須手動執行。

新增環境變數檢查清單

將變數新增至...

  • .env 檔案
  • .env.example 腳本。這非常重要
  • .github/workflows/deploy.yml 腳本中的 npm run build 命令
  • .github/workflows/ephemeralDeploy.yml 腳本中的 npm run build 命令
  • .github/workflows/ephemeralDestroy.yml 腳本中的 npm run build 命令
  • .github/workflows/playwright.yml 腳本中的 Create Envfile 區段

綁定環境變數

我們將大多數環境變數綁定到伺服器綁定中。要了解原因,請閱讀相關的 adr,以及它的附錄

重要的事情是要知道綁定內容是由讀取 .env.example 檔案並擷取其索引鍵來決定的。

您可以透過將某些索引鍵新增至 remix.config.js 檔案中的拒絕清單,來防止綁定這些索引鍵。

  const doNotBundleEnv = [
  'APP_DOMAIN' // deny list for the environmentPlugin
]

功能旗標

功能旗標是在生產環境中測試新功能的絕佳方式,而不必擔心破壞任何東西。它使您能夠將新程式碼的發布與新功能的發布分離。閱讀更多

讓我們來看一下 src/routes/_index.tsx 檔案中的範例

export const loader: LoaderFunction = async ({ request, context }) => {
  const isAuth = await hasFeature(request, Features.AUTH);
  return json({
    isHelloEnabled: await hasFeature(request, Features.HELLO),
    isAuthEnabled: isAuth
  });
};

export default () => {
  const { isHelloEnabled, isAuthEnabled } = useLoaderData<typeof loader>();
  if (isHelloEnabled) {
    return (<div>
      <Hello/>
      {isAuthEnabled ? <Login/> : null}
    </div>);
  }
  return <div>Goodbye World!</div>;
};

這裡頁面的所有元素都包裝在功能旗標中。只有在啟用 HELLO 功能時,才會呈現 Hello 元件。只有在啟用 AUTH 功能時,才會呈現 Login 元件。

區分環境

在 PostHog 中,您的主要單位稱為「組織」。一個組織可以擁有多個「專案」,這些專案本質上是環境。例如,您可以擁有一個 production 專案和一個 staging 專案。

這允許您為每個環境擁有不同的功能標誌、使用者和資料。請隨意為每個環境建立一個新專案,然後設定適當的環境變數。

I18N - 國際化

我們正在使用 i18next 進行國際化。您可以在 i18next 文件中閱讀更多相關資訊。為了將其與 Remix 整合,我們正在使用 remix-i18next 套件,而我們的設定是基於 remix-i18next Readme 檔案。

您可以在 src/i18n 目錄中找到 i18n 設定。 i18n.config.ts 檔案包含 i18next 預設值的設定。 i18n.server.ts 檔案包含伺服器端的設定,而 i18n.client.ts 檔案包含用戶端端的設定。

我們與 remix-i18next 範例設定的唯一偏差是,我們實際上正在將翻譯綁定到伺服器套件中。這是在 src/i18n/i18n.server.ts 檔案中完成的。

await i18nextInstance.init({
  debug: process.env.I18N_DEBUG === 'true',
  ...baseConfig,
  lng: locale,
  ns: remixI18next.getRouteNamespaces(remixContext),
  // The sample setup in remix-i18next
  //backend: {
  //  loadPath: resolve("./public/locales/{{lng}}/{{ns}}.json"),
  //},
  resources: {
    en: {
      translation: en
    }
  }
});

我們這樣做的原因是因為在 AWS Lambda 環境中,我們有一個單一檔案作為處理常式,而且它需要是獨立的。雖然傳統的 lambda 函數可以存取附加的檔案系統,但這會使部署更加複雜,而且該函數將變得與 Lambda@Edge 解決方案不相容。

因此,我們沒有使用 fs-backend,而是直接從 public/locales 目錄匯入資源。

這表示當您新增新的地區設定時,您必須將其新增至 i18n.server.ts 檔案中的資源。

使用翻譯

若要將翻譯用於您的應用程式中,您可以使用 react-i18next 套件中的 useTranslation hook。

import { useTranslation } from 'react-i18next';

export const Hello = () => {
  const { t } = useTranslation();
  return (
    <h1 data-testid={'greeting'} className={'hello'}>{t('microcopy.helloWorld')}</h1>
  );
};

您也可以將變數傳遞至翻譯中。這有助於翻譯人員建立更符合內容情境的翻譯。

以下範例取自應用程式初始登入後的儀表板

export default () => {
  const { t } = useTranslation();
  const { user } = useLoaderData<typeof loader>();
  return (<>
    <div>{t('dashboard.for', { name: user.nickname || user.givenName || user.name })}<br/><Logout/></div>
  </>);
};

這裡我們將 name 變數傳遞給翻譯。這表示名稱在最終文字中出現的位置在不同語言中可能會有所不同。例如,在某個情境下,我們可能會說「John 的儀表板!」,而在另一個情境下,我們可能會說「儀表板,為 John 而設!」。

在我們的儀表板案例中,翻譯檔案看起來像這樣

{
  "dashboard": {
    "for": "Dashboard for {{ name }}"
  }
}

新增語言環境

若要新增語言環境,您需要執行以下操作

  1. 將新的語言環境新增至 public/locales 資料夾。請參考現有語言環境的範例
  2. 將新的語言環境新增至 i18n.server.ts 檔案中的 resources 物件。
  3. 將新的語言環境新增至 i18n.config.ts 檔案中的 supportedLngs 陣列。

從專案中移除 i18n

如果您不想使用 i18n,可以從專案中移除它。您需要執行以下操作

  1. src 目錄中移除 i18n 資料夾
  2. public 目錄中移除 locales 資料夾
  3. 執行 npm remove i18next i18next* *i18next
  4. src/entry.server.tsxsrc/entry.client.tsx 檔案中移除 <<I18nextProvider ...>
  5. 依照編譯錯誤訊息,移除任何剩餘的 i18n 引用

注意

關於組織翻譯,您可以在 i18n Readme 檔案中找到一些很棒的提示。

Lefthook

提交驗證和自動依賴項安裝是由 Lefthook 完成的

設定檔位於 .lefthook.yml。您可以看到所有發生的命令以及它們所附加的 git hooks。

如果每次提交都執行所有測試太多,您可以隨時將其設定為在 pre-push 時發生。

NPMIgnore - 自動化

如果您想要將專案發佈到 NPM(您不應該這麼做),可以使用 npmignore 套件來自動產生一個 .npmignore 檔案。此檔案將根據 .gitignore 檔案產生。

package.json 檔案的 publishConfig 區段中,有一個基本的忽略設定。

Playwright - 端對端測試

我們使用 Playwright 來進行端對端測試。Playwright 是 Cypress 和 Puppeteer 的後繼者。它由 Microsoft 維護,是一個跨瀏覽器的測試工具。它也比 Cypress 快很多。

在此了解更多關於 Playwright 的資訊。

安裝 Playwright 依賴項

Playwright 需要安裝一些依賴項才能在本機執行。您可以透過執行以下命令來安裝它們

npx playwright install --with-deps

設定 Playwright

測試位於 playwright/e2e 目錄中。您可以自由更改目錄結構。如果您這樣做,請不要忘記更新 playwright.config.ts 檔案中的測試位置。

export default defineConfig({
  testDir: './playwright/e2e', // <-- Update this

您不需要在執行測試之前啟動開發伺服器。

Playwright 會為您啟動開發伺服器。它設定在 playwright.config.ts 檔案的最底部

  /* Run your local dev server before starting the tests */
webServer: {
  command: 'npm run dev',
    url
:
  'https://127.0.0.1:3000',
    timeout
:
  1 * 60 * 1000,
    reuseExistingServer
:
  !process.env.CI
}

執行測試

GitHub Actions 上的 Playwright

每次您向 main 分支開啟 pull request 時,測試都會在 GitHub Actions 上執行。

本機上的 Playwright

您可以透過執行以下命令在本機執行測試

npm run int

報告將會輸出到 reports/e2e 目錄。

Storybook

我們使用 Storybook V7 搭配 Webpack 5。Remix 在 Storybook 支援方面仍然有點落後,因此我們需要做一些事情才能讓它運作。

警告 Storybook 7 對 Storybook 的運作方式帶來了一些根本性的改變。強烈建議您閱讀遷移指南,看看有哪些變更。您習慣的事情可能不再以相同的方式運作。

在 Remix 社群中,有一個關於如何最好地解決這個問題的持續討論

此程式碼尚未包含 remixStub,但它可能會很快改變。

如果您知道如何正確設定它,請開啟 PR。

執行 Storybook

您可以透過執行以下命令來執行 Storybook

npm run storybook

如果您正在尋找如何組織您的 stories 的靈感,您可以查看 Telekom Scale 專案

發佈 Storybook

還記得我們在開始時設定的Pages嗎?

當您推送到 main 分支時,Storybook 會自動發佈到 GitHub Pages。

這是透過 .github/workflows/storybook.yml 工作流程完成的。

存取已發佈的 Storybook

在此 README 的最上方,您可以看到一個連結到已發佈 Storybook 的徽章。

樣式 / CSS

我們在這個專案中使用常規樣式表,這意味著結合了共用元件樣式呈現樣式

共用元件樣式

共用元件樣式位於 src/styles 目錄中。它們會被匯入使用它們的路由中。

// src/root.tsx
import styles from './styles/app.css';

export const links: LinksFunction = () => {
  return [
    { rel: 'stylesheet', href: styles }
  ];
};

整個應用程式通用的樣式會從 src/root.tsx 檔案載入,而特定於單一路線的樣式則會從路線本身載入。

這些都是累加的,因此您可以透過 root.tsx 在每個路由上載入單一樣式表,然後在特定路由上載入其他樣式表。

如果您需要特定於元件的樣式表,可以使用呈現樣式方法。

呈現樣式

若要讓每個元件有本機樣式,我們使用呈現樣式

因為這些不是路由,因此不與 URL 區段相關聯,所以 Remix 不知道何時預先擷取、載入或卸載樣式。我們需要將連結「呈現」到使用元件的路由。

此解決方案有點複雜,但它允許我們只在載入元件時才載入樣式。

Hello 元件為例

import { useTranslation } from 'react-i18next';
import styles from './hello.css';

export const links = () => [
  { rel: 'stylesheet', href: styles }
];

export const Hello = () => {
  const { t } = useTranslation();
  return (
    <h1 data-testid={'greeting'} className={'hello'}>{t('microcopy.helloWorld')}</h1>
  );
};

export default Hello;

請注意,它會匯入 hello.css 檔案。此檔案與元件位於相同的目錄中。它也有一個 links 匯出,會傳回樣式表連結。

但在 Remix 術語中,元件不是路由,因此我們需要將連結「呈現」到使用元件的路由。您可以在 src/routes/_index.tsx 檔案中看到此範例

import { Hello, links as helloLinks } from '~/components/Hello';

export const links: LinksFunction = () => ([
  ...helloLinks()
]);

我們從 Hello 元件匯入 links 匯出,並將其新增至 _index.tsx 路由的 links 匯出。

是的,這比應該的更複雜,但隨著 Remix 的快速發展,我們希望這會在未來得到簡化。

PostCSS

我們使用 PostCSS 來處理 CSS。Remix 有一個內建的 PostCSS 外掛程式,可讓您將 CSS 檔案直接匯入您的元件中。深入了解 Remix 中的 CSS 如何運作。

我們的 PostCSS 設定位於 postcss.config.js 檔案中,並且每次 Remix 建置應用程式時都會套用。這表示您無需考慮前綴或其他瀏覽器特定的 CSS 功能。只需編寫您的 CSS,PostCSS 會自動處理其餘的事情。

Typescript 路徑

我們使用 Typescript 路徑。這表示我們可以不用在匯入中使用雜亂的相對路徑,而是使用方便的別名。

我們預設定義了以下路徑

  • ~ - src 資料夾
  • @styles - src/styles 資料夾
  • @test - test 資料夾

這表示無論您在檔案樹中的哪個位置,您都可以始終使用 ~ 別名來參考 src 資料夾。

import Hello from '~/components/Hello';
import appStyles from '@styles/app.css';
import { renderWithi18n } from '@test';

您可以在 tsconfig.json 檔案中自由新增自己的路徑。

您可能想要新增的常見路徑有

  • @components - src/components 資料夾
  • @routes - src/routes 資料夾
  • @hooks - src/hooks 資料夾

我們選擇不新增這些,因為 ~/hooks@hooks 沒有太大差異,不需要額外的設定。

Typescript 路徑的問題

不幸的是,typescript 路徑有點深奧,而且在各個工具上的支援可能不穩定。

Vitest

例如,Vitest 需要特殊的設定才能處理它。您可以在 vitest.config.ts 檔案中找到設定。它既需要 vite-tsconfig-paths 外掛程式,在某些情況下,您還需要手動將路徑新增至 resolve.alias 陣列。

// vite.config.ts
resolve: {
  alias: {
    '~'
  :
    path.resolve(__dirname, './src')
  }
}
Storybook

也需要告知 Storybook 要尊重 typescript 路徑。我們使用 tsconfig-paths-webpack-plugin 來告知 storybook webpack 設定要尊重路徑。

我們將其新增至 .storybook/main.ts 檔案中的 webpackFinal 函式。

webpackFinal: async config => {
  config.plugins?.push(new DefinePlugin({
    __DEV__: process.env.NODE_ENV !== 'production'
  }));
  if (config.resolve) {
    config.resolve.plugins = config.resolve.plugins || [];
    config.resolve.plugins.push(new TsconfigPathsPlugin()); // <--- this line
  }
  return config;
}

單元測試

我們使用 Vitest 作為單元測試框架。如果您不熟悉 Vitest,請不要擔心,它的介面與 Jest 非常相似,您將可以輕鬆上手。

Vitest 的主要設定檔位於 vitest.config.ts

這裡做了一些經過深思熟慮的決策,所以讓我們一一檢視。

全域變數:true

全域變數預設是關閉的,但為了讓 js-dom 能與 vitest 搭配運作,需要將其開啟。

測試報告器

我們會根據環境使用不同的報告器。在 CI 環境中,我們會輸出 junitcobertura 報告,然後將其發佈到 GitHub Actions 摘要或作為 Pull Request 的評論。在您的本機上,我們會使用 html 報告器來產生覆蓋率報告,並使用預設的文字報告器來顯示測試結果。

在這兩種情況下,我們也會印出覆蓋率報告的文字表示。

所有的測試報告都會存放在 reports 目錄中。

設定檔案

如果您仔細觀察,您會發現我們有一個 setupFiles 區段,它會呼叫 vitest.setup.ts 檔案。這個檔案負責設定測試環境。它會安裝 @testing-library/jest-dom 套件,並設定一個通用的 afterEach hook,以便在測試後進行清理。

這可能不是每個人都喜歡的方式,所以您可以隨意更改。請記住,如果您移除全域的 afterEach hook,您將需要自行在測試後進行清理,因此請務必執行 npm run ci 並查看哪些地方出錯。

由於 Remix 依賴於瀏覽器 API,例如 fetch,這些 API 在 Node.js 中並非原生可用,因此當使用某些工具執行時,您可能會發現單元測試在沒有這些全域變數的情況下會失敗。

如果您需要新增更多全域變數,您可以在 vitest.setup.ts 檔案中進行。

只需新增

import { installGlobals } from '@remix-run/node';

// This installs globals such as "fetch", "Response", "Request" and "Headers".
installGlobals();

您可以在這裡閱讀更多相關資訊;

執行緒

雖然執行緒的承諾聽起來很吸引人,但開啟它們會大幅降低 vitest 的速度。這是一個已知問題,我們正在等待它被修復。

覆蓋率

這個堆疊提供了 100%+ 的覆蓋率,以涵蓋邊緣案例。我們知道這不是每個人的偏好,因此如果您想,您可以從 coverage 設定物件中移除 statementsbrancheslinesfunctions 區段。

或者,您可以修改 package.json 檔案中的 report 指令稿,以移除 --coverage 標誌。


堆疊本身的開發


注意

關於鎖定檔的說明。

由於這是一個 "create" 套件,因此不包含鎖定檔。這是為了確保在建立新專案時使用最新版本的相依性。