日本語
  • typescript
  • monorepo

TypeScriptオールインワン: モノレポとその苦悩と獲得点

この記事では、モノレポとポリレポを比較するつもりはありません、それはすべて哲学の問題です。代わりに、私は構築と進化の経験に焦点を当て、あなたがJS/TSエコシステムに精通していると仮定します。

Gao
Gao
Founder

はじめに

私は常にモノレポの夢を持っていました。

私はAirbnbで働いている間にモノレポのアプローチを見ましたが、それはフロントエンドのみ用でした。JavaScriptエコシステムへの深い愛と「楽しい」TypeScript開発体験とともに、私は約3年前からフロントエンドとバックエンドのコードを同じ言語で整列し始めました。それは募集のために素晴らしかったですが、開発のためにそれほど素晴らしいわけではありませんでした、なぜなら私たちのプロジェクトはまだ複数のリポジトリに散らばっていたからです。

それは言います、 「プロジェクトのリファクタリングの最良の方法は新しいものを始めることです」。だから、私が約1年前に私のスタートアップを始めるとき、私は完全なモノレポ戦略を使うことに決めました: フロントエンドとバックエンドのプロジェクトを一つのリポジトリに入れ、さらにデータベースのスキーマも同様にします。

この記事では、モノレポとポリレポを比較するつもりはありません、それはすべて哲学の問題です。代わりに、私は構築と進化の経験に焦点を当て、あなたがJS/TSエコシステムに精通していると仮定します。

最終結果は、GitHubで利用可能です。

##なぜTypeScriptなのか?

率直に言って、私はJavaScriptとTypeScriptの大ファンです。私はそれの柔軟性と厳格性の互換性が大好きです: あなたはunknownまたはany(ウチちはコードベースのあらゆる形式のanyを禁止しています)に後退するか、超厳格なlintルールセットを使用してチーム全体でコードスタイルを整えることができます。

私たちが「フルスタック」の概念について話しているとき、私たちは通常、少なくとも2つのエコシステムとプログラミング言語を想像します: フロントエンドのための1つとバックエンドのための1つです。

ある日、私は突然、それがより簡単になる可能性があると気づきました: Node.jsは十分に速く(私を信じてください、ほとんどの場合、コードの品質は実行速度よりも重要です)、TypeScriptは十分に成熟している(大規模なフロントエンドプロジェクトでうまく動作します)、そしてモノレポの概念は多くの有名なチームによって実践されています(React、Babelなど) - だから、なぜ私たちはすべてのコードを一緒に組み合わせて、フロントエンドからバックエンドまで、エンジニアが一つのリポジトリでコンテキストスイッチなしに仕事をすることができ、完全な機能を(ほぼ)一つの言語で実装することができませんか。

##パッケージマネージャの選択

開発者として、そしていつものように、私はコーディングを始めるのを待ちきれませんでした。でも、今回は、事情が違いました。

パッケージマネージャの選択は、モノレポでのdev体験にとって重要です。

###慣性の痛み

それは2021年7月でした。私は長い間それを使用していたので、 [email protected]から始めました。 Yarnは速かったのですが、すぐに私はYarn Workspacesでいくつかの問題に遭遇しました。例えば、依存関係を正しく持ち上げない、そしてたくさんの問題が「修正済みのモダン」でタグ付けされています、それは私をv2(berry)へと誘導します。

「はい、いいですね、私は今アップグレードしています。」私はv1での苦闘をやめて移行を開始しました。しかし、ベリーの長いmigration guideは私を怖がらせ、何度か試みた後に諦めました。

###それはちょうどうまく動く

そこでパッケージマネージャについての調査が始まりました。私は試した後、「pnpm」に魅了されました: yarnと同じように速く、ネイティブのモノレポサポート、 npmと似たコマンド、ハードリンクなど。最も重要なのは、それは単にうまく動くことです。パッケージマネージャを開発するのではなく、製品を始めるために必要な開発者として、私はただいくつかの依存関係を追加し、パッケージマネージャがどのように動作するかや他の任意のコンセプトを知らずにプロジェクトを開始したかっただけです。

同じ考えに基づいて、私たちは古い友人「lerna」をパッケージ間でコマンドを実行し、ワークスペースパッケージを公開するために選びました。

##パッケージスコープの定義

最初から各パッケージの最終的なスコープを明確に把握するのは難しいです。ただし、現状に基づいて最善の試みで始め、開発中に常にリファクタリングできることを覚えておいてください。

私たちの初期構造には4つのパッケージが含まれています:

  • core:バックエンドのモノリシックサービス。
  • phrases:i18nキー→フレーズリソース。
  • schemas:データベースと共有されているTypeScriptスキーマ。
  • uicoreとやり取りするWeb SPA。

フルスタックのためのテックスタック

私たちはJavaScriptエコシステムを受け入れて、TypeScriptを私たちの主なプログラミング言語として使用しているので、多くの選択肢は自明です(私の好みに基づいて😊):

  • バックエンドサービス(コア)のための koajs:私は expressasync/awaitを使用するのが困難だったので、私はnativeサポート有りで何かを使うことにしました。
  • i18n(フレーズ/ui)のための i18next/react-i18next:私はそのシンプルなAPIと良好なTypeScriptサポートが好きです。
  • SPA(ui)のための react:ただの個人の好みです。

スキーマはどうですか?

ここにはまだ何かが欠けています:データベースシステムとスキーマ <-> TypeScript定義マッピング。

一般的なv.s. 意見のある

その時点で、私は2つの人気のあるアプローチを試しました:

  • 多くのデコレータを持つORMを使用します。
  • Knex.jsのようなクエリビルダを使用します。

