Google Apps ScriptでVue.jsを使ってみる【GAS】

Google Apps Scriptではウェブアプリケーションを作成できますが、そのままでは何の作成支援もありません。最近は、JavaScriptの各種フレームワークでウェブアプリケーションを作成するのが当たり前になってきていて、GAS上でjQueryで頑張るというのは賢明ではありません。そこで今回利用するのが、Vue.js

GUIフレームワークではなくバックエンドの様々な操作を行う為のユーザインターフェースフレームワークで、GUIについては別途Vuetifyと呼ばれるフレームワークが存在する。

※今回はディレクティブと呼ばれる超基本部分だけ。

今回使用するサンプルファイル

Vue.jsとは

概要

Vue.jsとは、React.jsAnqularJSなどのようなフレームワークの1つで、軽量且つ高機能、おまけに学習コストが低くて済むと言われるもので、JavaScriptの学習でのある段階まで到達したら、手を出す必要のあるジャンル。それまでのシンプルなJavaScriptでは面倒なDOM操作などを手軽にし、且つ開発を容易にしてくれる裏方です。

Google Apps Scriptでもウェブプリケーション構築で利用する事が可能であり、また全体像があやふやな学習初期であっても、必要な部分にだけ取り入れて使うことも可能(要素の部品化が可能なので、ボタン要素だけまずは学習してみるとか)。

GASで使うメリット

Vue.jsは、シングルページアプリケーション作成が可能という謳い文句で、Google Apps Scriptのような複数のHTMLを駆使する事が難しいものにはもってこいの機能が満載。また、GASで開発をする人達は基本、VBAの開発者などと同様にライトユーザが多いこともあり、学習コストがガッツリ掛かるような他のフレームワークよりも、学習コストが低いメリットが強いです。

基本、CDNのJSファイルをロードすれば良いので、GASのようにあまり柔軟にJSやライブラリを配置できないような開発環境であっても利用が容易なのも利点の1つ。

また、他のフレームワークと違い、学習コストが低いという事は、そのフレームワーク前提でコードを組んでも共有しやすい(後任の人間がスキルに不足があっても、追いつける)。jQueryのように圧倒的に一般的に利用されているものに近いのは良いメリットです(追加でAngularJS学べと言われると絶望するかもしれませんし)

基本スタイル

最も基本となるビューというものを作り、既存のelementにある文字を置き換えるといったケースでは以下のようなコードになる。

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <!-- vue.jsをロード -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <!-- Google CSS Addon -->
    <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
  </head>
  <body>
    <!-- 基本となるビュー -->
    <div id="app">
      <button @click="cookpad" class="create">キノコを料理</button>
      <p>{{ cookman }}</p>
    </div>
    <script>
      //ビューの初期化
      var vm = new Vue({
        el: '#app',
        data: {
          cookman: "キノコを調理するよ"
        },
        methods: {
          cookpad: function () {
            //GAS内のコード読み出し
            vm.cookman = "キノコのシチューは如何?";
          }
        }
      })
    </script>  
  </body>
</html>
  • キノコを料理のボタンには、cookpadという関数を呼び出すように@clickがつけられている(本来はv-onを使う)
  • idがappのビューをロード時にそのまま初期化。{{cookman}}をキノコを調理するよというメッセージをまずセットしてる(dataというセクション)。本来はv-textを使う。
  • 続いて、methodsにて関数を定義。cookpadという関数を用意して、Vueをセットしたvm変数cookmanに値をセットすると、自動的にcookmanのメッセージが連動して入れ替わる(ここがポイント)
  • 予め変更が予定されてる要素とJavaScript、関連メソッドを一塊にしておく(構造化)のが、Vue.jsの特徴。直接DOMを操作するjQueryのような煩雑さが無い。

実行結果

上記のコードを実行してみる。すると、ボタンと初期メッセージが表示され、ボタンをクリックするとメッセージが入れ替わる。

図:jQueryでも出来るけれど、それをより構造化して行えるのがVue.js

method内の関数を別のメソッドから呼び出す

method内にボタンなどの様々なアクションに対応する関数を用意していくわけですが、この関数内で、vueの外側の普通の関数を呼び出すのは普通に関数名を記述するだけで呼び出せます。問題はこれを別のメソッドから呼び出すには以下のように呼び出します。

//外部の関数からmethodを呼び出す
vm.cookpad();

//method内から別のmethod内の関数を呼び出す
this.cookpad();
  • ビュー初期化時に変数vmにvueの内容を格納してるため、外部からmethod内の関数を叩くのであれば、vm.メソッド名()とすれば呼び出せます。
  • 一方、method内の関数内から、同じmethod内の別の関数を呼び出す場合は、this.メソッド名()で呼び出せます。
  • いずれも、初期化したvueの中に用意した変数を参照する場合とほぼ同じやり方になります。

