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 变量用于加密会话 cookie。它应该是一个长而随机的字符串。

GitHub 设置

注意 该项目使用 GitHub Actions。如果您不熟悉 GitHub Actions,可以在 此处阅读更多相关信息。

您需要执行一些操作以确保 GitHub Actions 可以与您的项目一起使用。

工作流权限

首先,转到 https://github.com/meza/trance-stack/settings/actions,然后在 工作流权限 部分下,确保它处于 读取和写入权限 选项。

如果没有此项,部署脚本将无法创建必要的 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 环境用于特性分支和拉取请求,并在临时工作流程中引用。

变量与密钥

一些配置值是敏感的,而另一些则不是。例如,COOKIEYES_TOKEN 不是敏感的,但 AUTH0_CLIENT_SECRET 是。这主要是因为其中一些值将被嵌入到应用程序的 HTML 中,并且对所有人可见。

警告 请仔细检查服务的文档,以确保您正确设置它们。

如果您将密钥添加为变量或将变量添加为密钥,应用程序将无法正常工作。

GitHub 令牌 - 首先执行此操作!

为了使发布正常工作,您需要创建个人访问令牌。它需要以下设置

  • 过期时间:永不过期
  • 范围

创建令牌后,转到密钥设置并将其添加为 GH_TOKEN

持续部署设置

部署过程在部署部分中描述,但要开始,请创建在环境变量部分中定义的环境变量和密钥。

使用 Auth0 进行身份验证

我们使用 Auth0 进行身份验证。您需要创建一个 Auth0 帐户并设置一个应用程序

创建新应用程序时,请确保设置以下设置

  1. 应用程序类型应为 Regular Web Applications(常规 Web 应用程序)
  2. 忽略快速入门部分
  3. 转到“设置”并复制 Domain(域)、Client ID(客户端 ID)和 Client Secret(客户端密钥),并将其粘贴到 .env 文件中
  4. 将“令牌端点身份验证方法”设置为 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.com 用于 Staging 环境,将 prod-123456.eu.auth0.com 用于 Production 环境。

但为了简单起见,您只需在主要的 Actions 密钥页面中设置一次相同的值,它将用于所有环境。

为特性分支/PR 部署启用 Auth0 集成

如果要为特性分支/PR 部署启用 Auth0 集成,您需要执行一些额外的步骤。由于特性分支/PR 部署是临时的,它们每次部署都将具有不同的域名。这意味着您需要将域名添加到 Allowed Callback URLs(允许的回调 URL)和 Allowed Logout URLs(允许的注销 URL)中

为了使此过程轻松进行,我们可以在域名中使用 * 通配符。这将允许使用任何域名。

在上面的初始设置中,您在几个地方添加了 https://127.0.0.1:3000。您需要在相同的地方添加 ,https://*.execute-api.us-east-1.amazonaws.com。(请注意开头的逗号。域名需要用逗号分隔)

注意 您需要将 us-east-1 部分替换为您正在使用的区域。

例如,“允许的回调 URL”部分应如下所示

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 对于操作设置为变量

从应用程序中删除 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 ... /> 组件及其导入。
  5. 运行 vitest --run --update 来更新快照。

Hotjar 集成

我们使用 Hotjar 进行热图和用户录制。您需要创建一个 Hotjar 帐户并设置一个新站点。

设置好站点后,前往 https://insights.hotjar.com/site/list 并复制站点的 ID,并将 HOTJAR_ID 变量设置在 .env 文件中。

您还需要转到变量设置,并添加与 .env 文件中相同的变量名称。

警告 HOTJAR_ID 对于操作设置为变量

从应用程序中删除 Hotjar 集成

  1. .env 文件和 GitHub 变量中删除 HOTJAR_ID 变量。
  2. 删除 src/components/Hotjar 目录。
  3. src/types/global.d.ts 文件中的 appConfig 类型中删除相关类型。
  4. src/root.tsx 文件中删除 <Hotjar ... /> 组件及其导入。
  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 ... /> 组件及其导入。
  5. 运行 vitest --run --update 来更新快照。
  6. package.json 文件中删除 posthog 依赖项。
  7. 按照编译和测试错误,删除所有使用 posthog 依赖项的代码。

