Help us understand the problem. What is going on with this article?

Google Apps ScriptでWebアプリケーションをつくる

こんにちは、otkと申します。Qiita初投稿です。よろしくお願いいたします。

はじめに

最近Google Apps Script(GAS)を触る機会があり、今までほとんど触ったことがなかったのですが、使ってみてかなりいろいろできることに驚きました。その中でGASでWebアプリをつくれるということを知ったので、それについて書いていこうと思います。

つくるもの

定番のTodoアプリです。
こちらのチュートリアルのコードを使わせていただきました。(CDN版のVue.jsを使っています。)チュートリアルではデータをローカルストレージに保存していますが、いちばん最後でGoogleスプレッドシートに保存するよう変更していきます。

必要なもの

手順

以下手順で進めます。

  1. GASプロジェクトを作成。
  2. GASでGETリクエストを受け取り、HTMLを表示するプログラムを書く。
  3. Webアプリ公開・動作確認。
  4. CSS、JSを別ファイルに分割してTodoアプリ作成。
  5. データをGoogleスプレッドシートに保存するように変更。

1. GASプロジェクトを作成。

https://script.google.com/ または Gooleドライブ からGASプロジェクトを作成します。

スクリーンショット 2019-12-18 19.42.43.png

2. GASでGETリクエストを受け取り、HTMLを表示するプログラムを書く。

サーバー側GASを記述します。
GASでGETリクエストを受け取るようにするためにはHTML ServicedoGet関数を使います。以下はGETリクエストを受け取ったときにindex.htmlファイルの内容を表示する処理です。

Code.gs
function doGet() {
  return HtmlService.createHtmlOutputFromFile('index');
}

次にフロント側のHTMLです。
[ファイル] > [New] > [HTMLファイル]でindex.htmlファイルを作成できます。

index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    Hello!
  </body>
</html>

3. Webアプリ公開・動作確認。

これだけで最低限の準備ができたので、公開して動作確認します。

[公開] > [ウェブアプリケーションとして導入]。

スクリーンショット 2019-12-18 20.54.44.png

以下のようにして [Deploy]。

スクリーンショット 2019-12-18 20.55.24.png

URLが表示されるので、そちらにアクセスすればindex.htmlの内容が表示されるはずです。これで一応Webアプリを公開することができました。

スクリーンショット 2019-12-18 20.56.04.png

スクリーンショット 2019-12-18 20.56.15.png

4. CSS、JSを別ファイルに分割する。

Todoアプリを作っていきます。

こちらのチュートリアルの完成形index.html, main.css, main.jsを書いていきたいのですが、GASではHTMLファイルとGSファイルのみしか作成できません。ですので<style>タグと<script>タグを使って全てHTMLファイルに記述していきます。

ファイルは分割したいので、main.css.htmlmain.js.htmlファイルを作成し、それらをindex.htmlファイルで読み込むようにします。

main.css.html
<style>
  * {
    box-sizing: border-box;
  }
  #app {
    max-width: 640px;
    margin: 0 auto;
  }

/* 〜〜〜 中略 〜〜〜 */

  button {
    border: none;
    border-radius: 20px;
    line-height: 24px;
    padding: 0 8px;
    background: #0099e4;
    color: #fff;
    cursor: pointer;
  }
</style>
main.js.html
<!-- CDN版Vue.js読み込み -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>

<!-- ローカルストレージに保存するための処理 -->
<script>
  // ★STEP2
  // https://jp.vuejs.org/v2/examples/todomvc.html
  var STORAGE_KEY = 'todos-vuejs-demo';
  var todoStorage = {
    fetch: function() {
      var todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
      todos.forEach(function(todo, index) {
        todo.id = index;
      });
      todoStorage.uid = todos.length;
      return todos;
    },
    save: function(todos) {
      localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
    },
  };
</script>


<!-- Vueインスタンス -->
<script>
  // ★STEP1
  const app = new Vue({
    el: '#app',

/* 〜〜〜 中略 〜〜〜 */

      // ★STEP10 削除の処理
      doRemove: function(item) {
        var index = this.todos.indexOf(item);
        this.todos.splice(index, 1);
      },
    },
  });
</script>

index.htmlで読み込ませるためには以下のように記述します。

index.html
...
  <head>
    ...
    <?!= HtmlService.createHtmlOutputFromFile('main.css').getContent(); ?>
  </head>
...
  <body>
    <?!= HtmlService.createHtmlOutputFromFile('main.js').getContent(); ?>
  </body>

<?!=?>の間にGASのコードを記述して処理を実行することができるので、上のように記述することで別ファイルのmain.css.html, main.js.htmlファイルを読み込むことができます。

あとはチュートリアルのコードと同じです。

以上でTodoアプリを作成・デプロイすることができます。簡単!

スクリーンショット 2019-12-19 6.35.55.png

5. データをGoogleスプレッドシートに保存するように変更。

最後に、ローカルストレージに保存していたデータを、GoogleスプレッドシートをDBのように使ってそちらに保存したいと思います。

以下のようなスプレッドシートを用意します。

スクリーンショット 2019-12-19 6.41.01.png

こちらをGASで取得する処理は以下になります。

