Google Apps Scriptと連携するElectronアプリを作る – 入門編【GAS】

G Suiteは現在世界で400万人が利用しているとのこと。月700円/人で利用できるクラウド型イントラ兼グループウェアとしてみた時、コストパフォーマンスは凄まじく高いと思います。そこへGoogle Apps Scriptが扱えるのであれば、拡張性やその自由度も飛躍的に高まります。特定のメンバー以外は特定機能だけ使ってもらえばと言った場合、アプリケーション毎の制限はAdminで設定可能なのです。

そんな中、Google Apps Script APIを利用し、Execution API + Node.jsを使っての読み書き、尚且つ、Electronでアプリ化してみようと思いました。また、Electron単体でMySQLデータベースを利用したC/Sなアプリの制作を行ってみたいと思います。Google Apps Scriptネタではありますが、今回はElectronアプリの作り方的なまとめになっています。今回は簡単なおみくじアプリを作ってみます。別途応用編にて、簡易タイムレコーダの作成とパッケージ化についてまとめたいと考えています。

※G SuiteやGoogle Apps Scriptではなく、それを外部から扱えるアプリケーションを作れるのもG Suiteの魅力の1つ。Microsoft365では出来ないことの1つですね。Electronだけでなく、Cordovaからも扱えます。

使用する素材類およびメソッド類

以前は、OAuth2関係は別のgoogle-auth-libraryモジュールというライブラリでしたが、googleapisに統合されましたので、現在は不要です。

Electronとは?

Electronとは、Node.js + JavaScript + HTML5の組み合わせで、オープンソースウェブブラウザのChromiumを土台としてアプリケーションとしてパッケージにし、クロスプラットフォームで動かせるようにしてくれる便利なツールです。ベースの開発環境そのものはNode.jsそのものなので、Node.jsの様々な拡張機能を扱えるだけでなく、Electron特有の機能によってよりデスクトップアプリケーションとして動くように作ることが可能です。

JavaScriptオンリーでデスクトップアプリが、しかもWindows, Mac OS X, Linux向けに無改造でリリースが出来る、Node.jsを利用してる為、通常のJavaScriptでは出来ない「ローカルファイルへのアクセス」や「クロスサイト」なアクセスが可能になってるのも特徴です。ウェブは得意だけれどローカルアプリケーション作成はちょっと・・・という人が、スキルそのままにアプリケーションを作れるのは一石二鳥です。

また、JavaScriptですので、クライアントサイド(GUIを担当)では、数々のJavaScriptライブラリを利用する事が可能になっています。よって、リッチなGUIを簡単に構築出来るだけでなく、フレームワークを利用したりも可能。

似たような仕組みとして、NW.jsというものがあります。Electronと比較した時に一長一短なので、どちらが優れているとは言えないのですが、そのあたりを比較した方がいらっしゃいますので、こちらのサイトを一度覗いてみて、どちらを使うかを決めても良いかと思います。

事前準備

作成に当たっては事前準備が必要です。ここで必要な準備は、お使いのマシンにNode.jsのインストールとElectron開発環境を整える事になります。基本、Mac OS Xでの利用を前提に話を進めますが、インストール自体は簡単なので、WindowsであってもLinuxであっても殆ど変わりません。インストールしたパスだけ注意しておいて下さい。

Node.jsとその他をインストール

コマンドラインでのインストール(homebrewを使った手法)もありますが、ここでは手軽に、インストールパッケージを利用します。以下の手順でパッケーを入手しダウンロードしましょう。また、Node.jsで利用する、今回使う予定のモジュールもインストールしておきましょう。

Node.jsをインストールする

2019年1月現在、最新版LTSはv10.15.0となっています。以下の手順でnode.jsをインストールします。

  1. Node.jsからnode-v10.15.0.pkgをダウンロードする。Windows(32bit)ならnode-v10.15.0-x86.msiとなります。64bit版も別に存在するので、注意してください。
  2. ダブルクリックで実行して、インストールを進める。
  3. 最後にインストールしたパスが表示されて終了。

図:インストール自体は超カンタン

ここで一応、ターミナルを起動して、node.jsおよびnpmのバージョンを確認しておきましょう。Windowsの場合は、コマンドプロンプトになります。

  1. ターミナルを起動する
  2. node -vコマンドにて、バージョンを確認する
  3. npm -vコマンドにて、バージョンを確認する

図:バッチリ最新版になってるのを確認した。

追加モジュールをインストール

