背景
自分用のクイズWebアプリを作成したかった。(目的は、TOEIC対策”金フレ”本の復習用)
アプリの要件は以下2点。
- 通勤時に手軽にできる(外出先・スマホからでも、アクセス可能であること)
- 問題(+答え)追加が簡単(外出先・スマホからでも、データ追記できること)
調べていると、まさに望むアプリを作られている先駆者様を発見しました。
Blog kon様 - 勉強用Google Apps Script (Quiz Script)
ここから、GAS(Google Apps Script)を利用し、Googleスプレッドシートから問題・答えを取得・出題するWebアプリを目指そうと決めました。
まず、kon様のコードをほぼコピペさせて頂き、自分用クイズWebアプリができました。
しかし、2018年4月現在、コードの2つのクラスがGoogleによって非推奨とされていました。
アプリ自体はよく出来ており現在動作もしますが、このまま非推奨クラス入りのアプリを使い続けていて、試験直前に突然動かなくなる等は避けたいな、と考えました。
目的
kon様のクイズWebアプリを、リファクタリングさせていただきます。(Google非推奨クラス利用を回避する)
また、私と同じくkon様のWebアプリを基にして、今後自分用クイズWebアプリをコーディングされる方のために、この資料が参考になればと思い、本記事を作成しました。
結論
リファクタリング成功しました。
廃止クラス対応のために内部処理総替え成功。見た目はほぼ変わってない
— すいばり@'18年度1戦0勝1敗 (@Suibari_cha) 2018年4月24日
Qiitaに投稿したくなる気持ちよさ pic.twitter.com/yHzj29v8cM
ご参考に、リファクタリング前(kon様コードのほぼコピペ)は以下。
GASでTOEIC対策アプリ作った
— すいばり@'18年度1戦0勝1敗 (@Suibari_cha) 2018年4月20日
予めGoogleスプレッドシートに登録した問題から、ランダムに出題してくれる仕組み
某有名単語帳でミスったところを登録して、復習用にしてる pic.twitter.com/WNOu9P1NUl
内容
コードは以下の通り。
以下2つのファイルをスプレッドシートスクリプトエディタ上に配置し、ウェブアプリケーションとして導入してください。(方法は、GASによるWebAppについて"もっと"入門向けに書いてみる をご参考に)
SpreadsheetApp.openByUrl(~)には、データ取得先のスプレッドシートURLをコピペしてください。
<html>
<head>
<title>Quiz Web App</title>
<script>
// ボタンがクリックされたとき呼び出されるハンドラ
function onbtnclick(){
// html読み取り
var answer = document.getElementById("answer");
var btn = document.getElementById("btn");
if(answer.style.visibility == "hidden"){
// 答えが表示されていないので、答えを表示しボタンを「次の問題」に
btn.innerHTML = "次の問題";
answer.style.visibility = "visible";
} else {
// 答えが表示されているので、問題・答えを取得して答えを非表示にしボタンを「答えを見る」に
new_quiz();
btn.innerHTML = "答えを見る";
answer.style.visibility = "hidden";
}
}
// サーバ側スクリプトnew_quiz_sv()が実行成功したとき呼び出されるハンドラ
function onSuccess (res){
// html読み取り
var quiz = document.getElementById("quiz");
var answer = document.getElementById("answer");
// 問題のセット
quiz.innerHTML = res[0];
// 回答のセット
answer.innerHTML = res[1];
answer.style.visibility = "hidden";
}
// サーバサイド関数を稼働させて、問題・答えを取得する関数
function new_quiz() {
google.script.run.withSuccessHandler(onSuccess).new_quiz_sv();
}
</script>
</head>
<body>
<table style="width: 100%;" cellspacing="100" cellpadding="0">
<tbody>
<tr>
<td style="vertical-align: top;" align="left">
<div id="quiz" style="font-size: 300%;"><br></div>
</td>
</tr>
<tr>
<td style="vertical-align: top;" align="left">
<button type="button" id="btn" style="width: 100%; height: 200%; color: white; background: rgb(80, 184, 216) none repeat scroll 0% 0%; font-size: 300%;" onclick="onbtnclick()">答えを見る</button>
</td>
</tr>
<tr>
<td style="vertical-align: top;" align="left">
<div id="answer" style="font-size: 300%;"></div>
</td>
</tr>
</tbody>
</table>
<script>
// 最初にHTMLが読み込まれたときに問題・答えを設定する
new_quiz();
</script>
</body>
</html>
// GASが呼び出されたときに実行。HTMLを表示する
function doGet(e) {
var app = HtmlService.createHtmlOutputFromFile("index")
return app;
}
// スプレッドシート処理関数
function new_quiz_sv()
{
const count_max = 10; // 最大出題回数
// スプレッドシート処理
var sheet =
SpreadsheetApp.openByUrl('https://docs.google.com/spreadsheets/d/xxxxxxxxxxxxxxxxxx/')
.getSheetByName('List');
var max_r = sheet.getLastRow() - 1;
// 最大出題回数を満たした問題は出題しない
do {
// 全行からランダムに1行選択
var r = Math.floor(Math.random() * max_r) + 2;
// 問題文、回答文、出題回数の取得
var text_quiz = sheet.getRange("B" + r).getValue().replace(/\n/g,"<br/>") + "<br/>" +
sheet.getRange("C" + r).getValue().replace(/\n/g,"<br/>");
var text_answer = sheet.getRange("D" + r).getValue();
var count_q = sheet.getRange("E" + r).getValue();
} while (count_q >= count_max)
// 出題回数を1増加
sheet.getRange("E" + r).setValue(count_q + 1);
return [text_quiz, text_answer];
}
なおスプレッドシートは、以下の内容を要求します。
- シート名は "List"
- 2行目からデータ開始
- B列に問題(英文)
- C列に問題(日文)
- D列に答え
- E列はこれまでの出題回数が入力される
考え方
Googleの指示通り、UiAppクラスの代わりに、HtmlServiceクラスを利用しました。
HtmlServiceクラスはhtmlファイルをブラウザにロードするためだけのクラスで、UiAppクラスでできていたDOM作成・操作ができないため、ただクラスを置換するだけではうまくいきません。
**DOM操作はhtmlファイル内(ブラウザ側)**で。**htmlファイル読込みおよびGoogleスプレッドシートからのデータ取得はGAS(サーバ側)**で、と処理を分離させる必要があると考えました。
つまづいた点
ブラウザからのサーバ側スクリプトの実行および同期処理でつまづきました。
ブラウザからサーバ側スクリプトを実行するために、google.script.runクラスがあります。これを利用し、ブラウザとGASサーバ間でデータ(問題・答え)の受け渡しをさせます。
メソッドはwithSuccessHandler(Function)を使います。理由は後述します。
コードは以下の様になります。
サーバ側関数new_quiz_svの実行が完了したら、その戻り値が、ブラウザ側のコールバック関数onSuccessの引数として実行される流れになります。
onSuccess(new_quiz_sv());
のイメージです。
<script>
google.script.run.withSuccessHandler(onSuccess).new_quiz_sv();
</script>
withSuccessHandlerメソッドを使わなければならない理由は、同期処理を行うためです。
これが無いと、スプレッドシートからの情報取得前にブラウザ側で描画を実行しようとするため、結果何も表示が変わりません。
同期処理・コールバック関数については、「参考文献」のリンクをご覧ください。
考察
- 元々UiAppクラスは、GASでGUIを簡単に作成できたらしいGUIビルダーのために用意されたクラスであったのだと推測します。GUIビルダーが廃止となったのに伴い、UiAppクラスも非推奨・廃止予定となったのかな?
- 問題・答えの追加が結構しんどい。書籍を見ながら手打ちするので、PCからでも10問追加当たり3分強程かかります。スマホからの入力は非現実的。いい方法は無いものでしょうか。。
- これで勉強をやってみてますが、学習効果はもの凄くありそう。自分の間違った問題のみがランダムで出題され、しかも隙間時間にさくさくテストできるので、嫌でも覚えられます。英語のボキャブラリー・フレーズの引き出しが広がってく感じがします。