paint-brush
TypeScript のモノレポジトリ: すべてを壊してより良くする方法の話@devfamily
1,784 測定値
1,784 測定値

TypeScript のモノレポジトリ: すべてを壊してより良くする方法の話

dev.family13m2023/05/05
Read on Terminal Reader

長すぎる; 読むには

dev.family は興味深いプロジェクトに約 6 か月間取り組んでおり、現在も継続しています。このプロジェクトは、エンドユーザーに特定のアクションに対する報酬を提供する暗号ロイヤルティ プログラムとして開始され、顧客はこれらの同じユーザーに関する分析を受け取ります。すべてが TypeScript という 1 つの言語で書かれています。 8 つのリポジトリがあり、いくつかは相互に通信する必要があります。
featured image - TypeScript のモノレポジトリ: すべてを壊してより良くする方法の話
dev.family HackerNoon profile picture
0-item
1-item
2-item

みなさん、こんにちは。dev.family から連絡があります。私たちが半年近く取り組んできた興味深いプロジェクトについてお話ししたいと思います。この間、多くのことが起こり、多くの変化がありました。私たちは自分自身にとって興味深いものを発見し、なんとかバンプを埋めました。


私たちの物語はどのようになりますか?

  • 私たちは何をしましたか
  • 開始方法
  • これは私たちをどこに導きましたか
  • 私たちはどのような問題に直面しましたか
  • モノレポを選ぶ理由
  • なぜPNPMなのか
  • TS を選ぶ理由 現在の仕組み
  • 私たちはどれだけ生活を楽にしてきたか

プロジェクトについて少し

それで、私たちはまだ何に取り組んでいますか?実際、この質問はある時点で非常に関連性が高くなりました。たとえば、かつてマクドナルド社の所有者がそうであったからです。私たちは、エンドユーザーに特定のアクションに対する報酬を提供し、顧客がこれらの同じユーザーに関する分析を受け取る暗号ロイヤルティ プログラムとしてプロジェクトを開始しました。はい、それはかなり表面的ですが、問題ではありません。


仕事の始まり

Shopify ストアに接続するための Shopify モジュール、ブランドのポータル、Google Chrome の拡張機能、モバイル アプリケーション + データベースを備えたサーバーを開発する必要がありました (実際には、それらがなければどこにもありません)。一般的に、必要なものを決定して作業を開始しました。プロジェクトはすぐに大規模であると想定されたので、誰もが遅延アクションの魔法の豆のように成長する可能性があることを理解していました.



すべてを「正しく」「すべての基準で」行うことが決定されました。つまり、すべてが TypeScript という 1 つの言語で書かれています。誰もが同じように書き、ファイル、リンター (多くのリンター) に不必要な変更がないように、すべてを「簡単に」再利用できるようにし、すべてを個別のモジュールに入れ、盗まれないようにします。 Github アクセス トークン。

だから私たちは始めました:

  • リンターと ts config の個別のリポジトリ (スタイル ガイド)

  • モバイル アプリケーション (react native) と Chrome 拡張機能 (react.js) のリポジトリ (これらは同じ機能を繰り返すため、異なるユーザーのみを対象としているため)

  • ポータル用の別のリポジトリ

  • Shopify モジュール用の 2 つのリポジトリ

  • ブロックチェーン関連リポジトリ APIリポジトリ (express.js) インフラ用リポジトリ


当時のリポジトリの一例


ええと...すべてをリストしたと思います。少しやりすぎましたが、続けましょう。そうそう、Shopify モジュールに 2 つのリポジトリが割り当てられたのはなぜですか?最初のリポジトリは UI モジュールであるためです。私たちの赤ちゃんとその設定のすべての美しさがあります。 2 つ目は、Shopify の統合です。これは実際には、すべてのリキッド ファイルを使用した Shopify 自体での実装です。全部で 8 つのリポジトリがあり、いくつかは相互に通信する必要があります。


TypeScript での開発について話しているので、モジュール、ライブラリをインストールするためのパッケージ マネージャーも必要です。しかし、私たちはそれぞれのリポジトリで独立して作業しており、誰が何を使用するかは問題ではありませんでした。たとえば、React Native でモバイル アプリケーションを開発している間、あまり長く考えずに YARN1 を保持していました。古き良き NPM の使用に慣れている人もいれば、新しいものすべてを愛し、新しい YARN3 を使用する人もいます。したがって、どこかに NPM があり、どこかに YARN1 があり、どこかに YARN3 がありました。


