はじめに
今年もアドベントカレンダーはじまりました!
この記事はHamee Advent Calendar 2017の1日目の記事です。
毎年この時期になると「アドカレやろうぜ!」と誘いまくって嫌な顔をされるアドカレおじさんです。
Hameeのエンジニア(一部デザイナー)が自分の興味あるテーマについて書いていきます。
Electronとは
wikiによると
Electronは、GitHubが開発したオープンソースのソフトウェアフレームワークである。
ChromiumとNode.jsを使っており、HTML、CSS、JavaScriptのようなWeb技術で、macOS、Windows、Linuxに対応したデスクトップアプリケーションをつくることができる。
Atom、Slack、Visual Studio Codeなどで使用されている。
簡単に言うとフロントの技術でデスクトップアプリが作れるツール、という感じです
javascriptの知識があればある程度簡単にアプリが作れちゃいます!
背景と動機
普段困ったことがあるとChrome拡張でサクッと作って解決することが多いんですが、
「google calendarで管理しているミーティングルームに関して、毎回予定が空いてるか見るのが手間」
という課題を解決したいと思った時にデスクトップの常駐アプリがいいなぁと思って
「Mac デスクトップアプリ 作り方」で検索した結果Electronに行き着きました
今回作るもの
今回はElectronを使ってGoogle Calendarで管理しているミーティングルームの予定を見ることができるデスクトップを作りたいと思います。
ElectronとGoogle Calendar APIの知識が必要となります。
手順
筆者は普段Macで開発してるのでその前提で書きます
各種ツールのインストール
node.jsのインストール
$ brew install node
Electronのインストール
※ npmはnode.jsのパッケージ管理ツール(railsのbundler的な)
$ npm -g install electron-prebuilt
アプリケーション化するためのツールのインストール
$ npm i electron-packager -g
たった3コマンドで必要なツールが揃っちゃいます
package.json
プロダクト用のディレクトリを切り
$ npm init -y
を実行するとpackage.json
ができあがります
平たく言うとこのプロダクトのマニフェストファイル、設定ファイルみたいなものです
デフォルトではindex.jsが設定されますがmain.jsに変更しています
また、今回はgoogle calendar apiを使うので以下コマンドを実行し必要なライブラリを入れておきます
(ついでに個人的に扱いやすいのでjqueryを入れておく)
$ npm install googleapis --save
$ npm install google-auth-library --save
$ npm install --save jquery
{
"name": "meeting_room_app",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "Yamamoto Hiroya",
"license": "MIT",
"dependencies": {
"google-auth-library": "^0.11.0",
"googleapis": "^22.2.0",
"jquery": "^3.2.1"
}
}
package-lock.json
とnode_modules
も生成されます
node_modules
はライブラリのファイルなのでgit ignore
推奨
main.js
とindex.html
次にmain.js
とindex.html
を作成します
こちらの記事からお借りしました
'use strict';
// Electronのモジュール
const electron = require("electron");
// アプリケーションをコントロールするモジュール
const app = electron.app;
// ウィンドウを作成するモジュール
const BrowserWindow = electron.BrowserWindow;
// メインウィンドウはGCされないようにグローバル宣言
let mainWindow;
// 全てのウィンドウが閉じたら終了
app.on('window-all-closed', function() {
if (process.platform != 'darwin') {
app.quit();
}
});
// Electronの初期化完了後に実行
app.on('ready', function() {
// メイン画面の表示。ウィンドウの幅、高さを指定できる
mainWindow = new BrowserWindow({width: 800, height: 600});
mainWindow.loadURL('file://' + __dirname + '/index.html');
// ウィンドウが閉じられたらアプリも終了
mainWindow.on('closed', function() {
mainWindow = null;
});
});
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Sample</title>
</head>
<body>
<p>Hello World</p>
</body>
</html>
アプリケーションの実行
上記までできたらとりあえずHelloできるアプリを作成してみます
実行
$ electron main.js
Hello Worldの画面が出たら成功です
アプリ化
$ cd ../
$ electron-packager meeting_room_app meeting_room_app --platform=darwin --arch=x64 --electronVersion=1.4.13
各引数は以下の通り
- ソースディレクトリ
- アプリ名
- 対象プラットフォーム(darwin=Mac)
- 対象アーキ(32bit or 64bit)
- Electronのバージョン(electron -vで確認)
これによりmeeting_room_app-darwin-x64
が生成されます
このディレクトリ内のmeeting_room_app.app
を実行するとアプリが起動できます
これにより配布が可能となります
Google Calendar API
ここからカレンダーAPIを叩いていきます
API利用手順は以下の記事が参考になりました。
GoogleカレンダーAPIとElectron
基本的には以下のquickstart.js
を元に作っていきます
https://developers.google.com/google-apps/calendar/quickstart/nodejs
これをmain.jsにコピペ
-
Google API console
にアクセスし新規プロジェクトを作成する -
client_id
とclient_secret
を発行 -
google calendar API
を利用許可 -
clinet_secret.json
を画面からダウンロード -
client_secret.json
をアプリケーションルートに設置してnode main.js
を実行 - URLが表示されるのでそれにアクセスし認証コードを取得
- 認証コードをコンソールにペーストするとアクセストークンの情報が入ったjsonファイルを出力する
- このファイルがあればAPIを実行できる
アクセストークンの入ったjsonファイルはTOKEN_DIR
の場所に出力されます(大体ホームに生成されます)
このあたりの読み込みファイルのパスは都合に合わせて調整してください
構造
これでひと通り役者が出揃いましたので一旦treeの結果を載せておきます
.
├── calendar-nodejs-quickstart.json
├── client_secret.json
├── index.html
├── main.js
├── node_modules
├── package-lock.json
└── package.json
中身実装
ここまで来てしまえばあとは純粋な実装のみです
main.js内のlistEvents
とindex.html
を自分の作りたいものに変えていきましょう
以下に実装した部分のみピックアップして載せます
function listEvents(auth) {
// マルセイユとかは弊社の会議室名
sendCalendarEvent(auth, 'カレンダーID', 'marseille');
sendCalendarEvent(auth, 'カレンダーID', 'hongkong');
}
/**
* @param {google.auth.OAuth2} auth An authorized OAuth2 client.
* @param {string} 取得対象のカレンダーID
* @param {string} 送信するイベント名
*/
function sendCalendarEvent(auth, calendar_id, event_name){
var calendar = google.calendar('v3');
// 取得範囲は+0~1分とする
// NOTE: APIにminとmax同時刻で入れたら現在のイベントが取得できなかったため1分間の幅をもたせている
var timeMax = new Date();
var timeMin = new Date();
timeMax.setMinutes(timeMax.getMinutes() + 1);
timeMin.setMinutes(timeMin.getMinutes() + 0);
calendar.events.list({
auth: auth,
calendarId: calendar_id,
timeMax: timeMax.toISOString(),
timeMin: timeMin.toISOString(),
maxResults: 1,
singleEvents: true,
orderBy: 'startTime'
}, function(err, response) {
if (err) {
console.log('The API returned an error: ' + err);
return;
}
var events = response.items;
if (events.length == 0) {
//console.log(event_name+': イベントなし');
mainWindow.webContents.send(event_name, 'イベントなし');
} else {
//console.log(event_name+': カレンダーのイベントの取得に成功');
mainWindow.webContents.send(event_name, events[0]);
}
});
}
/**
* ipc処理 非同期
*
* @param {async}
*/
const ipcMain = require('electron').ipcMain;
ipcMain.on('async', function( event, args ){
//シークレット取得
fs.readFile(__dirname + '/client_secret.json', function processClientSecrets(err, content) {
if (err) {
console.log('Error loading client secret file: ' + err);
return;
}
// Google Calendar API の認証処理へ
authorize(JSON.parse(content), listEvents);
});
});
viewは全部だと長くなるのでscript部分のみ
<script>
// レンダラプロセスでjQueryとElectron APIを使えるようにする
// requireの代わりにnodeRequireを使用する
// https://www.kabanoki.net/1156
window.nodeRequire = require;
delete window.require;
delete window.exports;
delete window.module;
</script>
<script type="text/javascript" src="http://ajax.microsoft.com/ajax/jquery/jquery-3.2.1.js"></script>
<script>
var ipcRenderer = nodeRequire( 'electron' ).ipcRenderer;
// 非同期(カレンダー分)
// 以下は弊社の会議室名
var meeting_rooms = ['marseille', 'hongkong', 'sanfrancisco', 'liverpool', 'copenhagen', 'casablanca', 'mumbai', 'dubai'];
$.each(meeting_rooms, function(index, value){
// IPC通信(プロセス間通信)を行う
ipcRenderer = nodeRequire( 'electron' ).ipcRenderer;
ipcRenderer.on(value, function(event, calendar_event) {
$('#' + value + '> p').empty();
if(typeof(calendar_event.summary) !== 'undefined'){
$('#' + value + '> p.event_name').append(calendar_event.summary);
}
if(typeof(calendar_event.organizer) !== 'undefined' && typeof(calendar_event.organizer.email) !== 'undefined'){
var organizer = calendar_event.organizer.email.replace(/@.*$/, '');
$('#' + value + '> p.organizer').append(organizer);
}
if(typeof(calendar_event.start) !== 'undefined' && typeof(calendar_event.end) !== 'undefined'){
var start_hour = ('0'+new Date(calendar_event.start.dateTime).getHours()).slice(-2);
var start_minu = ('0'+new Date(calendar_event.start.dateTime).getMinutes()).slice(-2);
var end_hour = ('0'+new Date(calendar_event.end.dateTime).getHours()).slice(-2);
var end_minu = ('0'+new Date(calendar_event.end.dateTime).getMinutes()).slice(-2);
$('#' + value + '> p.time').append(start_hour+':'+start_minu+'~'+end_hour+':'+end_minu);
}
});
});
// カレンダーデータを10分間隔で定期的に取得
function getData(){
console.log('reload');
ipcRenderer.send('async', 0);
}
setInterval(getData, 600000);
</script>
画面イメージ
実際にはhtmlのDOM要素とcssを作成していますが割愛しました
githubにソースあげようと思いましたがclient_secretなどのアクセス情報をgitに入れてしまっていたため断念(◞‸◟)
今後はアクセス情報をgitに入れないようにしよう…反省
(配置は実際の会議室の並びを元に作ってるのでこんな形になってます)
ハマりポイント(ipc)
基本的な流れは参考記事や公式ドキュメント等でカバーできたのですが、ipcだけはちょっとハマりました
プロセス間通信(IPC、英: interprocess communication)はコンピュータの動作において複数のプロセス間(の複数のスレッド間)でデータをやりとりするための仕組み。
Electron製のアプリにはJavaScript側のプロセス(メインプロセス)と、メインプロセスから立ち上げるBrowserWindowのプロセス(レンダラプロセス)の2種類があり、これらの間でデータのやりとりをするためには上記のipcの仕組みを使う必要がある
以下の記事が分かりやすかったです
Electronでipcを使ってプロセス間通信を行う
今回のアプリではindex.html
からsetInterval
で一定間隔でipcRenderer.send('async', 0);
を呼んでいます
これが実行されるとレンダラプロセスからメインプロセスのipcMain.on('async', function(event, args ){
が呼ばれます
次にメインプロセスで任意の処理をしレンダラプロセスにデータを返すためにmainWindow.webContents.send(event_name, events[0]);
を呼びます
これによりレンダラプロセス側のipcRenderer.on(value, function(event, calendar_event) {
がキックされAPIで取得したデータをDOM要素に反映できる、という仕組みになっています
理解するのにやや時間がかかった…
参考
以下の記事が大変参考になりました
30分で出来る、JavaScript (Electron) でデスクトップアプリを作って配布するまで
Electronでipcを使ってプロセス間通信を行う
GoogleカレンダーAPIとElectron
Node.js Quickstart
ElectronのRenderer Process(index.html)でrequire(‘electron’)がundefinedになる
おわりに
いかがでしたでしょうか?
上記のipcの他にもjqueryとelectronの共存どうすんの?とかでちょっとハマりましたが参考記事等で回避できました。
こういうハマりが最近減ってきた気がするのでエンジニアとして少しは成長できてるのかな?と感じます。
Electron
を使って手軽にデスクトップアプリが作れそうなイメージが湧いてくれると嬉しいです!
アドカレも3年目になりましたが毎年社員だけでカレンダーが1枚埋まるのはありがたいことです。
人も増えてきたし来年あたりは2枚埋めたい気持ちも…まだ分かりませんが
この記事でElectronとHameeに少しでも興味を持ってくれたら嬉しいです!w