フューチャー技術ブログ

Angularをがんばらないで書く

僕が案件でAngularを書きまくっていたのは、6とか8の時代ですが、最近のバージョンで気になるアップデートが入っていました。

  • シグナル(v16から)
  • スタンドアローンコンポーネント(v14から)

他のフレームワークをいろいろ触ると、Angularはかなり独特な雰囲気があります。隠しきれないJava感というか、他のフレームワークでは存在しない様々な概念が見え隠れして、それをキャッチアップするだけでも結構重いです。もちろん、それぞれ意味があって、「きっとこういうことをしたいんだろうな」というニーズがあってのことかとは思いますが、いろんなフレームワークをいったりきたりすると、ストレスが多少あります。

しかし、この新しい機能を使って、他のフレームワークでは見ない要素をそぎ落としてAngularを使ってみると、まあ悪くないんじゃないかと思いました。

モジュールを使わない

Angularでアプリケーションのひな型を作ると、NgModuleというのがまずできあがります。自分で作ったコンポーネントなどはここに登録していきます。外部のライブラリなどもこのモジュールで提供されていたりします。アプリケーションはこのモジュールの組み合わせとして実現されています。

ただ、これはES6 modulesなどが登場する前に、非同期にページごとのソースを読み込むといった高度な機能を実現するために生まれたものだと思いますが、ES6のモジュール管理と、さらに似たようなモジュール管理と2重の管理になってしまっているのが現状かと思います。今だと、ES6モジュールベースでバンドラーが賢くモジュール分割してくれたりするのが、それとは別にAngularモジュールの管理も必要ということで余計な作業が増えてしまっています。

NgModulesで提供されている標準機能とかサードパーティの機能を利用するのはそのまま使えばよいのですが、Angular 14で入ったスタンドアローンコンポーネントを使っていくと、自分たちが作るアプリケーションに関してはもうモジュールの存在を気にしないでコードが書けます。

詳しくはスタンドアローンコンポーネントを見れば詳しく書いてあります。アプリケーションのひな型を作るといまだにモジュールを使うコードが生成されますが、以下のコマンドを実行するとモジュールを使わないコードに変更されます。起動するとメニューが出てくるので3つのコマンドを1つずつ実行すればモジュールを使わないコードができあがります。

ng generate @angular/core:standalone

モジュールを使う場合、コンポーネントから使いたい部品があれば、すべて親のモジュールに登録してから使う必要がありますが、NgModuleがないすべてがスタンドアローンコンポーネントで構成されるアプリケーションであれば、自分が使いたいコンポーネントを.tsファイル内でES6 modulesのimportで取り込んで、デコレータに使いたいコンポーネントを足すだけになります。まあ、Options APIでVue.jsを使うぐらいの手間にはなったかな、と。

日本語だとlacolacoさんのZennの書籍がテストのやり方も含めてかなり詳しく書かれています。

DIを使わない

Angularのマニュアルを見ると、UIに直接関係のあるコードはコンポーネントにして、そうではないコードはサービスというクラスに実装していくことが書かれています。

サービスはコンストラクタインジェクション(Javaの人にわかる表現でいうと)でUIのコンポーネントのインスタンスにインスタンスがわたってきます。サーバー通信などはそのサービスのなかで行います。なお、サーバーへのリクエストはAngularが提供するHttpClientを使います。このHttpClientはレスポンスをRxJSの作法に従って返します。

DIを使ってアプリケーションを構成していくメリットとしては、細かく部品をテスト用のモック(テストダブル)に差し替えてテストできるというのがあります。テスト対象がコンポーネントであれば、そこが通信するサービスや、他のコンポーネントもすべてモックに差し替えられます。

ただ、そういう考え方自体がJava的というか、今では少数派な気がしています。単体テストの考え方/使い方という本でも、このやり方(ロンドン学派)ではなく、一緒に動くコンポーネントのうちグローバル変数的な動きをして他のテストとの独立性を失ってしまうようなもの以外はそのままテストしてしまえ(古典学派)の方を推奨しています。

