paint-brush
TypeScript 中的单一存储库:我们如何打破一切并使其变得更好的故事经过@devfamily
1,788 讀數
1,788 讀數

TypeScript 中的单一存储库:我们如何打破一切并使其变得更好的故事

经过 dev.family13m2023/05/05
Read on Terminal Reader

太長; 讀書

dev.family 从事一个有趣的项目已经将近六个月,并且仍在继续。我们将这个项目作为一个加密忠诚度计划启动,为最终用户提供某些行为的奖励,并且客户会收到对这些相同用户的分析。一切都是用一种语言编写的——TypeScript。我们有 8 个存储库,其中一些应该相互通信。
featured image - TypeScript 中的单一存储库:我们如何打破一切并使其变得更好的故事
dev.family HackerNoon profile picture
0-item
1-item
2-item

大家好,dev.family 有联系。我们想告诉您一个有趣的项目,我们已经进行了将近六个月并且仍在继续。在这段时间里,发生了很多事情,发生了很多变化。我们为自己发现了一些有趣的东西,设法填补了障碍。


我们的故事会怎样?

  • 我们做了什么
  • 我们如何开始
  • 这把我们引向何方
  • 我们遇到了什么问题
  • 为什么选择单体仓库
  • 为什么选择 PNPM
  • 为什么选择 TS 现在如何运作
  • 我们让生活变得更轻松了多少

关于该项目的一些信息

那么,我们还在努力做什么?事实上,这个问题在某个时候变得非常相关,例如,曾经是麦当劳公司的所有者。我们将这个项目作为一个加密忠诚度计划启动,为最终用户提供某些行为的奖励,并且客户会收到对这些相同用户的分析。是的,这很肤浅,但没关系。


开始工作

有必要开发用于连接 Shopify 商店的 Shopify 模块、品牌门户、Google Chrome 的扩展、移动应用程序 + 带有数据库的服务器(好吧,实际上,没有它们无处不在)。总的来说,根据我们的需要,我们决定并开始工作。由于该项目立即被认为是巨大的,每个人都明白它可以像延迟行动的魔豆一样成长。



决定“正确”和“按照所有标准”做所有事情。也就是说,一切都是用一种语言编写的——TypeScript。这样每个人都以相同的方式编写,并且文件,linters(很多 linters)没有不必要的更改,以便一切都“容易”重用,将一切都放入单独的模块中,这样他们就不会窃取Github 访问令牌。

所以我们开始:

  • linters 和 ts config 的存储库分开(样式指南)

  • 移动应用程序 (react native) 和 Chrome 扩展 (react.js) 的存储库(一起,因为它们重复相同的功能,只针对不同的用户)

  • 门户的另一个存储库

  • Shopify 模块的两个存储库

  • 区块链内容存储库 API 存储库 (express.js) 基础设施存储库


我们当时的存储库示例


嗯......我想我列出了一切。结果有点太多了,但是好吧,让我们继续前进。哦对了,为什么要为 Shopify 模块分配两个存储库?因为第一个存储库是 UI 模块。我们的婴儿及其环境充满了美丽。第二个是集成-Shopify。这实际上是它在 Shopify 本身中的所有流动文件的实现。我们总共有 8 个存储库,其中一些应该相互通信。


由于我们谈论的是 TypeScript 开发,因此我们还需要包管理器来安装模块和库。但是我们都在我们的存储库中独立工作,使用什么对任何人都没有关系。例如,在 React Native 上开发移动应用程序时,我没有考虑太久,保留了 YARN1。有些人可能更习惯使用旧的 NPM,而其他人则喜欢一切新事物并使用新鲜的 YARN3。因此,某处有 NPM,某处有 YARN1,某处有 YARN3。


所以我们都开始制作我们的应用程序。乐趣几乎立刻就开始了,但还没有那么完整。首先,有些人没有想过 TypeScript 是干什么用的,懒惰的地方,或者“不明白”写不出来的地方,就用“Any”。有人没有意识到 TypeScript 的所有功能以及在某些地方一切都可以变得更容易的事实。因此,类型来自宇宙维度。是的,我忘了说,我们决定使用 Hasura GraphQL 作为数据库。手动输入所有答案有时看起来像别的东西。在一种情况下,有些人甚至用优秀的旧 Javascript 编写。是的,事实证明情况很酷:第一个人再次输入“Any”以免过度紧张,第二个人用自己的双手写了类型的画布,而第三个人仍然根本不写类型。