db.gs
var DB_SPREADSHEET_URL = 'スプレッドシートのURL';
var ss = SpreadsheetApp.openByUrl(DB_SPREADSHEET_URL);
var sheet = ss.getSheetByName('Todo');

// スプレッドシートからデータを読み込む処理。
function getTodosFromDb() {
  var lastRow = sheet.getLastRow();
  var lastCol = sheet.getLastColumn();
  var data2dArray = sheet.getRange(1, 1, lastRow, lastCol).getValues();
  var keys = data2dArray[0];
  var dataObjects = [];
  for (var row = 1; row < lastRow; row += 1) {
    var dataRow = data2dArray[row];
    var dataObject = toObject(dataRow, keys);
    dataObjects.push(dataObject);
  }
  return dataObjects;
}

// スプレッドシートにデータを書き込む処理。
function setTodosToDb(todos) {
  sheet.clear();
  var keys = Object.keys(todos[0]);
  var data2dArray = [keys];
  for (var i = 0; i < todos.length; i += 1) {
    var dataObject = todos[i];
    var dataRow = toArray(dataObject, keys);
    data2dArray.push(dataRow);
  }
  var numRows = data2dArray.length;
  var numCols = keys.length;
  sheet.getRange(1, 1, numRows, numCols).setValues(data2dArray);
}

function toObject(array, keys) {
  var obj = {};
  for (var i = 0; i < keys.length; i += 1) {
    obj[keys[i]] = array[i];
  }
  return obj;
}

function toArray(obj, keys) {
  var array = [];
  for (var i = 0; i < keys.length; i += 1) {
    array.push(obj[keys[i]]);
  }
  return array;
}

こちらのgetTodosFromDbメソッドとsetTodosToDbメソッドをフロント側から呼び出したいのですが、それにはgoogle.script.runというAPIを使います。
https://developers.google.com/apps-script/guides/html/reference/run?hl=en

このgoogle.script.runが少し複雑ですので、以下の例で説明します。例では以下のような処理を行なっています。

  • サーバー側で定義されたdoSomethingメソッド(①)をフロント側でgoogle.script.runで呼び出しています(③)。
  • doSomethingを実行し、処理が成功するとonSuccessメソッド(②)が実行されます。(失敗した場合はonFailureメソッドが実行されます。)
  • このときdoSomethingの戻り値がonSuccessの引数doSomethingReturnValueに渡されます。
GASサーバー側
// ①
function doSomething(arg) {
  ...
  return returnValue;
}
HTMLフロント側
<script>
...
// ②
function onSuccess(doSomethingReturnValue) {
// 何か処理
}
...
// ③
google.script.run
  .withSuccessHandler(onSuccess)
  .withFailureHandler(onFailure)
  .doSomething('hello');
...
</script>

こんな感じで、このままだと少し使いにくいので、こちらを参考に、async/awaitが使えるように以下の処理を定義します。

HTMLフロント側
function scriptRunPromise() {
  const gs = {};
  // google.script.run contains doSomething() methods at runtime.
  // Object.keys(goog.sscript.run) returns array of method names.
  const keys = Object.keys(google.script.run);
  // for each key, i.e. method name...
  for (let i = 0; i < keys.length; i++) {
    // assign the function to gs.doSomething() which returns...
    gs[keys[i]] = (function(key) {
      // a function which accepts arbitrary args and returns...
      return function(...args) {
        // a promise that executes ...
        return new Promise(function(resolve, reject) {
          google.script.run
            .withSuccessHandler(resolve)
            .withFailureHandler(reject)
            [key].apply(google.script.run, args);
        });
      };
    })(keys[i]);
  }
  return gs;
}

こうすることで以下のような形でサーバー側のdoSomethingメソッドが呼び出せるようになります。

HTMLフロント側
var gs = scriptRunPromise();
var doSomethingReturnValue = await gs.doSomething('hello');

これを使ってチュートリアルのローカルストレージへのデータ保存部分を以下のように書き換えます。

main.js.html(変更前)
<script>
  var STORAGE_KEY = 'todos-vuejs-demo';
  var todoStorage = {
    fetch: function() {
      var todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
      todos.forEach(function(todo, index) {
        todo.id = index;
      });
      todoStorage.uid = todos.length;
      return todos;
    },
    save: function(todos) {
      localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
    },
  };
</script>

 ⬇️

main.js.html(変更後)
<script>
  var gs = scriptRunPromise();
  var db = {
    fetch: async function() {
      var todos = await gs.getTodosFromDb();
      console.log({ todos });
      todos.forEach(function(todo, index) {
        todo.id = index;
      });
      db.uid = todos.length;
      return todos;
    },
    save: async function(todos) {
      await gs.setTodosToDb(todos);
    },
  };
</script>

残りはfetch, saveメソッドの呼び出し部分を修正すればOKです。

最終的なコードはこちらです。

おわりに

以上がGASでのWebアプリの作成方法になります。
簡単に、なにより無料で使えるのが便利ですよね。
claspというGASをローカルで開発するためのツールがあるので、もっと凝ったことをしたい場合はこれを使うと便利です。
他にも色々便利な使い方があるようなので、色々やってみて、またアウトプットしていきたいと思います。

読んでいただきありがとうございました!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした