TRANCE 堆栈 Storybook

警告 此堆栈目前仅支持 typescript 和 NPM。

GitHub 操作脚本需要 NPM。我很快就会支持 pnpm 和 yarn,但这需要一些时间。在此之前,我希望能收到您对此堆栈的反馈。

包含的内容

这是一个 Remix 堆栈,提供了一种方式来发布生产就绪型的 Remix 应用程序。它的构建方式带有自己的观点,旨在用作 Remix 项目的起点。您可以根据自己的喜好对其进行修改,并将其用作 Remix 项目的基础。

📦 点击查看包含的技术列表

使用堆栈

使用堆栈创建您的项目

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

设置过程将询问您 GitHub 存储库名称。如果您没有,别担心,您可以在设置过程结束后创建它。

警告

从现在起 在您自己的项目目录中阅读此文档。它将包含与您相关的链接,因为 init 脚本将用自定义到您项目的链接替换此自述文件中的链接。

现在启动开发服务器

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

这些是将用于应用程序的不同阶段的分支。你可以根据自己的喜好设置这些分支的设置,但有一个设置你需要确保未选中:允许删除

Branch Protection

我们在后面的 部署 部分使用它来防止命名环境被删除。

Pages

接下来,转到 https://github.com/meza/trance-stack/settings/pages 并确保 来源 设置为 GitHub Actions。这将允许我们将项目的 storybook 部署到 GitHub Pages。

环境

注意我们使用 GitHub 环境来管理应用程序的不同阶段。你可以阅读更多相关信息 这里

GitHub 环境非常适合控制工作流中使用的环境变量。

现在,转到 https://github.com/meza/trance-stack/settings/environments 并创建以下环境

  • 生产
  • Staging
  • Ephemeral

这些在 部署工作流 中被引用,例如使用 environment 键。Ephemeral 环境用于功能分支和拉取请求,并在 临时工作流 中被引用。

变量与 Secrets

一些配置值是敏感的,而另一些则不是。例如,COOKIEYES_TOKEN 不是敏感的,但 AUTH0_CLIENT_SECRET 是。这主要是因为这些值中的一些会被嵌入到应用程序的 html 中,并对每个人可见。

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

如果你将一个 Secret 作为变量添加,或将一个变量作为 Secret 添加,应用程序将无法正常工作。

GitHub Token - 首先这样做!

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

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

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

持续部署设置

部署过程在 部署 部分有描述,但为了让你开始,请创建 环境变量 部分中定义的环境变量和 Secrets。

使用 Auth0 进行身份验证

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

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

  1. 应用程序类型应为 常规 Web 应用程序
  2. 忽略快速入门部分
  3. 转到设置,复制 域名客户端 ID客户端 Secret,并将其粘贴到 .env 文件中
  4. 将令牌端点身份验证方法设置为 Post
  5. 转到 允许的回调 URL 部分,添加 https://127.0.0.1:3000/auth/callback
  6. 转到 允许的注销 URL 部分,添加 https://127.0.0.1:3000
  7. 转到 允许的 Web 来源 部分,添加 https://127.0.0.1:3000
  8. 转到 允许的来源 (CORS) 部分,添加 https://127.0.0.1:3000
  9. 转到 刷新令牌轮换 部分,启用它,并启用 绝对到期时间 选项。

将 Auth0 变量添加到 GitHub

现在你有了 Auth0 变量,你需要将它们添加到上面创建的 GitHub 环境中。

转到 secrets 设置,并使用与 .env 文件中的变量相同的名称添加 Auth0 Secrets。

如果你愿意,可以为每个环境设置自定义值。例如,你可以将 AUTH0_DOMAIN 设置为 dev-123456.eu.auth0.com 用于 Staging 环境,并将 prod-123456.eu.auth0.com 设置为 Production 环境。

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

启用 Auth0 集成以进行功能分支/PR 部署

如果你想为功能分支/PR 部署启用 Auth0 集成,你需要执行一些额外的步骤。由于功能分支/PR 部署是临时的,因此每次部署它们时,它们都会有不同的域名。这意味着你需要将域名添加到 允许的回调 URL允许的注销 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 Secrets 中删除 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 帐户并 设置一个属性

完成属性设置后,你需要复制数据流的 测量 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 ... /> 组件及其导入。
  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 进行分析。你需要创建一个帐户并设置一个新项目。

项目设置完成后,前往 https://posthog.com/project/settings 复制项目的 API 密钥,并在 .env 文件中设置 POSTHOG_TOKEN 变量。你也要设置 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 应用.

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

Renovate GitHub App install button

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

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 - 组织标识
  • SENTRY_PROJECT - 项目标识

我们将使用这些令牌将源映射发送到 Sentry,以便错误正确映射到源代码。

部署脚本将自动将源映射上传到 Sentry,然后将其从本地删除,因此它们不会上传到环境中。

如何查找 DSN

首先,前往项目设置

Sentry Settings Icon

然后在侧边栏中点击 客户端密钥 (DSN)

Sentry Client Keys Icon

最后,复制 DSN