后来发现,在我们重复逻辑的情况下,并且以一种好的方式,它应该被放在一个单独的包中——没有人会这样做。每个人都为自己和其他一切编写和编写代码——从高高的钟楼吐出来。

这将我们引向何方?

我们有什么?我们有 8 个不同应用程序的存储库。有些人到处都需要,有些人则相互交流。因此,我们都创建 .NPMrc 文件,规定学分,创建一个 github 令牌,然后通过包管理器模块。总的来说有点麻烦,虽然不愉快,但没有什么不寻常的。


只有在更新包中的东西的情况下,你需要升级它的版本,然后上传它,然后在你的应用程序/模块中更新它,然后你才会看到发生了什么变化。但这完全不合适!特别是如果你可以改变某处的颜色。另外,有些代码是重复的,并没有被复用,只是悄悄地重写了一遍。如果我们谈论的是移动应用程序和浏览器扩展,则 redux 存储和所有 API 工作都在那里完全重复,只是完全重写或稍作修改。


总的来说,我们剩下的是:一堆存储库,相当长的应用程序/模块启动,同一个人写的很多相同的东西,大量时间花在测试和引入新人到项目上,以及以上所引起的其他问题。



简而言之,这导致我们执行了很长时间的任务。当然,这导致错过了截止日期,很难为项目引入新人,这再次影响了开发速度。在某些情况下,一切都将变得非常沉闷和漫长,这要归功于 webpack。


然后很明显,我们正在远离我们努力的地方,但谁知道在哪里。在分析了所有错误之后,我们做出了一些决定,现在将进行讨论。

为什么是单体仓库?

也许,对未来影响最大的最重要的事情是意识到我们不是在构建一个特定的应用程序,而是一个平台。我们有多种类型的用户,他们有不同的应用程序,但他们在同一个平台上运行。所以我们立即关闭了大量存储库的问题:如果我们在一个平台上工作,为什么在一个平台上更容易工作时将它分成多个存储库。


我想说的是,在 monorepo 工作让我们的生活变得轻松多了。某些应用程序或模块彼此之间存在直接关系,现在您可以在同一存储库的同一分支上安心地处理它们。但这远不是主要优势。


让我们继续。我们已将所有内容移动到一个存储库中。凉爽的!我们继续以同样的速度工作,直到涉及到可重用性。事实上,这是我们在工作中的“品位法则”。意识到在某些地方我们使用相同的算法、函数、代码,并在某些地方使用我们通过 github 安装的单独包,我们决定所有这些“闻起来不太好”,并开始将所有内容放入 monorepo 中的单独包中使用工作区。


工作区(workspaces)是 NPM cli 中的一组功能,您可以使用它们从单个顶级根包管理多个包。


事实上,这些是一个包中的包,它们通过特定的包管理器(任何 YARN / NPM / PNPM)链接,然后在另一个包中使用。说实话,我们并没有立即重写工作空间上的所有内容,而是根据需要进行重写。

这是它的样子:

从一个文件


{ "type": "module", "name": "package-name-1", ... "types": "./src/index.ts", "exports": { ".": "./src/index.ts" }, },


到另一个文件


{ "type": "module", "name": "package-name-2", ... "dependencies": { "package-name-1": "workspace:*", }, },


使用 PNPM 的示例


没有什么复杂的,实际上,如果你想一想:写几个命令和行,然后使用你想要的任何东西和任何你想要的地方。但是“有一个警告,同志们”。早些时候我写道,每个人都使用他们想要的包管理器。简而言之,我们有一个包含不同管理器的存储库。在某些地方,当有人写道他无法链接这个或那个包时,考虑到他使用 NPM 并且有 YARN 的事实,这很有趣。

我会补充说,问题不是因为不同的经理,而是因为人们使用了错误的命令或配置了错误的东西。例如,有些人通过 YARN 3 只是做了一个 YARN 链接,仅此而已,但对于 YARN 1,由于缺乏向后兼容性,它没有按照他们想要的方式工作。

切换到 monorepo 之后


为什么是 PNPM?

至此,很明显最好使用相同的包管理器。但是你需要选择哪一个,所以当时我们只考虑了 2 个选项—— YARNPNPM 。我们立即丢弃了 NPM,因为它比其他的慢而且丑。可以在 PNPM 和 YARN 之间做出选择。


YARN 最初运行良好——它更快、更简单且更易于理解,这就是当时每个人都使用它的原因。但是制作 YARN 的人离开了 Facebook,并且将其下一个版本的开发转移给了其他人。这就是 YARN 2 和 YARN 3 在没有向后兼容的情况下出现的原因。此外,除了 yarn.lock 文件外,它们还生成一个 yarn 文件夹,有时它的权重为 node_modules 并在其自身中存储缓存。


因此,我们和许多其他开发人员一样,将注意力转向了 PNPM。结果证明它和当时的第一个 YARN 一样方便。在这里可以轻松使用工作区,一些命令看起来与第一个 YARN 中的相同。此外,shamefully-hoist 被证明是一个不错的附加选项——一次在所有地方安装 node_modules 比每次都去某个文件夹并进行 PNPM 安装更方便。


Turborepo 和代码重用

此外,我们决定尝试 turborepo。 Turborepo 是一个 CI/CD 工具,它有自己的一组选项、cli 和通过 turbo.json 文件进行的配置。安装和配置尽可能简单。我们将 turbo cli 的全局副本通过


PNPM add turbo --global.


将 turbo.json 添加到项目中


turbo.json


{ "$schema": "https://turbo.build/schema.json", "pipeline": { "build": { "dependsOn": ["^build"] } } }


之后我们可以使用 turborepo 的所有可用功能。最吸引我们的是它的功能以及在 monorepo 中使用它的可能性。

是什么吸引了我们:

  • 增量构建(增量构建 - 收集构建非常痛苦,Turborepo 会记住构建的内容并跳过已经计算的内容);

  • Content-aware hashing(Content-aware hashing——Turborepo 查看文件的内容,而不是时间戳,以找出需要构建的内容);

  • 远程缓存(远程哈希 - 与团队和 CI/CD 共享远程构建缓存以实现更快的构建。);

  • 任务管道(定义任务之间关系然后优化创建内容和创建时间的任务管道。)。

  • 并行执行(使用每个内核以最大并行度执行构建,而不会浪费空闲 CPU)。


我们还从文档中采纳了组织 monorepo 的建议,并在我们的平台上实施了它。也就是说,我们将所有包拆分为应用程序和包。为此,我们还创建了 PNPM-workspace.yaml 文件并写入:


PNPM-workspace.yaml

packages:

'apps/**/*'

'packages/**/*'


在这里你可以看到我们的结构前后的例子:



现在我们有了一个带有自定义工作区和方便的代码重用的 monorep。我将补充一些我们并行完成的要点。我之前提到过两件事:我们有一个 chrome 扩展,我们决定 - 我们正在制作一个平台。


由于我们的平台优先与 Shopify 合作,因此我们决定不为 Chrome 扩展或除此之外,为 Shopify 制作另一个模块会很好,它可以简单地安装在网站上,以免一次再次迫使人们下载移动应用程序或 Chrome 扩展程序。但它必须完全重复扩展。最初,我们并行执行它们,但我们意识到我们做错了什么,因为我们只是重复了代码。在任何意义上,我们都在不同的地方写同样的东西。但是由于我们现在已经配置了所有工作区和重用,我们可以轻松地将所有内容移动到一个包中,我们在 Shopify 模块和 Chrome 扩展程序中调用了这个包。因此,我们节省了很多时间。


现在这个和 index.html 是整个 Chrome 扩展



为我们节省大量时间的第二件事是消除了 webpack,并且在某些地方,通常构建。 webpack 有什么问题?事实上,有两个关键点:复杂性和速度。我们选择的是 vite。为什么?它更容易设置,它正在迅速流行并且已经有大量可用的插件,来自码头的示例足以安装。相比之下,我们的 Chrome 网络扩展的 webpack 在 vite.js 上的构建花费了大约 15 秒



大约 7 秒(生成 dts 文件)。



感到不同。拒绝构建是怎么回事?一切都很简单,事实证明,我们并不真正需要它们,因为这些是可重用的模块,在 package.json 中,在导出中,您可以简单地将 dist/index.js 替换为 src/index.ts。


它怎么样


{... "exports": { "import": "./dist/built-index.js" }, ... }


现在怎么样了


{ ... "types": "./src/index.ts", "exports": { ".": "./src/index.ts" }, ... }


因此,我们不再需要运行 PNPM watch 来跟踪与这些模块相关的应用程序更新,也不需要执行 PNPM build 来拉取更新。我认为不值得解释它为我们节省了多少时间。

事实上,我们收集构建的原因之一是 TypeScript,更准确地说是 index.d.ts 文件。以便在导入我们的模块/包时,我们知道某些函数中期望的类型或其他函数将返回给我们的类型,例如这里:


所有预期参数立即可见


