スプレッドシートの内容をWebアプリケーションの画面に出力する3つの方法

#はじめに
　昔では考えられないことではあるが、「Google Workspace」（旧名:G-suite）が登場したことにより、ネット環境にさえあればソフトウェアを端末にインストールせず、ブラウザ環境でオフィス業務が可能になった。特にExcelマクロの機能に相当するGoogleAppsScriptを駆使すれば、スプレッドシートとフォーム、カレンダー等、多様なアプリケーションと連携して運用することが容易にできる。
　その一方で、あまり利用されていない使い方があり、その一つにWebアプリケーションがあげられる。*GoogleAppsScript*では、Webアプリケーションとして公開することができ、HTMLでレイアウトを調整することができる。しかし、*GoogleAppsScript*がV8にバージョンを変更したことにより、出来なくなってしまった。

　本稿では、V8に仕様が変更した後、改めてどのようにWebサイトに表示させるのか、その方法を紹介します。

##下準備

| No| 商品 | 個数 |
|:-----------|------------:|:------------:|
| 1| ハクサイ| 10 |
| 2| ネギ    | 20 |
| 3| トウフ  | 15 |
| 4| ツクネ  | 12 |
| 5| シイタケ| 18 |

　Web画面上に表示するデータを予め用意しました。
次にWebアプリケーションを公開してから、それが正常に動作するのかを確認します。
ファイル名はハイライトを適応させるため、便宜上、gsからjsに変更しております。

```web.js
const ss = SpreadsheetApp.getActiveSpreadsheet();
const getValues = () =>{
  const sh = ss.getActiveSheet();
  const values = sh.getRange(1, 1, sh.getLastRow(), sh.getLastColumn()).getValues();
  values.shift();
  return values;
}

function doGet() {
  const html = HtmlService.createTemplateFromFile("index");
  return html.evaluate().setTitle("Sample");
}
```

```index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <style>
      table{
        border-collapse: collapse;
        border: 2px #000000 solid;
      }
      td{
        padding: 4px 4px 4px 4px;
        border: 2px #000000 solid;
      }
    </style>
  </head>
  <body>
    Hello world
  </body>
</html>
```

　この状態でWeb画面を確認します。画面にHello Worldと表示させれば、最初の処理は終了です。

##1.HTMLに出力用メソッドを埋め込む
 まず簡単な方法として、テンプレートファイル(index.html)で、getValuesメソッドを直接呼び出し、それから埋め込みをする方法になります。コードは以下の通りです。

```index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <style>
      table{
        border-collapse: collapse;
        border: 2px #000000 solid;
      }
      td{
        padding: 4px 4px 4px 4px;
        border: 2px #000000 solid;
      }
    </style>
  </head>
  <body>
    <? const items = getValues() ?>
    <table>
      <? for(const item of items){ ?>
        <tr>
          <td><?= item[0] ?></td>
          <td><?= item[1] ?></td>
          <td><?= item[2] ?></td>
        </tr>
      <? } ?>
    </table>
  </body>
</html>
```

![実際の出力画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188968/0d99e721-547c-2409-3818-44531da17c5c.png)
図1.実際の出力画面

　このソースでは、*<? const items = getValues() ?>*により、テンプレートから直接値を持ってきてます。ただこれではテンプレートファイルを設定する際、そのファイルに何も渡していないようにも思えます。

##2.HtmlTemplateクラスのプロパティに埋め込む
次に紹介する方法は、テンプレートファイルに予め値を埋め込む方法です。まずテンプレートファイルは以下のように修正します。

```index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <style>
      table{
        border-collapse: collapse;
        border: 2px #000000 solid;
      }
      td{
        padding: 4px 4px 4px 4px;
        border: 2px #000000 solid;
      }
    </style>
  </head>
  <body>
    <? items ?>
    <table>
      <? for(const item of items){ ?>
        <tr>
          <td><?= item[0] ?></td>
          <td><?= item[1] ?></td>
          <td><?= item[2] ?></td>
        </tr>
      <? } ?>
    </table>
  </body>
</html>
```

前項で紹介した*<? const items = getValues() ?>* が*<? items ?>*に変更されております。ただこれだけでは動かないのでサーバー側も修正します。