今回作成するプログラムで使用する追加モジュールをインストールします。npmコマンドを使用してインストールするのですが、今後色々なシーンで必要になるコマンドなので覚えておきましょう。以下のようなコマンドで実行をします。

//インストール時共通のコマンド
npm install xxxxxxx

この時、オプションとして -gを指定しておくと、直接実行が可能になります。無い場合には自分のホームディレクトリのフォルダにインストールされます。なので、自分の場合必要な物はこのコマンドにオプションをつけてインストールしています。ちなみに、npm install xxxxxの場合、ネットからダウンロードしてインストールしますが、直接特定のフォルダ内にあるパッケージをインストールする場合には、そのフォルダまで移動してから、npm installと実行するだけで、中に入ってるpackage.jsonを読み取ってインストールしてくれます。

GoogleのNode.js Quickstartによると、googleapisをインストールします。その前に、まずは開発用のフォルダを作っておき、そこに移動してから作業を行いましょう。作成したフォルダと同じフォルダにnode_moduleというフォルダが生成され、ライブラリが追加されます。

//開発用フォルダを作って移動する(projectmanというフォルダを作ります)
cd Documents
mkdir projectman
cd projectman

//Google認証で使うライブラリのインストール
npm install googleapis --save

図:npmでモジュールをインストール中

Electronとその他をインストール

Electron自体もnpmの追加モジュールになっています。よって、node.jsの追加モジュールと同様にインストールするのですが、自分の場合エラーが出て嵌まりました。よって、その解決法も含めてここに記述します。

//electron本体のインストール
npm install electron -g

現在最新版は、v4.0.2となっています。バージョンの確認は、ターミナルにてelectron -vで確認が出来ます。

図:無事にインストールできた場合

本体インストール時にエラーが発生したら

electronモジュールをインストール出来ないケースがあります。以下のようなエラーが出るケースです。

EACCES: permission denied, mkdir '/usr/local/lib/node_modules/electron/.electron'

この場合、sudoでインストールを実行していればエラーは出ないのですが、あるケースではこれでもダメです。其の時には、以下のコマンドをターミナルより実行して、electron-v0.36.8-darwin-x64.zipがダウンロードされる~/.electronフォルダのパーミッションを変更して上げましょう。参考:Failed at the electron-prebuilt@0.25.3-2 install script ‘node install.js’

//ダウンロードフォルダのパーミッションを変更する
sudo chmod 777 ~/.electron

これでも解決しない場合には以下のコマンドでインストールしましょう。参考:Error: EACCES: permission denied, mkdir '/usr/local/lib/node_modules/electron/.electron' #17268

sudo npm install -g electron --unsafe-perm=true --allow-root

package.jsonを作成する

Electronがインストール出来、プロジェクトフォルダもこれで用意できました。プロジェクトフォルダに入り、以下のコマンドを入力して、プロジェクト用のpackage.jsonを作成します。

//package.json生成コマンド
npm init

指示に従っていくとプロジェクトフォルダにpackage.jsonが出来上がります。-yオプションを付けると自動で生成もします。以下のような感じの内容になっています。