実際、JestなどでもJavaScript単位でモックできたりもするのですが、今だとMock Service WorkerとかのサーバーAPIのレスポンスレベルでのモックの方がやりやすい(モックコードをミスしたときのトラブルシュートのストレスが少なくて禿げにくい)な、という実感があります。MSWをAngularと使うための詳しい説明のページもありますね。

モックする必要がなければ、ふつうの関数やクラスとして実装して、コンポーネントクラスの.tsファイルからimportして使ってあげればいいんじゃないですかね?

RxJSを使わない

RxJSはデータの流れを細かく制御できるリアクティブなライブラリです。ものすごく豊富な機能があり、使いこなせればアプリケーションコードの細かい動きの部分をフレームワークと独立したコードとして記述できます。標準のHttpClientを使うと、結果はRxJSのObservableを返してきて、RxJSの流儀データ処理できます。

ですが、今どきはfetch()を使って、await/asyncで非同期を扱うのが一般的です。Angular2が最初に公開されたときは、PromiseがJS公式になるかならないかぐらいです。まだfetch()はなく、サーバー通信はコールバックのXHRでした。また、fetch()が出た直後も通信のキャンセルができないなど機能が足りないということもあったようです。

ですが、awaitと書けるようになって非同期の表現はとてもシンプルになりました。また、キャンセルもできるようになって fetch()でできないことも減りました(そもそも、他のフレームワークとかでキャンセルの有無でfetch()使わないとかはあんまり聞かない)。for await ofとかも追加されましたしね。

Promiseが言語側で積極的に使いやすくサポートされている現状を踏まえると、当時と比べてRxJSを使うメリットよりも、今はデメリットの方が大きい気がします。機能が豊富ということは、その分、その機能をある程度頭に入れておかないと他の人のコードが読めないということにもなりますし。

Angular 16から入ったシグナルは、ReactのHooksから来ている超軽量(RxJSと比べて)リアクティブなツールで、effectやcomputeを使うことで、Reactなどと近いコーディングができます。RxJSのようなデータの流れをしっかり定義していくのとは対照的です。

唯一、ちょっと惜しいなと思ったのは直接非同期なコードをシグナルで扱いにくいという点です。ただ、RxJSとの相互接続のヘルパーが用意されているので、それを使うと、シグナルと非同期を簡単につなげそうです。僕が作ってみたのはこんな感じです。

import { toObservable, toSignal } from '@angular/core/rxjs-interop';

function asyncCompute<T, U>(source: Signal<T>, convert: (input: T) => Promise<U>, initialValue: U): Signal<U> {
const result = new Subject<U>();
toObservable(source).subscribe(async v => {
result.next(await convert(v));
})
return toSignal(result, { initialValue });
}

こんな感じで何かしらのシグナルを入力に受け取って非同期で加工して結果を反映するシグナルを生成できますね。ユーザーIDを管理するシグナルを作って、サーバーにアクセスして、ユーザー情報を格納するシグナルに入れる、みたいなこともできます。

これは入力をスリープしてちょっと遅らせて2倍するだけのコードですが。

count = signal(0);

lazyCount = asyncCompute(this.count, async (input: number) => {
await sleep();
return input * 2;
}, 0);

あとでStack Overflowをみたら、effectを使う事例もありました。

まとめ

モジュールをやめて、DIをやめて、RxJSをやめると、Angularを始めたときに学ばないといけないこと、書かなければいけないコードが、他のフレームワーク同等になりそうだな、と思ったのでブログにしてみました。これにTailwind CSSも組み合わせれば、だいぶ楽にアプリが書けそうな気がします。Angular Materialというしっかり作られた公式のUI部品がある、というのはAngularのメリットだと思いますし。

Angularも活発に開発され続けていますし、ちょっと趣味開発でも使ってみようかな、と思っています。