単一ファイルコンポーネントを使う

vueという拡張子の単一ファイルコンポーネントという形式でVue.jsをGAS上で使う手法が出来ました。Vue.jsの一連の処理の塊をファイルとしてGoogle Driveから読み込んで利用する方法は以下のエントリーにまとめました。

Google Apps ScriptでVueの単一ファイルコンポーネントを使ってみる【GAS】

Vue.jsでの実装例

ループでデータを生成

スプレッドシートのデータをHTML側で受け取り、ループでデータを生成する。非常によく使うパターンです。これまでの場合、コードを挿入する場所の要素を取得し、ForループでHTMLデータを生成し、要素にinnerHTMLで挿入する。そういったルーチンでした。Vue.jsを用いた場合、予めHTML内で定義してあるので、変数に値を入れただけで、これが完成します。

GAS側コード

var sheetid = "ここにスプレッドシートのIDを入れる";

//シートデータを取得して返す
function getsheetman() {
  //スプレッドシートのデータを取得
  var rows = SpreadsheetApp.openById(sheetid).getSheetByName("都道府県").getRange("A1:D").getValues();
  
  Logger.log(rows.length);
  
  //1行目をJSONのkeyとして設定
  var keys = rows.splice(0, 1)[0];
  
  //keyに対応するデータを元にJSONを構築
  return rows.map(function(row) {
    var obj = {};
    row.map(function(item, index) {
      obj[String(keys[index])] = String(item);
    });
    
    //JSONを返す
    return obj;
  });
}
  • 取得したスプレッドシートをそのままJSON.stringifyして返すだけでは駄目
  • JSON形式を組み立てて返してあげる際に、1行目のタイトル行をkeyとして利用する(これをHTML側で利用)