{
  "name": "projectman",
  "version": "1.0.0",
  "description": "testapp",
  "main": "index.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

追加モジュールをインストールする

Electronはこの段階で既に作成が出来るのですが、配布する為にはパッケージ化が必要です。その為のモジュールをインストールしておく必要性があります。これで、アーカイブ&パッケージ化するためのモジュールがインストールされ、準備万端です。

//パッケージングに必要なモジュールのインストール
npm install asar
npm install gulp --save-dev
npm install -D electron-packager
npm install -D -g electron-builder

以前は、packagerでパッケージングしていましたが、最近はbuilderでパッケージングするのが流行りのようです。とりあえず両方入れておきます。

jQueryを導入する

ElectronでもjQueryが利用可能です。しかし以前の方法ではロードできなくなっていました。stackoverflowによると以下の手順で導入するようになったみたい。

  1. 以下のnpmコマンドでnpmからjQueryを導入する(バージョンは3.3.1でした)
  2. node-modulesにjqueryが入ることになる。
  3. HTML側はでは以下のコードを追加して呼び出すようにする
//jQueryをインストールするコマンド
npm install jquery --save

npmで入れるという点がこれまでと違う点ですね・・・

<script>
      var $ = jQuery = require("jquery")
</script>

Google Apps Script側の準備

Electronの開発環境は整ったので、次にGoogle Apps Script側の準備をします。

プロジェクトを移動

今回の発表直前の2019年4月8日より、Google Apps ScriptからCloud Platform Projectへ直接アクセスが出来なくなりました。これまでにデプロイしてるものについては、これまで通り「リソース」⇒「Google Cloud Platform API ダッシュボード」からアクセスが可能です。

今回の変更はスプレッドシート上で動かすスクリプトやGoogleの拡張サービスを利用しないタイプのスクリプトであれば特に問題はありませんが、「Apps Script API」や「Google Picker API」、「Cloud SQL接続」などGCP上のAPIを利用する場合には以下の手順を踏んで、Google Apps Scriptにプロジェクトを連結する必要があります。これまでは、自動的にGCP上にGoogle Apps Script用のプロジェクトが生成されていたのですが、今後は自分の組織(もしくはGCPプロジェクト)上で作成されたプロジェクトでなければならないということです。詳細はこちらのページを見てください。

連結する手順は以下の通り

  1. Google Cloud Consoleを開く
  2. 左上にある▼をクリックする
  3. ダイアログが出てくるので、新規プロジェクトを作るか?既存のプロジェクトを選択する。この時、G Suiteであれば選択元は「自分のドメイン」を選択する必要があります。
  4. プロジェクト情報パネルから「プロジェクト番号」をコピーする
  5. 対象のGoogle Apps Scriptのスクリプトエディタを開く
  6. 「リソース」⇒「Cloud Platform プロジェクト」を開く
  7. 4.で入手した番号をプロジェクトを変更のテキストボックスに入れて、プロジェクトを設定ボタンをクリックする
  8. 無事に移動が完了すればメッセージが表示されます。
  9. この時、元の自動作成されたプロジェクトはシャットダウンされて消えます。これで設定完了です。

今回のこの変更だと1つ作ったプロジェクトに集約する必要があるので、クォータについてプロジェクト毎のカウントだったので問題なかったものが、集約されることで、クォータに引っ掛かる可能性があります。

図:プロジェクト番号をコピーしておきます

図:プロジェクトを他のプロジェクトに紐付けしました。

図:GCPの拡張サービスを使うには手順が必要になった

OAuthクライアントID等を取得する

今回使用するスプレッドシートをコピーしたら、Cloud Consoleで以下の作業を行い、OAuthクライアントIDとクライアントシークレットを取得します。

  1. メニューより「リソース」⇒「Googleの拡張サービス」を開きます。
  2. 出た画面の下にある「Google Cloud Platform API ダッシュボード」をクリックします。
  3. 上部にある「APIとサービスの有効化」をクリック
  4. 検索画面が出たら、「Apps Script API」を検索して、有効にします。
  5. 次にダッシュボードに戻り、APIとサービスを開く。認証情報を開き、「認証情報を作成」をクリックし、「OAuthクライアントID」を選択します。
  6. アプリケーションの種類は「その他」を選択します。
  7. 適当な名前を入れます。今回はElectronという名前をつけました。
  8. これで作成すると、クライアントIDが発行されます。
  9. 一旦そのダイアログを閉じて、OAuth2.0クライアントIDのリストがありますので、Electronと名前の付いたレコードの右にダウンロードのアイコンがあるので、クリックする
  10. OAuthクライアントIDとクライアントシークレットが含まれたjsonファイルがダウンロードされるので取っておく。後で使用します。名前はclient_secret.jsonという名前で保存しました。

図:今回はJSONファイルの形で利用します。

JSONファイルの中身は以下のようなもの。access tokenなどは入っていない。この情報を元にAccess TokenやRefresh Tokenを取得する事になります。

{"installed":
    {
        "client_id":"ここにクライアントIDが入ってる",
        "project_id":"ここにGCPのプロジェクトIDが入ってる",
        "auth_uri":"https://accounts.google.com/o/oauth2/auth",
        "token_uri":"https://oauth2.googleapis.com/token",
        "auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs",
        "client_secret":"ここにクライアントシークレットが入ってる",
        "redirect_uris":
            ["urn:ietf:wg:oauth:2.0:oob","http://localhost"]
    }
}

GAS側におみくじ関数を作る

今回は入門編という事で、おみくじアプリを作ります。スプレッドシートにはおみくじデータが、dataシートに入っています。これをランダムに取得するようなコードを書き、APIを叩くとそれらのうち1つが返ってくるというものです。以下のようなGASのコードを書きました。環境に合わせて変数sheetのシートIDを書き直すのを忘れずに。

//おみくじを引いて値を返すAPI
function omikuji() {
  //シートを取得
  var sheet = "このスプレッドシートのIDを入力する";
  var ss = SpreadsheetApp.openById(sheet);
  
  //0-9のランダムな数を取得する
  var shake = uidgene() + Number(2);
  
  //得られた乱数を元におみくじを取得する
  var mikuji = ss.getSheetByName("data").getRange("A" + shake).getValue();
 
  return mikuji;
}
 
//0から9までの乱数を出す
function uidgene(){
  var randam = Math.floor( Math.random()*10)
  return randam;
}

実行可能APIとして導入する

つづいて、スクリプトエディタ上でこのスクリプトの関数を実行可能APIとして導入します。実行可能APIとして導入はソースコードを修正するたびに、導入を実行しなければいけませんので、注意が必要です。

  1. スクリプトエディタのメニューより、「公開」⇒「実行可能APIとして導入」を実行します
  2. 適当にバージョンと説明を入力して、アクセスできるユーザは全員で良いでしょう。更新ボタンを押し、続行ボタンを押す。出てくる、API IDは控えておきます。あとでコードに記述します。

図:おみくじAPIがこれで完成です。

Electronアプリの作成

Electronでアプリを作る場合、Node.jsの流儀に従う事になるのですが、Electron特有の流儀というものもあるので、ちょっと複雑です。また、Node.jsの流儀は少し癖があって、Electronでアプリを作る場合、Node.js側(メインプロセスと呼ぶ)にHTML側(レンダラプロセスと呼ぶ)からアクションを送って、返り値をどうこうするといったプロセスだけでも結構大変でした。この辺りも含めて、解説してみたいと思います。

ファイルの作成

作成済みのプロジェクトフォルダにclient_secret.jsonを入れます。また、空のindex.htmlおよび空のindex.jsファイルを作成しておきます。

図:プロジェクト作成とファイルの配置

index.js

Electronアプリの最も中心となるのがこのindex.jsの作成です。今回の主要な部分は、Google Developerのサンプルコードを改造して使用しています。OAuth認証用コードと、Google Apps Script側にある「omikuji関数」を叩き、結果をHTML側へと渡す形になっています。

'use strict';

//追加モジュールの宣言
const electron = require('electron');
const { app } = require('electron');
const BrowserWindow = electron.BrowserWindow;
var fs = require('fs');
var readline = require('readline');
var {google} = require('googleapis');

//Node.js側とHTML側で通信をするモジュール
const ipcMain = require('electron').ipcMain;

//google apps関係
var scriptId = 'ここにAPP IDをいれます';
var result = "";    //認証時のコードを一時的に受ける関数

//スコープとアクセストークン関係
var SCOPES = ['https://www.googleapis.com/auth/spreadsheets'];
var TOKEN_PATH = './projectman/token.json';

// メインウィンドウはグローバル宣言
let mainWindow = null;

app.on('ready', function() {
  // メイン画面の表示。ウィンドウの幅、高さを指定できる
  mainWindow = new BrowserWindow({
  	'width': 450,
  	'height': 350,
  	'autoHideMenuBar':true,
  	//nodeIntegrationを有効にしないとrenderProcessでrequireを使えない。v5.0.0ではデフォルトで廃止
  	webPreferences: {
        nodeIntegration: true
    }
  });
  
  //初期ページの表示
  mainWindow.loadURL('file://' + __dirname + '/index.html');
  //デベロッパーツールを有効化
  mainWindow.webContents.openDevTools();
  mainWindow.on('closed', function() {
    mainWindow = null;
  });
});

// 全てのウィンドウが閉じたときの処理
app.on('window-all-closed', () => {
  // macOSの時以外はアプリケーションを終了させます(osxだとドックに残る)
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

// 非同期プロセス通信
ipcMain.on('async', function( event, args ){
  //HTML側からの引数を取得する
  var test = args;
  
  fs.readFile('./projectman/client_secret.json', function processClientSecrets(err, content) {
  	//client_secret.jsonがあるかないかチェック
    if (err) {
      mainWindow.webContents.send('message', "client_secret.jsonが見つかりません:" + err );
      return;
    }
    
    //token.jsonがあるかないかチェック
    fs.readFile('./projectman/token.json', function processClientSecrets(err, res) {
    	//token.jsonがないので認証開始
    	if(err){
    		//認証後にメインスクリプトを実行
    		result = "";
    		authorize(JSON.parse(content),authret);
    	}
    	return;
    });
    
    //おみくじを引くボタンを押したら実行
    //認証時コードを受け取る
    console.log(test);
    if(test == "exec"){
    	//result = "test"
        authorize(JSON.parse(content),callAppsScript);
    }
  });
});

//認証完了後メッセージ
function authret(){
    mainWindow.webContents.send("認証完了");
}

function authorize(credentials, callback) {
  const {client_secret, client_id, redirect_uris} = credentials.installed;
  const oAuth2Client = new google.auth.OAuth2(
      client_id, client_secret, redirect_uris[0]);

  // Check if we have previously stored a token.
  fs.readFile(TOKEN_PATH, (err, token) => {
    if (err) return getAccessToken(oAuth2Client, callback);
    oAuth2Client.setCredentials(JSON.parse(token));
    callback(oAuth2Client);
  });
}

function getAccessToken(oAuth2Client, callback) {
  const authUrl = oAuth2Client.generateAuthUrl({
    access_type: 'offline',
    scope: SCOPES,
  });
  
  //ipcRendererの値を取得して判定
  if(result == ""){
    //HTML側へ認証要求とコード入力を促す
    mainWindow.webContents.send('async-url', authUrl );
  }else{
    console.log("OK:");
    oAuth2Client.getToken(result, (err, token) => {
      if (err) return console.error('Error retrieving access token', err);
      oAuth2Client.setCredentials(token);
      // Store the token to disk for later program executions
      fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
        if (err) console.error(err);
            //アクセストークンを保存する
            function storeToken(token) {
                try {
                    //fs.mkdirSync(TOKEN_DIR);
                } catch (err) {
                    if (err.code != 'EEXIST') {
                        throw err;
                    }
                }
                fs.writeFile(TOKEN_PATH, JSON.stringify(token));
            }
      });
      callback(oAuth2Client);
    });    
  }
}

//Execution APIを叩くスクリプト
function callAppsScript(auth) {
  const script = google.script({version: 'v1', auth});
  script.scripts.run({
    resource: {
      function: 'omikuji',
    },
    scriptId: scriptId
  }, (err, res) => {
    if (err) {
      //APIの実行に失敗した場合
      console.log('api exection error: ' + err);
      return;
    }
    var unsei = res.data.response.result;
    mainWindow.webContents.send('message', "今日のあなたの運勢は" + unsei + "です。" );
  });
}

ポイント

★追加モジュールの宣言

requireと続けてモジュールの名前をつけて変数に入れています。こうする事でNode.jsは事前にnpm installしておいたモジュールをプログラムで利用する事が出来るようになります。デフォルトで用意されてるモジュールもありますが、今回は別途追加した

  1. googleapis

の他に、Electron特有の追加モジュールとして必須なモジュールとして以下の2つを必ず宣言します。

  1. app
  2. browser-window

また、Node.js側とHTML側とでやり取りをするのに利用するipcモジュールを今回は導入しています。それが、ipcMain = require(‘electron’).ipcMain;です。

★Google appsとスコープ関係

scriptIdには、Google Apps Script上で「スクリプトプロパティ」に表示されるスクリプトIDを入れます。スプレッドシートのIDではありませんので注意!また、SCOPEは本プログラムに於いて利用するGoogleのサービスを指定します。今回はSpreadsheetAppしか使っていないので、スコープとしてhttps://www.googleapis.com/auth/spreadsheetsを入れています。

★app.onとmainWindowについて

mainWindowをグローバル変数として宣言して起きます。続けて、app.onという項目が出てきますがこれが、Electron特有の仕組みです。この辺りのコードはお決まりとして必ず記述します。特に変わった記述をする必要性はありません。但し、mainWindowに対してnew BrowserWindowしてる部分。オプションとして縦横のサイズとautoHideMenuBarという設定を追加しています。デフォルトでメニューバーが表示されるので、falseで非表示にしています(但し、Mac OS Xでは無視されます)。

このオプション項目は非常に沢山あるので、オプション項目を見て追加しましょう。ウィンドウの挙動に関するものがズラっと列挙されています。

※オプションとして、mainWindow.webContents.openDevTools();の1行を入れておくと、Chromiumのデバッガ(デベロッパーツール)が最初使えるようになるので、開発中は入れておくと良いでしょう。

※尚、デベロッパーツール上記の1行をいれずとも、OSXならCommand + Option + iキー、WindowsならばCtrl + Shift + iキーで表示が可能です。

図:デベロッパーツールをオンにした状態

★ipcMain.onについて

HTML側からipc通信で送られてきたら受け取る側がコレ。asyncと名前がついています。相手はこの名前でこのコード宛にデータを送り、受け取った側はそれまでずっと待機しています(この辺りがNode.js特有の癖ですね)。受け取るとプログラムが動き、認証用スクリプトが動くようになっています。似たようなモジュールとして、remoteというモジュールがありますが、こちらはNode.js側やHTML側を直接双方から操作するものとの事。まだ使ったことないですが。

★OAuth認証関係のスクリプトについて

この辺はGoogle Developerに掲載されてるものです。authorize(), getAccessToken()はほぼそのまま利用しています。しかし、getAccessToken()については少々改造しています。元のコードがNode.jsアプリとして動かした時を想定してるものであって、このままではElectronでは動かせないからです(readlineのquestionという文字列の出力と入力待ちが動かない為です)。

ここはほぼスクリプトを削除して、mainWindow.webContents.send(‘async-url’, authUrl );  にて、ipc通信を行い、HTML側のasync-urlに対してauthUrlを送り込んでいます。JavaScript側でダイアログ表示、入力待ちをさせ、認証コードを受け取らせるようにしています。

※但しこのスクリプトはrefreshTokenを使ってexpireした時に新しいアクセストークンを取得するコードが欠けてる為、そのコードを追加してあげないといけません。

★callAppsScriptについて

ここがGoogle Apps Script Execution APIを叩くメインのスクリプトです。今回は引数なしでomikujiという関数を叩き、res変数でreturnされてきた値を受け取っています。その後失敗した場合のエラー処理が入っています。

最終行にあるmainWindow.webContents.send(‘message’, unsei ); がHTML側へipc通信で通知し、HTML側はこれを受け取ったらalert表示するようになっています。

index.html

今回のアプリケーションはおみくじアプリですので、ボタンが1個あるだけです。よって非常にシンプルなボタンと、押した時にNode.js側に問い合わせと返り値をダイアログで表示する簡単なプログラムが入ってるだけです。

ソースコード

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <script>
      var $ = jQuery = require("jquery")
    </script>
    <script type="text/javascript" src="js/jquery-ui.min.js"></script>
    <link rel="stylesheet" href="css/jquery-ui.css"></link>
    <!--  https://github.com/electron/electron/issues/4420 -->
    <style>
        ::selection {
            background: #fff;
        }
    </style>
    <script src="index.js"></script>
    <script>
      //ダイアログ表示用
      $(function() {
        $( "#dialog" ).dialog({
      autoOpen: false,
      width:400,
      height: 270,
      title: "OAuth2認証の実行",
      modal: true,
      show: {
        effect: "explode",
        duration: 500
      },
      hide: {
        effect: "explode",
        duration: 500
      }
        });
      });
      // IPC通信を行う
      var ipcRenderer = require( 'electron' ).ipcRenderer;
      window.onload = function () {
        testAsync();
      };
       
      // 非同期に通信を行う
      function testAsync() {
        // 非同期通信の結果を受けたときのコールバック
        ipcRenderer.on('async-reply', function(event,arg) {
          alert(arg);
        });
         
        //authURLを受け取り、inputboxで入力させる
        ipcRenderer.on('async-url', function(event,arg) {
          var html = "<b><a href='" + arg + "' target='_blank'>こちらをクリック</a></b>して、認証用コードを取得し、送信して下さい。"
          document.getElementById("authman").innerHTML = html;
          $( "#dialog" ).dialog( "open" );
          $( "#dialog" ).dialog("moveToTop");
        });
         
        //各種雑多なメッセージを受け取る
        ipcRenderer.on('message', function(event,arg) {
          alert(arg);
        });
         
        // メインプロセスに引数を送信
        ipcRenderer.send('async', "");
      }
      
      function gcodeinput(){
        var values = document.getElementById("inputman").value;
        $( "#dialog" ).dialog( "close" );
        //validation処理
        if(values == ""){
          alert("コードが空ですよ");
          return;
        }
        
        // メインプロセスに引数を送信
        ipcRenderer.send('async', values);
      }
      
      function reomikuji(){
        // メインプロセスに引数を送信
        ipcRenderer.send('async', "exec");
      }
        
    </script>

    <title>Electron おみくじ</title>
  </head>

  <body>
    <div align="center">
      <h1 id="header">おみくじひこうぜ!</h1>
      <button type="button" name="aaa" value="aaa" class="action" onclick="reomikuji();">
        <img src="https://officeforest.org/wp/library/img/syougatsu2_omijikuji2.png" width="48" height="48"><br>
        <font size="1">おみくじをひく</font>
      </button>
    </div>
     
    <!-- コード送信ダイアログ -->
    <div id="dialog" title="Basic dialog">
      <p id="authman"></p>
      <center>
        <p><input type="text" name="name" size="40" maxlength="100" id="inputman"></p>
        <p><button type="submit" class="share" value="送信" title="コード送信" width="200px" style="font-size: 14px;vertical-align: middle" onclick="gcodeinput();">
        <img src='https://officeforest.org/wp/library/icons/lock.png' />送信する</button></p>
      </center>
    </div>
   
  </body>
</html>

ポイント

★冒頭部分

Node.jsのメインファイルであるindex.jsを呼び出すために、<script src=”index.js”></script>として追記しています。また、jQueryUIやCSS関係の呼び出しの他に、script.jsを呼び出していますが、この中身は、var $ = jQuery = require(“./js/jquery-2.1.4.js”);のみです。jQuery本体だけは、このような特殊な呼び出し方をしないと動作してくれないので、このような書き方になっています。

★IPC通信を行う

Electronでは、HTML側でもNode.js側のようにモジュールをrequireする事が可能です。ここで、Node.js側でも使ってるipcモジュールをrequireしてipcRenderer.onでNode.js側からの通信を待機させる事が可能です。逆に送る時は、ipcRenderer.sendに名前を付けて上げるとNode.js側のipcMainが受け取ります。

★その他

最初の1回、認証を実行し認証コードを入れさせるためのダイアログを表示するようにしています。その部分が一番したのコード送信ダイアログの部分。jQueryにてダイアログで表示する時のコードが入っています。また、Node.js側から送られてくる認証用URLを動的に<p id=”authman”></p>に書込も行っています。

実行と結果

プログラムを実行

コードの記述が完了したらテスト実行していましょう。以下のコマンドで実行する事ができます。今回のプロジェクトフォルダはprojectmanなので、そのフォルダの中にまではいかずにホームフォルダ直下の状態で実行します。ターミナルやコマンドプロンプトを起動してすぐ実行してみましょう。

//プロジェクトフォルダに対して実行
electron projectman/

無事に問題がなければ、Electronアプリとして起動します。但し、本コードの場合、最初の1回は、OAuth2認証作業があるため、以下の作業を要求されます。

  1. OAuth2認証作業ダイアログが起動し、URLをクリックするよう要求されるので、クリックする
  2. 自分のGAアカウントでログインをすると、認証用のコードが発行されるので、コピーする
  3. ダイアログのテキストボックスにそれをペーストして、送信ボタンを押す
  4. 問題がなければ、これで続けてExecution APIが実行されて、おみくじが返って来る
  5. index.jsと同じフォルダにアクセストークンファイル(token.json)が格納される。
  6. 次回以降はそのまま起動するだけです。

図:無事に起動しました

Cannot read property 'on' of undefined

コードは正しく動いているのに、app.onでエラーが発生します。とくに弊害は出ておらず、普通に初期化も出来ているのですが、エラーとしてデバッガーのコンソールに残ります。

Cannot find module 'app'

以前のElectronと違い、最新版ではいくつか変更が加わっています。とくにappモジュールはrequireで導入する形ではなくなっているので、そのまま昔の流儀で書くとエラーが生じます。

Error: Cannot find module 'app'
    at Module._resolveFilename (internal/modules/cjs/loader.js:584:15)
    at Function.Module._resolveFilename (/usr/local/lib/node_modules/electron/dist/Electron.app/Contents/Resources/electron.asar/common/reset-search-paths.js:43:12)
    at Function.Module._load (internal/modules/cjs/loader.js:510:25)
    at Module.require (internal/modules/cjs/loader.js:640:17)
    at require (internal/modules/cjs/helpers.js:20:18)
    at Object.<anonymous> (/Users/xxxx/Documents/projectman/index.js:9:11)
    at Object.<anonymous> (/Users/xxxx/Documents/projectman/index.js:167:3)
    at Module._compile (internal/modules/cjs/loader.js:693:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:704:10)
    at Module.load (internal/modules/cjs/loader.js:602:32)

Electron Security Warning (Insecure Content-Security-Policy)

デベロッパーツールでデバッグをしているといつも表示される項目があります。Cordovaなどでも見かける「コンテンツセキュリティポリシー」に関するWarningです。これはアプリをパッケージ化すると表示されなくなるとのこと。とくにそのままでも問題はないので、気にせず。

Electron Security Warning (Insecure Content-Security-Policy) This renderer process has either no Content Security
    Policy set or a policy with "unsafe-eval" enabled. This exposes users of
    this app to unnecessary security risks.
 
For more information and help, consult
https://electronjs.org/docs/tutorial/security.
 This warning will not show up
once the app is packaged.

Couldn't set selectedTextBackgroundColor from default

以前からあるバグで、デバッグツールのconsoleには出ないWarningです。ターミナルのコンソール上に表示されます。とくに弊害はないので、そのままスルーしてます。

スクリプト実行の返り値

おみくじ実行時に、callAppsScript関数を実行され、返り値がGoogle Apps Scriptから帰ってきます。其の内容はJSONで帰ってきますが、膨大な情報が含まれています。其の中からres.data.response.resultとして取り出し、ダイアログで表示するようにしています。返り値は以下のような感じです。

{ status: 200,
  statusText: 'OK',
  headers:
   { 'content-type': 'application/json; charset=UTF-8',
     vary: 'Origin, X-Origin, Referer',
     date: 'Sun, 27 Jan 2019 01:14:41 GMT',
     server: 'ESF',
     'cache-control': 'private',
     'x-xss-protection': '1; mode=block',
     'x-frame-options': 'SAMEORIGIN',
     'x-content-type-options': 'nosniff',
     'alt-svc': 'quic=":443"; ma=2592000; v="44,43,39"',
     connection: 'close',
     'transfer-encoding': 'chunked' },
  config:
   { adapter: [Function: httpAdapter],
     transformRequest: { '0': [Function: transformRequest] },
     transformResponse: { '0': [Function: transformResponse] },
     timeout: 0,
     xsrfCookieName: 'XSRF-TOKEN',
     xsrfHeaderName: 'X-XSRF-TOKEN',
     maxContentLength: 2147483648,
     validateStatus: [Function],
     headers:
      { Accept: 'application/json, text/plain, */*',
        'Content-Type': 'application/json;charset=utf-8',
        'Accept-Encoding': 'gzip',
        'User-Agent': 'google-api-nodejs-client/0.4.0 (gzip)',
        Authorization:
         'Bearer ここにアクセストークンが表示される’,
        'Content-Length': 22 },
     method: 'post',
     url:
      'https://script.googleapis.com/v1/scripts/ここにAPPIDが表示される:run',
     paramsSerializer: [Function],
     data: '{"function”:”ここに実行する関数が表示される”}’,
     params: {} },
  request: 
  
  ・・・・中略・・・・

  data:
   { done: true,
     response:
      { '@type':
         'type.googleapis.com/google.apps.script.v1.ExecutionResponse',
        result: ‘大吉’ } } }

注意事項

  • OAuth2.0の最初の認証はGoogle Appsのアカウントを持つ人間が認証を実行し、token.jsonを取得しなければなりません。
  • 今回のElectronアプリはrefresh_tokenの処理がない為、一定時間を経過するとOAuthトークンがexpireしてしまいます。その為、その都度、script-nodejs-quickstart.jsonを廃棄する必要性があります。
  • script-nodejs-quickstart.jsonは、index.jsと同じフォルダに生成されますが、本質的にはどこか別の場所に配置しておいたほうが尚良いです。ただしその場合、ディレクトリやファイル生成の権限がないとエラーになりますので、注意が必要です。
  • ElectronはGUIアプリケーションですので、認証時取得コードやURLの表示はコマンドプロンプトに出力しても意味がありませんし、readline.questionが使えない為、取得コードを入力ができません。その為、jQuery DialogでURLの表示と入力を促す処理を追加しています。
  • Node.js側とHTML側との通信には、ipcを使用しています。よって、Node.js側でのmainWindow作成時オプションとして、nodeIntegration:falseといったオプションを指定してしまうと、使えなくなってしまいます。
  • 今回はDialog生成にjQueryを使用していますがそのままでは、Electronアプリでは利用ができません。そこで、var $ = jQuery = require(“./js/jquery-2.1.4.min.js”);といった形でライブラリを読みこませる必要があります。jQuery UIやCSS類はその必要性はありません。
  • OSXの場合、メニューバーの非表示はできません。また、Windowsの場合、デフォルトでメニューバーが表示されているので、これをmainWindow作成時オプションとして、autoHideMenuBar:trueを追加する事で非表示にする事が出来ます。
  • testAsync()は起動時に1回だけ実行させるだけでOKです。あとは待機しています。
  • スプレッドシートが非公開の場合、APIが実行できなくなります。また、実行可能APIの実行可能権限が自分だけの場合、そのGAアカウントで認証実行しないとやはり、APIは実行できません。

関連リンク

コメントを残す

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

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