GAS + API + スプレッドシート + vue.js で作ってみた

# はじめに

GASでスプレッドシートの値を使ってAPIを作成、そのAPIをコールしてGASで作成したサイトに表示した時のメモ。
必要なところだけ読んでもらえればと思います。

# 環境構築

Google Apps Scriptのスクリプトエディタから編集してもいいのだけれど、今回はClaspを使ってみたいと思います。
node.jsをインストールする必要があります。ここではnode.jsのインストール方法は省略します。

①[Google Apps Script API](https://script.google.com/home/usersettings)を有効化します。

②パッケージをインストール

```
npm init -y
npm install @google/clasp -g
npm install @types/google-apps-script
```

③Google アカウント認証

`clasp login --no-localhost`

ターミナルに表示されるURLにブラウザからアクセスします。
ブラウザでClaspを許可します。

ブラウザに表示される認証コードをコピーしてターミナルに貼り付けます。

# 開発

## clasp 初期動作確認

①ターミナルから`clasp create`を実行します。GASをどこに紐づけるか選択できます。
GitBashではカーソルを移動してどのアプリを作成するか決定するのですが、なぜかうまくいきませんでした。
とりあえず、standaloneで問題ないです。

②マイドライブ直下にアプリケーションが作成されます。

③api.jsファイルを作成します。ファイル名は任意です。

④普通にjavascriptのコードを記載します。とりあえず、Hello world. 関数名も任意です。

```javascript
function test() {
  console.log('Hello World!');
}
```

⑤ファイルをデプロイします。

`clasp push`

⑥WEBエディターを開きます

`clasp open`

⑦実行する関数を選択して(一番上の関数がデフォルト選択されている)実行する。コンソールにHello Worldが表示される

⑧設定画面からタイムゾーンを変更します。「appsscript.json」マニフェスト ファイルをエディタで表示するにチェックを入れます。

⑨ターミナルで `clasp pull` で変更ファイルを取得します

## API 開発

clasp createでAPIを選択してもいいのですが、今回は上記で作成したファイルを利用します。

①api.jsを以下のように書きかえ pushします

```javascript
function response(content) {
  const res = ContentService.createTextOutput();
  res.setMimeType(ContentService.MimeType.JSON);
  res.setContent(JSON.stringify(content));
  return res;
}
function doGet() {
  return response({ text: "成功" });
}
function doPost(e) {
  let contents;
  try {
    contents = JSON.parse(e.postData.contents);
  } catch (e) {
    return response({ error: "JSONの形式が正しくありません" });
  }

  return response({ text: "成功" });
}
```

②WEBエディター画面のデプロイをクリックします。種類の選択で「ウェブアプリ」を選択
権限周りはテスト用なので自分のみでよいです。

③表示されるURLにアクセスします

```json
{"text":"成功"}
```

と表示されれば成功です。

④clasp pullを実行して変更をローカルに取得します。

次回以降の変更後は、デプロイ -> 新しいデプロイではなく、デプロイ -> デプロイを管理 からバージョンを新バージョンに変更してデプロイします。
新しいデプロイをすると、URLが変更になります。

APIのベースは出来上がりました。あとは、javascriptをガリガリ書いていけば出来上がります。
注意点は以下の通り

* アクセスを自分のみとしているため認証済みのブラウザでのアクセスとなります。curlでの確認にはひと工夫必要です。
　APIにauth codeなどを実装して全ユーザーが使えるようにするのが良いかもしれませんが、趣味なら全公開でもいいかもしれません。

* doGetとdoPostだけなので、putやdeleteをしたいときはパラメータでやり取りすることになります

### スプレッドシートアクセス

`SpreadsheetApp.openById()` でスプレッドシートを開く。IDをプログラムに直書きするのはアレなので、スクリプトプロパティを使う。

#### read
スプレッドシートのシートを選択して、シートのレンジを取得してhashを作成する。

```javascript
const ss = SpreadsheetApp.openById(
  PropertiesService.getScriptProperties().getProperty("SampleSpreadSheet")
);

function response(content) {
  const res = ContentService.createTextOutput();
  res.setMimeType(ContentService.MimeType.JSON);
  res.setContent(JSON.stringify(content));
  return res;
}
function doGet() {
  return response(getData());
}
function doPost(e) {
  let contents;
  try {
    contents = JSON.parse(e.postData.contents);
  } catch (e) {
    return response({ error: "JSONの形式が正しくありません" });
  }

  return response(getData());
}

function getData(params) {
  const sheet = ss.getSheetByName("シート1");
  const lastRow = sheet ? sheet.getLastRow() : 0;

  const list = sheet
    .getRange("A2:E" + lastRow)
    .getValues()
    .map((row) => {
      const [id, name, created_at, created_by, status] = row;
      return {
        id,
        name,
        created_at,
        created_by,
        status,
      };
    });

  return list;
}
```

#### write

スプレッドシートに書き込んでみる。
postでjsonパラメータを受け取り、現在日時とプログラム実行者をgoogle apiを利用して取得し書き込む。
数が少ないうちは追加でいいと思うけど、数が多くなると降順で並んでるほうが便利なので２行目に追加して下にずらす。

```javascript
const ss = SpreadsheetApp.openById(
  PropertiesService.getScriptProperties().getProperty("SampleSpreadSheet")
);

function response(content) {
  const res = ContentService.createTextOutput();
  res.setMimeType(ContentService.MimeType.JSON);
  res.setContent(JSON.stringify(content));
  return res;
}
function doGet() {
  return response(readData());
}
function doPost(e) {
  let contents;
  try {
    contents = JSON.parse(e.postData.contents);
  } catch (e) {
    return response({ error: "JSONの形式が正しくありません" });
  }

  writeData(e);
  return response(readData());
}

function readData() {
  const sheet = ss.getSheetByName("シート1");
  const lastRow = sheet ? sheet.getLastRow() : 0;

  const list = sheet
    .getRange("A2:E" + lastRow)
    .getValues()
    .map((row) => {
      const [id, name, created_at, created_by, status] = row;
      return {
        id,
        name,
        created_at,
        created_by,
        status,
      };
    });

  return list;
}
function writeData(params) {
  const sheet = ss.getSheetByName("シート1");
  const currentID = sheet.getRange("A2:A2").getValue();
  const id = currentID == "" ? 1 : currentID + 1;

  const name = params.name.toString();
  const date = new Date();
  const createdAt =
    date.getFullYear() +
    "年" +
    (date.getMonth() + 1) +
    "月" +
    date.getDate() +
    "日" +
    date.getHours() +
    "時" +
    date.getMinutes() +
    "分" +
    date.getSeconds() +
    "秒";

  const contact = ContactsApp.getContact(Session.getActiveUser().toString());
  const createdBy = contact.getFullName();
  const status = params.status.toString();

  sheet.insertRowBefore(2);
  const record = [[id, name, createdAt, createdBy, status]];
  sheet.getRange("A2:E2").setValues(record);
}
```

## サイト

スプレッドシートをサーバー側で読み込みサイトに表示する方法とAPIを実行して表示する方法の２種類を確認したいと思います。

### サーバー側での処理

ここではサーバー側でスプレッドシートにアクセスして、取得したjsonデータをvue.jsを使って表示したいと思います。
まずは[clasp 初期動作確認](#clasp-初期動作確認)から。今回もstandaloneでいったん作ります。

GASPでつくるウェブサイトでは、拡張子htmlのファイルしか使えません。cssやjavascriptもhtmlとする必要があります。
正確には、cssやjavascriptをhtmlの別ファイルとしてincludeして、cssファイルやjavascriptファイルみたいに管理しているようにしているだけです。
興味がある人は、ブラウザでソースの表示で確認してみてください。

とりあえず、簡単なファイルを用意して動作確認してみます。

* app.js ... server処理を記述するファイル
* index.html ... viewファイル
* css.html   ... cssファイル
* js.html    ... javascriptファイル

app.js

```javascript
function doGet() {
  const htmlOutput = HtmlService.createTemplateFromFile('index').evaluate();
  htmlOutput
    .setTitle('サンプルページ')
    .addMetaTag('viewport', 'width=device-width, initial-scale=1')
  return htmlOutput;
}
```

index.html

```html
<!DOCTYPE html>
<html>

<head>
  <base target="_top">
  <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.css">
  <?!= HtmlService.createHtmlOutputFromFile('css').getContent(); ?>
</head>

<body>
  <div id="app">
    <h2>一覧</h2>

    <table class="design12">
      <tr>
        <th>ID</th>
        <th>名前</th>
        <th>作成日</th>
        <th>作成者</th>
        <th>ステータス</th>
      </tr>
    </table>
  </div>
  <?!= HtmlService.createHtmlOutputFromFile('js').getContent(); ?>
</body>

</html>
```

css.html と js.htmlはブランクのままでかまいません。

ファイルをプッシュして、設定変更後、ウェブアプリを選択してデプロイします。

```
clasp push
clasp open
```

ブラウザでURLにアクセスするとテーブルのヘッダーのみが表示されれば成功です。

ここからプログラムに処理を追加していきます。

スプレッドシートのデータを読み取りjsonを作成します。そのオブジェクトを任意の名前（今回はlist）にsetする。
viewファイルに渡す（evaluate)。

app.js
```javascript
function doGet() {
  const htmlOutput = HtmlService.createTemplateFromFile("index");

  // スプレッドシートのデータを読み取りobjectに設定 view側からアクセスできるようになる
  htmlOutput["list"] = read();

  return htmlOutput
    .evaluate()
    .setTitle("サンプルページ")
    .addMetaTag("viewport", "width=device-width, initial-scale=1");
}
function read() {
  const sheet = SpreadsheetApp.openById(
    PropertiesService.getScriptProperties().getProperty("SampleSpreadSheet")
  );
  const lastRow = sheet ? sheet.getLastRow() : 0;

  return sheet
    .getRange("A2:E" + lastRow)
    .getValues()
    .map((row) => {
      const [id, name, created_at, created_by, status] = row;
      return {
        id,
        name,
        created_at,
        created_by,
        status,
      };
    });
}
```

index.html
スクリプトレット <? ?> でjavascriptを書くイメージ。

```javascript
<!DOCTYPE html>
<html>

<head>
  <base target="_top">
  <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.css">
  <?!= HtmlService.createHtmlOutputFromFile('css').getContent(); ?>
</head>

<body>
  <div id="app">
    <h2>一覧</h2>

    <table class="design12">
      <tr>
        <th>ID</th>
        <th>名前</th>
        <th>作成日</th>
        <th>作成者</th>
        <th>ステータス</th>
      </tr>
      <? for(let i = 0; i < list.length; i++){ ?>
        <tr><td><?= list[i].id ?></td><td><?= list[i].name ?></td><td><?= list[i].created_at ?></td><td><?= list[i].created_by ?></td><td><?= list[i].status ?></td></tr>
      <? } ?>
    </table>
  </div>
  <?!= HtmlService.createHtmlOutputFromFile('js').getContent(); ?>
</body>

</html>
```

基本的な使い方は以上です。

#### vue.js

サーバー側で取得したデータをvue.jsで操作してみます。vue.jsは勉強中のため正しくないことを記載しているかもしれません。
信頼できるドキュメントを参考にしてください。

index.html

vue.jsとvuejs-paginateと使ってみたいと思います。htmlを以下のように変更します。

```html
<!DOCTYPE html>
<html>

<head>
  <base target="_top">
  <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.css">
  <?!= HtmlService.createHtmlOutputFromFile('css').getContent(); ?>
  <?!= HtmlService.createHtmlOutputFromFile('js').getContent(); ?>
</head>

<body>
  <div id="app">
    <h2>一覧</h2>

    <table class="design12">
      <tr>
        <th>ID</th>
        <th>名前</th>
        <th>作成日</th>
        <th>作成者</th>
        <th>ステータス</th>
      </tr>
      <tr v-for="result in getResults" v-cloak>
        <td>{{ result.id }}</td>
        <td>{{ result.name }}</td>
        <td>{{ result.created_at }}</td>
        <td>{{ result.created_by }}</td>
        <td>{{ result.status }}</td>
      </tr>
    </table>
    <paginate :page-count="getPageCount" :page-range="3" :margin-pages="2" :click-handler="clickPageCallback"
      :prev-text="'＜'" :next-text="'＞'" :container-class="'pagination'" :page-class="'page-item'">
    </paginate>

  </div>
</body>

</html>
```

js.html

先述した通り、htmlの中にjavascriptを書いているのと同じなので、scriptタグが必要になります。

サーバー側の処理を呼び出してデータを取得します。
`google.script.run.withSuccessHandler(initializeVue).read();`

簡単に言葉にすると、google script（api.js)に定義されている関数（read()）を実行し、その結果を指定された関数（initializeVue)に渡して実行します。
あとは、vue.jsです。お好みのように実装してください。

