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

# 背景
　自分用の**クイズWebアプリ**を作成したかった。（目的は、TOEIC対策[”金フレ”本](https://www.amazon.co.jp/TOEIC-TEST-%E5%87%BA%E3%82%8B%E5%8D%98%E7%89%B9%E6%80%A5-%E9%87%91%E3%81%AE%E3%83%95%E3%83%AC%E3%83%BC%E3%82%BA-%E7%89%B9%E6%80%A5%E3%82%B7%E3%83%AA%E3%83%BC%E3%82%BA/dp/4023315680)の復習用）
　アプリの要件は以下2点。

* **通勤時に手軽にできる**（外出先・スマホからでも、アクセス可能であること）
* **問題（＋答え）追加が簡単**（外出先・スマホからでも、データ追記できること）

　調べていると、まさに望むアプリを作られている先駆者様を発見しました。
　[Blog kon様　- 勉強用Google Apps Script (Quiz Script)](http://tsukinihinikeni.blogspot.com/2014/03/google-apps-script-quiz-script.html)

　

　ここから、**GAS（Google Apps Script）を利用し、Googleスプレッドシートから問題・答えを取得・出題するWebアプリ**を目指そうと決めました。
　まず、kon様のコードをほぼコピペさせて頂き、自分用クイズWebアプリができました。

　しかし、**2018年4月現在、コードの2つのクラスがGoogleによって非推奨**とされていました。

* [UiApp クラス](https://developers.google.com/apps-script/reference/ui/ui-app)
* [UserProperties クラス](https://developers.google.com/apps-script/reference/properties/user-properties)

　アプリ自体はよく出来ており現在動作もしますが、このまま非推奨クラス入りのアプリを使い続けていて、試験直前に突然動かなくなる等は避けたいな、と考えました。

# 目的
　**kon様のクイズWebアプリを、リファクタリングさせていただきます。**（Google非推奨クラス利用を回避する）
　また、私と同じくkon様のWebアプリを基にして、今後自分用クイズWebアプリをコーディングされる方のために、この資料が参考になればと思い、本記事を作成しました。

# 結論
　**リファクタリング成功しました。**

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">廃止クラス対応のために内部処理総替え成功。見た目はほぼ変わってない<br>Qiitaに投稿したくなる気持ちよさ <a href="https://t.co/yHzj29v8cM">pic.twitter.com/yHzj29v8cM</a></p>&mdash; すいばり@&#39;18年度1戦0勝1敗 (@Suibari_cha) <a href="https://twitter.com/Suibari_cha/status/988737926405083136?ref_src=twsrc%5Etfw">2018年4月24日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

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

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">GASでTOEIC対策アプリ作った<br><br>予めGoogleスプレッドシートに登録した問題から、ランダムに出題してくれる仕組み<br>某有名単語帳でミスったところを登録して、復習用にしてる <a href="https://t.co/WNOu9P1NUl">pic.twitter.com/WNOu9P1NUl</a></p>&mdash; すいばり@&#39;18年度1戦0勝1敗 (@Suibari_cha) <a href="https://twitter.com/Suibari_cha/status/987270500635168768?ref_src=twsrc%5Etfw">2018年4月20日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


# 内容
　コードは以下の通り。
 
　**以下2つのファイルをスプレッドシートスクリプトエディタ上に配置し、ウェブアプリケーションとして導入してください**。（方法は、[GASによるWebAppについて"もっと"入門向けに書いてみる](https://qiita.com/mandbjp505/items/5d5137a40d3730bb7823) をご参考に）
　
　SpreadsheetApp.openByUrl(～)には、データ取得先のスプレッドシートURLをコピペしてください。

```html: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>
```

```javascript: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行目からデータ開始
2. B列に問題（英文）
3. C列に問題（日文）
4. D列に答え
5. E列はこれまでの出題回数が入力される

## 考え方
　[Googleの指示通り](https://developers.google.com/apps-script/reference/ui/ui-app)、UiAppクラスの代わりに、HtmlServiceクラスを利用しました。
　HtmlServiceクラスはhtmlファイルをブラウザにロードするためだけのクラスで、UiAppクラスでできていたDOM作成・操作ができないため、ただクラスを置換するだけではうまくいきません。
　**DOM操作はhtmlファイル内（ブラウザ側）**で。**htmlファイル読込みおよびGoogleスプレッドシートからのデータ取得はGAS（サーバ側）**で、と処理を分離させる必要があると考えました。

## つまづいた点
　**ブラウザからのサーバ側スクリプトの実行および同期処理**でつまづきました。

　ブラウザからサーバ側スクリプトを実行するために、[google.script.runクラス](https://developers.google.com/apps-script/guides/html/reference/run)があります。これを利用し、ブラウザとGASサーバ間でデータ（問題・答え）の受け渡しをさせます。
　メソッドは[withSuccessHandler(Function)](https://developers.google.com/apps-script/guides/html/reference/run#withSuccessHandler(Function))を使います。理由は後述します。

　コードは以下の様になります。
　**サーバ側関数new_quiz_svの実行が完了したら、その戻り値が、ブラウザ側のコールバック関数onSuccessの引数として実行される**流れになります。
　`onSuccess(new_quiz_sv());` のイメージです。

```html:ブラウザ側からサーバ側スクリプトを実行する
<script>
  google.script.run.withSuccessHandler(onSuccess).new_quiz_sv();
</script>
```

　withSuccessHandlerメソッドを使わなければならない理由は、**同期処理を行うため**です。
　これが無いと、スプレッドシートからの情報取得前にブラウザ側で描画を実行しようとするため、結果何も表示が変わりません。
　同期処理・コールバック関数については、「参考文献」のリンクをご覧ください。

# 考察
1. 元々UiAppクラスは、GASでGUIを簡単に作成できたらしい[GUIビルダー](http://libro.tuyano.com/index3?id=647004)のために用意されたクラスであったのだと推測します。GUIビルダーが廃止となったのに伴い、UiAppクラスも非推奨・廃止予定となったのかな？
2. **問題・答えの追加が結構しんどい**。書籍を見ながら手打ちするので、PCからでも10問追加当たり3分強程かかります。スマホからの入力は非現実的。いい方法は無いものでしょうか。。
3. これで勉強をやってみてますが、**学習効果はもの凄くありそう**。自分の間違った問題のみがランダムで出題され、しかも隙間時間にさくさくテストできるので、嫌でも覚えられます。英語のボキャブラリー・フレーズの引き出しが広がってく感じがします。

# 参考文献
1. [Blog kon様　- 勉強用Google Apps Script (Quiz Script)](http://tsukinihinikeni.blogspot.com/2014/03/google-apps-script-quiz-script.html)
2. [Google Apps Scriptプログラミング 中級編](http://libro.tuyano.com/index3?id=926001&page=7)
3. [JavaScript中級者への道【5. コールバック関数】](https://qiita.com/matsuby/items/3f635943f25e520b7c20)