しかし、これらの両方は以前の開発中に奇妙な感じを生み出しました:

  • ORMのための:私はデコレータの大ファンではなく、データベースの別の抽象層はチームに更なる学習努力と不確実性を引き起こします。

-クエリビルダのために:それはいくらかの制限(良い方向で)でSQLを書くようなものですが、それは実際のSQLではありません。したがって、多くのシナリオで生のクエリのために .raw()を使用する必要があります。

次に私はこの記事を見た:「Knex.jsの使用を止めよう:SQLクエリビルダの使用はアンチパターンです」。タイトルは攻撃的に見えますが、内容は素晴らしいです。それは強く私に「SQLはプログラミング言語だ」とリマインドし、私は直接SQLを書くことができると気づきました(CSSのように、これをどうやって見逃すことができたのですか!)ネイティブ言語とデータベースの機能を利用するため、他のレイヤーを追加してパワーを抑えるのではなく。

結論として、私はPostgresとSlonik(オープンソースのPostgresクライアント)に付着していることにしました、その記事の記述によると:

…ユーザーが異なるデータベース方言の間で選択することを許す利益は微々たるものであり、一度に複数のデータベースの為に開発するオーバーヘッドは重要です。

SQL <-> TypeScript

SQLを書くもう一つの利点は、それをTypeScript定義の一元管理として簡単に使用できることです。私はSQLスキーマをTypeScriptコードにトランスパイルするcode generatorを書き、結果はそこまで悪くないと見えます:

必要に応じて、私たちは jsonbをTypeScriptタイプと接続し、バックエンドサービスでタイプ検証を処理することさえ可能です。

結果

最終的な依存関係の構造は以下のように見えます:

それは一方向図であり、それは私たちにプロジェクトの成長を促しながら、明確なアーキテクチャを保つ助けになりました。加えて、コードは(基本的に)全てTypeScriptです。

開発体験

パッケージと設定の共有

内部依存関係

pnpmlernaは内部ワークスペース依存性にすばらしい仕事をしています。私たちはプロジェクトのルートで以下のコマンドを使用して兄弟パッケージを追加します:

それは @logto/schemas@logto/coreの依存関係として追加します。pnpmが正確にそれらを pnpm-lock.yamlでリンクしている間、自分の内部依存関係のセマンティックバージョンを保持します。結果は以下のように見えます:

コンフィグの共有

私たちはモノレポの「独立」なすべてのパッケージを「独立」して扱います。したがって、私たちは tsconfigeslintConfigprettierstlyelintjest-configをカバーする標準的なアプローチを使用することができます。このプロジェクトを参照してください。

コード、lint、およびコミット

私は日常的な開発でVSCodeを使用しています、そして簡単に言えば、プロジェクトが適切に設定されているときは何も異なりません:

  • ESLintやStylelintは正常に動作します。
    • VSCodeのESLintプラグインを使用している場合は、パッケージごとのESLint設定を尊重するように以下のVSCode設定を追加します(patternの値を自分のものに置き換えます):
  • husky、commitlint、そしてlint-stagedは期待通りに動作します。

コンパイラとプロキシ

私たちはフロントエンドとバックエンドで異なるコンパイラを使用しています:UI(React)用の parceljsと、すべての他の純粋なTypeScriptパッケージのためのtsc。あなたがまだ試していないなら、「parceljs」を試すことを強くお勧めします。それは異なるファイルタイプを優雅に処理する実際の「ゼロコンフィグ」コンパイラです。

Parcelは自身のフロントエンド開発サーバーをホストし、製造出力は単なる静的ファイルです。私たちはCORSの問題を避けるためにAPIとSPAを同じ原点でマウントすることを望んでいるので、以下の戦略が機能します:

  • デヴ環境では、シンプルなHTTPプロキシを使用してトラフィックをParcel devサーバーにリダイレクトします。
  • 本番環境では、直接静的ファイルを提供します。

あなたはフロントエンドミドルウェア関数の実装をここで見つけることができます。

ウォッチモード

各パッケージの package.jsonには、必要なときに再コンパイルするファイルの変更を監視する「dev」スクリプトがあります。lernaのおかげで、 lerna execを使用して並列にパッケージスクリプトを実行すると簡単になります。ルートスクリプトは以下のように見えます:

まとめ

理想的には、新しいエンジニア/貢献者が始めるためのステップは2つだけで済みます:

  1. リポジトリをクローンします
  2. pnpm i && pnpm devを実行します

結びの言葉

私たちのチームはこのアプローチの下で1年間開発を続けており、それにとても満足しています。最新のプロジェクトの形を見るために私たちの GitHubリポジトリをご覧ください 。締めくくりとして:

苦痛

  • JS/TSエコシステムに精通している必要があります
  • 正しいパッケージマネージャを選ぶ必要があります
  • 追加的な一回限りの設定が必要です

利益

  • 1つのリポジトリでプロジェクト全体を開発、保守
  • コードスキル要件を単純化
  • 共有されたコードスタイル、スキーマ、フレーズ、ユーティリティ
  • 通信効率向上
    • モアAPI定義とは何ですか?という質問はありません -すべてのエンジニアが同じプログラミング言語で話しています
  • 簡単にCI/CDを使用可能 -ビルド、テスト、パブリッシング用の同じツールチェインを使用

この記事はいくつかのトピックをカバーしてはいますが:リポジトリの新規設定、新しいパッケージの追加、GitHub ActionsでCI/CDを活用するなど、それぞれを展開するとこの記事は長すぎます。コメントして、今後どのトピックを見たいか教えてください。