```javascript
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.11/dist/vue.js"></script>
<script src="https://unpkg.com/vuejs-paginate@2.1.0"></script>
<script>
  google.script.run.withSuccessHandler(initializeVue).read();
  function initializeVue(values) {
    new Vue({
      el: '#app',
      components: {
        'paginate': VuejsPaginate
      },
      data: {
        lists: values,
        perPage: 10,
        currentPage: 1,
      },
      methods: {
        clickPageCallback(pageNum) {
          this.currentPage = Number(pageNum);
        },
      },
      computed: {
        getResults() {
          let current = this.currentPage * this.perPage;
          let start = current - this.perPage;
          return this.lists.slice(start, current);
        },
        getPageCount() {
          return Math.ceil(this.lists.length / this.perPage);
        }
      },
    });
  }
</script>
```

### API利用

GASでつくったAPIをVUE.jsで表示してみます。以下条件で実装します

* GASでは拡張子がhtmlしか使えません。.vueファイルを使うにはひと工夫必要ですがここでは対応しません
* CDNを利用して作成します。（Node.jsは利用しません）
* GASで作ったAPIのアクセス権は全公開とします。

サーバー側の関数を実行してJSONを取得していた部分をAPI実行に変更するだけです。
言い換えると、

> サーバー側の処理を呼び出してデータを取得します。
> `google.script.run.withSuccessHandler(initializeVue).read();`