从应用程序中删除 Sentry 集成

  1. .env 文件和 GitHub 变量中删除 SENTRY_DSN 变量。
  2. 运行 npm remove @sentry/* 删除所有 sentry 包。
  3. src/types/global.d.ts 文件的 appConfig 中删除 sentryDsn,从 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 发布插件未包含在配置中。

要有效地使用常规提交,你需要了解以下基本原则

你的提交消息决定了是否向生产环境进行新部署。

触发构建的消息是

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

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

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

带有语义版本控制的分支策略

我们将在 部署 部分讨论部署是如何工作的。现在,让我们看一下分支策略如何与版本控制配合使用。

有 3 个主要分支

  • main - 这是主分支。它是部署到生产环境的分支。
  • 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 button

选择分支后,它将开始构建应用程序并将其部署到临时环境。

完成后,它将在运行的摘要仪表盘上发布一个摘要,其中包含部署应用程序的链接。它将看起来像这样。

Run workflow summary

拉取请求临时部署

创建拉取请求时,GitHub Actions 将自动为您创建一个临时环境,并且部署链接将作为评论添加到拉取请求中。

类似生产的环境

类似生产的环境是在创建一次后,在应用程序更新时更新的环境。

分支 main 被认为是生产分支,而 alphabeta 被认为是预发布阶段。

这在 deploy.yml 文件中决定。

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

这里的“生产”和“预发布”直接引用我们配置的 GitHub 环境

警告 这意味着 alphabeta 分支都将部署到 预发布 环境。

这样做是为了堆栈的方便,但强烈建议您根据需要进行更改。也许添加一个单独的 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 的堆栈。它们的命名应该是自解释的。我们有一个用于 临时 环境,一个用于 生产 环境。

如果您检查任一部署文件,您会注意到部署基本上是一个单一命令。

 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-ephemeral 或者 remix-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 脚本到“创建 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 功能时才会渲染。Login 组件只有在启用 AUTH 功能时才会渲染。

区分环境

在 PostHog 中,你的主要单元称为组织。一个组织可以有多个“项目”,这些项目本质上是环境。例如,你可以拥有一个 production 项目和一个 staging 项目。

这使你可以为每个环境拥有不同的功能标志、用户和数据。随时为每个环境创建一个新项目,然后设置相应的环境变量。

I18N - 国际化

我们使用 i18next 进行国际化。您可以在 i18next 文档 中了解更多信息。为了将其与 Remix 集成,我们使用 remix-i18next 包,我们的设置基于 remix-i18next 自述文件。

您可以在 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 钩子。

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 自述文件 中有一些关于组织翻译的绝佳技巧。

Lefthook

提交验证和自动依赖安装由 Lefthook 完成

配置文件位于 .lefthook.yml 中。您可以查看所有发生的命令以及它们附加到的 git 钩子。

如果在每次提交时运行所有测试过于繁琐,您可以将其设置为在 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

我们使用 Storybook V7 与 Webpack 5。Remix 在 Storybook 支持方面还略有落后,因此我们不得不做一些事情才能使其正常工作。

警告 Storybook 7 对 Storybook 的工作方式做出了一些根本性的改变。强烈建议您阅读 迁移指南 以了解发生了哪些变化。您习惯使用的东西可能不再以相同的方式工作。

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

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

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

运行 Storybook

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

npm run storybook

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

发布 Storybook

还记得我们之前设置 页面 吗?

当您推送到 main 分支时,Storybook 会自动发布到 GitHub Pages。

这是通过 .github/workflows/storybook.yml 工作流完成的。

访问已发布的 Storybook

在本自述文件的最顶部,您可以看到一个指向已发布的 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 配置要尊重这些路径。

我们将其添加到 `webpackFinal` 函数中,该函数位于 `.storybook/main.ts` 文件中。

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 环境中,我们输出 `junit` 和 `cobertura` 报告,然后将这些报告发布到 GitHub Actions 摘要或拉取请求评论中。在你的本地机器上,我们使用 `html` 报告器来进行覆盖率分析,并使用默认的文本报告器来显示测试结果。

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

所有测试报告都位于 `reports` 目录中。

设置文件

如果你仔细观察,你会发现我们有一个 `setupFiles` 部分,它调用 `vitest.setup.ts` 文件。这个文件负责为测试设置环境。它安装 `@testing-library/jest-dom` 包,并设置一个通用的 `afterEach` 钩子来清理测试后的环境。

这可能不是每个人的喜好,所以请随意更改它。请记住,如果你删除全局 `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%+ 的覆盖率,以覆盖边缘情况。我们知道这并非每个人的喜好,因此,如果你想删除 `statements`、`branches`、`lines` 和 `functions` 部分,可以从 `coverage` 配置对象中移除它们。

或者,你也可以修改 `package.json` 文件中的 `report` 脚本,以删除 `--coverage` 标志。


代码库本身的开发


注意

关于锁定文件的说明。

由于这是一个 "create" 包,所以没有包含锁定文件。这是为了确保在创建新项目时使用依赖项的最新版本。