1. Web API設計の基本原則
設計原則は、Ethical Web Principles [ethical-web-principles]で定められた倫理的な枠組みに直接基づいています。これらの原則は、ウェブプラットフォームの開発に伴う高次の倫理的責任に応えるための、具体的で実践的な助言を提供します。
1.1. ユーザーのニーズを最優先にする(関係者優先順位)
トレードオフが必要な場合は、常にユーザーのニーズを最優先してください。
同様に、APIの設計を始める際には、そのAPIが解決しようとしているユーザーのニーズを理解し、文書化することを必ず行ってください。
インターネットはエンドユーザーのためのものです。 ウェブプラットフォームに加えられるあらゆる変更は、 非常に多くの人々に影響を及ぼす可能性があり、 それぞれの人の人生に深い影響を与える場合があります。[RFC8890]
ユーザーのニーズはウェブページの作成者のニーズよりも優先され、 作成者のニーズはユーザーエージェントの実装者のニーズよりも優先され、 実装者のニーズは仕様策定者のニーズよりも優先され、 仕様策定者のニーズは理論的純粋性よりも優先されます。
すべての原則と同様に、これは絶対的なものではありません。 オーサリングの容易さは、コンテンツがユーザーに届く方法に影響します。 ユーザーエージェントは限られたエンジニアリング資源を優先する必要があり、 それは機能が作成者に届く方法に影響します。 仕様策定者も限られた資源しか持たず、理論的な懸念はこれらすべてのグループの基礎的なニーズを反映しています。
関連資料:
1.2. Webページの閲覧を安全にする
新しい機能を追加する際は、 Webページの閲覧が基本的に安全であるというユーザーの期待を維持するよう設計してください。
Webはハイパーリンク構造に由来する名前です。 Webが活気を保つためには、 ユーザーがどのリンクをクリックしても、 コンピューターのセキュリティやプライバシーの重要な側面に影響を及ぼさないと期待できる必要があります。
例えば、任意のWebサイトが 支援技術の利用を検出できるAPIがあれば、 これらの技術の利用者は未知のWebページの閲覧に不安を感じるかもしれません。なぜなら、あらゆるWebページがこの個人情報を検出できるからです。
ユーザーが安全性について現実的な期待を持てるならば、 Webベース技術と他技術の選択を情報に基づき決定できます。 例えば、ユーザーはWeb上の食品注文ページを利用し、 アプリをインストールしない選択をすることができます。ネイティブアプリのインストールはWebページの閲覧よりもリスクが高いからです。
Web上の安全性がユーザーの期待と一致するようにするために、 新機能の追加にあたり補完的なアプローチを取ることができます:
-
Webのユーザーインターフェースを改善し、ユーザーが何を期待すべきか(何を期待してはいけないか)を明確にする;
-
Webの技術的基盤を変更し、プライバシーに関するユーザーの期待に一致させる;
-
ユーザーの期待が高いほど有益となるケースも考慮し、 その場合は技術的基盤と期待の両方を変えるよう努める。
新しい機能が安全性リスクをもたらす場合でも、 それによってユーザーがWebページ上でより安全にタスクを実行できるならば、 ネイティブアプリをインストールするよりも安全性が向上することがあります。 しかし、この利点はWebページ上でユーザーが合理的に安全性を期待できるという共通目標と天秤にかけて検討する必要があります。
関連情報:
1.3. 信頼できるユーザーインターフェースを保証する
新機能が信頼できるユーザーインターフェースに影響を及ぼすかどうか検討してください。
ユーザーは、アドレスバーやセキュリティインジケータ、権限要求プロンプトなどの信頼できるユーザーインターフェースに依存して、 自分が誰と、どのようにやり取りしているかを理解しています。 これらの信頼できるユーザーインターフェースは、 ユーザーが信頼・確認できる方法で設計される必要があります。 提供される情報が本物であり、Webサイトによる詐称や乗っ取りがないことを保証しなければなりません。
もし新機能が非信頼インターフェースに 信頼できるインターフェースと似た外観を持たせてしまう場合、 ユーザーがどの情報を信頼すべきか理解しづらくなります。
例えば、JavaScriptのalert()は、
ブラウザーの一部のように見えるモーダルダイアログを表示できます。
これはユーザーを詐欺サイトへ誘導するために悪用されることがあります。
この機能が今提案されたとしたら、おそらく採用されないでしょう。
1.4. 意味のある同意をユーザーに求める
ユーザーニーズを満たすために、 Webページは害を及ぼす可能性のある機能を利用したい場合があります。 このような潜在的な害のある機能は、 人々が意味のある同意を与えられるように設計されるべきであり、 拒否も効果的にできなければなりません。
意味のある同意を得るためには、ユーザーが以下を満たす必要があります:
-
何に対して許可を与えるかを理解すること
-
その許可を効果的に与えるか拒否するか選択できること
機能がユーザー同意を必要とするほど強力でありながら、 一般的なユーザーにその内容を説明できない場合は、 機能設計の再考が必要な兆候です。
権限プロンプトが表示され、 ユーザーが許可しなかった場合は、 Webページはユーザーが拒否したと認識していることを何もできないようにするべきです。
同意を求めることで、 Webページがどんな機能を持っているか・持っていないかをユーザーに知らせ、 Webの安全性への信頼を強化できます。 ただし、ユーザーの利益が 新機能の追加によって ユーザーが権限を許可するかどうか決定する負担を正当化できる必要があります。
仕様には、request permission to useやprompt the user to chooseのアルゴリズム([permissions])を使うと良いでしょう。
拒否が最も効果的なのは、サイトが拒否と他の一般的な状況を区別できない場合です。 これにより、サイトがユーザーに同意を強要しづらくなります。
例えば、 Geolocation APIは ユーザーの位置情報へのアクセスを許可します。 これは地図アプリなどで役立ちますが、 他の文脈では危険になる場合もあります ― 特にユーザーの知らないうちに利用された場合は特にです。 ユーザーがWebページによる位置情報利用を許可するかどうか判断できるよう、 権限プロンプトを表示するべきです。 ユーザーが許可しない場合、Webページは位置情報を取得できません。
関連情報:
1.5. 文脈に応じたアイデンティティの利用
Web上の異なる文脈で自分に関する識別情報をどのように提示するか、 人々がコントロールできるようにし、 透明性を持たせてください。
「アイデンティティ」は多様な理解が可能な複雑な概念です。 自分自身の提示・認識方法、他者・集団・組織との関係、 行動や他者からの扱い方など、 さまざまな側面が含まれます。 Webアーキテクチャでは、「アイデンティティ」はしばしば識別子やそれに付随する情報の略称として使われます。
識別子や個人データの紐付けを用いる・依存する機能は、 しばしば1つのAPIやシステムを越えてプライバシーリスクをもたらします。 これは、Web上の行動など受動的に生成されたデータや、 フォームで入力されたような能動的に収集されたデータの双方が含まれます。
このような機能については、利用される文脈を理解し、 他のWeb機能との組み合わせも含めて検討してください。 ユーザーが適切な同意を与えられるようにしてください。 API設計では、 最小限のデータのみ収集するようにしましょう。 永続的な識別子が絶対必要な場合を除き、短期間のみ有効な一時的な識別子を使いましょう。
1.6. すべてのデバイス・プラットフォームをサポートする(メディア独立性)
可能な限り、 Webの機能が異なる入力・出力デバイス、画面サイズ、操作モード、プラットフォーム、メディアで利用できるようにしてください。
Webの主な価値のひとつは、非常に柔軟であることです: Webページはほぼすべての消費者向けコンピュータデバイスで閲覧でき、 多様な画面サイズに対応でき、 印刷メディアの生成にも使えます。 多様な操作方法にも対応できます。 新しい機能はWebプラットフォームの既存の柔軟性に合わせて設計しましょう。
これらの機能は多様な文脈で機能し続け、 本来の意図に対応しないデバイスにも適応できます ― 例えば、モバイルデバイスのタップはclickイベントをフォールバックとして発火します。
機能は最も簡単な使い方でも柔軟性を維持できるよう設計しましょう。
機能がある実装やプラットフォームでは利用可能でも、 他の実装やプラットフォームでは未対応の場合があります。 その場合でも、コードが優雅に失敗したりポリフィルできるよう設計しましょう。 § 2.6 新機能は検出可能にするもご参照ください。
1.7. 新機能の追加は慎重に
既存の機能やコンテンツを考慮した上でWebに新しい能力を追加しましょう。
Webには追加のための多くの拡張ポイントがあります; 例えばHTML § 1.7.2 拡張性をご参照ください。
項目を追加する前に、既存の類似機能との統合を検討してください。 それによって追加のみでは実装できない設計手法が望ましい結果になる場合もありますが、 その場合でも実現可能かもしれません。§ 1.8 現状の利用状況を理解した上で機能の削除・変更を行うも参照してください。
変更や削除が不可能だと決めつける前に、必ず確認してください。
1.8. 現状の利用状況を理解した上で機能の削除・変更を行う
機能や動作を削除・変更する際は、既存のコンテンツとの互換性を最優先してください。
多くのコンテンツが特定の動作に依存するようになった場合、 その動作の削除・変更は推奨されません。 機能や能力の削除・変更は可能ですが、 まず既存コンテンツへの影響範囲と内容を十分に理解する必要があります。 そのためには、既存コンテンツでの機能の利用状況を調査する必要があるかもしれません。
既存の利用状況を理解する義務は、 コンテンツが依存しているあらゆる機能に適用されます。 これはベンダー独自の機能や 実装上のバグとみなされる動作も含みます。 Webの機能は仕様書だけでなく、 コンテンツの利用方法によっても定義されています。
1.9. Webをより良いものにする
Webプラットフォームに新機能を追加する際は、 プラットフォーム全体の品質向上を目指しましょう。
プラットフォームのある部分に欠陥があることを理由に、 その欠陥をさらに拡張・追加することは正当化されません。 それはプラットフォーム全体の品質をさらに低下させることになります。
既存の問題ある設計パターンとの一貫性を理由に、 それらのパターンを新しいWeb APIやプラットフォーム機能に拡張するのは良い理由ではありません。 可能な場合は、既存の欠陥を軽減することで プラットフォーム全体の品質を高める新しいWeb機能を構築しましょう。
既知の欠陥を持つプラットフォーム機能を拡張する場合は、 欠陥への対処方法を検討してください。 既存の問題を完全に解決する必要はありませんが、 欠陥のある機能を拡張する際は何らかの工夫が期待されます。 そうすることで新規・増加した利用によって 欠陥がもたらす害が増大しないようにできます。
Webプラットフォームの各部分は独立して進化します。 特定のWeb技術に現時点で問題があっても、 次回の改訂で修正される可能性があります。 問題の複製は修正を困難にします。 この原則を守ることで、プラットフォーム全体の品質向上を確実にしていけます。
1.10. ユーザーデータの最小化
機能は、ユーザーの目的を達成するために必要最小限のデータで動作するよう設計してください。
データ最小化は、 データの不適切な開示や悪用リスクを制限します。
Web APIは、サイトが大量のデータや汎用的なデータよりも、 少量のデータやより細かく限定されたデータを要求・収集・送信しやすくなるよう設計しましょう。 APIは、特に個人データについて、 ユーザーによる細かい制御も提供すべきです。 追加機能に追加データが必要な場合は、ユーザー同意(例:権限プロンプトやユーザーアクティベーション)に基づいて有効化できるようにしましょう。
2. 言語横断的なAPI設計
2.1. シンプルな解決策を優先する
解決しようとするユーザーニーズに対して、シンプルな解決策を粘り強く探してください。
シンプルな解決策は複雑なものよりも一般的に優れていますが、見つけるのが難しい場合もあります。 シンプルな機能はユーザーエージェントの実装やテストが容易で、 相互運用性も高くなり、 作者の理解もしやすくなります。 特に、最も一般的な利用ケースが簡単に達成できるよう設計することが重要です。
ユーザーニーズが明確に定義されていることを確認してください。 これによりスコープの膨張を防ぎ、 APIが本当にすべてのユーザーのニーズを満たすことができます。 もちろん、 複雑または稀な利用ケースも解決する価値がありますが、 その解決策は利用が多少複雑になるでしょう。 アラン・ケイの言葉通り、 「シンプルなことはシンプルに、複雑なことは可能に」しましょう。
ただし、一般的なケースはしばしばシンプルですが、 共通性と複雑性が必ずしも一致するとは限らないことに注意してください。
関連情報:
2.2. 高水準APIと低水準APIのトレードオフを考慮する
高水準APIはユーザーエージェントがユーザーのためにアクセシビリティ・プライバシー・使いやすさ等さまざまな介入を行いやすくなります。
フォントピッカー(高水準API)は TAGにより推奨され、 Font Enumeration API(低水準API)よりも ほとんどの利用ケースをカバーしつつ、 指紋取得リスクもないためユーザープライバシーも保護できます。 ネイティブのフォントピッカーはアクセシビリティも内蔵されており、ユーザーに一貫性を提供します。
低水準APIは作者に実験の余地を与え、 高水準APIが利用パターンから自然発生的に生まれることを可能にします。 また、高水準APIでは対応できない利用ケースへの脱出口にもなります。
低水準のビルディングブロックは常にWeb APIとして公開できるわけではありません。 その理由には、ユーザーのセキュリティやプライバシー保護、 Web APIを特定のハードウェア実装に縛り付けないためなどがあります。 それでも可能な限り、 高水準APIは低水準APIのビルディングブロックを使って設計するべきです。 これによりAPIの抽象度の判断材料にもなります。
層構造がよく設計された解決策は、 使いやすさとパワーのトレードオフ曲線に連続性を持たせ、 利用ケースの複雑さが少し増すだけでコードの複雑さが急増する「断崖」を避けることができます。
2.3. 名前付けは慎重に
APIの命名は慎重に行いましょう。 適切な命名は作者が正しくAPIを使う助けになります。
詳細なガイダンスは命名原則セクションをご覧ください。
2.4. 一貫性を保つ
API設計では先例を考慮し、一貫性を持たせることが良い慣習です。
APIの使いやすさと一貫性の間には緊張関係があり、 既存の先例が使いづらい場合は一貫性を破って改善する価値もありますが、 その改善が非常に大きなものでない限りは慎重に判断しましょう。
Webプラットフォームは時間とともに徐々に進化してきたため、 互いに排他的な複数の先例が存在することもあります。 どの先例に従うかは、 普及度(同じならより普及している方)、 APIの使いやすさ(同じならより使いやすい方)、 APIの新しさ(同じならより新しい方)などを考慮して判断しましょう。
内部一貫性と外部一貫性の間にも緊張関係があります。 内部一貫性はそのシステム内での一貫性、 外部一貫性は外部世界との一貫性です。 Webプラットフォームの場合、APIが属する技術(例:CSS)内での一貫性、 Webプラットフォーム全体での一貫性、 または特定分野との外部一貫性の三層が現れることがあります。 その場合、多数のユーザーがどちらかに馴染みがあるかを重視しましょう。 多くのAPIのターゲットユーザーは定義された技術に慣れているので、 その技術内の一貫性を優先しましょう。
命名一貫性については別途セクションがあります。
2.5. 機能仕様のガイダンスに従う
機能を使用する際は、その機能の仕様書のガイダンスに従いましょう。
他仕様で利用される機能の仕様書には、 それらの機能の利用ガイダンスを含めるべきです。 これによりプラットフォーム全体で機能が正しく一貫して使われます。
以下はそのようなガイダンスの一部です:
該当するコミュニティに相談しながら仕様を利用することで、 新しい利用をコミュニティに知らせ、 仕様の双方に改善の機会を生み出せます。
2.6. 新機能は検出可能にする
作者がプログラムから自分の機能の可用性を検出できるようにしましょう。 そうすればWebコンテンツは機能が存在しない場合も優雅に対応できます。
既存の機能でもページで利用できない理由は様々です。 よくある理由は、未実装である場合やセキュアなコンテキストでのみ利用可能な場合です。
作者が各ケースごとに異なるコードを書く必要がないようにしましょう。 作者が特定のケースしか知らなくても、コードがすべてのケースに対応できるようになります。
機能が利用可能でも、必要なデバイスが存在しないため利用できない場合は、 機能自体の可用性とデバイスの可用性を別々に検出できるようにしましょう。 これは、作者がデバイス未接続時には接続や有効化を提案できるなど、異なる対応ができるようにします。
§ 9.2 デバイス選択・列挙APIの公開は慎重にも参照ください。
作者は常にJavaScriptから機能を検出できるようにし、 場合によっては利用する言語でも検出できるようにしましょう(例:CSSの@supports)。
場合によっては機能検出を許可しないことが適切な場合もあります。 機能検出の可否はユーザーニーズに基づいて判断してください。 機能検出が可能だと設計原則が満たされなくなる場合は、検出をサポートしないでください。
機能の可用性検出は、同意が得られたかの検出とは異なります。 通常、機能が実装されているかの検出は、利用許可が与えられたかの判断とは別に行います。 場合によっては機能検出を無効にすることで利用要求の拒否が可能になることもあります。
また、一般に開発者に公開されていない機能については、 機能検出をサポートすべきではありません。 例えば、プライベートブラウジングモードは仕様上認識されていますが、 作者には公開されていません。 プライベートブラウジングモードがユーザーニーズを支援するためには、機能検出できてはいけません。
関連情報:
2.7. テキスト形式は人間のために設計する
テキスト形式は人間が容易に作成・利用できるよう設計しましょう。 テキスト形式は透明性も高めます。
可読性をコンパクトさより優先しましょう。 ファイルサイズの最適化はツールで行えますし、 時間が経つほど重要度は下がります。 ファイルサイズが特に重要な場合は、テキスト形式が不適切かもしれません。
テキスト形式を扱う人はテキストエディタで容易に作成・修正できるべきです。 編集時に多様なエラーが生じますが、発見と修正に苦労することもあります。
編集の処理方法にある程度柔軟性があることが期待されています。 構文上の柔軟性(空白・引用符・区切り記号など)を明確に定義することで、 編集しやすく一貫した結果が得られるようにしましょう。
構文エラーの影響範囲を限定することで、 ロバスト性や人間の使いやすさを改善できます。 そのためには、仕様で処理方法やエラー処理を完全に定義し、 すべての入力で一貫した結果を得られるようにしましょう。
機械のみが利用する形式であれば、バイナリ形式の方が効率的ですし、 人が直接編集することも防げます。
2.8. 新機能をセキュアなコンテキストに限定することを検討する
認証・完全性・機密性などセキュアなコンテキストでしか得られない安全がなければリスクとなる機能は、 常にセキュアなコンテキストに限定してください。
その他の機能については、TAGのメンバー間でも一般的な方針で合意がありません。 すべての新機能(既存機能の拡張以外)はセキュアなコンテキストに限定すべきという意見もあります。 これはHTTPS利用の促進につながり、ユーザー全体の安全性向上に役立ちます。
一方で、明確なセキュリティ・プライバシー影響がある場合のみ限定すべきという意見もあります。 これにより、ユーザーの安全性やプライバシーに影響しない新機能を使ったWebページ作成の障壁が下がります。
SecureContext]
拡張属性をインターフェース・名前空間・各メンバー(メソッドや属性)に付与することでセキュアなコンテキストに限定できます。
ただし、一部のAPI(例:イベントの発火)などは、仕様の規定文で限定を定義すべきです。
この場合、将来のAPI開発者が簡単に限定できるよう[SecureContext]のメカニズム拡張も検討しましょう。
ただし、コードが機能未対応時に優雅に対応できない場合、 セキュアなコンテキスト限定がライブラリなどに問題をもたらすこともありますので注意してください。
2.9. プライベートブラウジングモードの利用を明かさない
機能が作者にプライベートブラウジングモードの利用を検出させないようにしてください。
一部の人はプライベートブラウジングモードを自身の安全のために利用しています。 そのため、プライベートモード利用の事実自体がセンシティブな情報となり得ます。 この情報が権力を持つ第三者(雇用主・親・パートナー・国家権力など)に知られると 害となる場合もあります。
このような危険性から、 サイトはプライベートブラウジングモードの利用状況を検出できてはいけません。
ユーザーが即座に支払いを中止したかのように見せることを可能にします。
これによりユーザーエージェントは プライベートモード中に支払い要求を自動で中止でき、 (ユーザーの配送先や支払い情報など)センシティブな情報を保護しつつ プライベートモード利用をサイトに明かしません。
関連情報:
2.10. APIのプライベートブラウジングモード時の挙動を検討する
必要に応じて、APIがプライベートブラウジングモード時にどのように振る舞うか仕様で定めてください。
例えば、APIが単一ユーザーのアクティビティを プライベート・非プライベート両方で関連付け可能にしてしまう場合は、 ノイズ追加や許可プロンプトなど緩和策を検討し、 ユーザーに意味のある同意(§ 1.4 意味のある同意をユーザーに求める)を促す情報を提供しましょう。
プライベートモードは利用者の端末に痕跡を残さずWebを閲覧できるものです。 そのため、クライアントサイドストレージを提供するAPIは プライベートモード中に保存されたデータを モード解除後に永続化しないようにすべきです。 この処理はサイトから機能の違いが検出できないように行ってください。
同一サイトで通常モードとプライベートモードのセッションが同時にある場合、 プライベートモード側のストレージ変更は他のセッションに明かされてはいけません(逆も同様)。 (storage eventは 他のセッションのwindow objectには発火してはいけません。)
関連情報:
2.11. 支援技術の利用状況を明かさない
APIが作者にユーザーの支援技術利用を検出させないようにしてください。
Webプラットフォームは障害のある人にもアクセシブルでなければなりません。 サイトが支援技術利用者を検出できる場合、そのユーザーへのサービス提供を拒否・制限できてしまいます。
支援技術利用者はしばしば社会的に脆弱な立場であり、 利用状況はセンシティブな情報です。 APIがこの情報を公開すると、他者(国家権力含む)に知られるリスクがあります。
時には支援技術利用者の体験向上を目的とした提案がなされますが、 副作用として利用状況を公開してしまうことがあります。 こうした提案は善意ですが、 § 1.2 Webページの閲覧を安全にするに違反するため、代替策を検討してください。
Accessibility Object Model (AOM) は以前 特定イベントの発火で支援技術利用が明かされる設計でした。
現在AOMはこれらのイベントを除去し、 支援技術利用を明かさない疑似DOMイベントに置き換えています。
関連情報:
2.12. 強力なAPIにはユーザーアクティベーションを必須とする
一部の強力なAPIは、迷惑なUI(自動再生音声など)、 ユーザーデータの公開(クリップボード操作など)、 ユーザーに分かりづらいバックグラウンド動作(ローカルストレージへのアクセスなど)、 または信頼できるUIとの対話(許可プロンプト・ハードウェア機能など)を引き起こす場合があります。 これらのAPIは、ユーザーの意図(ユーザーアクティベーションなど)を何らかの形で示してから動作するよう設計しましょう。 これはユーザーがそのWebページと意図的にやり取りしていることを示します。
ユーザーアクティベーションの定義は HTML標準で詳しく説明されています。 APIのユーザー体験への影響やリスクを考慮し、 アクティベーションが一度だけ(sticky)か、 定期的に(transient)か、 API呼び出しごとに(transient consuming)必要かを検討してください。
ユーザーアクティベーションは多くの場合必要ですが、 十分とは限らず、 意味のある同意も重要です。
2.13. 非完全アクティブなBFCached文書もサポートする
可能なら、非完全アクティブなBFCached(Back/forward cached)文書での挙動も仕様で定めてください。
機能が以下のいずれかに該当する場合:
-
文書の「外部」とやり取りする(情報を文書に送るなど)
-
クロスドキュメントの相互作用・リソース共有が可能(ロック保持など)
-
ユーザーが離脱・復帰時に 非完全アクティブ状態が維持されると不具合が生じる(保存状態が複数ナビゲーションにまたがらないことを期待するなど)
BFCached文書サポートガイドに従い、 非完全アクティブなBFCached文書での動作を仕様で明記しましょう。
注意: 文書がBFCache以外の理由(iframeの切り離しなど)で非完全アクティブになる場合もありますが、 ここではBFCacheが関与するケースのみを対象とし、 その他の非完全アクティブ化は対象外です。
2.14. サードパーティツールとの互換性より使いやすさを優先する
新機能設計では使いやすさを第一に、サードパーティツールとの互換性は第二に考えましょう。
Webプラットフォームは多様なツールエコシステムの恩恵を受けています。 新機能の構文がサードパーティツールと衝突して問題が生じることもよくあります。 サードパーティツールは新機能の試作にもよく使われるため、特にありがちです。
一般にWebプラットフォームの機能はサードパーティツールよりも長く使われるため、 最適な構文や機能性を優先すべきです。
場合によっては衝突が多くのWebサイトに問題をもたらし、 機能の構文を再設計して回避する必要が生じることもあります。
Array.prototype.contains()は
PrototypeJS(数百万サイトで利用されていたライブラリ)との衝突を避けるため
Array.prototype.includes()に改名されました。
ただし、こうしたケースは例外とすべきです。
サードパーティツールとの衝突回避の可否は、 破壊の重大さ・ツールの普及度など複数の要素を考慮しましょう。
最も重要なのは、構文を変更してサードパーティツールとの衝突を避けた場合、 Webプラットフォーム機能の使いやすさがどれほど損なわれるかです。 同程度の使いやすさの選択肢が複数ある場合は、 なるべくツールへの影響が少ないものを選びましょう。
ただし、ツールの衝突回避で機能の使いやすさが大きく損なわれる場合、 重大な実運用サイトの破壊でもない限り、妥協は許容できません。
将来的なジレンマを減らすためにも、 言語には作者が将来のネイティブ機能を壊さずに拡張できる仕組みを用意しましょう。
2.15. 複雑な型は単純な型の組み合わせで構築する
サブタイプは常にスーパータイプの代わりとして使えるよう定義しましょう。 親インスタンスに当てはまることが子にも常に当てはまらないなら、継承を使ったインターフェースは避けましょう。
継承ではサブタイプがスーパータイプの代替となります。 サブタイプは親と同じ属性・メソッドを提供し、動作も一貫させる必要があります。 サブタイプが挙動を変えると、スーパータイプ扱いの処理が正しく動かなくなります。
型理論ではこれは リスコフ置換原則 と呼ばれます。
Event
型は、KeyboardEvent
や PointerEvent
のスーパータイプです。
イベントは常に、bubbles
のように、
適用可能な同じ基本的なプロパティや動作セットを持ちます。
サブタイプは新しいプロパティや動作を追加しますが、
それらのサブタイプのインスタンスも依然としてすべて Event
として振る舞います。
より単純なアプローチは、しばしば継承を避けて 既存の機能を合成によって再利用することです。 新しい項目は、必要な既存コンポーネントを保持するプロパティを定義できます。
HTMLInputElement
を特殊化するのは困難です。
なぜなら
input
要素は非常に複雑であり、
HTMLInputElement
の特殊化はその複雑さを維持する必要があるからです。
カスタム要素は、フォームと連携するために必要な項目をアタッチ(つまり合成)することで、
HTMLInputElement
の複雑さを扱うことなく実現できます。
3. HTML
このセクションでは、HTMLを通じて公開される機能の設計原則について説明します。
3.1. 類似機能にはHTML属性名を(のみ)再利用する
HTML属性で指定する機能を追加する場合は、他の要素に同様の機能を指定する既存の属性名があるか確認してください。 既存のHTML属性名を再利用することで、著者は既存の知識を活用でき、 言語全体の一貫性を維持し、 用語集も小さく保てます。
既存のHTML属性を再利用する場合は、可能な限り既存属性の構文に近づけるようにしてください。
for
属性は
label
要素で関連付けるフォーム要素を指定するために導入されました。
後に
output
要素でも、どの要素が値計算に影響するかを指定するために再利用されました。
後者はスペース区切りのidリストを受け入れ、前者は1つのidのみです。
しかし、どちらも同じ構文に従っています。もし一方がidリスト、もう一方がセレクターだった場合はアンチパターンです。
逆もまた然りです。 既存のHTML属性名と機能が類似していない場合は、再利用しないでください。
type属性は
input
や
button
要素では要素タイプの専門化に使われますが、
他の要素(
link、
script、
style
など)ではMIMEタイプ指定に使われます。
これはアンチパターンであり、どちらか一方は別名にすべきでした。
3.2. 短い値リストにはスペース区切り属性、長いリストには個別要素を使用する
要素のメタデータとして値のリストを指定する場合、一般的にはスペース区切りリストを使い、DOMTokenList
として公開します。
class
属性はスペース区切りのクラス名リストを取ります。
classList
はDOMTokenList
で、著者がクラス名を追加・削除できます。
sandbox
属性はスペース区切りのサンドボックスフラグリストを取ります。
iframe.sandboxはDOMTokenList
で、著者がフラグを追加・削除できます。
Webプラットフォームの他部分との一貫性も重要です。値の区切り文字が異なっても同様です。
構文に関わらず、属性は短い値リストのみに使うべきです。 長いリストを属性に埋め込むのは推奨されません。 現在の慣習では、リスト項目やそのメタデータを個別の要素で表現します。 これらの要素は対象要素の子要素にも、属性でリンクしても構いません。
まれに他のトレードオフが必要な場合もあります。
srcset
属性はカンマ区切りの画像候補文字列リストを許容します。
この構文は子要素リストより冗長性を避けるため、
そして
img
要素が空要素で子要素を持てないため選ばれました。
各リスト項目に複数値を含むため、スペース区切り構文は不可能です。
3.3. HTMLパーサーを停止しない
設計が外部リソースの処理のためにHTMLパーサーを停止する必要がないようにしてください。
ページをパースする際、ブラウザーは必要なアセットを発見し、 優先順位を決めて並列にロードします。 そのパースが、後続リソースの発見をブロックするリソースによって妨害されることがあります。 最悪の場合、ブラウザーはアイテムを並列ではなく逐次でダウンロードします。 良くても推測的なパースに基づいてダウンロードをキューし、間違っている場合があります。
パーサーをブロックする機能は、一般に後続コンテンツの前に追加コンテンツをパーサーに供給したいためです。
これはレガシーの<script src="…">要素などで
document.write(…)を使ってパーサーに注入する場合です。
上記のパフォーマンス問題のため、新機能ではこの方法を使ってはなりません。
3.4. レンダリングをブロックする機能は避ける
リソースのロードやレンダリング前の操作が必要な機能は、 しばしば空白ページ(または前のページ)をもたらし、ユーザー体験が悪化します。
このような機能は、全体のユーザー体験が向上する場合のみ追加を検討してください。 典型的な例は、スタイルシートのダウンロード・処理のためにレンダリングをブロックすることです。 代替案である非スタイルの一瞬の表示は望ましくありません。
詳細は§ 10.2.1 一部APIはDedicated Workerのみに公開するべきも参照。
3.5. 属性の同期を保つ
新しい内容属性には同名のIDL属性を対応させ、両者の状態を同期させてください。 同期したIDL属性で命名が不一致だと混乱の元となるため、避けてください。
inputの
value、
optionの
selected、
さらに
inputの
checked
などがあります。これらの内容属性は同名のIDL属性に反映されません。
各IDL属性は
defaultValue、
defaultSelected、
defaultChecked
です。
3.6. URLを含む属性は主目的に基づいて命名する
要素が属性内のURLへのナビゲーションを可能にする場合、その属性名はhrefとしてください。
例:
a
要素の
href
属性。
Note: 振り返ると
formの
action
属性はhrefとすべきでした。
要素が指定されたURLのリソースをロードする場合、その属性名はsrcとしてください。
例:
imgの
src属性、
scriptの
src属性。
Note: HTMLにはレガシーの不一致が複数存在し、真似るべきではありません。
例として
linkの
href
属性は、要素の
rel
属性値によってナビゲーションやリソースロードどちらにも使われ得ます。
属性が要素の主目的に補助的なURLを指定する場合は、
その意味に沿った名前にしてください。
例:
videoの
poster、
qの
cite、
aの
ping
など。
Note: URLを含む属性はIDLではUSVString
型で表現すべきです。
詳細は§ 8.2 文字列は適切な型で表現する参照。
3.7. 各HTML要素は単一の目的を持たせる
各HTML要素には明確な目的を持たせてください。 HTML属性で要素のセマンティクスを修正できますが、属性で本来の目的を根本的に変えてはいけません。
複数のモードで動作可能な要素を定義するよりも、それぞれのモードごとに個別要素を持つ方が望ましいです。 その方が著者にとって使いやすくなります。
input
要素は
type
属性で非常に多様なモードを切り替えます。
これらのモードはすべてユーザー入力という共通目的を持っていますが、目的が広すぎます。
それぞれの意味は個別要素名によってより明確に表現できたはずです。
属性によるセマンティクスのオーバーロードで、異なる振る舞いを選択する場合もあります。 ただし、どの属性値でも目的が変わらない場合のみ意味があります。
反例としては、過度な専門化や文脈依存のセマンティクスも挙げられます。
4. カスケーディングスタイルシート(CSS)
このセクションでは、CSSを通じて公開される機能の設計原則について説明します。
4.1. CSSプロパティは独立してカスケードすべきものごとに分ける
CSSプロパティとしてまとめるべき値、分離すべき値を、独立して設定する意味があるかどうかで判断してください。
CSSのカスケードは、異なるルールやスタイルシートからの宣言が互いに上書きできるようにします。 一緒に上書きすべき値は1つのプロパティでまとめてカスケードさせます。 一方、独立して上書きすべき値は別プロパティにしてください。
一方、initial-letter-alignプロパティは、文書全体に対する配置方針を設定するため、 別プロパティにすべきです。これは全体のスタイル選択や使用スクリプト(ラテン、キリル、アラビアなど)によるものです。
4.2. CSSプロパティの継承有無を適切に選択する
プロパティを継承すべきかどうかは、祖先と子孫両方で設定した場合に効果が上書きされるべきか、加算されるべきかで判断してください。
子孫要素でプロパティを設定したとき、祖先で設定した効果を上書きする必要があるなら、継承すべきです。
子孫要素で設定したプロパティが祖先の設定に加算される独立した効果なら、継承しない方がよいです。
継承しないプロパティについて、要素の処理時に祖先の値も参照する仕様は「コードの臭い」であり、継承すべきだった可能性があります。 逆に、継承プロパティについて、親と同じ値の場合は無視する仕様になっている場合も「コードの臭い」であり、継承すべきでなかった可能性があります。
もしbackground-imageプロパティが継承されていた場合、 半透明画像が各子孫要素で繰り返し表示されるのを避けるために、仕様が複雑になったはずです。 この複雑さは、親と同じ値の場合だけ振る舞いを変えるなど、上記の「コードの臭い」に該当します。 これは継承すべきでなかったことを示します。
もしfont-sizeプロパティが継承されない場合、 初期値として祖先チェーンをたどって値を探す必要があり、これも「コードの臭い」です。 つまり継承すべきだったことを示します。
4.3. 継承方法に応じて算出値型を選ぶ
CSSプロパティの算出値は、 どのように継承されるか、それが他のプロパティに依存する場合も含めて決定してください。
継承とは、要素が親のプロパティの算出値をそのまま受け取ることです。 算出値到達前の処理ステップが継承値に影響し、算出値後(例えば使用値)は影響しません。
しかし、この場合の算出値は<number>1.4であり、 <length> 28pxではありません。 (使用値は28pxです。)
line-heightプロパティは異なるfont-sizeを持つ要素にも継承でき、 それら要素でline-heightに依存するプロパティは、 継承元要素ではなく自分のfont-sizeを使って計算します。
< body style = "font-size: 20px; line-height: 1.4" > < p > この本文はline-heightが28pxです。</ p > < h2 style = "font-size: 200%" > この見出しのline-heightは56pxですが、bodyで宣言されたにも関わらず28pxにはなりません。 つまり、40pxのフォントがline-heightをはみ出しません。</ h2 > </ body >
このようなnumber値はlength値より継承性が良いため、line-heightには一般的に推奨されます。
関連情報:
4.4. CSSプロパティや値の命名を適切に行う
CSSプロパティ名は通常名詞、値は形容詞(時に名詞)です。
プロパティや値の単語はハイフンで区切ります。 略語は基本的に避けます。
できるだけ語根(例:"size")を使い、接頭・接尾辞付きの形(例:"sizing")は避けます。
プロパティ値リストは新しい値を追加できるように選びます。 yes, no, true, falseのような値や、実質同じ複雑な名前は避けます。
プロパティ名に "mode" や "state" のような語は避けます。 プロパティ自体がモードや状態を設定するものだからです。
命名全般については§ 12 命名原則も参照してください。
4.5. コンテンツはデフォルトで見えてアクセス可能であるべき
CSSプロパティやCSSレイアウトシステム(たいていdisplayプロパティ値)は、デフォルトでコンテンツが可視・アクセス可能・利用可能になるよう設計してください。
overflow: hiddenやleft: -40emのように、明示的にその挙動を選んだ場合のみ起こるべきです。
display: flexやposition: relativeのようなデフォルト値で起きてはいけません。
5. JavaScript言語
5.1. Web APIにはJavaScriptを使用する
Web用の命令型APIを設計する際はJavaScriptを使いましょう。 特に、言語固有のセマンティクスや慣習に自由に依存して構いません。一般化する必要はありません。
CustomElementRegistry.define()
メソッドは
Constructor Methodへの参照を取ります。
これはJavaScriptにクラスが導入されたことと、メソッド参照が容易であることを活用しています。
5.2. 実行完了セマンティクスを維持する
JavaScript実行コンテキスト外から状態変更が発生した場合は、タスク間で変更を伝播してください(例:タスクをキューする、またはレンダリングの更新の一部として)。
C++やRustのような低レベル言語と異なり、JavaScriptは歴史的に常に1つのコードのみが同時実行される前提です。 そのため、JavaScriptの著者は、関数実行中にデータが予期せず変化することはないと考えています。
開発者の操作以外で発生する変更や非同期で届く変更は、他のJavaScript実行中(マイクロタスク間も含む)に発生してはいけません。
whileループ)や、解決済みPromiseにawaitした直後は、開発者は次のようなことは起こらないと考えます。
-
HTMLパーサーがネットワークから新しいコンテンツを読み込んでDOMが更新される
-
img.widthが画像データのロードで変化する -
Gamepadのボタン状態が変化する -
scrollTopが変化する(スクロール自体は発生しても) -
同期メソッドの挙動が非同期状態変化で変化する。例えば
LockManagerが同期メソッドを持つ場合、他ウィンドウでの同時呼び出しに左右される。
これらは現在実行中のスクリプトによって更新されるものではないので、現在のタスク中には変化すべきではありません。
データは開発者操作の結果として同期的に更新できます。
node.remove()
はDOMを同期的に変更し、即座に観測できます。
このルールに違反が許される状況もいくつかあります:
-
現在時刻の観測(
Date.now()やperformance.now()など)。ただし、document.timeline.currentTimeのようにタスク全体で一貫した時刻を示すのも有用です。 -
同期的作業を中断するための関数(例:
IdleDeadline.timeRemaining())。 -
驚くべきUI変更からユーザーを守る状態(例:一時的アクティベーション)。
navigator.userActivation.isActiveはこの場合はメソッド推奨に違反します。
5.3. ガベージコレクションを公開しない
JavaScript Web APIで、ガベージコレクションのタイミングを著者が知る手段を提供しないようにしてください。
ガベージコレクションのタイミングはユーザーエージェントごとに異なり、パフォーマンス改善で今後も変化します。 タイミングが公開されると、プログラムが文脈ごとに異なる挙動となり、著者はそれに対応する追加コードが必要となります。 また、十分なコードがタイミングに依存する場合、ユーザーエージェントが異なるGC戦略を実装しにくくなります。
したがって、ガベージコレクション後にプロパティがになるような弱参照APIは公開してはいけません。
JavaScriptコード内のオブジェクトやデータの寿命は予測可能であるべきです。
getElementsByTagName
はHTMLCollection
オブジェクトを返します。
同じDocumentオブジェクトで同じタグ名を指定して2回呼ぶと、同じオブジェクトが返されることがあります。
実際には、ガベージコレクションされていなければ同じオブジェクトが返ります。
これはGCタイミングによって挙動が変わることを意味します。
もしgetElementsByTagName
が今設計されるなら、
デザイナーへの助言は「常に同じ出力を再利用する」か「毎回新しいHTMLCollectionを生成する」のいずれかにすべきです。
getElementsByTagName
はGCタイミングに依存することを示しません。
対照的に、GCに明示的に依存するよう設計されたAPI(WeakRefやFinalizationRegistryなど)は、GCとの連携について著者に正しい期待を持たせます。
6. JavaScript API設計
6.1. WebIDLの辞書・インターフェース・名前空間を適切に使用する
新しいAPIには適切なWebIDLメカニズムを使用してください。
WebIDLはWeb API定義のために複数の構造(辞書・インターフェース・名前空間)を定義しています。 それぞれ目的に応じた異なる特徴を持ちます。
目的は、Web開発者にとって使いやすく一貫性のあるAPIを保証しつつ、「ダミークラス」や機能のないクラスなどの落とし穴を避けることです。
「設定」や「入力専用」データには辞書を使う
APIの一部が一時的なデータ(特にパラメータ・設定・オプション)を受け付ける場合は辞書を選択してください。
辞書はデータを保存・変更せず、呼び出し時だけ使う場合に最適です。
例えば、Web ShareのShareDataメンバー([WEB-SHARE]):
dictionary ShareData { USVString title ; USVString text ; USVString url ; };
一般的な利用例:
await navigator.share({text: "Text being shared" });
辞書は拡張性が高く、必要に応じてオプションフィールドを追加しやすいです。
辞書のメンバーはデフォルトでオプション(任意)ですが、必要ならrequiredで必須にもできます。
辞書はJavaScriptで非常に自然(慣用的)に使えます。
{ ... }をインラインで渡すのがJavaScriptで設定値を渡す最も自然な方法です。
辞書はユーザーエージェントの扱いにより将来性も高いです。 実装が理解しない辞書メンバーは無視されるため、古いコードを壊さず新しいメンバーが追加できます。
辞書はオブジェクトの型判定(instanceof)が本質的に意味を持たない場合(常にObject)に最適です。
辞書は「値渡し」でメソッドに渡されます(コピーされる)。 ブラウザーエンジンはJavaScriptオブジェクトからWebIDL表現へ変換する際に未知のメンバーを削除します。 つまりAPIに渡した後で値を変更しても効果はありません。
再びShareData辞書を例に:
const data= { "text" : "Text being shared" , // "whatever"パラメータを含まないブラウザでは無視されます。 "whatever" : 123 , }; let p= navigator. share( data); // .share()呼び出し後に変更しても反映されません data. text= "New text" ;
機能・状態・識別にはインターフェースを使う
インターフェースはJavaScriptのクラスにほぼ相当します。 状態(可視プロパティや内部スロット)とその状態に対する操作(メソッド)をまとめる必要がある場合に使います。
辞書と違い、インターフェースは:
-
状態を持つインスタンスができる
-
読み取り専用プロパティも持てる
-
代入で副作用を起こせる
-
グローバルスコープで特定クラスの
instanceof判定ができる
インターフェース定義はグローバルスコープにも公開され、静的メソッドの指定も可能です。
例:URLインターフェースのcanParse()静的メソッド
if ( URL. canParse( someURL)) { // 何か処理... }
状態を持つインターフェースには可能ならコンストラクタを与えてください。
状態を持たないクラスにはコンストラクタを追加しないでください。
これは「ダミークラス」(インスタンスが静的メソッドと同じことしかできない)の悪い慣習です。
DOMParserやDOMImplementationはダミークラスの例です。
インターフェースデータを使いやすくするためのシリアライザーを提供する
インターフェースのインスタンスを多くのアプリケーションで使いやすい形に変換するシリアライザーを追加してください。
.toJSON()メソッドはインスタンスの有用なJSONシリアライズを生成します。
.toBlob()メソッドはバイナリ表現を抽出できます。
これによりオブジェクトがAPIで自然に使えるようになります。
例:GeolocationPositionはtoJSON()メソッドを提供します:
const position= await new Promise(( resolve, reject) => { navigator. geolocation. getCurrentPosition( resolve, reject); }); const message= JSON. stringify({ user: userId, time: Date. now(), position, // .stringify()は.toJSON()を自動で呼び出す });
振る舞いのみのユーティリティには「ダミークラス」を避け名前空間を選択する
名前空間は一連の静的プロパティやメソッドをまとめるために存在します。
名前空間にはプロトタイプはありません。
例:JSのMath、Intl、Atomics、Consoleなど。
静的関数が1~2個しかない場合は新しい名前空間を作るのは大げさです。 既存オブジェクトに追加した方が適切な場合もあります。
逆に、名前空間が大きくなりすぎたり広すぎる場合は、よりよい整理や分割が必要です。
「疑似名前空間」
WebIDLインターフェースは属性を持てるため、非構築可能インターフェースを属性として定義することで「疑似名前空間」を作ることができます。
よくある例はnavigatorオブジェクトに付与された各属性です。
navigatorオブジェクトはインターフェース定義(Navigator)を持ち、
それ自体もさらに機能をインターフェースインスタンス経由でアクセスする属性を含みます。
例:
-
navigator.credentials- Credentials Management APIのCredentialsContainerインターフェース -
navigator.geolocation- Geolocation仕様のGeolocationインターフェース -
navigator.permissions- Permissions仕様のPermissionsインターフェース
疑似名前空間は、仕様著者が特定インスタンス(例えば特定ブラウジングコンテキストに適用される権限など)を参照したい場合に有用です。 そのインスタンスがページに可視状態を持たない場合でも使えます。
6.2. 属性はデータプロパティのように振る舞うべき
[WEBIDL]属性は単純なJavaScriptオブジェクトプロパティのように振る舞うべきです。
実際にはIDL属性はgetter/setterメソッドで実装されますが、JSプロパティのように振る舞わせるには:
-
getterは副作用を持ってはいけません。
-
getterは複雑な操作をすべきではありません。
-
obj.attribute === obj.attributeが常に真になることを保証してください。 getter呼び出しごとに新しい値を生成しないでください。 -
可能なら
obj.attribute = xの後にobj.attribute === xが成立するようにしてください。 (型変換などが必要なら成立しない場合もありますが、できる限り通常コードでは成立させる。)
属性にしようと考えた機能でこの動作にならない場合は、メソッドの方が適切です。
6.3. オブジェクトをライブにするか静的にするか検討する
APIが内部状態を表すオブジェクトへのアクセスを提供する場合、そのオブジェクトが状態変化に合わせて更新され続けるべきか決定してください。
常に最新状態を表すオブジェクトはライブオブジェクト、 作成時点の状態のみを表すオブジェクトは静的オブジェクトです。
ライブオブジェクト
オブジェクトが内部状態を著者が変更できる場合、そのオブジェクトはライブにしてください。
例:DOMのNodeはライブオブジェクトで、
ドキュメントの現在状態を意識して変更できます。
ライブオブジェクトのプロパティはアクセス時に毎回計算しても良いので、データ計算が複雑な場合にも有利です(返却前に全データ計算不要)。 また静的版へのデータコピーが不要なため、メモリ消費も抑えられることがあります。
静的オブジェクト
オブジェクトが変化する可能性のあるリストを表す場合、通常は静的にすべきです。 これはリストを反復処理するコードが途中でリストが変わる可能性を考慮しなくて済むようにするためです。
getElementsByTagName
はリストを表すライブオブジェクトを返します。
このため反復処理時に注意が必要です:
let list= document. getElementsByTagName( "td" ); for ( let i= 0 ; i< list. length; i++ ) { let td= list[ i]; let tr= document. createElement( "tr" ); tr. innerHTML= td. outerHTML; // これによりtdがリストから削除され、反復処理が予測不能になります。 td. parentNode. replaceChild( tr, td); }
querySelectorAll()
が静的オブジェクトを返すようになったのは、getElementsByTagNameの問題を仕様著者が認識したためです。
URLSearchParams
はリストを表しますが静的ではありません。これは著者がURLのクエリ文字列を変更するためです。
Note: maplikeやsetlike型の場合はこの助言が当てはまらないこともあります。これらは反復中に変化しても安全に動作するよう設計されています。
アクセス時にプロパティ計算が不可能な場合は、静的オブジェクトにして、GCされるまで状態を更新する必要を避けてください。
頻繁に変化する状態を表す静的オブジェクトは、属性ではなくメソッドから返すようにしてください。
関連情報:
6.4. アクセサはプロパティのように、メソッドのように振る舞わせない
IDL属性でオブジェクトのプロパティやgetterがオブジェクト状態に関する情報を提供します。
-
getterは(観測可能な)副作用を持ってはいけません。副作用が必要ならメソッドにしてください。
-
getterは例外を投げるべきではありません。getterは通常のデータプロパティと同様に振る舞い、通常のデータプロパティは読み取り時に例外を投げません。無効状態は書き込み時に拒否し、読み取り時は避けるべきです。既存getterを例外投げに変更するのは避けてください。API利用者が列挙やラップを想定していて、例外で後方互換性が壊れるためです。
-
getterはブロッキング操作を行うべきではありません。必要ならメソッドにしてください。
-
基礎となるオブジェクトが変化しない場合、getterは毎回同じオブジェクトを返すべきです。つまり
obj.property === obj.propertyが常に成立します。getterが毎回新しい値を返すのは許されません。成立しない場合はメソッドにしてください。
Note:
ブロッキング操作のアンチパターン例は、レイアウトを計算するgetter(例:offsetTop)です。
IDL属性定義時は、可能ならsetterに与えた値をgetterで返すようにしてください。
obj.property = x後に必ずobj.property === xとなるべきです。
(型変換などで成立しない場合もありますが、通常パスでは成立させるべきです。)
返すべきオブジェクトはライブまたは静的のいずれかです。つまり:
-
ライブの場合、状態変化が必要な時以外は毎回同じオブジェクトを返す。プロパティ・getter・メソッドいずれでも返してよい。
-
静的の場合は毎回新しいオブジェクトを返す。その場合はメソッドにしてください。
6.5. オプションやプリミティブ引数は辞書で受け付ける
APIメソッドは通常、オプション引数の列よりも辞書引数を使うべきです。
この方が呼び出しコードの可読性が高く、メソッドシグネチャも覚えやすいです。 また将来の拡張性も高く、同型引数が複数必要な場合にも有効です。
new Event( "example" , { bubbles: true , cancelable: false })
の方が
より可読性が高いです。new Event( "example" , true , false )
必須パラメータも、APIの可読性が上がるなら辞書で受け付けることも検討してください。特にプリミティブ型の場合。
辞書自体はオプション引数にし、デフォルト値で満足なら追加引数を渡さず済むようにしてください。
関連情報:
6.6. メソッド引数は可能ならオプションにする
APIメソッドの引数に合理的なデフォルト値がある場合は、引数をオプションにしてデフォルト値を指定してください。
boolean引数のデフォルトはfalseが強く推奨されます。
デフォルト値は著者が最も選ぶ値にすべきです。
boolean属性の場合、属性名をfalseが一般的になるように選ぶ必要もあります。
APIのリスト型引数を決める際は、特別な理由がなければ次の型を使ってください:
-
メソッドのリスト引数はsequence<T>
-
メソッドの戻り値はsequence<T>
関連情報:
6.7. オプション引数の命名は適切に
オプション引数の命名は、デフォルトの動作が明確で、否定形の名前にならないようにしてください。
これは辞書で提供する場合も、単独引数の場合も当てはまります。
addEventListener()
はoptionsオブジェクトを受け取り、その中にはonceオプションがあります。
これはリスナーが繰り返し呼ばれないことを示します。
このオプションはrepeatとも命名できましたが、その場合はデフォルトがtrueになります。
noRepeatではなく、API著者はonceと命名し、デフォルト動作を否定形ではなく表現しました。
その他の例:
-
passive(activeではなく) -
isolate(connectではなく) -
private(publicではなく)
関連情報:
6.8. オーバーロードは賢く使う
メソッドの振る舞いが渡す引数により大きく異なる場合、通常はオーバーロードせず別メソッドで定義してください。
引数の一つが単一値または配列で受け付けられるようなオーバーロードは有用です。 またオプション辞書を渡すパターンも柔軟性の面で良いパターンです。
しかし、最初の引数によって後続引数が変わる場合や、入力型によってメソッド振る舞いが大きく変わる場合は、可読性を損ないAPI機能発見を難しくするため避けるべきです。
6.9. クラスには可能ならコンストラクタを持たせる
APIのクラスには、適切ならコンストラクタを持たせてください。
デフォルトでは[WEBIDL]インターフェースは「非構築可能」クラスを生成します。
new X()でインスタンス化しようとするとTypeErrorが投げられます。
構築可能にするには、適切なコンストラクタ操作をインターフェースに追加し、インスタンス生成アルゴリズムを定義してください。
これによりJavaScript開発者は、テスト・モック・サードパーティライブラリとの連携などでクラスインスタンスを作成できます。 また、サブクラス化も可能になります(通常JavaScriptのサブクラス化の仕組みで妨げられる)。
すべての場合で適切とは限りません。例:
-
特権リソースへのアクセスを表すオブジェクトは、ファクトリメソッドでのみ構築可能にし、直接newできないようにします。
-
ライフサイクル管理が厳密なオブジェクトは、特定メソッドを通じてのみ作成・アクセスできるようにします。
-
抽象基底クラスを表すオブジェクトは、構築不可にし、著者がサブクラス定義できないようにします。
Event
クラスとその派生インターフェースは構築可能です。
これはイベント処理コードのテスト時に有用で、著者はEventを構築してイベント処理メソッドに渡せます。
Window
クラスは構築不可です。新しいウィンドウ作成は特権操作で副作用が大きいためです。
代わりにwindow.open()メソッドで作成します。
ImageBitmap
クラスは構築不可です。これは不変で描画可能なビットマップ画像を表し、描画準備工程が非同期だからです。
代わりにcreateImageBitmap()ファクトリメソッドで作成します。
DOMTokenList
クラスは残念ながら構築不可です。
これによりカスタム要素がtoken
list属性をDOMTokenListとして公開できません。
Navigator、
History、
Cryptoなどの非構築可能クラスは、ウィンドウごとの情報アクセスを表すシングルトンです。
この場合はWebIDLのnamespace機能の方が適していたかもしれませんが、これらは名前空間設計前に作られ、現在の名前空間機能を超えています。
この種のシングルトンが必要ならnamespace利用を検討し、問題があればWebIDLにissueを提出してください。
ファクトリメソッドはコンストラクタを補完できますが、基本的に代替として使うべきではありません。 追加のメリットがある場合は、コンストラクタに加えてファクトリメソッドを含めても良いです。 よくある例は、APIに基底クラスと複数の専門サブクラスがあり、ファクトリメソッドでパラメータに応じて適切なサブクラスを生成するパターンです。 ファクトリメソッドは多くの場合、返却結果の最も近い共通基底サブクラスの静的メソッドになります。
MouseEvent.initMouseEvent()ファクトリメソッドはMouseEvent
オブジェクトのみ生成しますが、もともと構築不可でした(技術的理由はなかった)。最終的には非推奨となり、MouseEvent
オブジェクトは単純に構築可能になりました。
6.10. 適切な場合は同期的にする
可能な限り、新しいAPIを設計する際は同期APIを優先してください。
同期APIは利用が簡単で、関数をasync化するなどの追加インフラも少なくて済みます。
以下の経験則に当てはまる場合、APIは一般的に同期的であるべきです:
-
APIが権限プロンプトやデバイス選択などのダイアログによってブロックされることがない。
-
APIの実装がロック、ファイルシステム、ネットワークアクセス(例えばプロセス間通信)などによってブロックされない。
-
実行時間が短く、決定的である。
6.11. 非同期APIはPromiseで設計する
APIメソッドが非同期である必要がある場合は、コールバック関数ではなくPromiseを使ってください。
PromiseをWebプラットフォーム全体で一貫して使うことで、API同士の連携(Promiseのチェーンなど)が容易になります。 Promiseを使うコードは、コールバック関数を使うコードより理解しやすい傾向があります。
APIが非同期である必要がある場合の例:
-
ユーザーエージェントが権限をユーザーに求める必要がある場合
-
ディスクから情報を読み込む、またはネットワークから取得する必要がある場合
-
ユーザーエージェントが他スレッドや他プロセスで多くの作業を行う必要がある場合
関連情報:
コールバック関数を使うべき場合
即時応答が必要な場合のみ同期コールバックを使ってください。
APIによっては、遅延に敏感なアクションが関わる場合、同期的な動作が必要です。 例えば、イベントのバブリング完了直後に即座に処理が必要な場合や、メディア処理で次のタスク境界を待てない場合などです。 同期アクションはPromiseでモデル化できません。 この場合は明示的なコールバックやイベントを使ってください。
Promiseリアクションコールバックでは、 一定数のマイクロタスク内でのアクションを要求してはいけません。
これはPromise合成(await、ヘルパー関数ラップ、ロギング、Promise.race()や複数.then()ブランチなど)のパターンを守るためです。
これらは追加マイクロタスクをキューし、ラップ対象コードの挙動を大きく変えてはいけません。
イベントハンドラは同期的に呼ばれ、キャンセルされます。
開発者がevent.
やpreventDefault()fetchEvent.を呼び出せる必要がある場合は、
コールバックでなければなりません(Promiseのthenリアクションでは不可)。respondWith()
非イベント例として
captureController.
メソッドは、ユーザーが選択した画面共有ウィンドウを前面に出します。アプリケーションは選択ウィンドウに基づいて決定できます。
遅延を許すとサイトによるクリックジャッキング攻撃の機会が生まれるため、Promiseのリアクションマイクロタスク上ではなく、同じPromiseリアクションタスク上で1秒以内に呼び出すことが要求されています。
setFocusBehavior()
6.12. 非同期APIや操作のキャンセルはAbortSignalで行う
非同期メソッドがキャンセル可能な場合は、オプション辞書の一部としてAbortSignalを渡せるようにしてください。
const controller= new AbortController(); const signal= controller. signal; geolocation. read({ signal});
AbortSignalで非同期操作のキャンセルを一貫して行うことで、著者は複雑なコードを書かずに済みます。
例えば、複数の操作に1つのAbortSignalを使い、「キャンセル」ボタンやSPAナビゲーション時にAbortControllerで一括キャンセルするパターンがあります。
キャンセルが保証できない場合でも、AbortControllerは使えます。
abort()はリクエストであり、保証ではありません。
6.13. 定数やenum値には文字列を使う
定数やenum値には文字列を使ってください。
文字列は値の確認やコードの読みやすさが向上します。 JavaScriptでは整数を使っても性能向上はありません。 WebIDLのenum型値もこの原則に従って文字列です。
複数プロパティの組み合わせ(他言語のビットマスクで表現されるような状態)が必要な場合は、辞書オブジェクトで表現してください。 辞書はビットマスク値同様に簡単に受け渡せます。
WorkerType
enum引数で指定します。
値は"classic"と"module"の2つで、本来Boolean型でもよかったかもしれませんが、文字列を使うことでコードが明確になります。
6.14. 同期・非同期両方が必要なら同期は例外とする
稀なケースとして、同じ目的の同期・非同期メソッド両方を必要とする場合は、非同期をデフォルトとし、同期は例外扱いにしてください。
現時点ではWebプラットフォームの一部領域に非同期対応がないため、非同期に加えて同期メソッドを持つことで利便性が向上します。 こうしたケースは例外とし、Webプラットフォームの進化に伴い同期メソッド廃止への道筋を明確にしてください。
ほとんどの場合、同期バージョンはSync()サフィックス付きで区別してください。
6.15. バイト配列の出力にはUint8Arrayを使う
APIがバイト配列を返す場合は、Uint8Arrayを使い、
ArrayBufferは使わないでください。
ArrayBufferは直接読み取りできず、Uint8Arrayなどのビューを作成する必要があります。
Uint8Arrayを提供することで余計な手間を省けます。
バッファ内のバイトが他のTypedArray型として自然に解釈できる場合は、その型を使ってください。
例:バイトがFloat32値ならFloat32Arrayを使う。
6.16. 副作用のみの関数はundefinedを返す
関数の目的が副作用のみで値計算ではない場合、
その関数はundefinedを返すように指定してください。
このような戻り値に依存するサイトはほぼなく、将来的に意味ある値への拡張も容易です。
HTMLMediaElementの
play()
メソッドは
当初undefinedを返すよう定義されていました。
目的はメディア要素の状態を変更することだからです。
メディア再生リクエストは失敗することもあるため、play()
はPromise返却に変更されました。
もし当初からundefined以外
(例えばメディア要素自体、APIチェーンでよくあるパターン)を返す設計だった場合、
この改良は後方互換性が保てなかったでしょう。
関連情報:
6.17. 既存のタスクソースを優先する
可能な限り既存のタスクソースを使って作業を分配してください。
明確な理由がある場合のみ、新しいタスクソースを作成します。 仕様書では、関連する作業(または「タスク」)をグループ化するためにタスクソースを使用し、UAが合理的にスケジューリングできるようにしています。 タスクはタスクソースにキューされ、各タスクソースはイベントループ用のタスクキューに関連付けられます。 ソース内の順序は保証されますが、UAは次にどのキューを処理するかを選択します。
可能な限り汎用または既存のタスクソースを利用してください。 HTMLでは、仕様全体で広く使えることを意図した汎用タスクソースが定義されています。 これには、以下が含まれます(これらに限りません): DOM操作タスクソース、 ユーザー操作タスクソース、 ネットワークタスクソースなどです。 もし機能がこれらのカテゴリのいずれかに該当する場合は、新しいソースを作成するのではなく、そのソースにキューしてください。 そうすることで、プラットフォーム全体の相互運用性、予測可能性、パフォーマンススケジューリングが向上します。
タスクソースを追加すると、すべてのブラウザ実装がパフォーマンス面と複雑性のコストを負担することになります。 汎用タスクソースから隔離されたタイミング/実行モデルが必要など、具体的でテスト可能なニーズがある場合のみ新しいタスクソースを作成してください。 例えば、メディア要素はデコーダーやタイムラインのセマンティクスに合わせるため、独自のメディア要素イベントタスクソースを持ちます。
仕様を書く際は、HTMLのラッパーアルゴリズム (グローバルタスクをキューする、要素タスクをキューする) を優先し、特定のイベントループに依存しないようにしてください。 また、必要な場合のみイベントループについて明示してください(§ 7 イベント設計も参照)。
7. イベント設計
7.1. 一度限りのイベントにはPromiseを使う
Promise利用仕様の書き方ガイドの助言に従ってください。
7.2. キャンセル可能なイベントにはPromiseを使わない
コールバック関数を使うべき場合も参照。
7.3. 関連するPromiseが解決される前にイベントを発火させる
Promiseベースの非同期アルゴリズムがイベントを発火する場合は、Promise解決前にイベントを発火させるべきです。
Promiseが解決されると、リアクションコールバック実行のためのマイクロタスクがキューされます。 マイクロタスクはJavaScriptスタックが空になった時に処理されます。 イベントの発火は同期的で、各リスナー間でJavaScriptスタックが空になります。 そのため、Promiseを解決してから関連イベントを発火すると、Promiseリアクションによるマイクロタスクがイベントのリスナー間で実行されてしまいます。
先にイベントを発火すればこの混入を防げます。すべてのイベントリスナーがPromiseリアクションコールバックより先に実行されます。
7.4. 独自のイベントリスナー様インフラを発明しない
通知を発生させる処理の開始・停止を著者が制御できるAPIを作る場合は、既存のイベントインフラを使って通知をリッスンできるようにしてください。 処理開始・停止のAPIは別途用意してください。
BluetoothRemoteGATTCharacteristicグローバルオブジェクトにstartNotifications()メソッドを提供し、
オブジェクトを「アクティブ通知コンテキストセット」に追加します。
ユーザーエージェントがBluetoothデバイスから通知を受け取ると、アクティブ通知コンテキストセット内のBluetoothRemoteGATTCharacteristicオブジェクトにイベントを発火します。
関連情報:
7.5. 常にイベントハンドラ属性を追加する
APIで新しいイベントタイプを追加する場合は、
そのイベントを処理できるあらゆるEventHandlerインターフェースに対応するonyourevent
イベントハンドラIDL属性を追加してください。
イベントハンドラIDL属性を定義し続けることは重要です:
-
プラットフォームの一貫性を維持できる
-
対応イベントの機能検出(§ 2.6 新機能は検出可能にする)が可能になる
一貫性のため、HTML・SVG要素両方でイベント処理が必要な場合は、GlobalEventHandlersインターフェースミックスインにイベントハンドラIDL属性を追加してください(要素インターフェースに直接追加せず)。
同様にWindowEventHandlersに追加し、Windowには直接追加しないでください。
7.6. 通知にはイベントを使う
イベントは状態変更をトリガーするためではなく、変更が完了したことを通知するためだけに使ってください。
resize
イベントがWindowオブジェクトに発火します。
イベントをインターセプトしてresize自体を止めることはできません。 また、resizeイベントを発火してウィンドウサイズを変更することもできません。 イベントは変更がすでに発生したことだけを通知します。
7.7. 再帰の可能性に注意する
APIに長時間実行や複雑なアルゴリズムが含まれる場合、アルゴリズムが実行中に再度呼び出しされないように防止してください。
APIメソッドが長時間アルゴリズムの実行を開始する場合は、イベントで進捗通知をしてください。 しかし、イベントを処理するユーザーコードが同じAPIメソッドを呼び出し、アルゴリズムが再帰的に実行される可能性があります。 同じイベントが再度発火し、同じハンドラが繰り返し呼ばれることもあります。
これを防ぐには、APIメソッドへの「再帰呼び出し」が即座にreturnするようにしてください(ガード技法)。
AbortSignalの
add,
remove、
signal abort
は、まずsignalがabortedかどうかチェックします。
abortedなら以降のアルゴリズムは実行しません。
この場合、重要な複雑さはsignal abortステップで実行されるアルゴリズム群にあります。 これらのステップはaddやremoveメソッドが管理するアルゴリズムコレクションを反復します。
例えば
ReadableStreamPipeTo
定義はaddで
AbortSignal
のアルゴリズムセットに追加し、
signal abortステップで呼び出されます(abort())。
このアルゴリズムはPromiseを解決し、コードが実行され、
AbortSignalのメソッドが呼ばれることもあります。
signal abortはアルゴリズムコレクション反復なので、実行中コレクション変更は不可です。
また、signal
abortが再帰呼び出しを発生させた場合、すでにsignal abortステップ実行中なら再度実行しないようにガードしてください。
Note: 早期終了の注意点: 終了するアルゴリズムが重要な状態一貫性を保証する場合は、早期終了前に必ず状態調整も行ってください。そうしないと、実装時に不整合やエンドユーザー向けバグが生じます。
Note: 早期終了で例外を投げる場合も注意。開発者が本当にその例外を扱う想定か、アルゴリズム内で唯一の例外かなどを考慮してください。
常にガードできるとは限りません。エントリポイントが多すぎて全てチェックできない場合もあります。 その場合は、著者コード呼び出しを後続タスクやマイクロタスクに延期する方法もあります。 これで再帰スタックは回避できますが、追従タスクの無限ループは防げません。
イベントの延期はよく「タスクをキューして、イベントを発火する...」と指定されます。
アルゴリズムが別スレッド・プロセスで実行される場合は、必ずイベントを延期してください。 この場合、延期でイベントが正しいタスクキュー上で処理されます。
「ガード」と「延期」にはそれぞれ利点・欠点があります。
「ガード」アルゴリズムの利点:
-
イベント発火時点で、ガードアルゴリズム終了とイベント発火の間に状態変化が起きない。
-
アルゴリズム内で状態変更通知イベントを即座に発火でき、次タスク待ち不要。
-
イベントハンドラでインスタンスの直接状態を観測でき、イベントに状態コピー不要。
延期の場合:
-
アルゴリズム完了後、タスクキューの最初に発火される保証はない。
-
他タスクでオブジェクト状態が変わる可能性があるため、イベントに関連状態も含めるべき。
-
新しいEventサブクラスを使い、属性で状態を保持することが多い。
例:
ProgressEventはloaded・total属性などで状態を保持。
-
-
アルゴリズムの複数部分が協調する必要があるなら、明示的な状態マシン(明確な状態遷移)の定義も必要です。延期イベント発火時に状態観察・変更の挙動を明確化します。
例:[payment-request] では
PaymentRequestの[[state]]内部スロットが明確な遷移で状態管理します。-
こうした状態遷移は通常ガード技法も用い、状態遷移が適切に行われるようにします。
例:[payment-request]の
[[state]]内部スロット周り(show()アルゴリズムなど)でガードを使っています。
-
-
延期イベントに追加状態や状態マシンが不要なら、イベントはアルゴリズム完了通知のみです。 この場合、APIはイベント発火よりPromise返却の方が適切です。 § 7.1 一度限りのイベントにはPromiseを使う参照。
Note: この節で説明した再帰可能性のあるイベントは「同期イベント」と呼ばれることがありますが、この用語は推奨されません。 非同期イベントのようにイベント発火自体が非同期可能であるかのような誤解を招くためです。 全てのイベントは同期発火です。ここで言う「非同期イベント」は発火の延期を意味します。
7.8. 状態には通常のEvent
を使う
可能であれば、指定したtypeを持つ通常のEvent
を使い、状態情報はtarget
オブジェクトに保持してください。
Event
の新しいサブクラスを作る必要は通常ありません。
7.9. イベントとオブザーバーを適切に使い分ける
一般的には、EventTarget
と通知用のEvent
を使い、ObserverパターンはEventTarget
でうまく機能しない場合のみ検討してください。
EventTarget
を使うことで、once
の追加など、共通基底クラスの改良恩恵を受けられます。
イベント利用が問題(例:避けられない再帰)を引き起こす場合は、Observerパターンの利用も検討してください。
MutationObserver、
Intersection Observer、
Resize Observer、
IndexedDB Observer
はObserverパターンの例です。
MutationObserver
は非推奨の
DOM Mutation Events
の代替です。開発者が
DOM Mutation Eventsが
-
頻繁に発火しすぎる
-
イベント伝播の恩恵を受けられず、遅すぎて役に立たない
-
再帰が発生しやすくガード困難
MutationObserverは次の利点があります:
-
変更が完了した後でバッチ処理して通知できる
-
イベントキャプチャやバブリングフェーズ不要
-
発生した変更内容を豊富なAPIで表現できる
Note: イベントも通知のバッチ処理は可能ですが、DOM Mutation Eventsは設計上対応していません。 イベントは必ずしも伝播に参加する必要はありませんが、DOMノード上のイベントは通常伝播します。
Observerパターンは次のように動作します:
-
Observerクラスの各インスタンスはコールバックと、必要なら監視内容をカスタマイズするオプションを指定して構築されます。
-
インスタンスは
observe()メソッドで監視対象を指定して監視を開始します。 オプションはここで渡しても、コンストラクタで渡しても構いません。 コンストラクタで指定したコールバックは監視対象に変化があったときに呼び出されます。 -
コールバックは変更記録(change records)を引数で受け取ります。これは発生した内容の詳細で、複数同時配信も可能です。
-
著者はObserverインスタンスの
unobserve()やdisconnect()メソッドで監視停止できます。 -
必要なら、未配信の全変更記録を即座に返す
takeRecords()メソッドも提供できます。
IntersectionObserver
の利用例:
function checkElementStillVisible( element, observer) { delete element. visibleTimeout; // タスクキューに残っている観測を処理 processChanges( observer. takeRecords()); if ( 'isVisible' in element) { delete element. isVisible; logAdImpressionToServer(); // この要素の監視を停止 observer. unobserve( element); } } function processChanges( changes) { changes. forEach( function ( changeRecord) { var element= changeRecord. target; element. isVisible= isVisible( changeRecord. boundingClientRect, changeRecord. intersectionRect); if ( 'isVisible' in element) { // 要素が可視になった element. visibleTimeout= setTimeout(() => { checkElementStillVisible( element, observer); }, 1000 ); } else { // 要素が非表示になった if ( 'visibleTimeout' in element) { clearTimeout( element. visibleTimeout); delete element. visibleTimeout; } } }); } // コールバックとオプションでIntersectionObserverを作成 var observer= new IntersectionObserver( processChanges, { threshold: [ 0.5 ] }); // "ad"要素の監視を開始 var ad= document. querySelector( '#ad' ); observer. observe( ad);
(IntersectionObserver explainerより一部改変)
Observerパターンを使う際は以下を定義してください:
-
新しいObserverオブジェクト型
-
監視オプション用オブジェクト型
-
観測記録用オブジェクト型
追加作業のトレードオフとして次の利点があります:
-
インスタンスは監視開始時や作成時にカスタマイズ可能。Observerのコンストラクタやobserve()メソッドでコールバックごと監視内容を指定できます。これは
addEventListener()ではできません。 -
disconnect()やunobserve()メソッドで複数コールバックの監視停止が容易。
-
takeRecords()のようなメソッドでイベント発火を待たずに即座にデータ取得可能。
-
Observerは単一目的なのでイベントタイプ指定不要。
ObserverとEventTarget
の共通点:
-
両方とも作成時にカスタマイズ可能
-
両方ともバッチ処理で任意タイミングで配信可能。
EventTargetも同期的である必要はなく、マイクロタスク・idle・animation-frameタイミングなど利用可能。特別なタイミングやバッチ処理のためだけにObserverを使う必要はありません。 -
EventTargetもObserverもDOMツリー(バブリング/キャプチャやキャンセル)に参加不要。主要なEventTargetはDOMノードですが、多くのイベントは独立しています。例えばIDBDatabaseやXMLHttpRequestEventTarget。DOMノードでも非バブリング・非キャンセルイベント設計も可能です。
IntersectionObserver
がEventTarget
のサブクラスの場合の例:
const io= new ETIntersectionObserver( element, { root, rootMargin, threshold}); function listener( e) { for ( const changeof e. changes) { // ... } } io. addEventListener( "intersect" , listener); io. removeEventListener( "intersect" , listener);
Observer版との比較:
-
同じオプションで複数要素を監視するのが難しい
-
即座にデータ取得する手段がない
-
同一イベントの複数リスナー削除が面倒
-
冗長な
"intersect"イベントタイプの指定が必要
Observer版との共通点:
-
バッチ処理可能
-
タイミング(JSイベントキュー基準)は同じ
-
著者はリッスン対象をカスタマイズ可能
-
イベントはキャプチャ・バブリングしない
これらの設計はどちらでも実現できます。
関連情報:
8. WebIDL、型、単位
8.1. 数値型を適切に使う
設計するAPIで数値を使う場合、特別な理由がない限り[WEBIDL]の以下の数値型を使ってください:
unrestricted double-
すべてのJavaScript数値(±Infinity, NaN含む)
double-
すべてのJavaScript数値(±Infinity, NaN除く)
- [
EnforceRange]long long -
-263~263のJavaScript数値(整数丸め)。 範囲外の場合、生成バインディングは
TypeErrorを投げます。 - [
EnforceRange]unsigned long long -
0~264のJavaScript数値(整数丸め)。 範囲外の場合、生成バインディングは
TypeErrorを投げます。
JavaScriptの数値型はNumberのみです。
IEEE 754倍精度浮動小数点(±0, ±Infinity, NaN含む)。
[WEBIDL]の数値型は、
JavaScript数値を特定性質の部分集合に変換する規則です。
この規則は、メソッドやプロパティsetterでIDL定義インターフェースに数値が渡されたときに実行されます。
追加の規則が必要ならアルゴリズムで指定してください。
octet(8bit、[0,255]範囲))に変換する際、JS値のmoduloを取ります。
例:JS値300をoctetに変換すると、まず300
mod 255を計算し、結果は45になります。これは驚くかもしれません。
代わりに[EnforceRange]octetで範囲外ならTypeError、[Clamp]octetで範囲外値を255に丸めることもできます。
bigint
は値が253より大きい、または-253より小さい場合のみ使ってください。
APIでBigInt
とNumber
両方を同時にサポート(多態/別API)してはいけません。暗黙変換で精度損失しBigIntの意味が失われます。
8.2. 文字列は適切な型で表現する
Webプラットフォーム機能で文字列を扱う場合、特別な理由がなければDOMStringを使ってください。
ほとんどの文字列操作は文字列内のコード単位を解釈する必要がないため、DOMStringが最適です。
下記で説明する特定ケースではUSVStringやByteStringが適切な場合もあります。[INFRA] [WEBIDL]
USVString
はWebIDL型でスカラ値文字列を表します。
主要なアルゴリズムがスカラ値単位で動作(例:パーセントエンコーディング)したり、入力でサロゲートを扱えない(例:ネイティブAPIへ文字列を渡す)場合はUSVStringを使ってください。
IDL属性の反映で、内容属性がURLを含む場合
(例:href)
はUSVStringを使ってください。
[HTML]
ByteString
はHTTPなど、バイトと文字列を区別しないプロトコルデータを表す場合のみ使ってください。一般の文字列型ではありません。
byte列を表す必要があるならUint8Arrayを使ってください。
8.3. 時間計測にはミリ秒を使う
APIで時間計測値を受け付ける場合は、ミリ秒単位で表現してください。
APIのドメインで秒(または他の単位)が自然であっても、ミリ秒に統一することでAPI間の相互運用性が保たれます。 これにより、著者があるAPIの値を他のAPIで使う際に変換が不要になり、どこでどの単位が必要かを管理する必要がなくなります。
この慣習はsetTimeout()
や
Date
APIで始まり、以降継続して使われています。
注: 高分解能時間は通常、浮動小数点値で分数ミリ秒として表現され、ナノ秒などの整数値ではありません。
8.4. 時間・日付は適切な型で表現する
プラットフォーム上で日付・時刻を表す場合はDOMHighResTimeStamp
型を使ってください。
DOMHighResTimeStamp
でタイムスタンプ同士の比較が可能で、ユーザーの時刻設定に関わらず扱えます。
DOMHighResTimeStamp
の値はミリ秒単位の時刻値です。
詳細は[HIGHRES-TIME]
を参照してください。
特定の日時値を表現する目的でJavaScriptのDate
クラスは使わないでください。
Date
オブジェクトは値が変更可能(mutable)で、イミュータブルにする方法がありません。
Date
を使ってはいけない理由の詳細は以下を参照:
-
Frozen date objects?(es-discuss)
-
Remove Date from Web IDL (Web IDL Bugzilla)
8.5. エラーはErrorかDOMExceptionで表現する
Web APIのエラーはECMAScriptのエラーオブジェクト(例:Error)
またはDOMException
で表現してください。例外・Promiseの拒否値・プロパティいずれの場合も同様です。
9. デバイスやブラウザー機能へのアクセスをラップするAPI
現在Webプラットフォームではデバイス操作用APIの開発が進められています。 例えば、著者はWebで マイクやカメラ、 各種センサー(ジャイロ・加速度など)、 Bluetoothや USB周辺機器、 自動車などを利用したいと考えています。
同様に、ホストシステムや外部サービスがオプションで提供する機能も含まれます。 これには、ユーザーが利用権を購入した場合のみ有効な機能も含みます。
これらの機能はOSが提供する場合もあれば、ネイティブサードパーティライブラリが提供する場合もあります。 APIは、ネイティブ機能を「ラップ」しつつ、ブラウザー側のAPI面をセキュアに保ちながら、大きな複雑さを持ち込まずに抽象化できます。 このため、ラッパーAPI(wrapper API)と呼ばれます。
このセクションでは、こうした機能向けAPI設計時に考慮すべき原則を示します。
9.1. 機能に関する不要な情報は公開しない
データ最小化原則に従い、Webサイトに機能情報を提供する必要がある場合は、必要最小限のデータのみ公開してください。
まず、本当に情報公開が必要か慎重に検討してください。 より弱いAPIでもユーザーニーズを満たせるか検討しましょう。
デバイスの存在や追加情報、デバイス識別子の公開はどれもユーザーのプライバシーリスクを高めます。
ユーザーがデバイスや機能へのアクセスを拒否した場合、「その機能が存在するか」自体を公開しないようにしてください。 この場面での情報漏洩防止は、機能が許可された場合よりも重要です。
より詳細な情報が共有されるほど、 サイトに提供されるフィンガープリント用データ が多くなり、ユーザー固有の機微情報が漏れる可能性も高まります。
より弱いAPI設計が不可能な場合は、以下の指針に従ってデバイス情報を公開してください:
- 識別子内の情報は最小限に
-
Webプラットフォームに公開するデバイス識別子には、識別可能な情報(ブランド名、型番など)を可能な限り含めないでください。 通常はランダム生成識別子にできます。 識別子が推測できたり使い回されたりしないようにしてください。
- ユーザーがコントロール可能に
-
ユーザーが閲覧データを消去した際は、保存されたデバイス識別子も必ず消去してください。
- 機微情報は権限で保護
-
匿名化できないデバイス識別子の場合は、アクセス制限を設け、 Webページがこの情報へアクセスする際に意味のある同意を得られるようにしてください。
- 識別子はsame-originモデルに紐付け
-
同じ物理デバイスでも、アクセスするoriginごとに別の識別子を生成してください。
同一originから同じデバイスに複数回アクセスした場合は、(ユーザーが閲覧データを消去しない限り)同じ識別子を返してください。 これにより、著者は同じデバイスの重複を防げます。
- 必要なら永続化
-
デバイス識別子の取得に時間がかかる場合は、著者が一度生成した識別子を後のセッションでも使えるようにしてください。 同じデバイス・同じoriginなら、生成手順が一貫して同じ値になるようにしてください。
関連情報:
9.2. デバイス選択や列挙API公開時は慎重に
デバイス列挙を避ける方法を探してください。避けられない場合は公開情報を最小限にしてください。
APIが複数デバイスの存在・機能・識別子を公開する場合、§ 9.1 機能に関する不要な情報は公開しないのリスクがデバイス数分増加します。 より弱いAPIでユーザーニーズを満たせるか再検討してください。[LEAST-POWER]
APIの目的が「特定種別デバイスの中からユーザーが選択する」なら、スクリプトにリストを公開する必要はありません。 ユーザーエージェント提供のデバイスピッカーを呼び出すAPIで十分です。 このようなAPIは:
-
ユーザーがコントロール可能
-
ユーザーの同意なしにデバイス情報を公開しない
-
ユーザー環境のフィンガープリント用データをデフォルトで公開しない
-
一度に一つのデバイス情報だけ公開する
ユーザーがデバイスを選択できるAPI設計では、「選択可能デバイスがあること」自体の公開も必要な場合があります。 これはWebサイトにユーザー環境のフィンガープリント用データ1ビットを公開することになるため、完全に安全ではありません。
RemotePlayback
インターフェースは利用可能なリモート再生デバイスのリストを公開しません。
代わりに、ユーザーがUA提供のデバイスピッカーから1つ選択できます。
ただし、Webサイトは リモート再生デバイス が利用可能か検出できるため、利用可能時のみピッカー表示UIを出し分けできます。 このトレードオフにより、UI混乱を避けられます。
デバイスリスト公開が必要な場合は、ユーザーニーズを満たす最小限のサブセットのみ公開するようにしてください。
例えば、Webサイトがフィルタ済みや制約付きのリストを要求できるAPIは、デバイス数を減らす有効な選択肢です。 ただし、著者が異なる制約で複数回リクエストできる場合は、結局全リストにアクセス可能になる場合もあります。
最終的に特定種別デバイスの全リスト公開が必要な場合は、デバイスの並び順を厳密に定義してください。 これにより相互運用性問題やフィンガープリントリスクが軽減します。 (並び順が追加情報を漏らす場合も:Mitigating Browser Fingerprinting in Web Specifications § 6.2 Standardization参照)
注: APIは実装依存順で全リスト公開すべきではありませんが、Web互換性上必要な場合もあり得ます。
9.3. 基盤機能ではなくユーザーニーズに基づいて設計する
新たなネイティブ機能をWebに公開する際は、ユーザーニーズに基づいて設計してください。
既存のネイティブAPIをそのままWebに翻訳するのは避けてください。
代わりに、ネイティブAPIが提供する機能とその対応するユーザーニーズを分析し、実装が既存ネイティブAPIに依存していても、ユーザーニーズを満たすAPIを設計してください。
基盤ネイティブAPIのライフサイクルやデータ構造をそのまま公開する際は特に注意してください。 可能な場合は新しいハードウェアへの柔軟性も考慮しましょう。
つまり新提案APIは、「現状のハードウェア・デバイス・ネイティブAPIでどう動くか」ではなく、「どのような利用方法を想定するか」を基準に設計してください。
9.4. 安全性を積極的に考慮する
ネイティブ機能をWebプラットフォームに導入する際は、防御的に設計してください。
ネイティブ機能のWeb導入には多くの影響があります。 ユーザーは自分のPCに特定機能があることをWebサイトに知られたくない場合があります。 そのため、論理的なオリジン境界外へのアクセスは必ず権限管理してください。
例えば、デバイスが状態保存可能で、複数originから同時に読める場合、その状態を読み書きできるAPIはWebのオリジンモデルを破る副チャネルとなります。
このため、デバイスが排他アクセス不要でも、originごとに排他アクセスを強制したり、現在アクティブなタブだけに制限したりすることも検討してください。
加えて、APIはデバイスの物理的な切断などにもアプリケーションが優雅に対応できるよう設計してください。
9.5. ネイティブAPIの適応にはWebプラットフォーム原則を適用する
OSネイティブAPIをWebに適応する場合は、Webプラットフォームの原則に沿って新APIを設計してください。
- Web APIは複数プラットフォームで実装可能に
-
ラッパーAPI設計時は、各プラットフォームで機能がどう提供されるか考慮してください。
理想的には全実装が完全に同じ動作ですが、場合によっては一部プラットフォームだけで使えるオプションを公開する理由があるかもしれません。 その場合は、全プラットフォームで動作するコードの書き方を必ず説明してください。 § 2.6 新機能は検出可能にするも参照。
- 基盤プロトコルはオープンであること
-
外部ハードウェアやサービスとの交換を必要とするAPIは、閉じた・独自プロトコルに依存すべきではありません。 非オープンプロトコルへの依存はWebのオープン性を損ないます。
- オフライン時の挙動も設計すること
-
APIがリモートサーバ提供のサービスに依存する場合は、ユーザーが何らかの理由でサーバにアクセスできない場合でもAPIが適切に動作するよう設計してください。
- フィンガープリント面の追加を避ける
-
ラッパーAPIはユーザーに広範なフィンガープリントリスクを与える可能性もあります。 unsanctioned trackingも参照ください。
10. その他のAPI設計上の考慮事項
10.1. 新機能にはポリフィルを可能にする
ポリフィルは新機能のWebプラットフォーム展開を大きく助けます。 Technical Architecture GroupのPolyfills and the Evolution of the Web の提言を新機能開発時に考慮してください。主なポイント:
-
「ポリフィル可能」であることは必須ではないが有益である
-
ポリフィル開発を奨励する
10.2. 可能な限りDedicated WorkerにもAPIを公開する
機能を公開する際は、Dedicated Worker(DedicatedWorkerGlobalScope
インターフェース)でも公開する意味があるか検討してください。
多くの機能はDedicated Workerでもそのまま動作可能で、未対応だと非ブロッキングでコードを実行する能力が制限されます。
Dedicated Worker対応には課題もあり、特に権限要求やピッカー・セレクター表示などユーザー入力が必要な場合は困難です。 こうした理由でDedicated Worker対応を避ける仕様著者もいますが、 将来的なDedicated Worker対応を容易にするため、設計段階でDedicated Worker対応も考慮することを推奨します。
10.2.1. 一部APIはDedicated Workerのみに公開するべき
開発者は複雑なコードよりもシンプルなコードを好みます。 APIも、最も簡単な利用法がそのAPIの推奨利用法となりやすいです。
レンダリングをブロックする機能追加は避けてください。§ 3.4 レンダリングをブロックする機能は避ける
APIの最も簡単な利用法がレンダーブロックや「ジャンク」になりやすい場合、ユーザー体験が悪化します。 (この問題は低スペック端末ほど顕著で、社会的弱者ユーザーほど影響を受けやすいです。 Webはすべての人のためのものであることを忘れないでください。)
そのため、意図通り使うとメインスレッドをブロックするAPIはWindow
インターフェースで公開すべきではありません。
DedicatedWorkerGlobalScope
インターフェース限定にすることで、開発者が「簡単な道」で最良のユーザー体験を提供できます。
ScriptProcessorNode
はWeb Audio APIでAudioWorklet
に置き換えられました。メインスレッドでScriptProcessorNode
を使うとユーザー体験が悪化しやすかったためです。[WebAudio]
10.3. 純粋な計算機能だけを全ての環境で公開する
機能公開時は、全ての環境([Exposed=*]
アノテーションや全グローバルスコープインターフェースへの追加)で公開する意味があるか検討してください。
純粋な計算機能だけを全環境で公開してください。 つまりI/Oを行わず、ユーザーエージェントや端末の状態を変化させないものです。
TextEncoder
インターフェースは文字列をUTF-8バイト列に変換します。
純粋な計算インターフェースで、JS言語機能として広く有用なので全環境で公開すべきです。
localStorage はユーザーエージェントの状態を変更するため、全環境で公開すべきではありません。
技術的にはconsole
はユーザーエージェント(開発者ツールへのログ表示)やユーザー端末(ログファイルへの書き込み)に影響し得ますが、
実行中コードからは観測できず、全環境でconsole
を持てる利点が欠点を上回ります。
イベントループ依存機能も全環境で公開すべきではありません。 全てのグローバルスコープがイベントループを持つわけではありません。
timeout
メソッド(AbortSignal
)はイベントループ依存なので全環境で公開すべきではありません。
他のAbortSignal
機能は純粋計算処理なので全環境で公開すべきです。
[Exposed=*]
アノテーションも慎重に適用してください。
他の非公開機能と組み合わせて初めて有用な場合は、デフォルトで公開しない方が良いです。
Blob
インターフェースは純粋計算機能ですが、主用途がI/Oで得られる/使われるため、原則に従い全環境公開すべきではありません。
10.4. 新しいデータフォーマットは正しく追加する
新しいデータフォーマット追加時は必ず対応するMIMEタイプを定義し、既存APIにもこのタイプ対応を追加してください。
Web新機能で新しいデータフォーマット追加が必要な場合があります。 画像・動画・音声・テキスト・その他ブラウザーが扱うあらゆるフォーマットです。 新フォーマットには厳格に検証された標準MIMEタイプを使い、テキスト系はUTF-8限定としてください。
レガシーメディアフォーマットはMIMEタイプ厳密運用がされない場合や、ヘッダー覗き(sniffing)に頼ることもありますが、これは主に互換性理由であり、新フォーマットでは期待・実装すべきではありません。
仕様著者は新フォーマットを既存APIにも統合し、Ingress(ReadableStreamからデコード)・Egress(WriteableStreamへエンコード)両方でブラウザー視点で安全リスト化することが期待されます。
例えば新しい画像フォーマット追加なら、まずMIMEタイプ追加し、HTMLImageElementでデコード対応、さらにHTMLCanvasElement.toBlob()やHTMLCanvasElement.toDataURL()などEgressにも対応追加してください。
レガシー理由でブラウザーはMIMEタイプスニッフィング対応もしますが、pattern matching algorithmの拡張はセキュリティ上推奨せず、新フォーマットでは厳格なMIMEタイプを推奨します。
新しいMIMEタイプには仕様が必要で、Internet Assigned Numbers Authority (IANA)に登録してください。
10.5. 新しいマニフェストを作るより既存マニフェスト拡張を優先する
機能でマニフェストが必要な場合は、既存マニフェストスキーマの拡張可否を調査してください。
新しいWeb機能は自己完結・自己記述型が理想で、追加マニフェストファイルを必要とすべきではありません。 既存マニフェスト例:
-
Web App Manifest(Webアプリ関連機能)
-
Payment Method Manifest(Web決済API用)
-
Publication Manifest(Web出版WG標準で利用)
-
Origin Policy(セキュリティポリシー設定用)
既存マニフェストの拡張を推奨します。 変更は必ず元仕様に反映するよう働きかけるか、少なくとも仕様編者と議論してください。 この議論により設計が洗練され、プラットフォーム統合も良くなる可能性が高いです。
新しいキーや値設計時は、必要性(十分に考慮されたユースケースがあるか)を確認し、類似キーの有無も調べてください。 既存キーでほぼ目的が満たせる場合は、既存仕様と連携して拡張してください。
ただし、機能が特定機能領域向けの複雑なメタデータ集合を必要とする場合は、新しいマニフェスト作成も正当化されます。
新マニフェストが既存マニフェストと異なる領域用途の場合(例:取得タイミングが違う、複雑さが正当化されるなど)は作成もあり得ます。 アプリ用メタデータはWeb App Manifestへ追加、またはその拡張が基本です。 特定用途や非ブラウザーとの相互運用要件がある場合は別アプローチもあり得ます。 Payment Method Manifest・Publication Manifest・Origin Policyがその例です。
例えば、メタデータが1つだけの場合は取得タイミングが違っても既存マニフェスト利用(理想はマニフェスト不要設計)が最善です。 ただし、複雑なメタデータ集合が必要なら新しいマニフェスト作成も正当化されます。
いずれの場合も命名規則は統一してください(§ 12 命名原則参照)。
注: 原則として既存マニフェストは小文字・アンダースコア区切りです。 辞書形式をDOM APIで再利用したい場合はキャメルケースに変換することもあります。 image resourceがその例です。 このため、明確に単語1個で表現できるキーを推奨します。
10.6. シリアライズ時は利用者を考慮する
パーサやシリアライザを含む機能追加・拡張時は、シリアライズへの影響も考慮してください。シリアライズ結果は以下の関係者に影響します:-
ユーザー - シリアライズ結果が最終利用者に表示されることがあるため
-
ツール - シリアライズ出力に依存する場合がある(例:パーサが入力エラー修正の必要有無を検出)
-
Web API - シリアライズされたデータが他のWebプラットフォームAPIに渡されることがあるため
言語固有の期待も考慮してください。例えば、言語によって空白の有無が重要だったり、浮動小数点数の精度許容範囲が異なります。 シリアライズ結果は次の点に注意:
-
開発者の期待に合うべき(例:CSSプロパティのシリアライズがCSS著者の記述と著しく異なる出力にならないこと)
-
べき等性 - パーサ出力をシリアライズした結果を再度パース・シリアライズしても同じになるべき
-
エラーの累積を招かないこと - APIのシリアライズ出力を同じAPIに繰り返し渡しても内部状態が変わらないこと
10.7. 機能は開発者フレンドリーであること
新しい機能は開発者フレンドリーであるべきです。 フレンドリーさの定量化は難しいですが、最低限次の点は考慮してください。
例外のエラーテキストは一般的であるべきですが、開発者向けエラーメッセージ(開発者コンソールなど)は意味のある内容にしてください。 開発者がエラーに遭遇した際、メッセージはそのエラーケースに固有で、過度に一般化されていないこと。
理想的には、開発者向けエラーメッセージは問題箇所を特定するための十分な情報を持つべきです。
CSSなど宣言型機能では、実装でデバッグ性向上のため追加作業が必要になる場合があります。 仕様でこれを定義することで、機能がより開発者フレンドリーとなり、ユーザーにも一貫した開発体験を提供できます。
デバッグ性が仕様の一部として定義された良い例はWeb Animationsです。
10.8. 最良の暗号技術を使い、進化を想定する
セキュリティ専門家による公平なレビューを受けた暗号アルゴリズムのみ利用し、選択したアルゴリズムが実証済みかつ最新であることを確認してください。 暗号プロトコル・アルゴリズムは陳腐化・危殆化だけでなく、急速に進化します。
10.9. Client Hintsで新情報を公開しない
Client Hints利用時は、Webページが既に他手段で取得可能な情報以外を公開しないでください。
Client Hintsは重要な最適化ですが、サイトへの情報公開手段がこれだけにならないようにしてください。RFC 8942 §4.1(Client Hints定義)より:
したがって、この文書に依存するClient Hintヘッダーを用いる機能は、 必ずユーザーエージェントがすでにアプリケーションに公開している既存のリクエストヘッダー、HTML、CSS、JavaScriptなど以外の新情報を提供してはならない。
新しいClient HintでWebページが他手段で取得できない情報を公開しようとする場合は、まずAPIで公開することを検討してください。
11. よい仕様を書くために
本ドキュメントは主にWeb API設計を扱っていますが、API設計者は同時に設計したAPIの仕様執筆も行うはずです。
11.1. 仕様の各要件の対象読者を明確にする
APIの良いコードの書き方(著者向け)と、API実装者が不適切なコードをどう扱うべきか(実装者向け)両方を文書化してください。
Webは他プラットフォームと比べても、不適切なマークアップ受容に対し堅牢に設計されています。 つまり、古いWeb標準利用ページも新しいUAで表示でき、著者の学習コストも低くなります。
これを支えるため、Web仕様執筆者は不適切なマークアップと適切なマークアップ両方の解釈方法も記述する必要があります。
実装者は「対応言語」(supported language)を理解する必要がありますが、これは著者が目指すべき「適合言語」(conforming language)より複雑です。
11.2. 仕様は完全に記述し曖昧さを避ける
機能の動作仕様を書く際は、著者が実装ごとに異なるコードを書かなくて済む十分な情報を必ず与えてください。
仕様が十分に具体的でないと、実装者が異なる選択をしてしまい、その違いに対応するため著者が余分なコードを書かざるを得なくなります。
実装者が他実装の詳細を調べて回避しなくて済むよう、仕様自体を完全かつ明確にしてください。
注: これは実装が異なる描画や権限プロンプトなどのUIを持てないことを意味しません。
注: 実装者は実装方針が明確でない仕様にバグ報告すべきです。
11.2.1. アルゴリズムは明確に定義する
アルゴリズムは明確かつ簡潔に記述してください。
アルゴリズムを書く最も一般的な方法は、明示的なステップ列で記述することです。これは擬似コードになることが多いです。
showModal()
メソッドは、例外発生タイミングや他HTML仕様のアルゴリズム呼び出しタイミングも明確に説明した番号付きステップ列で記述されています。
ステップ列を書く際は、機能的コード片であることを想定してください。
-
入力・出力を明確に指定し、アルゴリズムと変数命名も適切に行い、結果やエラーを返すポイントも明示してください。
-
可能な限り副作用のあるアルゴリズムは避けてください。
アルゴリズムの目的は詳細化前にまとめて説明し、読者がステップを読むか飛ばすか判断できるようにしてください。 例:トップレベルブラウジングコンテキストごとに保留中Xコールバックが最大1個になることを保証するステップ。
単純なステップ列が常に最適なアルゴリズム記述法とは限りません。 繰り返しを避けるために正式な構文や文法を定義・再利用したり、状態機械の状態を定義する場合もあります。 こうした追加構造利用時も上述の助言は適用されます。
できる限り、アルゴリズムは実装方法に近い形で記述してください。 これは仕様執筆の難易度が上がりますが、実装者が仕様記述を実装方法に翻訳する必要が減り、実装ごとの判断違いによる後続機能の実装可否差も減ります。 例えば、実装ごとに異なる判断が将来的に新機能の追加可否に違いを生むこともあります。
関連情報:
11.2.2. 状態は明示的なフラグで管理する
状態の記述には単語ではなく明示的なフラグを使ってください(アルゴリズム記述時)。
明示的なフラグを使うことで、エラー条件ごとに状態が変化するか、フラグの状態がリセットされるタイミングが明確になります。
11.3. 相互運用性と実装容易性の対立解消
機能仕様化はすべての工学作業同様、トレードオフの評価と妥協が必要です。
関係者全員が最善を尽くしても、すべての実装者が実装可能と認める形で相互運用的仕様化ができない場合もあります。
道筋選択はエンドユーザー最優先で判断してください。
まず実装容易性の懸念の本質を見極めてください。 実装者がエンドユーザー被害の可能性を認識し、機能未実装を選ぶ場合は、その機能は仕様化しない方がよく、既に実装済みの実装者もアンシップすべきです。 ただし、アンシップはユーザー・著者の混乱を招くことも忘れないでください。
全エンジンでAPI実装可能でも、動作が完全に相互運用化できない場合もあります。 実装間で動作が異なる場合は、エンドユーザーへの影響を考慮してください。 この方法の欠点は、動作の違いが機能検出不能であることです。 逆に、将来的にすべての実装が同じ動作に収束した場合、サイト側で更新不要で収束恩恵を受けられます。
注: 著者は支配的実装の動作を正しいと見なし、他の動作をバグと判断しやすく、それが支配的実装のさらなる定着につながることもあります。
backdrop-filterプロパティは、実装間で目に見える動作差が多数存在します。 現時点では、関係者はAPI共有のメリットが差異による相互運用性コストを上回ると考えています。
動作差が重大かつ収束不能なら、異なるAPIを仕様化し、各実装者が実装可能な代替案を選ぶ方が良い場合もあります。
この手法のリスクは、将来的に実装が同じ動作に収束した場合、複数APIの恒久サポートが必要となり、開発者負担や実装メンテコスト増につながることです。
同じユースケース向けに2つのAPIを公開すると、著者は機能検出でブラウザーごとに異なるコードを書く必要があります。
注: 著者は支配的実装のAPIバリアントを「正しい」とみなしがちで、他APIバリアントの存在を知らない場合もあり、これが支配的実装の定着をさらに強めることもあります。
この方法を取る場合は、API間の差異を最小化して著者負担を減らしてください。 非標準部分をより大きな標準フレームワークの小さな独立コンポーネントに切り出す設計も可能です。
[ENCRYPTED-MEDIA]や[payment-request]は、非標準コンポーネント(Content Decryption Modulesやpayment methods)をAPI表面から切り離すことでAPI差異を最小化した例です。
一部実装者が実装意思のないAPIでも仕様化を選ぶ場合もあります。 著者は機能検出でAPI利用可否を判断し、ユーザーは対応UAでメリットを得られます。 これは常に問題含みですが、実装者が1社のみの場合は特に問題です。 一部標準化団体では単一実装者しか関心を持たない機能の標準化禁止ルールがあります。 そうでなくても、単一実装者機能の標準化は強く非推奨です。
どの選択肢も受け入れがたい場合もあるでしょう。 その場合は仕様化を見送るのが最善ですが、一部実装者が非標準APIとして機能を出荷してしまうリスクもあります。
11.4. モンキーパッチは避ける
モンキーパッチは、既存仕様に新機能をレイヤーし、既存仕様の動作を拡張・上書き・他の方法で変更するものです。 モンキーパッチは一般的には悪い慣習とされ、以下の理由で避けるべきですが、時に回避困難な場合もあります(どうしても必要な場合の指針参照)。モンキーパッチの例:仕様Aが機能の内部アルゴリズムを定義し、仕様Bがそのアルゴリズムを拡張点を使わず直接上書き・変更する。
このようなモンキーパッチは、基盤機能が変わらない・変わることはないと誤って前提するもので、次の問題を生じます:
-
既存仕様が変更された場合、モンキーパッチ部分が不適合となり、壊れたり混乱を招く。
-
複数仕様が同じ部分をモンキーパッチした場合、パッチ適用順で動作が不整合になる。
-
既存仕様著者があなたのモンキーパッチに気づかない場合、修正合意の機会を失う。最悪、当該動作が他の仕様や動作と密接に関連しており、全体状況が悪化する。
-
実装者がコードメンテ時に基盤仕様だけ読み、モンキーパッチ動作をバグとみなして戻してしまう場合がある。
Wikipediaでもモンキーパッチの落とし穴が説明されています。
11.4.1. どうしてもモンキーパッチが必要な場合
モンキーパッチが不可避な場合(例:新仕様の設計初期)は、必ず次のことを守ってください:-
仕様内で、モンキーパッチは他仕様への一時的変更提案であることを明確に記載してください。例:
-
変更箇所は引用や定義済み用語へのリンクで特定し、番号付きステップの場合は現在の番号も記載(ただし番号だけでは不十分)。複数箇所変更なら定義全文を
<blockquote>で貼り、<ins>や<del>で変更点を明示してもよい。HTTP fetchのステップ4.2「requestのredirect modeが"follow"なら、requestのservice-workers modeを"none"に設定する」の前に次のステップを挿入:
-
新しいステップ。
CSS2.1のブロックレベル整形[CSS2]に関して、CSS 2 § 10.3.3 Block-level, non-replaced elements in normal flowの「over-constrained」計算ルールはここで指定するアライメントに従い無視され、marginプロパティの使用値も調整されません。
とURLSearchParams.delete()の定義にURLSearchParams.has()optional USVString valueパラメータを追加:partial interface URLSearchParams {undefined delete (USVString name ,optional USVString value );boolean has (USVString name ,optional USVString value ); };font-size-adjust定義を次のように変更:
Name: font-size-adjust Value: none | [ ex-height | cap-height | ch-width | ic-width | ic-height ]? [ from-font | <number> ] Initial: none Applies to: all elements and text Inherited: yes Percentages: N/A Computed value: a number or the keyword nonethe keyword none, or a pair of a metric keyword and a <number>Canonical order: per grammar Animation type: discrete if the keywords differ, otherwise by computed value type -
-
モンキーパッチは短く保つ。ステップ追加が多い場合は、仕様内に独立した新アルゴリズムを定義し、パッチから新アルゴリズムを呼ぶ。
-
アルゴリズムのステップ追加・置換時は、エディタが上流アルゴリズムにそのまま貼れるよう新ステップを書く。制御フロー(return, abortなど)はパッチ部分だけでなく上流全体から返ることを前提に。
-
自コミュニティで機能レビューが済み十分な合意が得られたら、既存仕様にissueを提出し、上流コミュニティにもパッチレビューを依頼する。より良い方法の提案があれば真剣に検討する。既存拡張点の利用や早期パッチ除去も可能になる場合がある。
-
上記の
<p class="issue">ブロックは、既存仕様へのissueリンクに更新する。 -
上流仕様にパッチ統合の合意が得られたら、仕様管理者と連携して統合作業を行う。
-
既存仕様にパッチが統合されたら、自仕様からパッチを削除する。
モンキーパッチは「モジュール化」とは異なり、モジュール化は既存技術を独立的に拡張し他仕様に副作用を与えず良い慣習です(例:CSS Modules)。
WebIDLのpartial interfaceやpartial dictionaryによる仕様拡張も通常許容されますが、その場合も既存仕様著者との調整を強く推奨します。
12. 命名原則
名前は次の要素から意味が生まれます:
-
看板性(名前そのもの)
-
利用(人々が時間をかけて名前の意味を理解する方法)
-
文脈(例:左辺のオブジェクトなど)
12.1. 一般的な単語を使う
APIの命名は必ず読みやすい米国英語で行うこと。 多くのWeb開発者が英語ネイティブでないことを念頭に置いてください。 可能な限り、初見でも大多数の英語話者に分かるような一般的な語彙を選んでください。
簡潔さよりも可読性を重視してください。 ただし、短い名前の方が分かりやすい場合も多いです。 例えば、API定義仕様内では専門用語や有名な技術用語を使うのが適切な場合もあります。
例:
Fetch APIのBody
ミックスインのjson()
メソッドは戻り値の種類を表す名前です。
JSONはWeb開発者に広く知られた技術用語なので、これを戻り値型に直結した名前にすることで理解が容易になります。[FETCH]
12.2. ASCII名を使う
名前はローカル言語の制約(例:CSS ident規則など)を遵守し、 できる限りASCII範囲で記述してください。
12.3. 命名は他者と広く相談する
APIの名前は広く相談してください。
意外なところで良い名前やヒントが得られることもあります。
-
他プラットフォームや有名ライブラリでは似たAPIがどんな名前か?
-
エンドユーザーや開発者がAPIが扱う物事・操作対象を何と呼ぶか?
-
他のWebプラットフォーム仕様を調べたり、関連分野の他者に意見を求める
-
名前がインクルーシブかどうかも相談する
基本原則に基づいた明確な根拠のある助言には特に注意を払ってください。
Tantek Çelik はURLの各部分の命名方法を詳細に調査しました。 URL仕様の編集者はこの調査結果を参考にしています。[URL]
Web一貫性のある名前を使う
他技術スタックにも露出する機能・API名を選ぶ際は、他コミュニティよりもWebエコシステムの命名規則を優先してください。NFC標準ではWebでのMIMEタイプにあたるものをmediaと呼びますが、Web
NFCの機能・API命名はMIME type一貫性を優先すべきです。
インクルーシブな言語を使う
可能な限りインクルーシブな言葉遣いをしてください。
例えば、 blacklistやwhitelistの代わりにblocklistやallowlist、 masterやslaveの代わりにsourceやreplicaを使うべきです。
汎用的な人物(著者・ユーザーなど)を指す場合は、"they"や"their"などの汎用代名詞を使ってください。 例:"A user may wish to adjust their preferences"(ユーザーは自分の設定を調整したいかもしれない)。
12.4. 目的を説明する名前を使う
物事を「どうやるか」ではなく「何をするか」で名付けてください。
目的に基づいた名前は将来性が高くなります。 WebからAPIを削除するのは困難なので、名前は実装の詳細よりも長く残る必要があります。
特に、 コードネームやブランド、実装技術の詳細を名前に使うのは避けてください。 これらは陳腐化し、将来的に置き換えが必要になることがあります。
Remote Playback APIはインスピレーション元の既存プロプライエタリシステム(ChromecastやAirPlayなど)の名前を使わず、 APIの機能を表す一般的な用語を選びました。[REMOTE-PLAYBACK]
WebTransport APIはQUICプロトコルによるネットワーク機能を提供しますが、名前はより一般的な「データ輸送」の目的を表しています。 [WebTransport][RFC9000]
12.5. 命名は一貫性を持たせる
命名規則は一貫性を目指し、混乱を避けてください。関連する名前群は次の点で一致させるべきです:
-
品詞(名詞、動詞など)
-
否定表現(セット内で全て許可を表すか、全て拒否を表すか揃える)
Booleanプロパティとboolean戻り値メソッド
Booleanプロパティ・オプション・API引数で質問形式の場合はisで始めないこと。ただし副作用のない質問メソッドならisで始めるべきです(プラットフォーム全体の一貫性のため)。
既存APIと一貫した大文字・小文字規則を使う
完全な一貫性は守られてきませんでしたが、WebプラットフォームAPI設計史で次の規則が定着しています:
| 大文字・小文字規則 | 例 | |
|---|---|---|
| メソッド・プロパティ (WebIDL属性、操作、辞書キー) | キャメルケース | createAttribute()compatMode
|
| クラス・ミックスイン (WebIDLインターフェース) | パスカルケース | NamedNodeMapNonElementParentNode
|
| APIの頭字語 | すべて大文字。ただしメソッド・プロパティの先頭語の場合は除く | HTMLCollectioninnerHTMLbgColor
|
| APIでの頭字語繰り返し | 同じ規則に従う | HTMLHRElementRTCDTMFSender |
| "identity"/"identifier"の略語 | Id。ただしメソッド・プロパティの先頭語の場合は除く
| getElementById()pointerIdid
|
| 列挙値 | 小文字・ダッシュ区切り | "no-referrer-when-downgrade"
|
| イベント | 小文字・連結 |
|
| HTML要素・属性 | 小文字・連結 | figcaptionmaxlength |
| JSONキー | 小文字・アンダースコア区切り | short_name |
isMapプロパティとして
HTMLImageElement
で反映されるように、属性とプロパティの大文字・小文字は必ずしも一致しません。
JSONキー規則は、HTTP送信やディスク保存用の特定JSONファイル形式に適用され、一般的なJSオブジェクトキーには適用されません。
頭字語繰り返しは特に一貫性が低く、有名な例外はXMLHttpRequestやHTMLHtmlElementです。
これらを真似せず、頭字語は繰り返しでも必ず大文字にしてください。
ファクトリメソッド名はcreateまたはfromで始める
ファクトリメソッド名はcreateまたはfromで始め、必要なら後ろに具体的な名詞を続けてください。
新しい空オブジェクトを構築する場合はcreateで始め、
既存データからオブジェクトを作る場合はfromで始めてください。
ファクトリメソッドは例外的に使うべきで、妥当な理由がある場合のみ利用してください。
妥当なファクトリ利用例は、オブジェクト作成時に親オブジェクトへの関連付けが必要な場合(例:document.createXXX())です。
ソースオブジェクトから変換する場合はfromを接頭辞に使ってください。
例:Foo.fromBar()はBarオブジェクトからFooを生成することを示します。
汎用的なファクトリメソッド名はcreate()やfrom()がよく使われます。
他の接頭辞を作ったり、レガシー接頭辞を使うのは避けてください。
例外を設ける理由は、同一オブジェクト配下で既存ファクトリメソッドとの一貫性維持(例:document.initXXX())などですが、新しいファクトリメソッドではこの慣習を継承しないでください。
12.6. 危険な機能は命名で警告する
可能な限り、開発者への保証を弱める機能は名前を"unsafe"で始めて目立たせるようにしてください。
例:
Content Security Policy (CSP)
は特定のコンテンツインジェクション脆弱性を防ぎますが、
unsafe-inlineなどCSP自身の保護を弱める機能もあり、インラインスクリプト許可で保護が減少します。
13. 参考リソース
仕様執筆に関する有用な助言は他にもあります:
-
Writing specifications: Kinds of statements (Ian Hickson, 2006)
-
QA Framework: Specification Guidelines (W3C QA Working Group, 2005)
謝辞
本ドキュメントはTAG現・元メンバーが TAGのデザインレビューで集めた原則で構成されています。 デザインレビュー依頼してくださった皆様に感謝します。
TAGは下記の方々に感謝します: Adrian Hope-Bailie、 Alan Stearns、 Aleksandar Totic、 Alex Russell、 Alice Boxhall、 Andreas Stöckel、 Andrew Betts、 Anne van Kesteren、 Benjamin C. Wiley Sittler、 Boris Zbarsky、 Brian Kardell、 Charles McCathieNevile、 Chris Wilson、 Dan Connolly、 Daniel Ehrenberg、 Daniel Murphy、 David Baron、 Domenic Denicola、 Eiji Kitamura、 Eric Shepherd、 Ethan Resnick、 fantasai、 François Daoust、 Henri Sivonen、 HE Shi-Jun、 Ian Hickson、 Irene Knapp、 Jake Archibald、 Jeffrey Yasskin、 Jeremy Roman、 Jirka Kosek、 Kenneth Rohde Christiansen、 Kevin Marks、 Lachlan Hunt、 Léonie Watson、 L. Le Meur、 Lukasz Olejnik、 Maciej Stachowiak、 Marcos Cáceres、 Mark Nottingham、 Martin Thomson、 Matt Giuca、 Matt Wolenetz、 Michael[tm] Smith、 Mike West、 Nick Doty、 Nigel Megitt、 Nik Thierry、 Ojan Vafai、 Olli Pettay、 Pete Snyder、 Philip Jägenstedt、 Philip Taylor、 Reilly Grant、 Richard Ishida、 Rick Byers、 Rossen Atanassov、 Ryan Sleevi、 Sangwhan Moon、 Sergey Konstantinov、 Stefan Zager、 Stephen Stewart、 Steven Faulkner、 Surma、 Tab Atkins-Bittner、 Tantek Çelik、 Tobie Langel、 Travis Leithead、 そして Yoav Weiss 前身のHTML Design Principlesドキュメントへの貢献も含め、感謝します。
特に Anne van Kesteren および Maciej Stachowiak 両氏に感謝します。両氏はHTML Design Principlesドキュメントの編集を担当しました。
もし本ドキュメントへの貢献者で上記にお名前がない方は、編集者までご連絡ください。記載漏れを訂正します。