Edited at

Google Apps ScriptによるクイズWebアプリを作って英語学習する

More than 1 year has passed since last update.


背景

 自分用のクイズ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アプリをコーディングされる方のために、この資料が参考になればと思い、本記事を作成しました。


結論

 リファクタリング成功しました。

 ご参考に、リファクタリング前(kon様コードのほぼコピペ)は以下。


内容

 コードは以下の通り。

 以下2つのファイルをスプレッドシートスクリプトエディタ上に配置し、ウェブアプリケーションとして導入してください。(方法は、GASによるWebAppについて"もっと"入門向けに書いてみる をご参考に)

 

 SpreadsheetApp.openByUrl(~)には、データ取得先のスプレッドシートURLをコピペしてください。


index.html(ブラウザ側)

<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>



main.gs(サーバ側)

// 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];

}


 なおスプレッドシートは、以下の内容を要求します。


  1. シート名は "List"

  2. 2行目からデータ開始

  3. B列に問題(英文)

  4. C列に問題(日文)

  5. D列に答え

  6. 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メソッドを使わなければならない理由は、同期処理を行うためです。

 これが無いと、スプレッドシートからの情報取得前にブラウザ側で描画を実行しようとするため、結果何も表示が変わりません。

 同期処理・コールバック関数については、「参考文献」のリンクをご覧ください。


考察


  1. 元々UiAppクラスは、GASでGUIを簡単に作成できたらしいGUIビルダーのために用意されたクラスであったのだと推測します。GUIビルダーが廃止となったのに伴い、UiAppクラスも非推奨・廃止予定となったのかな?


  2. 問題・答えの追加が結構しんどい。書籍を見ながら手打ちするので、PCからでも10問追加当たり3分強程かかります。スマホからの入力は非現実的。いい方法は無いものでしょうか。。

  3. これで勉強をやってみてますが、学習効果はもの凄くありそう。自分の間違った問題のみがランダムで出題され、しかも隙間時間にさくさくテストできるので、嫌でも覚えられます。英語のボキャブラリー・フレーズの引き出しが広がってく感じがします。


参考文献


  1. Blog kon様 - 勉強用Google Apps Script (Quiz Script)

  2. Google Apps Scriptプログラミング 中級編

  3. JavaScript中級者への道【5. コールバック関数】