の部分をaxiosを利用するように変更するだけで、複雑なことはここではやりません。参考程度にご覧ください。

index.htmlは既出から変更ありません。

js.html

API側でエラーが起きていた時、CORSのエラーが表示され、パニックになるので注意。GASでリダイレクトして表示されるエラーページがCORSエラーなだけです。

```javascript
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.11/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://unpkg.com/vuejs-paginate@2.1.0"></script>
<script>
    new Vue({
      el: '#app',
      components: {
        'paginate': VuejsPaginate
      },
      data: {
        lists: [],
        perPage: 10,
        currentPage: 1,
      },
      async mounted() {
        try {
          const response = await axios.get(
            'api url'
          );
          this.lists = response.data;
        } catch (error) {
          console.log(error);
        }
      },
      methods: {
        clickPageCallback(pageNum) {
          this.currentPage = Number(pageNum);
        },
      },
      computed: {
        getResults() {
          let current = this.currentPage * this.perPage;
          let start = current - this.perPage;
          return this.lists.slice(start, current);
        },
        getPageCount() {
          return Math.ceil(this.lists.length / this.perPage);
        }
      },
    });
</script>
```

### スクリプトプロパティ

パスワードなんかは直接コードには記述しないでスクリプトプロパティに記述します。

WEBエディタの歯車マークから、プロパティと値を設定します。

プログラムからは、`PropertiesService.getScriptProperties().getProperty('プロパティ')` で取得します

## コマンドラインからのデプロイ

`clasp deployments` を実行します。

<Deploy ID> @HEAD

表示されるDeploy IDを利用して以下コマンドを実行します。

`clasp deploy -i <Deploy ID>`

# まとめ

基本的にGASでウェブサイトをつくる必要性はないかなと思います。アクセス権限をgoogleでできるのは大きなメリットですが、少なくとも一般公開用には使わないと思います。
APIを作成して利用制限内で利用するのはいいかなと思います。
社内ツールとしてクローズドなところで利用するのもいいかなとは思ったのですが、webサーバーは別のところに用意して、APIはGAS、DBの代わりにスプレッドシートと言う組み合わせのほうが良いかなぁと思いました。APIのアクセス権限どうするか検討する必要はありそうですが。

SPAでつくるには、ひと工夫必要なので、パラメータでviewを変更するような作りにするのが良いと思います。

こんなこともできるよ。と言う選択肢の一つに知っておく程度が良いのかもしれません。