Google Apps Script でAngularJSを使った Single Page Application を構築・公開する方法(応用編)

#前置き
[Google Apps Script でAngularJSを使った Single Page Application を構築・公開する方法(基礎編)](http://qiita.com/dq-nozomi-yokomura/items/fefb3d1f4f3fa48db8f9)
↑こちらの続きになります。

前回はGoogle Apps Script(以下、GAS)にて
・一般公開のやり方
・簡単なルーティング方法
・JavaScript・CSSの読み込み方
・画像の読み込み
をお話ししました。

今回は、では実際にサイト作ってみるとこんな感じになりますよ、的なお話しをします。
ざっくり下記の流れになります。
1.サーバーサイドスクリプトと連携
2.AngularJSを導入
3.Google Spreadsheetと連携(ログ＆DB代わり)
4.Google Analyticsを設定
5.Google Adsenseを設定
6.Search Consoleを設定(失敗)

#1.サーバーサイドスクリプトと連携
[Class google.script.run (Client-side API)](https://developers.google.com/apps-script/guides/html/reference/run)

[[GAS] Google Apps Script のHtmlServiceまとめ](http://dackdive.hateblo.jp/entry/2015/02/01/010540)

リファレンスや参考サイト(他にもあったのですが忘れました爆)を見ていろいろ実装してみた限り、下記の書き方が良さそうです。

```Code.gs
// Getリクエストの受け取り口、起動時はここが呼ばれる模様
function doGet(request) {
    return HtmlService.createTemplateFromFile('index').evaluate().setSandboxMode(HtmlService.SandboxMode.IFRAME);
}

function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename)
  .getContent();
}

// クライアントへ引数のパラメータを結合したものを返す
function apiGetMessage(params){
  return params["key"]+":"+params["value"];
}
```

```index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <?!= include('index.js'); ?>
  </head>
  <body>
    <div>testですよ</div>
    <div><a href="javascript:void(0)" onClick="getMessage()">message</a></div>
    <div id="message"></div>
  </body>
</html>
```

```index.js.html
<script>
var getMessage = function(){
 (function (params, api, onSuccess, onFailure, userObject) {
   if (!onSuccess || typeof onSuccess !== "function")onSuccess = function (e) {};
   if (!onFailure || typeof onFailure !== "function")onFailure = function (e) {};
   google.script.run
    .withSuccessHandler(onSuccess)
    .withFailureHandler(onFailure)
    .withUserObject(userObject)[api](params);
  })(
    {key:1,value:"hoge"},
    'apiGetMessage',
    function (ret) {
      document.getElementById("message").textContent=ret;
    }
  ); 
};
</script>
```

messageと書かれたリンクをクリックすると、画面に通信結果を表示する、という動作をするだけのプログラムになってます。

フロントのJavaScriptは[基礎編](http://qiita.com/dq-nozomi-yokomura/items/fefb3d1f4f3fa48db8f9)で紹介しましたファイル分割方法で別ファイル化しています。
今回の例では成功時のコールバックのみ指定していますが、即時実行形の引数部分で失敗時のコールバックを渡しても動作します。

#AngularJSを導入
先ほどのサンプルにAngularJSを入れるとこのようになります。
※Code.gsはそのままなのでindex.htmlとindex.js.htmlのみ

```index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <script src="https://code.angularjs.org/1.5.0/angular.min.js"></script>
    <?!= include('index.js'); ?>
  </head>
  <body ng-app="app">
    <div>testですよ</div>
    <div ng-controller="mainPageController">
      <div ng-click ="getMessage()">message</div>
      {{message}}
    </div>
  </body>
</html>
```

```index.js.html
<script>
  //module
  (function () {
    angular.module("app", []);
  })();
  //factory
  (function (module) {
    if (!angular)return;
    function gsrun() {
      var exec = function (params, api, onSuccess, onFailure, userObject) {
        if (!onSuccess || typeof onSuccess !== "function")onSuccess = function (e) {};
        if (!onFailure || typeof onFailure !== "function")onFailure = function (e) {};
        google.script.run
          .withSuccessHandler(onSuccess)
          .withFailureHandler(onFailure)
          .withUserObject(userObject)[api](params);
      }
      return {exec: exec}
    }
    module.factory("gsrun", gsrun);
  })(angular.module("app"));

  //controller
  (function (module) {
    module.controller("mainPageController", [ "$scope","gsrun", function($scope,gsrun) {
      $scope.getMessage = function(){
        gsrun.exec(
          {key:1,value:"hoge"},
          "apiGetMessage",
          function (ret) {
            $scope.message = ret;
            $scope.$apply();
          }
        );
      };
    }]);
  })(angular.module("app"));
</script>
```

AngularはCDNから読み込む形式にしています。
サーバサイドスクリプトとの通信部分はファクトリー化してます。


#Google Spreadsheetと連携(ログ＆DB代わり)

[こちら](http://qiita.com/dq-nozomi-yokomura/items/1bf688b84fa4bfe2802e)の記事で紹介したGoogle Spreadsheet(以下、スプレッドシート)との連携を組み込みます。
今回はページのロード時にスプレッドシートへ書き込む形式のものです。
なのでCode.gsのみの書き換えです。

```Code.gs
// Getリクエストの受け取り口、起動時はここが呼ばれる模様
function doGet(request) {
  _write_log(request);
  return HtmlService.createTemplateFromFile('index').evaluate().setSandboxMode(HtmlService.SandboxMode.IFRAME);
}

function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename)
  .getContent();
}

function apiGetMessage(params){
  return params["key"]+":"+params["value"];
}

function _write_log(value){
  
  var spreadSheet = SpreadsheetApp.openByUrl("https://docs.google.com/spreadsheets/d/xxxxxxxxxxxxxxxxx/edit#gid=0");
  var targetSheet = spreadSheet.getSheetByName("log");
  
  // 書き込む行番号を取得
  var target_row = targetSheet.getRange(1,1).getValue();
 
  // 書き込む内容を2次元配列へ
  var writeArray = [];
  writeArray.push([
    target_row,
    HtmlService.getUserAgent(),
    value
  ]);
  
  // 書き込み
  targetSheet.getRange(
    target_row+1, // データ取得の開始行
    1, //　データ取得の開始列
    1, //　行数は1レコード分
    3 // 列数はデータの個数
  ).setValues(writeArray);
  
  // 次回書き込みの行番号を更新
  targetSheet.getRange(1,1).setValue(target_row+1);
}
```

スプレッドシートへの書き込みを一括で行う部分が2次元配列を使っているのでやや複雑に見えてしまうかもしれませんが、やっている事はシンプルです。
行番号・ユーザエージェント・doGetの引数、の3項目分のRangeを取得し(getRange)、そこに2次元配列をセットしている(setValues)だけです。


#Google Analyticsを設定
GASで公開したサイトは、Chromeだと検証でソースを見ていただければ分かるのですが、iframeの中でさらにiframeを呼んでいるっぽいものすごく複雑な作りをしています。
ですが、Analyticsに関して言えば、HTMLのヘッダにJavaScriptのコードを追記するというごく一般的なものできちんとAnalyticsへの情報送信を行ってくれました。

```index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <script src="https://code.angularjs.org/1.5.0/angular.min.js"></script>
    <?!= include('index.js'); ?>
    
    <script>
      (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
        (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
        m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
      })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');

      ga('create', 'UA-xxxxxxxx-x', 'auto');
      ga('send', 'pageview');
    </script>
    
  </head>
  <body ng-app="app">
    <div>testですよ</div>
    <div ng-controller="mainPageController">
      <div ng-click ="getMessage()">message</div>
      {{message}}
    </div>
  </body>
</html>
```

今回の例ではSingle Page Application(以下、SPA)なので、情報収集したいページに
ga('send', 'pageview', { page: "hogehoge" });
を設置していかないといけませんが、そちらの方も問題なく動作します。

ちなみに、Angularticsなるものを活用すればSPAのAnalytics問題はすっきりするみたいなのですが、GASサイトネタというよりはSPAネタなのと、GASではまだ検証出来てないので、ノータッチでいきます。


#Google Adsenseを設定
こちらもAnalytics同様、案外普通に設置可能でした。
GASサイトではファイル分割して読み込ませる形式の方が扱いが便利そうなので、下記のようにしています。

```index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <script src="https://code.angularjs.org/1.5.0/angular.min.js"></script>
    <?!= include('index.js'); ?>
  </head>
  <body ng-app="app">
    <div>testですよ</div>
    <div ng-controller="mainPageController">
      <div ng-click ="getMessage()">message</div>
      {{message}}
    </div>
    <!-- 広告設定 -->
    <?!= include('sponsored_link'); ?>
  </body>
</html>
```

```sponsored_link.html
<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<ins class="adsbygoogle"
     style="display:inline-block;width:XXXpx;height:XXXpx"
     data-ad-client="ca-pub-xxxxxxxxxxxxxxxx"
     data-ad-slot="xxxxxxxxxx"></ins>
<script>
(adsbygoogle = window.adsbygoogle || []).push({});
</script>
```

そのままですね。

#Search Consoleを設定(失敗)
Analyticsで流入が確認出来、Adsenseで広告もバッチリ！
となれば後はSearch Consoleでgooglebotさんを呼べばもう一丁前のサイトやん(￣￢￣*)
と思い意気揚々とトライしたのですが、こちらはダメでした…

どこがダメだったかと言うと、サイトの所有権の確認が何をしても通りません。
所有権の確認は以下の5パターンのうち
・HTMLタグ(推奨)
・HTMLファイルをアップロード
・ドメイン名プロバイダ
・Googleアナリティクス
・Googleタグマネージャ
よく分からなかったドメイン名プロバイダ以外は試行錯誤してみたのですが玉砕しましたorz

最終的に周囲のエンジニアの方に相談した結果、

GASで公開したサイトのURLは下記のようになっているのですが
https://script.google.com/macros/s/xxxxxxxxxxxxxxxxxxxxxxxxxxxxx/exec
※execより前はこちらで変更不可

大元のrobotsを見てみると
https://script.google.com/robots.txt

User-agent: *
Disallow: /

アクセス拒否∑(￣◇￣)
つまりGASのサイトの仕様的に無理っぽい、という結論に達しました。

#まとめ
上記で紹介した内容以外にも、サーバ側の設定がいじれない関係でクロスドメイン制約が突破出来なかったりするので、やはりGASのサイトの用途は社内システムのような、限られたコミュニティでの活用が今はメインなのかなと思います。
※サーバサイトスクリプトだけに限った話だと、APIとして公開出来るのでもうちょっと用途が多いかも
それでも、個人的にGASは思ったよりいろいろな事が出来ると感じたので、これからも何か出来ないか可能性を探っていこうと思います。
Googleの各種サービスとの連携はすごく楽ですしね(￣▽￣*)
という事でGASとGAS使いの方々に幸あれ。