ウェブアプリをシンプルに維持するのは、驚くほど複雑です。このモジュールでは、ウェブ API とスレッドが連携する仕組みと、状態管理などの一般的な PWA パターンにウェブ API を使用する方法について説明します。
シンプルさと複雑さ
彼の講演「Simple Made Easy」の中で、Rich Hickey 氏はシンプルなものと複雑なものの品質について語っています。彼は、シンプルなことを次のように説明しています。
「1 つの役割、1 つのタスク、1 つのコンセプト、1 つの要素」です。
ただし、シンプルさは以下を目的としたものではないことを強調します。
「1 つのインスタンスを持つか、1 つのオペレーションを実行すること。」
シンプルなものかどうかとは、それがいかに相互接続されているかということです。
複雑さは、バインディング、織り、またはリッチの用語を使用して、ものを一緒に複雑にすることから生じます。管理対象のロール、タスク、コンセプト、ディメンションの数を数えることで、複雑さを計算できます。
コードがシンプルなほど理解も保守も簡単なため、ソフトウェア開発にはシンプルさが不可欠です。ウェブアプリの場合もシンプルであることは、あらゆる状況でアプリを高速で利用しやすいものにするために役立つためです。
PWA の複雑さの管理
ウェブ用に記述する JavaScript はすべて、ある時点でメインスレッドとやり取りします。とはいえ、メインスレッドにはすぐに使える複雑な問題が数多く存在します。開発者はこれを制御できません。
メインスレッドは次のとおりです。
- ページの描画を担当します。描画自体は、スタイルの計算、レイヤの更新と合成、画面への描画を含む複雑な複数ステップのプロセスです。
- スクロールなどのイベントをリッスンし、それに反応する役割を担います。
- ページの読み込みとアンロードを行います。
- メディア、セキュリティ、ID の管理。次のように、コードを記述してスレッド上で実行できるようになるのは、これですべてです。
- DOM の操作
- デバイスの機能、メディア/セキュリティ/ID など、機密性の高い API へのアクセス。
Surma が 2019 年の Chrome Dev Summit の講演で述べたように、メインスレッドは過負荷と過小評価されています。
それでも、ほとんどのアプリケーション コードもメインスレッドにあります。
そのすべてのコードがメインスレッドの複雑さを増大させます。メインスレッドは、ブラウザが画面上のコンテンツの配置とレンダリングに使用できる唯一のスレッドです。そのため、コードの処理能力がさらに必要な場合は、コードを迅速に実行する必要があります。アプリケーション ロジックの実行に 1 秒 1 秒かかるため、ブラウザはユーザー入力に応答できず、ページを再描画もできないからです。
インタラクションが入力に接続されなかったり、フレーム落ちが発生したり、サイトの利用に時間がかかりすぎたりすると、ユーザーは不満を感じ、アプリケーションが壊れていると感じて、アプリケーションに対する信頼が低下します。
残念なことに、メインスレッドを複雑にすることは、こうした目標の達成を困難にするほぼ確実な方法です。簡単な手順で解決できますので、メインスレッドが何を行う必要があるかは明確だからです。メインスレッドをガイドとして使用すれば、アプリの他の部分でメインスレッドへの依存を減らすことができます。
関心の分離
ウェブ アプリケーションにはさまざまな処理がありますが、大まかに言うと、UI に直接作用する処理と、そうでない処理に分けることができます。UI 作業とは、以下のような作業です。
- DOM に直接アクセスする。
- 通知やファイル システムへのアクセスなど、デバイスの機能を操作する API を使用します。
- ID(ユーザーの Cookie、ローカル、セッション ストレージなど)に触れます。
- メディア(画像、音声、動画など)を管理する。
- ウェブシリアル API など、承認にユーザーの介入が必要になるセキュリティ上の影響がある
UI 以外の作業には、次のようなものがあります。
- 純粋な計算。
- データアクセス(フェッチ、IndexedDB など)。
- 暗号。
- メッセージ。
- blob またはストリームの作成または操作。
UI 以外の作業は、多くの場合、UI 作業で終わります。ユーザーがボタンをクリックすると、解析結果が返される API に対するネットワーク リクエストがトリガーされ、その結果を使用して DOM が更新されます。コードを記述する際、このようなエンドツーエンドのエクスペリエンスがよく考慮されますが、通常、フローの各部分が存在する場所は考慮されません。UI 処理と UI 以外の処理の境界は、エンドツーエンド エクスペリエンスと同様に、メインスレッドの複雑さを軽減できる最初の場所であるため、考慮することが重要です。
1 つのタスクに集中する
コードを簡素化する最も簡単な方法の一つは、関数を分割して、それぞれが 1 つのタスクに集中できるようにすることです。タスクは、エンドツーエンドのエクスペリエンスを順を追って特定された境界によって決定できます。
- まず、ユーザー入力に応答します。これは UI による作業です。
- 次に、API リクエストを行います。これは UI 以外の作業です。
- 次に、API リクエストを解析します。これも UI 以外の作業です。
- 次に、DOM に対する変更を決定します。これは UI による処理の場合もあれば、仮想 DOM の実装などを使用している場合、UI による処理ではない可能性があります。
- 最後に、DOM に変更を加えます。これは UI による作業です。
1 つ目の明確な境界は、UI による作業と UI 以外の作業の間です。次に、判断呼び出しを実行する必要があります。API リクエストを作成して解析するのは、1 つか 2 つのタスクでしょうか。DOM の変更が UI 以外の作業である場合、API による作業にバンドルされていますか?同じスレッド内、別のスレッドですか?ここでの適切なレベルの分離は、コードベースを簡素化し、メインスレッドから正常に移動できるようにするための鍵となります。
コンポーザビリティ
大規模なエンドツーエンドのワークフローを小さな部分に分割するには、コードベースのコンポーザビリティを検討する必要があります。関数型プログラミングからヒントを得て、次のことを考慮してください。
- アプリケーションが行う作業のタイプを分類する。
- それらのインスタンスに共通の入出力インターフェースを構築する。
たとえば、すべての API 取得タスクは API エンドポイントを受け取って標準オブジェクトの配列を返し、すべてのデータ処理関数は標準オブジェクトの配列を受け取って返します。
JavaScript には、複雑な JavaScript オブジェクトをコピーするための構造化クローン アルゴリズムがあります。ウェブワーカーはメッセージ送信時にこれを使用し、IndexedDB はオブジェクトを保存するためにこれを使用します。構造化クローン作成アルゴリズムで使用できるインターフェースを選択すると、より柔軟に実行できるようになります。
この点を念頭に置いて、コードを分類し、それらのカテゴリに共通の I/O インターフェースを作成することで、コンポーズ可能な機能のライブラリを作成できます。コンポーズ可能なコードは、シンプルなコードベースの特徴です。疎結合された、相互に「次」に配置され、相互にビルドできる部分です。これは、相互接続が深く、簡単に分離できない複雑なコードとは対照的です。ウェブでは、コンポーズ可能なコードがメインスレッドのオーバーワークの分かれ目になる可能性があります。
コンポーザブルのコードを入手したら、メインスレッドからその一部を取得します。
ウェブワーカーを使用して複雑さを軽減
ウェブワーカーは利用率が低いことが多いウェブ機能ですが、広く利用されているため、作業をメインスレッドから移すことができます。
ウェブ ワーカーを使用すると、PWA がメインスレッドの外部で(一部の)JavaScript を実行できます。
ワーカーは 3 種類あります
専用ワーカーは、ウェブワーカーの記述時に最も一般的に考えられるもので、PWA の 1 つの実行中のインスタンスの 1 つのスクリプトで使用できます。パフォーマンスを向上させるため、可能な限り、DOM と直接やり取りしない処理をウェブ ワーカーに移動する必要があります。
共有ワーカーは専用ワーカーと似ていますが、複数のスクリプトで、開いている複数のウィンドウで共有できる点が異なります。これにより、専用のワーカーによるメリットが得られますが、ウィンドウとスクリプト間で状態と内部コンテキストが共有されます。
たとえば、共有ワーカーは、PWA の IndexedDB へのアクセスとトランザクションを管理し、すべての呼び出しスクリプトにトランザクションの結果をブロードキャストして、変更に対応できるようにします。
最後のウェブワーカーは、このコースで幅広く取り上げる Service Worker です。Service Worker はネットワーク リクエストのプロキシとして機能し、PWA のすべてのインスタンス間で共有されます。
実際に試す
いよいよコードタイムです!このモジュールで学習した内容に基づいて、PWA をゼロから作成します。