```web.js
const ss = SpreadsheetApp.getActiveSpreadsheet();
const getValues = () =>{
  const sh = ss.getActiveSheet();
  const values = sh.getRange(1, 1, sh.getLastRow(), sh.getLastColumn()).getValues();
  values.shift();
  return values;
}

function doGet() {
  const html = HtmlService.createTemplateFromFile("index");
  html.items = getValues();
  return html.evaluate().setTitle("Sample");
}
```

　先ほどテンプレートファイルに埋め込んだ*items*が*html*のプロパティに埋め込まれております。
　こうすることで、サーバー側で処理した内容を直接埋め込むことができます。
例えば*getValuesメソッド*に引数を設定し、アクションパラメータに応じて返す値を変更する処理を書き加えれば、サーバー側でもある程度はフロント側を制御することが可能となります。

　なお欠点として、Webアプリケーションを実装する際には、*doGetメソッド*と*doPostメソッド*がシンプルトリガーという形で*GoogleAppsScript*の標準メソッドとして組み込まれており、Webアプリケーションを開いた時には表示されたが、データ送信時にはエラー画面に入る問題があります。
　画面表示時（Get時）とデータ送信時（Post時）、共通して使用するのか否か使い分けるのか、はたまたリダイレクト用ページに飛ばしてから改めて*index.html*に行く処理に統一するのか、対策が必要になる。

##3.google.script.runを実行して、HTML要素を生成する
　最後は最もめんどくさい方法になります。*web.gs*は一旦、元の形に戻します。最後に行う方法は非同期処理になり、動的にHTML要素を生成するものとなります。
　この方法はV8の仕様変更で最も影響を受けた箇所になるのかと思われます。
　コードは以下のものとなります。

```index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <style>
      table{
        border-collapse: collapse;
        border: 2px #000000 solid;
      }
      td{
        padding: 4px 4px 4px 4px;
        border: 2px #000000 solid;
      }
    </style>
  </head>
  <body>
    <table></table>
    <script>
      'use strict'
      const createTableData = function(items){
        console.log("Hay");
        const table = document.querySelector("table");
        for(const item of items){
          const tr = document.createElement("tr");
          for(const i of item){
            const td = document.createElement("td");
            const text = document.createTextNode(i);
            td.appendChild(text);
            tr.appendChild(td);
          }
          table.appendChild(tr);
        }
      }
      
      google.script.run
        .withSuccessHandler(createTableData)
        .getValues();
    </script>
  </body>
</html>
```

　*getValuesメソッド*はサーバー側で定義したメソッドになります。*google.script.run.getValues()*で動作させることはできます。しかし、*getValuesメソッド*の戻り値をどう扱うのかがわかりません。
　その際に用いているのが*withSuccessHandlerメソッド*になります。このメソッドは*google.script.run*で実行したメソッドが正しく戻値を返却した時、指定したメソッドにそれを渡して動作させるメソッドになります。
　つまり実行している内容は、①*getValuesメソッド*によりスプレッドシートのデータを取得、②その結果が*withSuccessHandlerメソッド*に渡る、③*withSuccessHandlerメソッド*で指定した*createTableDataメソッド*に①で取得した値が引数として渡され、要素が作成された、という流れになります。

　コード量は前述の方法に比べて重く、実行されるタイミングもWeb画面が表示されてから実行されるもののため、表示速度は高いとはいえません。その一方で、ボタン操作やセレクトボックスの値が変更された時に実行したり、Postさせずにデータの入出力を行う時に活用できるのかと思います。

追記：jQueryを使えばもっと綺麗なコードになったのではと思うのだが、あまり使い慣れていないうえ、標準環境でも現状は特に問題なく書けているので、フロント側も標準環境で実装しております。

##終わりに
　一昔前、非同期処理のプログラムは開発環境を整える前の段階から躓きやすい難解なものでした。しかしGASはおそらくNode.jsの環境下で動いているので、少し勉強すれば誰でもSPAが作れるのかと思います。だがまだ私自身、勉強が足りていないことからこれら上記の違いがどこまであるのか理解しきれていないところがあります。なので勉強不足、理解不足な点がございましたら、ご指摘頂けると幸いです。