コンテンツにスキップ

JavaScript

useEffect? useYourBrain!!

原則

JavaScriptはエンジニアのためのマルチツールではありません。

JavaScriptの機能を実行する必要がある場合JavaScriptを使用します。
JavaScript以外で実現できる機能には、JavaScriptを使用しません。

環境

JavaScriptフレームワークを使用する場合はそのフレームワークのドキュメントに従ってください。 静的ウェブサイトを構築する場合、Viteを推奨します。

Biome(またはESLint)に適合させる

設定ファイルは以下です。要件に合わせて調整してください。
monosus/lint-tools

コーディングスタイル全般も確認してください。

TypeScriptのエラーはコミット時に取り除く

lefthookなどでprecommit時にタイプチェックを行うように徹底してください。 プロジェクトとして、CI(継続的インテグレーション)による証跡が必要でない限り、ローカルで静的テスト(リント)を行う形式で十分です。キックオフミーティングなど導入時に、lefthookなどがが全員の環境で機能しているか確認します。

簡単で良いのでDesign docを作成してからコーディングする

Design doc(デザインドキュメント)とは、エンジニアが実装前の設計や考えていることをまとめるためのフレームワークおよびその成果物です。

  • 目的
  • 概要
  • 設計するもの(やりたいこと)
  • 考慮した代替案/設計しないもの
  • 実装の計画

というアウトラインに沿って制作するものの抽象度を下げていきます。

とはいえ、この章で取り扱うJavaScriptは静的ウェブサイトで用いられるインタラクションなどを主としています。 そのため、重厚なDesign docを用意する必要はなく、GitHubのissueなどに記載するレベルで構いません。何をやろうとしていて何をやらないのかを記述し計画します。
参考:リッチなDesign docの例

最終案に到達するまでに考案した代替案もできる限り記載しましょう。将来そのコードをリファクタリングする場合になぜ別の実装ではなかったのかなどを対応者が把握するのを助けることができます。 また、いくつかの候補から検討したうえで選択した実装は品質が高いものとなるでしょう。将来のためだけでなく、現時点で実装するタイミングでもなぜその様にコーディングしたかを鮮明にしておきましょう。

Design docは未完成のままで他の人と共有して構わないドキュメントです。チームメンバーやレビュアーと積極的に共有してください。ドキュメントどおりのコードになれば最高ですが、最終的な実装がドキュメントと相違していても構いません。無計画にコードを書き始めることがないことが重要です。

参考リソース:メルカリShopsでのDesign Docs運用について | メルカリエンジニアリング

jQueryを使用しない

jQueryは現代のJavaScriptには不要です。我々は工数ではなくプログラムを作成し提供しています。(そしてVanilla JSでも工数は変わりません)

HTML内で単独機能のファイルを読み込まない

ページにはscript.js,entry.js,main.js,about-page.jsの様なエントリーポイントとなるjsファイルを読み込みます。 これはJavaScriptの読み込み順などによる思わぬバグがでないようにするための処置であると同時に、あるページはmain.jsでアコーディオン機能を提供、あるページではaccordion.jsでアコーディオン機能を提供などの揺れがでないようにするためです。

⛔Bad
<script src="accordion-helper.js">
👍Good
<script type="module" src="entry.js">

モダンJavaScriptを使用する

ES6以降の機能を積極的に使用して、コードをより簡潔で読みやすく、保守しやすくします。

ES6以降の機能例

例1:アロー関数

// ⛔Bad:伝統的な関数宣言を使用する
function add(a, b) {
return a + b;
}
// 👍Good:アロー関数を使用する
const add = (a, b) => a + b;

例2:テンプレートリテラル

javascript
// ⛔Bad:文字列連結を使用する
const name = 'John';
const greeting = 'Hello, ' + name + '!';
// 👍Good:テンプレートリテラルを使用する
const name = 'John';
const greeting = `Hello, ${name}!`;

例3:分割代入

// ⛔Bad:オブジェクトのプロパティに個別にアクセスする
const person = { name: 'John', age: 30 };
const name = person.name;
const age = person.age;
// 👍Good:分割代入を使用する
const person = { name: 'John', age: 30 };
const { name, age } = person;

例4:スプレッド構文

// ⛔Bad:配列の結合を手動で行う
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = arr1.concat(arr2);
// 👍Good:スプレッド構文を使用する
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];

例5:デフォルトパラメータ

// ⛔Bad:デフォルト値を手動で設定する
function greet(name) {
const actualName = name || 'Guest';
return Hello, ${actualName}!;
}
// 👍Good:デフォルトパラメータを使用する
function greet(name = 'Guest') {
return Hello, ${name}!;
}

カスタムよりもネイティブ

独自のものを作成するのではなく、ネイティブ関数、API、機能を優先して使用します。

// 🤬Not Good:カスタム関数を使用して配列をソートする
function customSort(arr) {
for (let i = 0; i < arr.length; i++) {
for (let j = i + 1; j < arr.length; j++) {
if (arr[i] > arr[j]) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
return arr;
}
const numbers = [5, 3, 8, 1, 2];
const sortedNumbers = customSort(numbers);
console.log(sortedNumbers); // [1, 2, 3, 5, 8]
// 👍Good:ネイティブのsortメソッドを使用して配列をソートする
const numbers = [5, 3, 8, 1, 2];
const sortedNumbers = numbers.sort((a, b) => a - b);
console.log(sortedNumbers); // [1, 2, 3, 5, 8]
// 🤬Not Good:カスタム関数を使用して非同期処理を行う
function fetchData(url, callback) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = () => callback(JSON.parse(xhr.responseText));
xhr.send();
}
fetchData('https://api.example.com/data', (data) => {
console.log(data);
});
// 👍Good:ネイティブのfetch APIを使用して非同期処理を行う
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data));

不要なAPI呼び出しを控える

メモリリークなどに影響があるため、ループ処理などで同一APIを何度も叩くことのないようにします。

// ⛔Bad
for(const item of document.querySelectorAll(".item-list > li")) {/* ... */}
// 👍Good
const listItems = document.querySelectorAll(".item-list > li");
for(const item of listItems){/* ... */}

バックエンドやサーバーから取得したデータの保存についてはクライアントと相談する

同一ページで何度もデータベースにアクセスすると、何度もレスポンス時間が発生するためにUX(ユーザーエクスペリエンス)に悪影響がでるかもしれません。 1秒未満の待機時間では、待機時間を埋める必要はありません。1〜3秒の待機時間では、不確定な進行状況インジケーターを使用します。3〜10秒の待機時間では、確定的な進行状況インジケーターを使用します。10秒以上の待機時間では、確定的な進行状況インジケーターを使用し、バックグラウンドでタスクを処理します。

Loading & progress indicators — UI Components series

データベースに保存されている情報が必要で、コード内でその情報が複数回必要になる(または要求される可能性がある)場合は、データベースに1回だけアクセスし、後続の処理のためにデータをローカルに保存することが望ましいです。 ただし、どの様にフロントでデータを管理するかについてはクライアントと相談し、合意したうえで対応しましょう。

ユーザーストレージの参考:

  • 再訪者に同じデータを使用する目的 → Cookie
  • 今回のセッション中のみ使用 → セッションストレージ
  • 取得したデータは一般的な内容であり個人情報などセキュアなものを含んでいない、そのデータをある程度永続的に管理したい → ローカルストレージまたはIndexedDB

あとから追加されたDOMにも対応出来るようにデリゲーションする

クライアントと相談し、運用フェーズで接客ツールなどを使用しUI(ユーザーインターフェース)の複製や上書きがLoad時に行われることが見込まれる場合はイベントデリゲーションでの実装を検討してください。

// ⛔Bad:直接イベントリスナーを追加する
document.querySelectorAll('.dynamic-button').forEach(button => {
button.addEventListener('click', () => {
console.log('Button clicked!');
});
});
// 👍Good:イベントデリゲーションを使用する
document.addEventListener('click', (event) => {
if (event.target.matches('.dynamic-button')) {
console.log('Button clicked!');
}
});

関数の引数はオブジェクト形式で渡す

関数を呼び出す際の見通しはとても重要です。特に関数制作者と使用者が異なる場合に実装間違いやケアレスミスを防ぐためにルールを統一します。 (あなた以外の開発者も対象として)将来拡張される可能性がなく、引数は一つしか受け取らないようであればオブジェクトではない形式で構いません。

// ⛔Bad:複数の引数を個別に渡す
function createUser(name, age, email) {
console.log(Name: ${name}, Age: ${age}, Email: ${email});
}
createUser('John Doe', 30, 'john.doe@example.com');
// 👍Good:オブジェクト形式で引数を渡す
function createUser({ name, age, email }) {
console.log(Name: ${name}, Age: ${age}, Email: ${email});
}
createUser({ name: 'John Doe', age: 30, email: 'john.doe@example.com' });

エラーハンドリングを盛り込む

エラーハンドリングは、予期しないエラーが発生した際にアプリケーションが適切に対応し、クラッシュを防ぐために重要です。 エラーハンドリングを行うことで、ネットワークエラーやAPIの不具合などに対してログを出したり、スクリプトの失敗がページ全体の問題にならないように設計できます。

例:fetch操作

// ⛔Bad:エラーハンドリングを行わない
function fetchData(url) {
return fetch(url)
.then(response => response.json())
.then(data => console.log(data));
}
fetchData('https://api.example.com/data');
// 👍Good:エラーハンドリングを行う
function fetchData(url) {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('There was a problem with the fetch operation:', error));
}
fetchData('https://api.example.com/data');

例:DOM操作

// ⛔Bad:エラーハンドリングを行わない
function updateElementText(id, text) {
document.getElementById(id).innerText = text;
}
updateElementText('myElement', 'Hello, World!');
// 👍Good:エラーハンドリングを行う
function updateElementText(id, text) {
try {
const element = document.getElementById(id);
if (!element) {
throw new Error(Element with id ${id} not found);
}
element.innerText = text;
} catch (error) {
console.error('Error updating element text:', error);
}
}
updateElementText('myElement', 'Hello, World!');

サードパーティスクリプトはクライアントが指定したものだけを実装する

CDNからライブラリを読み込むことは基本的にNGです。

可能な場合は計測スクリプトを遅延読み込みする

ウェブパフォーマンスを阻害する最大のボトルネックはサードパーティ製の計測タグやカスタムスクリプトです。 計測はマーケティング上必須であってもサイトに訪れるユーザーには必要のないものです。
可能であればユーザー体験を優先し、
third-party-script.jsのようなjsファイルを作成し、DOMContentLoadedイベント発火時にを計測タグなどをDOMに挿入する形でレンダリングをブロックさせないようにします。

サードパーティjsを使用してユーザー体験を向上させる読み込みイメージ

  • クライアントサイドでABテストを行う場合、上記の順番ではテスト前の状態が一度レンダリングされてしまうため適しません
  • 遅延と言っても多大に遅れるわけではありません。コーディングのプロがしっかりと設計したウェブページはDOMContentLoadedまでの時間も最小限になります

この項目については、クライアントと話し合いながらそのプロジェクトにあった方法を採用してください。

ライブラリを使用する場合は容量だけでなくライブラリ内の依存関係も確認する

依存ライブラリが無いライブラリであればアップデート漏れによる不具合などが出る可能性が下がります。
ライブラリを使用しなければ依存関係に悩むことはありません。
また、フロントエンド領域のコーディングであればdevDependenciesで読み込んでいるライブラリの依存関係は一定目を瞑って問題ありません。

module形式で読み込む

module形式で読み込むことで読み込みタイミングがDOMContentLoaded時点とになります。またグローバルスコープに記述が漏れることもなくなります。

main.js
import { add, subtract, PI } from './mathUtils.js';
console.log('Add:', add(5, 3)); // 出力:Add: 8
console.log('Subtract:', subtract(5, 3)); // 出力:Subtract: 2
console.log('PI:', PI); // 出力:PI: 3.14159

Dynamic importを使用し必要なモジュールだけを読み込む

対象の要素やセレクターがあった場合にだけ必要となるモジュールを読み込みます。
サイト全体で使うことがすでに決まっているものは通常のimportで問題ありません。

// ⛔Bad:ページロード時に常に読み込む
import './accordion-helper.js';
// 👍Good:必要な場合にのみ動的に読み込む
document.addEventListener('DOMContentLoaded', () => {
if (document.querySelector('details')) {
import('./accordion-helper.js')
.then(module => {
// モジュールの使用
module.initializeAccordion();
})
.catch(error => {
console.error('Error loading accordion-helper.js:', error);
});
}
});

条件分岐のネストを浅くする

例えばif文のネストは最大でも2つまでとし、さらに複雑になるようであれば別の関数や文に切り出して処理の見通しを確保します。

scrollresizeイベントを使用する前に別の方法を検討する

scrollイベントの代わりにIntersectionObserver
resizeイベントの代わりにResizeObserver

を使用できないか検討してください。パフォーマンスと汎用性の観点からより望ましい選択肢となります。

scrollresizeイベントを使用する場合はthrottledebounce処理を組み込む

スクロールにはthrottle、リサイズにはdebounceを組み込みます。

参考リソース:Debounce と Throttle(JavaScript)

// ⛔Bad:直接イベントリスナーを追加する
window.addEventListener('scroll', () => {
console.log('Scroll event fired');
});
window.addEventListener('resize', () => {
console.log('Resize event fired');
});
// 👍Good:throttleを使用する
window.addEventListener('scroll', throttle(() => {
console.log('Scroll event fired');
}, 200));
// 👍Good:debounceを使用する
window.addEventListener('resize', debounce(() => {
console.log('Resize event fired');
}, 200));

debug用のコンソールログを納品ファイルに混ぜない

サイトオーナー側で継続的に開発する場合において、
引き続き開発時に必要なログはその旨のコメントとともにコメントアウトします。

// ⛔Bad:debug用のコンソールログをそのまま残す
function processData(data) {
console.log('Processing data:', data); // デバッグ用
// データ処理のコード
}
// 👍Good:debug用のコンソールログをコメントアウトする
function processData(data) {
// console.log('Processing data:', data); // デバッグ用
// データ処理のコード
}