はじめに
本業で静的サイトのコーディング案件を担当しているのですが、AWS S3で公開されるので、SSIやPHPインクルードが使えません。サイト内で共通するHTMLを各ページのHTMLに直接書くのは修整が入った時に非常に面倒なのでJSでインクルード化を行いました。
素のJSでHTMLをインクルードさせるのは初めてだったので、実際に書いたコードや躓いたところをまとめます。
HTML
インクルードファイルの挿入先となるHTMLです。当記事ではヘッダーをインクルードさせる想定で書いていきます。
<div class="header" data-include="header"></div>
上記コードはインクルードファイルの挿入先です。data-includeをJSで取得します。
data属性の値=ファイル名のルールにしているのでインクルードファイル名とdata-includeの値は一致させます。
インクルード対象となるヘッダーの中身は正直何でも良いので割愛します。
JavaScript
続いてはJSです。
document.addEventListener("DOMContentLoaded", async () => {
const includeElements = document.querySelectorAll("[data-include]");
await Promise.all(
[...includeElements].map(async (element) => {
const includeName = element.dataset.include;
const filePath = `${includeName}.html`;
try {
const response = await fetch(`/assets/include/${filePath}`);
if (!response.ok) {
throw new Error(
`Failed to load ${filePath}: HTTP ${response.status}`,
);
}
const html = await response.text();
element.insertAdjacentHTML("afterbegin", html);
} catch (error) {
console.error(`Failed to include ${filePath}:`, error);
}
}),
);
});
やっている事はシンプルです。data-include属性の値と一致するファイルをfetchで取得しています。取得に成功したファイルの記述はresponse.textで非同期に文字列として取得します。
インクルードファイルを同一ページで複数読み込む場合もmapのループで返ってくるPromiseをPromise.allで監視し、全てのインクルードファイルの読み込みが完了するのを待つことが出来ます。
もし、インクルードファイルの取得でエラーが発生した場合でも、try...catchでエラーを捕捉して処理することができます。今回のコードではcatchでエラーを処理したあとにエラーを再度throwしていないため、そのPromiseはエラー状態にならず、Promise.allも他のインクルード処理の完了を待つことができます。
インクルードファイルの取得が成功したら、insertAdjacentHTMLで挿入先の要素にインクルードファイルの記述を挿入します。
insertAdjacentHTMLはHTMLを指定の場所に挿入するメソッドで今回は対象要素の最初の子要素に追加しています。指定出来る挿入先は以下の4つです。
| 第一引数 | 挿入位置 |
|---|---|
| beforebegin | 指定要素の直前に兄弟要素として追加 |
| afterbegin | 指定要素の先頭に子要素として追加 |
| beforeend | 指定要素の最後に子要素として追加 |
| afterend | 指定要素の直後に兄弟要素として追加 |
躓いたところ
JSを使っているので仕方ないですが、コードの読み込みタイミングを把握しておかないと思わぬところに影響が出ます。
例えば、ヘッダーはハンバーガーメニューやメガメニューなどがある事がほとんどです。その処理の実行タイミングをインクルードファイルの読み込みが完了した後にしないと存在しないDOMに対しての処理となってしまい、イベントが発火しない原因になります。
document.addEventListener("DOMContentLoaded", async () => {
const includeElements = document.querySelectorAll("[data-include]");
await Promise.all(
[...includeElements].map(async (element) => {
const includeName = element.dataset.include;
const filePath = `${includeName}.html`;
try {
const response = await fetch(`/assets/include/${filePath}`);
if (!response.ok) {
throw new Error(
`Failed to load ${filePath}: HTTP ${response.status}`,
);
}
const html = await response.text();
element.insertAdjacentHTML("afterbegin", html);
} catch (error) {
console.error(`Failed to include ${filePath}:`, error);
}
})
);
});
// インクルードが完了する前に実行しても動作しない
setHamburgerMenu();
setMegaMenu();
この対策としては、インクルードファイルの読み込みが完了してから実行したい関数はPromise.allの後ろに書きましょう。
document.addEventListener("DOMContentLoaded", async () => {
const includeElements = document.querySelectorAll("[data-include]");
await Promise.all(
[...includeElements].map(async (element) => {
const includeName = element.dataset.include;
const filePath = `${includeName}.html`;
try {
const response = await fetch(`/assets/include/${filePath}`);
if (!response.ok) {
throw new Error(
`Failed to load ${filePath}: HTTP ${response.status}`,
);
}
const html = await response.text();
element.insertAdjacentHTML("afterbegin", html);
} catch (error) {
console.error(`Failed to include ${filePath}:`, error);
}
})
);
// インクルード側で使用している関数はここで実行する
setHamburgerMenu();
setMegaMenu();
});
await Promise.allでインクルードファイルの読み込みの完了を待ち、その後ろに記述している処理は全てのインクルードファイルの読み込み処理が完了した後に実行されるからです。
特に外部サービスを利用する場合は要注意で、CDNのようにHTMLにscriptタグを設置するタイプの場合は、実行のタイミングをインクルードファイルの読み込みが終わった後にしないと正常に動作しない原因になります。
外部サービス側のオプションで初期化メソッドがあるのが理想ですが、無い場合は次のように対応できます。
document.addEventListener("DOMContentLoaded", async () => {
const includeElements = document.querySelectorAll("[data-include]");
await Promise.all(
[...includeElements].map(async (element) => {
const includeName = element.dataset.include;
const filePath = `${includeName}.html`;
try {
const response = await fetch(`/assets/include/${filePath}`);
if (!response.ok) {
throw new Error(
`Failed to load ${filePath}: HTTP ${response.status}`,
);
}
const html = await response.text();
element.insertAdjacentHTML("afterbegin", html);
} catch (error) {
console.error(`Failed to include ${filePath}:`, error);
}
})
);
// 外部サービスのscriptタグをインクルードファイルの読み込み後に挿入
addScriptTag();
});
// scriptタグを生成してbodyタグの最後尾に追加する
const addScriptTag = () => {
const script = document.createElement("script");
script.src = 'url';
document.body.appendChild(script);
}
ちなみに、scriptタグをinsertAdjacentHTMLやinnerHTMLで挿入するとDOMとして追加はされますが、scriptタグは実行されません。
※@Florian 様からのコメントで知りました。
ご指摘ありがとうございます!
JSインクルードで表示する際の問題
JSでインクルードする場合、
HTML読み込み
↓
JavaScript実行
↓
fetchでインクルードファイル取得
↓
HTMLをDOMに挿入
という流れになるため、どうしてもインクルードしている箇所が表示されないのが見えてしまいます。特にヘッダーのようなページ上部に配置するものは「ページを開いた瞬間は何もない → 少し経ってから表示される」というチラつきが発生してしまいます。
またHTMLの読み込み後にインクルードが完了する都合上、レイアウトシフトが起きる原因にもなります。
対策として出来ることは、CSSでインクルードする要素が挿入される場所に明示的にheightを指定しておきましょう。
header {
height: 80px;
}
こうすることで遅れて要素が追加されてもレイアウトシフトが起きることは防げます。
ただこれも、インクルードする要素によっては具体的なheightを指定するのは難しいと思うので万能な方法ではないです。
SSIを使えるのがベストですが、JSでインクルードするデメリットを理解した上でサイトの要件と照らし合わせて採用するかを検討するのが良いと思います。
おわりに
こちらのコードを書く前に色々調べてみましたが、非推奨のdocument.writeやXMLHttpRequestなど結構古い情報が多い印象でした。
基本的に静的サイトはSSIかPHPインクルードを使うことが多いですし、SPAであればコンポーネント化出来るのであまり使う機会は多くないと思いますが、頭の片隅に置いておくといつか役に立つかもしれません。