Renovate 机器人设置

我们使用 Renovate 来管理依赖项更新。要利用它,您需要安装 Renovate GitHub App

首先,导航到 https://github.com/apps/renovate 并单击“安装”按钮。

Renovate GitHub App install button

在下一个屏幕上,我们建议选择“所有仓库”以简化操作,但您可以将其配置为仅在您当前所在的仓库上工作。

Select which repositories to use Renovate on

Sentry 集成

注意 由于与 Architect 的兼容性问题,Sentry 的服务器端检测目前不起作用。请关注 此问题以获取更新。相关代码在 entry.server.tsx 文件中注释掉。

我们使用 Sentry 进行错误报告。您需要创建一个 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 文件包含您需要了解其工作原理的所有信息。

自动化语义版本控制

我们使用 约定式提交 来自动确定软件包的下一个版本。它使用 semantic-release 包来自动化版本控制和发布过程。

该功能由 .releaserc.json 文件控制。由于从此堆栈创建的项目很可能不是 npm 库,因此配置中不包含 npm 发布插件。

要有效使用约定式提交,您需要了解以下基本原则

您的提交消息决定是否将新部署发布到生产环境。

触发构建的消息是

  • fix: ... - 修复了一个错误
  • feat: ... - 添加了一个新功能

不触发新版本(因此不触发构建)的消息是

  • docs: ... - 对文档的更改
  • chore: ... - 对构建过程或辅助工具和库(如文档生成)的更改
  • refactor: ... - 既不修复错误也不添加功能的代码更改
  • style: ... - 不影响代码含义的更改(空格、格式、缺少分号等)
  • test: ... - 添加缺失的测试或更正现有测试
  • ci: ... - 对 CI 配置文件和脚本的更改
  • perf: ... - 提高性能的代码更改

使用语义版本控制的分支策略

我们将在部署部分讨论部署的工作原理。现在,让我们看看分支策略如何与版本控制一起工作。

有 3 个主要分支

  • main - 这是主分支。它是部署到生产环境的分支。
  • beta - 这是部署到 beta(预发布)环境的分支。
  • alpha - 这是部署到 alpha(预发布)环境的分支。

当您推送到 main 分支时,会发布一个新版本到生产环境。该版本由提交消息确定,并且每个推送到 main 分支的提交都会触发一个新版本。

当您推送到 alphabeta 分支时,会创建一个新的预发布版本。这允许您迭代即将发布的功能,而不必担心每次推送引入新功能或修复的提交时都会增加版本号。

例如,如果您的生产环境中的版本是 1.0.0,并且您向 alpha 分支推送了一个提交,则版本将为 1.1.0-alpha.0。如果您向 alpha 分支推送另一个提交,则版本将为 1.1.0-alpha.1,依此类推。

当您将来自 alphabeta 分支的拉取请求合并到 main 分支时,这些分支中的所有更改将被收集并捆绑到一个单独的版本中。为了遵循上面的示例,如果您的生产环境中的版本是 1.0.0,并且合并了 alpha 分支及其 1.1.0-alpha.1 版本,则您在生产环境上新创建的版本将为 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 来检查提交消息。配置在 package.json 文件中。代码检查会在您每次提交时发生。如果提交消息不符合约定式提交格式,则提交将失败。

代码检查本身由 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 部分

运行时依赖项是我们用来运行应用程序的库。这也意味着安全性和错误修复对于这些依赖项很重要。

我们希望尽快更新这些依赖项,因此我们有以下配置

  • 次要版本和补丁版本 - 创建一个以 fix: 前缀开头的提交消息的拉取请求,并在可能的情况下自动合并
  • 主要版本 - 创建一个以 fix: 前缀开头的提交消息的拉取请求,并且不要自动合并

开发依赖项

开发依赖项是 package.json 文件中的 devDependencies 部分