但鉴于您可以简单地从 index.tsx 导出,还有另一个放弃构建的原因。

打字稿 + GraphQL

但是,为什么要使用 TypeScript?我认为现在描述 TS 的所有优点是没有意义的:类型安全、由于类型而促进开发过程、接口和类的存在、开源代码、代码修改期间的错误立即可见,而不是在运行时, 等等。


正如我一开始所说,我们决定用一种语言编写所有内容,这样如果有人停止工作或离开,我们就可以提供支持或保障。首先我们选择了JS。但是 JS 不是很安全,没有在大型项目上测试是相当痛苦的。因此,我们决定支持 TS。正如实践所示,在 monorepo 中非常方便,因为您可以简单地导出 *.ts 文件,并且在使用组件时,预期的数据及其类型立即一目了然。


但其中一个主要有用的功能是自动生成用于 GraphQl 查询和突变的类型。对于不是很了解的人来说,GraphQl 是一种技术,可以让你通过相同的查询(获取数据)和变异(更改数据)来访问数据库,看起来像这样:


query getShop {shop { shopName shopLocation } }


与 REST API 不同,在收到它之前,您不会知道会收到什么,在这里您可以自己确定所需的数据。


Let's get back to our President-elect.我们使用了 Hasura,它是 PostgreSQL 之上的 GraphQL 包装器。由于我们正在使用 TS,因此我们必须以一种好的方式键入来自请求和我们发送到负载的请求的数据。如果我们谈论的是上面示例中的代码,应该没有问题。但实际上,一个查询可以达到一百行,加上一些字段可能来也可能不来,或者数据类型不同。打字这样的画布是一项非常漫长且吃力不讨好的任务。


选择?当然,我有!让类型通过命令生成。在我们的项目中,我们做了以下事情:


  • 我们使用了以下库:graphql 和 graphql-request

  • 首先,创建具有 *.graphql 分辨率的文件,其中写入查询和变更。


    例如:


测试.graphql


query getAllShops {test_shops { identifier name location owner_id url domain type owner { name owner_id } } }


  • 接下来我们创建了 codegen.yaml


代码生成.yaml


schema: ${HASURA_URL}:headers: x-hasura-admin-secret: ${HASURA_SECRET}

emitLegacyCommonJSImports: false

config: gqlImport: graphql-tag#gql scalars: numeric: string uuid: string bigint: string timestamptz: string smallint: number

generates: src/infrastructure/api/graphQl/operations.ts: documents: 'src/**/*.graphql' plugins: - TypeScript - TypeScript-operations - TypeScript-graphql-request


在那里我们指出了我们要去的地方,最后 - 我们用生成的 API (src/infrastructure/api/graphQl/operations.ts) 保存文件的地方以及我们从 (src/**/*.图表)。


之后,将一个脚本添加到 package.json 中,为我们生成相同的类型:


包.json


{... "scripts": { "generate": "HASURA_URL=http://localhost:9696/v1/graphql HASURA_SECRET=secret graphql-codegen-esm --config codegen.yml", ... }, ... }


它们指示脚本访问以获取信息的 URL、秘密和命令本身。


  • 最后,我们创建客户端:


import { GraphQLClient } from "graphql-request"; import { getSdk } from "./operations.js"; export const createGraphQlClient = ({ getToken }: CreateGraphQlClient) => { const graphQLClient = new GraphQLClient('your url goes here...'); return getSdk(graphQLClient); };


因此,我们得到一个生成具有所有查询和突变的客户端的函数。 operations.ts 中的 bonus 奠定了我们可以导出和使用的所有类型,并且对整个请求进行了完整的类型化:我们知道需要提供什么以及将要提供什么。除了运行命令并享受打字之美外,您无需考虑其他任何事情。

结论

因此,我们摆脱了大量不必要的存储库和不断推动最细微的变化以检查事情如何运作的需要。相反,他们提出了一种结构,其中所有内容都根据其用途进行了结构化和分解,并且所有内容都可以轻松重用。所以我们让我们的生活更轻松,减少了将新人引入项目、分别启动平台和模块/应用程序的时间。一切都已键入,现在无需进入每个文件夹并查看这个或那个功能/组件想要什么。结果,开发时间减少了。



最后,我想说你永远不应该着急。与其故意使生活复杂化,不如更容易地了解自己在做什么以及如何做。问题无处不在,总是迟早会在某个地方出现,然后故意的复杂化会让你膝盖受伤,但无济于事。

dev.family 团队与您同在,再见!