HTML側コード

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <!-- vue.jsをロード -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <!-- Google CSS Addon -->
    <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
    
    <!-- テーブルデザインCSS -->
    <!-- https://webliker.info/75964/ -->
    <style>
      table{
        width: 100%;
        border-collapse:separate;
        border-spacing: 0;
      }
      
      table th:first-child{
        border-radius: 5px 0 0 0;
      }
      
      table th:last-child{
        border-radius: 0 5px 0 0;
        border-right: 1px solid #3c6690;
      }
      
      table th{
        text-align: center;
        color:white;
        background: linear-gradient(#829ebc,#225588);
        border-left: 1px solid #3c6690;
        border-top: 1px solid #3c6690;
        border-bottom: 1px solid #3c6690;
        box-shadow: 0px 1px 1px rgba(255,255,255,0.3) inset;
        width: 25%;
        padding: 10px 0;
      }
      
      table td{
        text-align: center;
        border-left: 1px solid #a8b7c5;
        border-bottom: 1px solid #a8b7c5;
        border-top:none;
        box-shadow: 0px -3px 5px 1px #eee inset;
        width: 25%;
        padding: 10px 0;
      }
      
      table td:last-child{
        border-right: 1px solid #a8b7c5;
      }
      
      table tr:last-child td:first-child {
        border-radius: 0 0 0 5px;
      }
      
      table tr:last-child td:last-child {
        border-radius: 0 0 5px 0;
      }
    </style>
  </head>
  <body>
    <!-- 基本となるビュー -->
    <div id="app">
      <button @click="sheetman" class="create">データ取得</button>
      <p>{{ preflength }} 件</p>
      <table>
        <tr>
          <th>都道府県</th>
          <th>エリア</th>
          <th>政令指定都市</th>
          <th>人口</th>
        </tr>
        <tr v-for="rec in spread">
          <td>{{rec.都道府県名}}</td>
          <td>{{rec.エリア}}</td>
          <td>{{rec.政令指定都市}}</td>
          <td>{{rec.人口}}</td>
        <tr>
      </table>
    </div>
    
    <script>
      //ビューの初期化
      var vm = new Vue({
        el: '#app',
        data: {
          spread: null,  //初期値は空にしておく
        },
        methods: {
          sheetman: function () {
            //スプレッドシートのデータを取得
            google.script.run.withSuccessHandler(
              function (data) {
                //返り値をspreadにそのまま流し込む
                vm.spread = data;
              }
            ).getsheetman();
          }
        },
        computed: {
          preflength: function() {
            if(this.spread == null){
              return 0;
            }else{
              return this.spread.length;
            }
          }
        },
      })
    </script>  
  </body>
</html>
  • 今回テーブルデザインCSSは、こちらのサイトの情報を利用いたしました。
  • Vue.jsの変数spreadの初期値は空にしてあるので、表示しただけではテーブル表示されません。
  • sheetman関数@clickで呼び出すと、GAS側からJSONデータを取得し、vm.spreadに値を格納します。
  • 変数spreadに値が格納されると、<tr v-for="rec in spread">以下にデータがループで格納されます。それぞれのTDタグに指定のJSONの値を入れ込むようになっています。
  • v-forがループになりますが、for eachだと思えば良いです。
  • JavaScriptでループの構文やinnerHTMLといったコードは記述の必要がありません。
  • jQueryだと配列にデータを入れて、HTMLにも追加して・・・削除の時は逆を。vue.jsはデータを操作すれば、要素が変わるので、処理を落とすようなことも防げます。
  • computedにて、spread変数の中の件数を自動的に取得し反映しています。

実行結果

入力データの反映と取得

テキストボックスにユーザが入れた情報を取得し、HTML上に即反映。また、その値を取得して次の処理を行う。これも非常によく利用するパターンですね。テキストボックスとHTMLの指定部分とを連結するテクニックです。また、取得したデータでスプレッドシートのデータを検索し、返します。

GAS側コード

var sheetid = "ここにスプレッドシートのIDを入力";

//シートから一致するデータを返す
function chksheetman(kenmei){
  //スプレッドシートのデータを取得
  var rows = SpreadsheetApp.openById(sheetid).getSheetByName("都道府県").getRange("A2:D").getValues();

  //値を調べる
  var jinkou = 0;
  for(var i = 0; i<rows.length;i++){
    //都道府県名が一致したら人口を取得
    if(rows[i][0] == kenmei){
      jinkou = rows[i][3];
    }
  }
  
  //値を返す
  return jinkou;
}
  • 単純にシートのデータを取得、配列から合致するレコードがあったら、人口列の値を返します。

HTML側コード

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <!-- vue.jsをロード -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <!-- Google CSS Addon -->
    <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">

  </head>
  <body>
    <!-- 基本となるビュー -->
    <div id="app">
      <input type="text" v-model="spread" placeholder="都道府県を入力"/>
      <p><button @click="sheetchk()">人口検索</button>
      <div>{{ spread }}</div>
    </div>
    
    <script>
      //ビューの初期化
      var vm = new Vue({
        el: '#app',
        data: {
          spread: ""  //初期値は空にしておく
        },
        methods: {
          sheetchk: function () {
            //スプレッドシートのデータを検索
            google.script.run.withSuccessHandler(
              function(data){
                //人口をalert表示する
                alert(vm.spread + "の人口は" + data + "千人です。");
              }
            ).chksheetman(this.spread);
          }
        }
      })
    </script>  
  </body>
</html>
  • this.spreadでテキストボックスの値を取得させています。
  • また、取得時にはv-modelにてデータの表示先を指定してるので、何もせずとも入力後に値がHTMLへ反映します。
  • sheetchk関数にてGAS側検索。結果をalertで単純に表示しています。

実行結果

タグの属性に連結させる

HTMLのタグには例えば、aタグの場合、href属性などがあります。素のJavaScriptだと、document.getElementById(id).hrefといった具合に指定するアレです。この属性値に対してデータを連結する事が可能です。

GAS側コード

//全国47都道府県のHPリストを返す
function getpreflist(){
  //スプレッドシートのデータを取得
  var rows = SpreadsheetApp.openById(sheetid).getSheetByName("都道府県").getRange("A2:E").getValues();
  
  //配列に都道府県名とURLを格納する
  var array = [];
  
  //二次元配列を作成
  for(var i = 0;i<length;i++){
    //一時配列を用意
    var temparray = [];
    
    //都道府県名とURLをpushする
    temparray.push(rows[i][0]);
    temparray.push(rows[i][4]);
    
    //配列にpushする
    array.push(temparray);
    
  }
  
  //配列データを返す
  return JSON.stringify(array);

}
  • 単純にスプレッドシートのデータの一部の列だけを取得して二次元配列で返しています。

HTML側コード

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <!-- vue.jsをロード -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <style>
      [v-cloak] {
        display: none;
      }
    </style>
  </head>
  <body>
    <!-- 基本となるビュー -->
    <div id="app">
      <h2 v-cloak>{{ title }}</h2>
      <p v-cloak>{{ description }}</p>
      <ol v-cloak>
        <li v-for="list in prefs"><a v-bind:href="list[1]" target="_blank">{{ list[0] }}</a></li>
      </ol>
    </div>
    
    <script>
      //起動時にGAS側からデータを取得する
      google.script.run.withSuccessHandler(function(ret) {
        //データを取得する
        var result = JSON.parse(ret);
        
        //ビューの初期化
        var vm = new Vue({
          el: '#app',
          data: {
            title: "都道府県のページ",
            description: "全国47都道府県のホームページへのリンク集です。",
            prefs: result  
          },
        })
      }).getpreflist();    
    </script>  
  </body>
</html>
  • GAS側からの二次元配列データをv-forにて順番にロードさせています。
  • v-bindは省略記述可能。けれど、aタグの場合わかりにくくなるので、あえてつけています。
  • listは1レコード分入っているので、配列の指定で値を取り出してます。
  • GAS側からのデータの取得がもたつくので、表示は工夫が必要(v-ifやv-cloakを使うと良いかも)

実行結果

インライン画面遷移を実現する

Vue.jsにはcompnentといったものは、routerといったシングルページアプリケーションに役立つ機能があるのですが、Vue.jsをGoogle Apps Scriptで利用する場合に有効なのは、「v-html」です。このv-htmlは使い方を間違えるとXSS問題を引き起こす事があるので、安全なソースや環境下でだけ使うようにと、公式サイトにも記述されています。

componentの場合、templateにて参照できるのは自身のVueのスコープの範囲内であるのと、templateにスクリプトレットでHTMLを突っ込んでも、表示はなされません。そこで利用するのが、v-htmlですが、こちらは外部からのHTMLを取得し流し込めるので、GASで使う場合には非常に有用です。

ただし、使う場合には以下の注意点があります。

  • GAS側で用意したHTMLファイルをgetContentで引っ張って来れるのは、HTML, CSS。
  • innerHTMLと同じような動きをしますが、ロードしたJavaScriptは別途用意するか?実行できる状態になければならない。
  • ロードしたHTML内でもVueを使えますが、呼び出し元のvueとidが被らないように注意しなければならない。

GAS側コード

//指定のHTMLの中身を取得して返す
function retvmnormal(htmlname){
  var html = HtmlService.createHtmlOutputFromFile(htmlname).getContent();
  return html;
}
  • 単純にHTMLファイルの中身を取得してそのまま返しています。引数はHTML名を取っています。

HTML側コード(moveview.html)

<html lang="ja">
<head>
	<meta charset="utf-8">
	<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    
    <!-- Google CSS Addon -->
    <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
</head>
<body>
	<div id="app">
            <button @click="transPage('vm_normal')" class="create">Home</button>
            <button @click="transPage('vm_table')" class="create">Table</button>
            <button @click="transPage('vm_link')" class="create">Link</button>
            <div v-html="rawHtml"></div>
	</div>

    <script>
      var app = new Vue({
        el: '#app',
        data: {
          rawHtml : null,
        },
        methods : {
          transPage : function(page){
            //GAS側からHTMLを取得する
            google.script.run.withSuccessHandler(function(ret){
              //HTMLを返す
              app.rawHtml = ret;
              
              //vm_linkの時はmakelinkを実行する
              if(page == "vm_link"){
                makelink();
              }
            }).retvmnormal(page);
          }
        }
      })
      
      //リンクを生成する
      function makelink(){
        //起動時にGAS側からデータを取得する
        google.script.run.withSuccessHandler(function(ret) {
          //データを取得する
          var result = JSON.parse(ret);
          
          //ビューの初期化
          var vm2 = new Vue({
            el: '#app2',
            data: {
              title: "都道府県のページ",
              description: "全国47都道府県のホームページへのリンク集です。",
              prefs: result  
            },
          })
        }).getpreflist();    
      }
    </script>
    
</body>
</html>
  • ボタンをクリックすると該当するHTMLをGAS側から読みに行き、返り値をv-htmlで指定されている場所へ挿入してくれます。
  • vm_linkの時だけ、makelink関数をHTMLのロード後に実行しています。

HTML側コード(vm_table.html)

<!-- https://cotodama.co/table-design/ -->
<style>
  table{
    border-collapse: separate;
    border-spacing: 5px 0;
    margin: 0 auto;
  }
  td,th{
    padding: 10px;
  }
  th{
    color: #fff;
    background: #005ab3;
    border-radius: 5px;
  }
  td{
    color: #005ab3;
  }
</style>

<table>
  <tr><th>項目A</th><th>項目B</th><th>項目C</th><th>項目D</th></tr>
  <tr><td>A-1</td><td>B-1</td><td>C-1</td><td>D-1</td></tr>
  <tr><td>A-2</td><td>B-2</td><td>C-2</td><td>D-2</td></tr>
  <tr><td>A-3</td><td>B-3</td><td>C-3</td><td>D-3</td></tr>
  <tr><td>A-4</td><td>B-4</td><td>C-4</td><td>D-4</td></tr>
</table>

  • GAS側からgetContentで取得するHTMLの例です。
  • サンプルのHTMLおよびCSSデータはこちらから利用させていただきました。
  • まるで、vueファイルでパッケージしたかのようなスタイルでGAS側からコードをロードしてそのまま利用する事が可能です。

実行結果

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)