GAS(HtmlService)でウェブアプリを作成する時、ページ遷移をどうするかで悩むことがあるかと思います。
Vue.jsやReactのRouter機能を使う方法もあるようですが、GASのお手軽感が損なわれてしまいます。
・・・ということで、上記のようなライブラリを使わずに、ブラウザの 戻る / 進む にも対応したSPAのページ遷移を実装してみました。
URLフラグメント(#hoge)を利用した方法を例にしていますが、
記事の最後に、URLパラメーター(?page=hoge)を利用した方法も掲載しています。
スクリプト
function doGet() {
return HtmlService.createTemplateFromFile("index").evaluate();
}
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
.page:not(.isActive) {
display: none;
}
</style>
</head>
<body>
<!-- ナビゲーション -->
<div>
<a class="link" href="home">ホーム</a>
<a class="link" href="products">製品紹介</a>
<a class="link" href="contact">お問合せ</a>
</div>
<!-- ここにコンテンツを表示 -->
<div class="page" id="home">
<h1>ホーム</h1>
<p>homeを表示しています</p>
</div>
<div class="page" id="products">
<h1>製品紹介</h1>
<p>productsを表示しています</p>
</div>
<div class="page" id="contact">
<h1>お問合せ</h1>
<p>contactを表示しています</p>
</div>
<div class="page" id="error">
<h1>NOT FOUND</h1>
<p>ページが見つかりませんでした</p>
</div>
<!-- JavaScript -->
<script>
// 各ページ(div要素)を配列に格納
const pages = Array.from(document.getElementsByClassName("page"));
// ページ切り替え処理
function switchPage(pageName) {
// ページ未指定の場合はhomeを,ページが存在しない場合はerrorを表示
if (!pageName) {
pageName = "home";
} else if (!pages.some(page => page.id === pageName)) {
pageName = "error";
}
// 表示対象ページのみisActiveクラスを付与
for (const page of pages) {
page.id === pageName
? page.classList.add("isActive")
: page.classList.remove("isActive")
}
}
// ウェブアプリアクセス時の処理
google.script.url.getLocation(location => {
switchPage(location.hash);
});
// ブラウザバック・フォワード時の処理
google.script.history.setChangeHandler(e => {
switchPage(e.location.hash);
});
// aタグクリック時の処理(ページ切り替え・履歴追加)
Array.from(document.getElementsByClassName("link")).forEach(el => {
el.addEventListener("click", e => {
e.preventDefault(); // aタグのページ遷移動作を無効化
const pageName = e.target.getAttribute("href"); // aタグのhref属性を取得
switchPage(pageName); // ページを切り替える
google.script.history.push(null, null, pageName); // URLフラグメント(#ページ名)をブラウザの履歴にプッシュ
});
});
</script>
</body>
</html>
aタグをクリックした時、そのaタグのhref属性を取得し、その値と一致するidの要素のみにisActiveクラスを付与します。
isActiveクラスを持たない要素は、cssのdisplay: none;によって非表示になります。
その次に、google.script.history.pushを使い、ブラウザの履歴に記録させます。
第3引数に値を設定すると、URLフラグメントとして扱われますので、https://script.google.com/macros/…省略…/dev#homeのようなURLになります。
aタグのクリック時はhref属性からリンク先を取得しましたが、
ウェブアプリへのアクセス時は、google.script.url.getLocationでURLフラグメント(URLの#以降の文字列)を取得、
ブラウザバック・フォワード時については、google.script.history.setChangeHandlerでコールバック関数を設定し、その引数となるイベントオブジェクトからURLフラグメント(URLの#以降の文字列)を取得しています。
通常のウェブアプリであれば、もう少しシンプルな実装(参考ページ)も可能なのですが、HtmlServiceで作成したウェブアプリは、iframeの中で実行されるという特徴があるため、少し複雑になっています。
HtmlServiceにおけるブラウザの履歴管理については、こちらが参考になります。
コードの分割
先ほどのようにindex.htmlにすべて記載すると、読みにくくなってしまいますので、コードを分割するのが良いと思います。
HtmlServiceでのコード分割は非常に簡単です。
- htmlファイルを作成し、
- コードをそのファイルにカット&ペーストし、
- HtmlService.createHtmlOutputFromFile(“ファイル名“).getContent(); で切り出したコードを読み込む
という手順でOKです。
今回のプロジェクトでコード分割した場合のファイル構成は、このような感じです。
ファイル | 説明 |
---|---|
コード.gs | サーバー側スクリプト |
index.html | メインのhtmlファイル |
navigation.html | 各ページへのリンク |
home.html products.html contact.html error.html | index.htmlに差し込むコンテンツ |
script.html | JavaScriptを切り出したファイル |
各ファイルの中身は以下のとおりです。
products.html, contact.html, error.htmlは、home.htmlと同様なので省略しています。
function doGet() {
return HtmlService.createTemplateFromFile("index").evaluate();
}
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<?!= HtmlService.createHtmlOutputFromFile("css").getContent(); ?>
</head>
<body>
<!-- ナビゲーション -->
<?!= HtmlService.createHtmlOutputFromFile("navigation").getContent(); ?>
<!-- ここにコンテンツを表示 -->
<?!= HtmlService.createHtmlOutputFromFile("home").getContent(); ?>
<?!= HtmlService.createHtmlOutputFromFile("products").getContent(); ?>
<?!= HtmlService.createHtmlOutputFromFile("contact").getContent(); ?>
<?!= HtmlService.createHtmlOutputFromFile("error").getContent(); ?>
<!-- JavaScript -->
<?!= HtmlService.createHtmlOutputFromFile("script").getContent(); ?>
</body>
</html>
<style>
.page:not(.isActive) {
display: none;
}
</style>
<div>
<a href="home">ホーム</a>
<a href="products">製品紹介</a>
<a href="contact">お問合せ</a>
</div>
<div class="page" id="error">
<h1>ホーム</h1>
<p>home.htmlを表示しています</p>
</div>
<script>
// 各ページ(div要素)を配列に格納
const pages = Array.from(document.getElementsByClassName("page"));
// ページ切り替え処理
function switchPage(pageName) {
// ページ未指定の場合はhomeを,ページが存在しない場合はerrorを表示
if (!pageName) {
pageName = "home";
} else if (!pages.some(page => page.id === pageName)) {
pageName = "error";
}
// 表示対象ページのみisActiveクラスを付与
for (const page of pages) {
page.id === pageName
? page.classList.add("isActive")
: page.classList.remove("isActive")
}
}
// ウェブアプリアクセス時の処理
google.script.url.getLocation(location => {
switchPage(location.hash);
});
// ブラウザバック・フォワード時の処理
google.script.history.setChangeHandler(e => {
switchPage(e.location.hash);
});
// aタグクリック時の処理(ページ切り替え・履歴追加)
Array.from(document.getElementsByClassName("link")).forEach(el => {
el.addEventListener("click", e => {
e.preventDefault(); // aタグのページ遷移動作を無効化
const pageName = e.target.getAttribute("href"); // aタグのhref属性を取得
switchPage(pageName); // ページを切り替える
google.script.history.push(null, null, pageName); // URLフラグメント(#ページ名)をブラウザの履歴にプッシュ
});
});
</script>
HtmlService…getContents();の記述の繰り返しが冗長に感じられる場合は、こちらの記事を参考にされるとよいかと思います。
参考:URLパラメーターで実装する
ほとんど同じコードで、URLパラメーターを使う方法も実装できました。
先述の方法(URLフラグメント)では、https://script.google.com/macros/…省略…/dev#homeのようなURLになりましたが、こちらの方法ではhttps://script.google.com/macros/…省略…/dev?page=homeのようなURLになります。
参考として掲載させていただきます。(濃い色部分が変更箇所です)
<script>
// 各ページ(div要素)を配列に格納
const pages = Array.from(document.getElementsByClassName("page"));
// ページ切り替え処理
function switchPage(pageName) {
// ページ未指定の場合はhomeを,ページが存在しない場合はerrorを表示
if (!pageName) {
pageName = "home";
} else if (!pages.some(page => page.id === pageName)) {
pageName = "error";
}
// 表示対象ページのみisActiveクラスを付与
for (const page of pages) {
page.id === pageName
? page.classList.add("isActive")
: page.classList.remove("isActive")
}
}
// ウェブアプリアクセス時の処理
google.script.url.getLocation(location => {
switchPage(location.parameter.page);
});
// ブラウザバック・フォワード時の処理
google.script.history.setChangeHandler(e => {
switchPage(e.location.parameter.page);
});
// aタグクリック時の処理(ページ切り替え・履歴追加)
Array.from(document.getElementsByClassName("link")).forEach(el => {
el.addEventListener("click", e => {
e.preventDefault(); // aタグのページ遷移動作を無効化
const pageName = e.target.getAttribute("href"); // aタグのhref属性を取得
switchPage(pageName); // ページを切り替える
google.script.history.push(null, { page: pageName } ); // URLパラメーターをブラウザの履歴にプッシュ
});
});
</script>
コメント