开发依赖项是我们用来开发应用程序的库。这意味着当我们更新这些依赖项时,我们不需要发布应用程序的新版本。

我们仍然希望尽快更新这些依赖项,因此我们有以下配置

  • 次要版本和补丁版本 - 创建一个以 chore: 前缀开头的提交消息的拉取请求,并在可能的情况下自动合并
  • 主要版本 - 创建一个以 chore: 前缀开头的提交消息的拉取请求,并且不要自动合并

部署

此堆栈的主要关注点之一是创建一个部署策略,该策略对于任何从此堆栈构建的人来说都是一个很好的起点。

我们结合使用 GitHub ActionsAWS CDK 将应用程序部署到类生产环境和临时环境。

临时环境

临时环境是按需创建并在不再需要时销毁的环境。我们将它们用于功能分支和拉取请求。

它们会自动为拉取请求创建,但如果您只想部署功能分支,则必须手动触发一个。

手动临时部署

导航至 https://github.com/meza/trance-stack/actions/workflows/ephemeralDeploy.yml 并点击 “Run workflow” 按钮。

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 云开发工具包 (CDK) 是一个开源软件开发框架,用于以代码定义云基础设施并通过 AWS CloudFormation 部署它。

注意 如果您有兴趣了解我们为什么选择 CDK,请查看 相关的 ADR

大部分基础设施都在 deployment 目录中定义。 deployment/lib 目录包含用于构建基础设施的自定义 构造

环境变量

为了部署应用程序,您需要设置以下环境变量

变量 密钥 描述
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/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/拉取请求。

它比实际需要的要复杂一些的原因是我们不想每次部署同一分支时都发布 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 分支打开拉取请求时,测试将在 GitHub Actions 上运行。

本地运行 Playwright

您可以通过运行以下命令在本地运行测试

npm run int

报告将进入 reports/e2e 目录。

Storybook

我们使用带有 Webpack 5 的 Storybook V7。Remix 在 Storybook 支持方面仍然有点落后,因此我们必须做一些事情才能使其正常工作。

警告 Storybook 7 对 Storybook 的工作方式进行了一些根本性的更改。强烈建议您阅读迁移指南,了解发生了哪些变化。您习惯的事情可能不再以相同的方式工作。

在 Remix 社区中,关于如何最好地解决此问题,有一个正在进行的讨论

此代码尚未包含 remixStub,但它可能很快会更改。

如果您知道如何正确配置它,请打开一个 PR。

运行 Storybook

您可以通过运行以下命令来运行 Storybook

npm run storybook

如果您正在寻找有关如何组织 stories 的灵感,可以查看 Telekom Scale 项目

发布 Storybook

还记得我们在开始时设置的 页面吗?

当您推送到 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

这里已经做出了许多深思熟虑的决定,因此让我们来详细了解一下。

Globals: true

全局变量默认关闭,但要使 js-dom 与 vitest 一起使用,必须启用它们。

测试报告器

我们根据环境使用不同的报告器。在 CI 环境中,我们输出 junitcobertura 报告,然后将其发布到 GitHub Actions Summary 或作为 Pull Request 注释。在您的本地计算机上,我们使用 html 报告器进行覆盖率测试,并使用默认的文本报告器进行测试结果。

在这两种情况下,我们还会打印出覆盖率报告的文本表示形式。

所有测试报告都进入 reports 目录。

设置文件

如果您仔细观察,您会发现我们有一个 setupFiles 部分,该部分调用 vitest.setup.ts 文件。此文件负责设置测试环境。它安装 @testing-library/jest-dom 包,并设置一个通用的 afterEach hook,以便在测试后进行清理。

这可能不符合每个人的喜好,所以请随意更改。请记住,如果您删除了全局的 afterEach 钩子,您将需要自己清理测试后的状态,因此请务必运行 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 标志。


堆栈本身的开发


注意

关于锁定文件的说明。

由于这是一个“创建”包,因此不包含锁定文件。这是为了确保在创建新项目时使用最新版本的依赖项。