それで、私たちは皆、アプリケーションを作り始めました。そして、ほぼすぐに楽しみが始まりましたが、完全ではありませんでした。まず、TypeScript が何のためにあるのかを考えず、怠けすぎたり、書き方が「わからない」場合は「Any」を使用する人もいました。誰かが、TypeScript のすべての力と、場所によってはすべてをはるかに簡単にできるという事実に気づいていませんでした。したがって、タイプは宇宙次元から出てきました。はい、言い忘れていましたが、Hasura GraphQL をデータベースとして使用することにしました。そこからのすべての回答を手動でタイプ化すると、別のように見えることがありました。また、あるケースでは、古き良き Javascript で記述したものさえありました。はい、状況はクールであることが判明しました。最初の男は、緊張しすぎないようにもう一度「Any」を入力し、2番目の男は自分の手で型のキャンバスを書き、3番目の男はまだ型をまったく書きません。



後で、ロジックを繰り返した場合、良い意味で、別のパッケージに取り出されたはずであることがわかりました-誰もこれを行うつもりはありませんでした。誰もが自分自身のために、他のすべてのためにコードを書き、書きます-高い鐘楼から唾を吐きます。

これは私たちをどこに導きましたか?

私たちは何を持っていますか?異なるアプリケーションの 8 つのリポジトリがあります。どこにでも必要なものもあれば、相互に通信するものもあります。したがって、.NPMrc ファイルを作成し、クレジットを指定し、github トークンを作成してから、パッケージ マネージャー モジュールを使用します。一般的に、不快ではありますが、少し面倒ですが、珍しいことではありません。


パッケージ内の何かを更新する場合にのみ、そのバージョンをアップグレードしてからアップロードし、アプリケーション/モジュールで更新する必要があります。そうして初めて、何が変更されたかがわかります。しかし、これはまったく不適切です。特にどこかで色を変えることができれば。さらに、一部のコードは繰り返され、再利用されず、静かに書き直されます。モバイル アプリケーションとブラウザー拡張機能について話している場合、redux ストアと API を使用したすべての作業がそこで完全に繰り返されます。何かが完全に書き直されているか、わずかに変更されています。


合計すると、私たちが残したものは次のとおりです。多数のリポジトリ、アプリケーション/モジュールのかなり長い起動、同じ人によって書かれた多くの同じもの、プロジェクトへの新しい人たちのテストと紹介に費やされた多くの時間、上記に起因するその他の問題。



要するに、これはタスクが非常に長い間実行されたという事実に私たちを導きました.もちろん、これは締め切りに間に合わないことにつながり、プロジェクトに新しい人を紹介することは非常に困難であり、開発の速度に再び影響を与えました.場合によっては、webpack のおかげで、すべてが非常に単調で長くなりました。


その後、私たちが努力していた場所から遠く離れていることが明らかになりましたが、どこにいるのかは誰にもわかりません。すべての間違いを分析した後、いくつかの決定を下しましたが、これについては後で説明します。

なぜモノレポ?

おそらく、将来に大きな影響を与えた最も重要なことは、特定のアプリケーションではなくプラットフォームを構築していることに気付いたことです。ユーザーにはいくつかのタイプがあり、さまざまなアプリケーションがありますが、同じプラットフォーム内で動作します。そのため、多数のリポジトリに関する問題をすぐに解決しました。1 つのプラットフォームで作業している場合、1 つのプラットフォームで作業する方が簡単なのに、なぜそれを複数のリポジトリに分割する必要があるのでしょうか。


モノレポで作業することで、私たちの生活は非常に楽になりました。一部のアプリケーションまたはモジュールは相互に直接関係があり、同じリポジトリの同じブランチで安心して作業できるようになりました。しかし、これは主な利点からはほど遠いです。


続けましょう。すべてを 1 つのリポジトリに移動しました。いいね!再利用できるようになるまで、同じペースで作業を続けました。実際、これは私たちの仕事における「センスの法則」です。一部の場所では同じアルゴリズム、関数、コードを使用し、一部の場所では github 経由でインストールした別のパッケージを使用していることに気づき、これらすべてが「あまりいい匂いがしない」と判断し、すべてを monorepo 内の別のパッケージに入れ始めました。ワークスペースを使用します。


ワークスペース (ワークスペース) は、NPM cli の関数のセットであり、単一の最上位ルート パッケージから複数のパッケージを管理できます。


