はじめに
Google Apps ScriptのWebアプリで、フォーム自体は表示されているのに、全画面ローディングのスピナーが止まらないことがあります。
選択肢が空のまま、ボタンも反応せず、利用者から見ると「読み込み中で固まった」状態です。
このとき、原因はサーバー処理の遅さではなく、google.script.runの成功後に実行する初期化関数でJavaScript例外が起きていることがあります。
よくある原因
初期データ取得後にonInit(data)を呼び、その中でプルダウン作成、ボタン設定、イベント登録、ローディング解除をまとめて行う構成はよくあります。
google.script.run
.withSuccessHandler(onInit)
.withFailureHandler(function(err) {
hideMask();
showToast('読み込み失敗: ' + err.message, true);
})
.getInitialData();
問題は、onInitの途中で例外が起きると、後続のhideMask()まで到達しないことです。
たとえば、増減ステッパー用の.stepperを処理する関数があり、別用途のボタンまで同じclassで囲んでしまうと、必要な属性がない要素に対して処理してしまいます。
// NG: data-steps がない .stepper があると null.split で落ちる
document.querySelectorAll('.stepper').forEach(function(box) {
var steps = box.getAttribute('data-steps').split(',');
// ...
});
getAttribute('data-steps')がnullを返し、null.splitでTypeErrorになります。ここで初期化処理が止まるため、マスク解除もイベント登録も行われません。
こんな場面で使えます
- GAS Webアプリの初期表示で全画面スピナーを出している
google.script.runで初期データを取得してから画面を組み立てている- フォームは表示されるが、選択肢が入らず操作できない
- クライアント側の初期化処理が増えてきた
フォームHTML自体が出ているなら、doGetではなく、初期データ取得後のonInitかクライアント側処理を疑うと切り分けやすくなります。
実装コード
初期化はtry/catch/finallyで囲む
onInitでは、失敗しても必ずマスクを消すようにします。
function onInit(data) {
try {
fillSelects(data);
buildSteppers();
bindEvents();
} catch (e) {
showToast('初期化エラー: ' + (e && e.message ? e.message : e), true);
console.error(e);
} finally {
hideMask();
}
}
finallyにhideMask()を置くのがポイントです。初期化のどこかで落ちても、画面を完全に操作不能なまま残しにくくなります。
failure handlerでもマスクを消す
サーバー側のgetInitialData()自体が失敗した場合にも、同じようにローディングを解除します。
function loadInitialData() {
showMask('読み込み中...');
google.script.run
.withSuccessHandler(onInit)
.withFailureHandler(function(err) {
hideMask();
showToast('読み込み失敗: ' + (err && err.message ? err.message : err), true);
console.error(err);
})
.getInitialData();
}
withFailureHandlerを付けていないと、サーバー側の例外が画面上では分かりにくくなります。ローディング表示とセットで必ず入れておきましょう。
querySelectorAllの対象を属性でガードする
複数の部品で似たclassを使う場合は、必要な属性があるかを確認してから処理します。
function buildSteppers() {
document.querySelectorAll('.stepper').forEach(function(box) {
var target = box.getAttribute('data-target');
var stepsAttr = box.getAttribute('data-steps');
if (!target || !stepsAttr) {
return;
}
var steps = stepsAttr.split(',').map(Number);
// ...
});
}
さらに安全にするなら、別用途のボタンは.stepperを使い回さず、.rate-actionsのような専用classを付けます。見た目の都合でclassを共有すると、初期化処理の対象まで広がることがあります。
切り分けの見方
フォームHTMLすら表示されない場合は、doGetやHTMLテンプレートの生成で止まっている可能性があります。
一方で、フォームは表示されるが選択肢が空、スピナーが止まらない、ボタンが動かない場合は、google.script.runの成功後に走る初期化処理を確認します。
最初に見るべき場所は、ブラウザの開発者ツールのConsoleです。TypeErrorやCannot read properties of nullのようなエラーが出ていれば、サーバーではなくクライアント側の例外です。
注意点
hideMask()は成功時の最後だけでなく、finallyとwithFailureHandlerの両方から呼べるようにする- 共通classを使う処理では、必要な
data-*属性があるかを先に確認する - 初期化処理の中で例外を握りつぶすだけにせず、
console.errorにも残す - スピナーを表示する関数と解除する関数は、画面全体で呼び方を統一する
innerHTMLへエラーメッセージを出す場合はHTMLエスケープを行い、XSSを避ける
関連記事
- google.script.runのエラーハンドリング完全パターン【GAS Webアプリ】
- GASでCSSだけのローディングスピナーを実装する方法
- GAS Webアプリのスマホ余白を抑えるHTMLテンプレート
- GASのSPA風画面遷移で画面を軽く切り替える方法
まとめ
GAS Webアプリの無限ローディングは、サーバー処理ではなくクライアント側の初期化例外が原因のことがあります。
onInitをtry/catch/finallyで囲み、withFailureHandlerでもマスクを解除し、querySelectorAllの対象を属性でガードしておくと、固まったように見える画面をかなり減らせます。