実際、これらは特定のパッケージ マネージャー (任意の YARN / NPM / PNPM) を介してリンクされ、別のパッケージで使用される 1 つのパッケージ内のパッケージです。実を言うと、ワークスペースのすべてをすぐに書き直すわけではなく、必要に応じて書き直しました。

外観は次のとおりです。

1つのファイルから


{ "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なのか?

この時点で、同じパッケージ マネージャーを使用する方がよいことが明らかになりました。ただし、どちらを選択する必要があるため、その時点ではYARNPNPMの 2 つのオプションのみを検討しました。 NPM は他のものよりも遅く、醜いため、すぐに破棄しました。 PNPM と YARN のどちらかを選択できました。


YARN は最初はうまく機能しました。より速く、よりシンプルで、より理解しやすかったため、当時は誰もが YARN を使用していました。しかし、YARN を作った人は Facebook を去り、次のバージョンの開発は他の人に移されました。これが、YARN 2 と YARN 3 が前者との下位互換性なしで登場した方法です。また、yarn.lock ファイルに加えて、yarn フォルダーを生成します。このフォルダーは、node_modules として重み付けされ、それ自体にキャッシュを格納することがあります。


したがって、他の多くの開発者と同様に、私たちは PNPM に注目しました。当時の最初の YARN と同じくらい便利であることが判明しました。ここでは、ワークスペースを簡単に使用できます。一部のコマンドは、最初の YARN と同じように見えます。さらに、恥ずべきことにホイストは素晴らしい追加オプションであることが判明しました。毎回どこかのフォルダーに移動して PNPM インストールを行うよりも、node_modules を一度にどこにでもインストールする方が便利です。


ターボレポとコードの再利用

さらに、ターボレポを試してみることにしました。 Turborepo は CI/CD ツールであり、turbo.json ファイルによる独自のオプション セット、cli、および構成を備えています。可能な限り簡単にインストールおよび構成できます。ターボ cli のグローバル コピーを配置します。


PNPM add turbo --global.


プロジェクトへの turbo.json の追加


ターボ.json


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


その後、turborepo の利用可能なすべての機能を使用できます。私たちは、その機能とモノレポで使用できる可能性に最も惹かれました。

私たちを夢中にさせたもの:

  • インクリメンタル ビルド (インクリメンタル ビルド - ビルドを収集するのは非常に面倒です。Turborepo はビルドされたものを記憶し、既に計算されたものをスキップします);

  • コンテンツ認識ハッシュ (コンテンツ認識ハッシュ - Turborepo は、タイムスタンプではなくファイルの内容を調べて、何を構築する必要があるかを判断します);

  • リモート キャッシング (リモート ハッシュ - リモート ビルド キャッシュをチームおよび CI / CD と共有して、ビルドをさらに高速化します。);

  • タスク パイプライン (タスク間の関係を定義し、何をいつ作成するかを最適化するタスク パイプライン)。

  • 並列実行 (アイドル状態の CPU を無駄にすることなく、最大の並列処理で各コアを使用してビルドを実行します)。


また、ドキュメントからモノレポを整理するための推奨事項を採用し、それをプラットフォームに実装しました。つまり、すべてのパッケージをアプリとパッケージに分割します。これを行うために、PNPM-workspace.yaml ファイルも作成し、次のように記述します。


PNPM-workspace.yaml

packages:

'apps/**/*'

'packages/**/*'


ここで、前後の構造の例を確認できます。



これで、カスタマイズされたワークスペースと便利なコードの再利用を備えた monorep ができました。並行して行ったいくつかのポイントを追加します。前に 2 つのことを述べました。Chrome 拡張機能があり、プラットフォームを作成することを決定しました。


私たちのプラットフォームは Shopify を優先的に使用していたため、Chrome の拡張機能の代わりに、またはそれに加えて、Shopify 用の別のモジュールを作成することをお勧めします。これは、サイトに簡単にインストールできます。モバイル アプリケーションや Chrome 拡張機能のダウンロードを再び強制する。ただし、拡張を完全に繰り返さなければなりません。最初は並行して実行していましたが、単純にコードを複製したため、何か間違ったことをしていることに気付きました。あらゆる意味で、同じことを別の場所に書いています。しかし、これですべてのワークスペースと再利用が構成されたので、すべてを簡単に 1 つのパッケージに移動し、Shopify モジュールと Chrome 拡張機能で呼び出しました。したがって、多くの時間を節約できました。


これと index.html が Chrome 拡張機能全体です



時間を大幅に節約できた 2 番目のことは、webpack を削除したことと、一部の場所ではビルド全般を削除したことです。ウェブパックの何が問題になっていますか?実際には、複雑さと速度という 2 つの重要なポイントがあります。私たちが選んだのは vite です。なぜ?セットアップが簡単で、急速に人気が高まっており、すでに多数の機能するプラグインがあり、インストールにはドックの例で十分です.これに対し、vite.js では、Chrome Web 拡張機能の Webpack でのビルドに約 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 ウォッチを実行し、更新をプルするために PNPM ビルドを実行する必要がなくなりました。どれだけの時間を節約できたかを説明する価値はないと思います。

実際、ビルドを収集した理由の 1 つは TypeScript、より正確には index.d.ts ファイルでした。モジュール/パッケージをインポートするときに、特定の関数で期待される型や、他の関数が返す型を知ることができます。たとえば、次のようになります。


期待されるすべてのパラメータがすぐに表示されます


しかし、index.tsx から簡単にエクスポートできることを考えると、ビルドを放棄する別の理由がありました。

TypeScript + GraphQL

それでも、なぜ TypeScript なのか? TS のすべての利点を説明するのは意味がないと思います: タイプセーフ、タイピングによる開発プロセスの促進、インターフェースとクラスの存在、オープンソースコード、コード変更中に発生したエラーは実行時ではなくすぐに表示されます、 等々。


最初に言ったように、私たちはすべてを 1 つの言語で書くことに決めました。そうすれば、誰かが仕事をやめたり退職したりした場合でも、サポートしたり保証したりできるからです。最初にJSを選択しました。しかし、JS はあまり安全ではなく、大規模なプロジェクトでテストを行わないと非常に苦痛です。したがって、私たちはTSを支持することにしました。実践が示しているように、*.ts ファイルを簡単にエクスポートでき、コンポーネントを使用すると、期待されるデータとそのタイプがすぐに明確になるため、monorepo では非常に便利です。


しかし、主な便利な機能の 1 つは、GraphQl クエリとミューテーションの型の自動生成です。あまり詳しくない人のために説明すると、GraphQl は、同じクエリ (データを取得するため) とミューテーション (データを変更するため) を介してデータベースにアクセスできるようにするテクノロジーであり、次のようになります。


query getShop {shop { shopName shopLocation } }


受け取るまで何が来るかわからない REST API とは異なり、ここでは必要なデータを自分で決定します。


会長エレクトに戻りましょう。 PostgreSQL 上の GraphQL ラッパーである Hasura を使用しました。 TS を使用しているため、適切な方法で、要求とペイロードに送信するデータの両方からデータを入力する必要があります。上記の例のコードについて話している場合、問題はないはずです。しかし、実際には、クエリは 100 行に達する可能性があり、一部のフィールドは含まれる場合と含まれない場合や、データ型が異なる場合があります。そして、そのようなキャンバスを入力することは、非常に長く、報われない作業です。


別?もちろん、私が持っています!コマンドを介してタイプを生成します。私たちのプロジェクトでは、次のことを行いました。


  • 次のライブラリを使用しました: graphql および graphql-request

  • まず、クエリとミューテーションが記述された *.graphql 解像度のファイルが作成されました。


    例えば:


test.graphql


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


  • 次に codegen.yaml を作成しました


codegen.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/**/*. Graphql)。


その後、同じ型を生成するスクリプトが 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 のボーナスは、エクスポートして使用できるすべての型を配置し、要求全体の完全な型付けがあります。何を与える必要があり、何が来るかを知っています。コマンドを実行してタイピングの美しさを楽しむこと以外は、何も考える必要はありません。

結論

このようにして、多数の不要なリポジトリを取り除き、物事がどのように機能するかを確認するために常にわずかな変更をプッシュする必要がなくなりました。代わりに、すべてが構造化され、目的に応じて分解され、すべてが簡単に再利用できるものを思いつきました。そのため、私たちは生活を楽にし、プロジェクトに新しい人を紹介する時間を短縮し、プラットフォームとモジュール/アプリケーションを別々に立ち上げました.すべてが入力されたので、各フォルダーに移動して、これまたはその関数/コンポーネントが何を望んでいるかを確認する必要はありません。その結果、開発期間が短縮されました。



結論として、決して急いではいけません。故意に人生を複雑にするよりも、自分が何をしているのか、そしてそれをより簡単に行う方法を理解する方が良い.問題はどこにでもあり、常に、遅かれ早かれどこかに出てくるでしょう。

dev.family チームがあなたと共にいました。またお会いしましょう!