ElectronでMySQLへ接続するアプリを作る
個人利用のアプリであるならば、データの保管場所はsqliteなどで良いかと思います。しかし、複数名共有して使うデータベースの場合には、やはりMySQLなどのDBサーバが必要になります。中央集権なこのDBがあることで、様々な社内ツールをElectronで実現する事ができます。
相手のサーバにWeb Serverがなくとも、Electron側がそれを担うので、非常に単純な構成でちょっとしたウェブアプリケーション的なクラサバ構築できるのは魅力的です。VBA卒業して挑戦するならElectronは良い選択肢だと思います。ウェブアプリと同じUI表現が利用できますし。
今回は、MySQL接続に加えて、DB接続パスワードなどを安全に格納できるように、keytarモジュールを利用してOS標準のキーストアを使ったセキュリティ対策も実施しておこうと思います。
図:今回作るアプリはこんな感じです
目次
今回使用するモジュール他
Node.jsモジュール
- keytarモジュール - OS標準のパスワード管理システムを利用して、安全にパスワード等のやり取りをする
- electron-store - 各種設定情報を格納する為のモジュール
- Promise-MySQLモジュール - Promiseが使えるMySQLへアクセスする為のモジュール
- jQueryモジュール - ElectronでjQueryを使えるようにする為のモジュール
- node-gyp - keytarモジュールをリビルドする為に使用する
HTML側で利用するライブラリ
- jQuery UIライブラリ - プロジェクトファイルに同梱
- jQuery Shapeshiftライブラリ - プロジェクトファイルに同梱
使用するDBファイルとプロジェクトファイル
また、今回のサンプルアプリで用いたファイル類は以下からダウンロードできます。今回はローカル仮想環境内のMySQLを利用しましたが、Google Cloud SQLへ接続して利用も可能です。Cloud SQLの場合一旦、Cloud Storageにバケットを作成して、SQLファイルを配置しなければ、インポートが出来ないので注意(GB/月あたりの価格: $0.023)。なぜか1回目はインポート失敗するけれど、2回目は成功します。
- useridテーブルのSQLファイル - MySQLでインポートして利用する
- seatテーブルのSQLファイル - MySQLでインポートして利用する
- 座席表アプリプロジェクトファイル - package.jsonやnode_modulesは含んでいません
図:Cloud StorageのバケットからSQLファイルインポート中
図:Cloud SQLにインポートしてみた
事前準備
モジュールを追加する
まずは、プロジェクトファイル作成、package.json作成、index.htmlおよびindex.jsを作成して置きます。ターミナルを起動して以下のコマンドで今回利用する予定のモジュールを入れておきましょう。
1 2 3 4 |
npm install promise-mysql --save npm install keytar npm install electron-store npm install jquery |
promise、promise-mysql、keytarの3つを追加しました。node-gypは最新版からは標準搭載されているので、別途導入不要です。
また、モジュールを追加時に以下のようなエラーに遭遇した場合には、いちどnpm cache clean --forceを実施してキャッシュをお掃除してから再度チャレンジすると良いでしょう。(Unexpected end of JSON input while parsing near)
1 2 3 4 5 |
C:\Users\hiropon>npm i -g electron@3.0.0 npm ERR! Unexpected end of JSON input while parsing near '...Wes1LfeY2u6ougn36sf9V' npm ERR! A complete log of this run can be found in: npm ERR! C:\Users\hiropon\AppData\Roaming\npm-cache\_logs\-debug.log |
プロキシーに阻まれて追加できない時
社内でnpmでモジュールを追加する時に、プロキシーに阻まれて(getaddrinfo ENOTFOUNDというエラーがでます)追加できない場合があります。その場合には以下の手順でnpmに対してProxyのセッティングを通しておくと追加が可能です。アドレスはお使いのプロキシーのURLを入力します。
- npm config set proxy http://proxy.example.com:8080 を実行する
- npm config set https-proxy http://proxy.example.com:8080 を実行する
- npm config set registry https://registry.npmjs.org/ を実行する
また、3.にてhttpで追加をしてしまうと、418 I'm a teapot:エラーが出てしまいますので注意が必要です。
keytarについて
最新の環境でのKeytar対応について
2021年3月現在、keytarがネイティブ対応して、以下のようなとっても面倒なkeytarのリビルドが不要になりました。普通にnpm i keytarでインストールすることでリビルドする事なく、そのまま利用可能になっています。
keytarの概要と使い方
現代のOSには、OS標準のパスワード管理の為のセキュリティシステムが用意されています。macOSで言えば「keychain」、Linuxならば「libsecret」や「Gnome Keyring」、Windowsですと「Credential Vault(資格情報マネージャ)」がそれになります、keytarはElectronでセンシティブな情報を扱う場合に、ローカルストレージではなく、こうした管理システムへの情報の登録と呼び出しをサポートする為のモジュールです。
接続パスワードの保管や、OAuth2認証で使うClient_IDとSecretの格納、Access_TokenやRefresh_Tokenの格納などを安全に格納する事が可能で、この手のアプリケーションを作る上では必須とも言える機能ですね。
メソッドもえらくシンプルで
1 2 |
keytar.addPassword('サービス名', 'アカウント名', 'パスワード') keytar.getPassword('サービス名', 'アカウント名') |
が基本。他に削除、検索のコマンドがあります。但しパスワードの取得だけは少しだけコードが異なります。非同期処理なので、注意してください。
1 2 3 4 |
const secret = keytar.getPassword(servicename,account); secret.then((result) => { pass = result; }); |
もしくはIPC通信を利用して
1 2 3 4 5 6 7 |
//メインプロセス側 ipcMain.on('get-password', (event, user) => { event.returnValue = keytar.getPassword('ServiceName', user); }); //レンダラプロセス側 const password = ipcRenderer.sendSync('get-password', user); |
といった取り出し方をすると良いでしょう。
Linuxに於けるKeytarの問題点
まだ、Linux上でElectronのビルドを試してはいないのですが、少なくともNode.jsアプリとして、Ubuntu 18.04上でkeytarモジュールを動かす為には、以下のライブラリをインストールしておかなければなりません。
1 2 |
//libsecretと依存ライブラリのインストール sudo apt install libssl-dev libsodium-dev libsecret-1-dev |
macOSに於けるkeytarの問題点
また、環境によってはそのままだと動かないことがあるので、keytarモジュールをリビルドする必要があります。表示されるエラーは以下のような感じ。electron v4.0.3, keytar 4.3.1でテストしています。
1 2 3 4 5 6 7 |
dyld: lazy symbol binding failed: Symbol not found: __ZN2v816FunctionTemplate3NewEPNS_7IsolateEPFvRKNS_20FunctionCallbackInfoINS_5ValueEEEENS_5LocalIS4_EENSA_INS_9SignatureEEEiNS_19ConstructorBehaviorENS_14SideEffectTypeE Referenced from: /[... route to project folder ...]/node_modules/keytar/build/Release/keytar.node Expected in: flat namespace dyld: Symbol not found: __ZN2v816FunctionTemplate3NewEPNS_7IsolateEPFvRKNS_20FunctionCallbackInfoINS_5ValueEEEENS_5LocalIS4_EENSA_INS_9SignatureEEEiNS_19ConstructorBehaviorENS_14SideEffectTypeE Referenced from: /[... route to project folder ...]/node_modules/keytar/build/Release/keytar.node Expected in: flat namespace |
この問題は、素で入れたkeytarがelectronのバージョンに合っていない為に起こる問題で以下のような形でリビルドをすると、osxであれば無事にキーチェーンに資格情報が格納されます。
- electronのバージョンを、electron -vで調べておく
- 使用している環境が32bit環境下?64bit環境かしらべておく。通常今どきは64bitだと思うので、x64だと思います。
- ターミナルを立ち上げて、プロジェクトフォルダの中のnode-modulesフォルダ、さらに其の中のkeytarフォルダに入ります。
- 以下のコマンドを実行する(electronのバージョンは4.0.2で、アーキテクチャはx64で指定しています。)
1 2 |
//keytarをリビルドするコマンド HOME=~/.electron-gyp node-gyp rebuild --target=4.0.2 --arch=x64 --dist-url=https://atom.io/download/atom-shell |
electronで実際にリファレンスに従い、setPasswordをしてみたところキーチェーンに無事に資格情報が格納されました。getPasswordで取り出す事が可能です。
図:keytarをリビルド中
図:zasekiというサービス名にアカウント名とパスワードが格納される
Windowsでkeytarを使う場合
macOSの場合は前項のようにkeytarモジュールをリビルドするだけで利用する事ができました。しかし、問題はWindows。そもそもリビルドするにもビルド環境が必要だったり、リビルドしても動かずに停止してしまったりと、問題解決が非常に大変でした。すごく良いモジュールなのに、この問題が残念ですね。。。ネイティブモジュール特有の問題です。
2020年3月、electron 5.0.0とkeytar4.6.0でも成功したので、Windowsでビルドする場合には、以下の記事を参照してください。
以下の記事は古いバージョンでビルドした記録として残しておきます。
結果的には、electronはv3.0.0、keytarは4.2.1を利用し、electron-rebuildを使ってのリビルドで使えるようにはなりました。以下に使えるようにする為の手順を残しておきます。参考になったのは、こちらのサイトと、issue議論のサイト、issue議論サイト2、node-gypインストール方法サイトでした。また、node-gypについてはpython 2.7系に対応している為、3.x系が入っていると具合が悪いです。また、以下の作業は「管理者権限」で行うようにしましょう。
- electronは一旦、npm uninstall -g electronでアンインストールする
- keytarも一旦、プロジェクトフォルダに入ってから、npm uninstall keytarでアンインストールする(Windowsでは、keytar v4.3.0は問題有り)
- npm install -g electron@3.0.0でバージョン指定でインストール
- keytarをインストールする前に、管理者権限のPowerShellにてnpm install -g windows-build-toolsを実行して、インストールする(結構時間が掛かる)
- Visual C++ Buildtoolsをインストールする(Visual Studio 2017相当)。2015だとエラーが出ます。
- npm config set msvs_version 2017コマンドを実行
- npm config listで現在の設定値を確認できます。
- エクスプローラの検索にて、python.exeがある場所を探しだし、npm config set python python.exeのフルパス コマンドを実行する(パスが通って入れば、python2.7でも可)なお、通常はc:¥Users¥ユーザ名¥.windows-build-tools¥python27以下にpython.exeがインストールされています。
- npm install -g node-gypで、node-gypをインストール(但し、Node.js最新のNode.jsは初めからバンドルしてるのでこの作業は不要。入れるとオカシナことになります)。
- 一旦再起動する
- 実はこの時点ではまだnode-gypはエラーになることがあります。正しいnode-gypへのパスが通っていない(間違ったパスが環境変数に登録されている)のです。コントロールパネルのユーザアカウントを開きます
- 左サイドの「環境変数の変更」をクリックします。
- ユーザ環境変数の中にある「path」をクリックし、編集ボタンをクリック。
- 中身をテキストエディタにでもコピーして、npm-lifecycleの文字がある変数の部分だけを削除する
- 続けて、同じ所に「;」で繋げて、「C:\Users\ユーザ名\AppData\Roaming\npm」を加える(node-gypを-gでインストールした場合)。いちばん手間のない方法は、electron-rebuildをインストールし、「C:\Users\ユーザ名\AppData\Roaming\npm\node_modules\electron-rebuild\node_modules\.bin」を指定する事。Windows10ではこれで問題なくリビルドできました。他にもしnode-gypらしきものを参照してる環境変数があったら削除しておく。
- 13.に15.の内容を上書きして保存する。これでnode-gypコマンドが使えるようになります。
- コマンドプロンプトよりnode-gypと打って、Usageなどの使い方に関する項目が表示されればオッケー
- npm install -g electron-rebuildで、electron-rebuildをインストール
- プロジェクトフォルダに入り、npm install keytar@4.2.1を実行して、ネイティブコンパイルインストールする
- この時、node-gypがKeyError '2017'というエラーを吐くことがあります。Windows10 64bitだと問題なかったのですが、Windows8.1で確認。こちらでもissue報告されています。
- keytarディレクトリに入り、node-gyp configureを実行する
- プロジェクトフォルダ直下に戻る
- electron-rebuild -w keytarでリビルドをする。rebuild complateが出れば完了。
- これで、Windowsでもkeytarが使えるようになり、アプリが落ちることがなくなりました。
- 試しにkeytarを使ってパスワードをセットしてみて、該当の設定がWindows資格情報に入っていれば成功!!
図:Windows-Build-Toolsがやけに時間が掛かる
図:node-gypのパスが間違って登録されているので修正が必要
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
C:\Users\googl>node-gyp Usage: node-gyp <command> [options] where <command> is one of: - build - Invokes `msbuild` and builds the module - clean - Removes any generated build files and the "out" dir - configure - Generates MSVC project files for the current module - rebuild - Runs "clean", "configure" and "build" all at once - install - Install node development files for the specified node version. - list - Prints a listing of the currently installed node development files - remove - Removes the node development files for the specified version node-gyp@3.8.0 C:\Users\googl\AppData\Roaming\npm\node_modules\node-gyp node@10.15.3 |
コマンド:node-gypを実行した結果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
C:\Users\googl\Documents\ootebook\node_modules\keytar>node-gyp configure gyp info it worked if it ends with ok gyp info using node-gyp@3.8.0 gyp info using node@10.15.3 | win32 | x64 gyp info spawn C:\Users\googl\.windows-build-tools\python27\python.EXE gyp info spawn args [ 'C:\\Users\\googl\\AppData\\Roaming\\npm\\node_modules\\node-gyp\\gyp\\gyp_main.py', gyp info spawn args 'binding.gyp', gyp info spawn args '-f', gyp info spawn args 'msvs', gyp info spawn args '-G', gyp info spawn args 'msvs_version=2015', gyp info spawn args '-I', gyp info spawn args 'C:\\Users\\googl\\Documents\\ootebook\\node_modules\\keytar\\build\\config.gypi', gyp info spawn args '-I', gyp info spawn args 'C:\\Users\\googl\\AppData\\Roaming\\npm\\node_modules\\node-gyp\\addon.gypi', gyp info spawn args '-I', gyp info spawn args 'C:\\Users\\googl\\.node-gyp\\10.15.3\\include\\node\\common.gypi', gyp info spawn args '-Dlibrary=shared_library', gyp info spawn args '-Dvisibility=default', gyp info spawn args '-Dnode_root_dir=C:\\Users\\googl\\.node-gyp\\10.15.3', gyp info spawn args '-Dnode_gyp_dir=C:\\Users\\googl\\AppData\\Roaming\\npm\\node_modules\\node-gyp', gyp info spawn args '-Dnode_lib_file=C:\\Users\\googl\\.node-gyp\\10.15.3\\<(target_arch)\\node.lib', gyp info spawn args '-Dmodule_root_dir=C:\\Users\\googl\\Documents\\ootebook\\node_modules\\keytar', gyp info spawn args '-Dnode_engine=v8', gyp info spawn args '--depth=.', gyp info spawn args '--no-parallel', gyp info spawn args '--generator-output', gyp info spawn args 'C:\\Users\\googl\\Documents\\ootebook\\node_modules\\keytar\\build', gyp info spawn args '-Goutput_dir=.' ] gyp info ok |
コマンド:node-gyp configure実行して成功した様子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
KeyError: '2017' gyp ERR! configure error gyp ERR! stack Error: `gyp` failed with exit code: 1 gyp ERR! stack at ChildProcess.onCpExit (C:\Program Files\nodejs\node_module s\npm\node_modules\node-gyp\lib\configure.js:345:16) gyp ERR! stack at ChildProcess.emit (events.js:189:13) gyp ERR! stack at Process.ChildProcess._handle.onexit (internal/child_proces s.js:248:12) gyp ERR! System Windows_NT 6.3.9600 gyp ERR! command "C:\\Program Files\\nodejs\\node.exe" "C:\\Program Files\\nodej s\\node_modules\\npm\\node_modules\\node-gyp\\bin\\node-gyp.js" "rebuild" gyp ERR! cwd C:\Users\Hiroyuki\Documents\ootebook\node_modules\keytar gyp ERR! node -v v10.15.3 gyp ERR! node-gyp -v v3.8.0 gyp ERR! not ok npm WARN ootebook@1.0.0 No description npm WARN ootebook@1.0.0 No repository field. npm ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR! keytar@4.2.1 install: `prebuild-install || node-gyp rebuild` npm ERR! Exit status 1 npm ERR! npm ERR! Failed at the keytar@4.2.1 install script. npm ERR! This is probably not a problem with npm. There is likely additional log ging output above. npm ERR! A complete log of this run can be found in: npm ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR! keytar@4.2.1 install: `prebuild-install || no npm ERR! Exit status 1 npm ERR! npm ERR! Failed at the keytar@4.2.1 install script. npm ERR! This is probably not a problem with npm. Ther ging output above. npm ERR! A complete log of this run can be found in: |
コマンド:node-gypでリビルド中の厄介なエラー(Windows8.1で確認)
図:Windowsの資格情報マネージャに登録できた
- ※keytar4.3.0やelectron4.0.3でもelectron-rebuildをすれば使えるようになるかもしれませんが、試していません。とりあえず、動く環境にたどりつけたので良かったです。
- ※keytarを使ったアプリをmacOSとWindowsの両方でリリースする場合は、それぞれに環境を作ってビルドするほうが良いと思います。
- ※64bit Windows上でrebuildした場合、64bit Windows上でしか動作しませんので注意。また、ia32でelectron-packagerでパッケージを作ろうとした場合にも同様のエラーが出ます。
社内で使うと接続が途切れる場合
小さな企業内の場合、せいぜいルーターが1個ある程度の環境なので、普通にサーバを立てれば普通に運用が可能だと思いますが、ある程度の組織の場合、途中に存在するルータ、スイッチその他いろいろなものが入っています。ネットワークの設計上早めにTCPのコネクションを解除するような設定が入っていると、立てたMySQL Serverに他の方が接続できなくなったり、しにくくなったりすることがあります。
そこで、OS側に定期的に生存している旨のパケットを送る設定がありますが、これを少し変更しておくと接続を維持できるようになります。もちろん、OS自体が勝手にスリープしてしまったり、サスペンドしないように設定しておくようにしましょう。今回はUbuntu Linuxをベースに記述します。これらの作業はターミナルとテキストエディタで行います。
logind.confでサスペンドをオフ
1 2 |
//geditでlogind.confを編集 sudo gedit /etc/systemd/logind.conf |
geditが起動したら、以下の項目を新規に追記
1 |
HandleLidSwitch=ignore |
追記したら再起動すればサスペンドしないようになります。
InterfacesにDNS設定
1 2 |
//geditでlogind.confを編集 sudo gedit /etc/network/interfaces |
geditが起動したら以下の項目を新規に追記。すでにあるdns-nameserversはコメントアウト。DNSは社内にあるならそれを指定。
1 |
dns-nameservers 8.8.8.8 |
追記したら再起動するだけです。
nsswitch.confでhostsの設定
1 2 |
//geditでnsswitch.confを編集 sudo gedit /etc/nsswitch.conf |
geditが起動したら以下の項目に編集しなおす。
1 |
hosts: files dns |
追記したら再起動するだけです。
resolv.conf関係にDNSを設定
1 2 |
//geditで編集 sudo gedit /etc/resolvconf/resolv.conf.d/base |
geditが起動したら、以下の項目を追記する
1 |
nameserver 8.8.8.8 |
さらに以下のコマンドで編集を行う
1 2 |
sudo resolvconf -u sudo gedit /etc/resolv.conf |
起動したら既存のdns-nameserversをコメントアウトして、以下の項目を追記
1 |
nameserver 8.8.8.8 |
追記したら再起動するだけです。
sysctl.confでkeepaliveの設定
最後に一番重要な設定。キープアライブの設定とIPv6の設定を行います。UbuntuはIPv6の設定をオフにしておきます。
1 2 |
//geditでsysctl.confを編集する sudo gedit /etc/sysctl.conf |
geditが起動したら以下の項目を追記する
1 2 3 4 5 6 7 8 |
#IPv6の設定を無効にする net.ipv6.conf.all.disable_ipv6 = 1 net.ipv6.conf.default.disable_ipv6 = 1 #キープアライブの設定 net.ipv4.tcp_keepalive_time = 10 net.ipv4.tcp_keepalive_probes = 2 net.ipv4.tcp_keepalive_intvl = 3 |
追記したら以下のコマンドで設定を反映する。sysctl -a | grep keepalive_timeは保存した設定を確認するコマンドです。
1 2 |
sudo sysctl -p sysctl -a | grep keepalive_time |
これで即時にキープアライブとIPv6の設定が反映されます。
仮想環境で構築した場合
仮想環境にMySQLサーバを構築した場合、通常であれば仮想マシン側に固定IPを割り当てて運用するところですが、ネットワークによってはこれが非常に遅く、またブツ切れを招くことがあります。つながったりつながらなかったり・・・
そこで、親機に固定IPを持たせて、仮想マシン側はNAT接続にすると具合がよかったりします。また、その場合親のポートを仮想マシン側へポートフォワーディングさせる設定が必要です。VirtualBoxの場合、NATで接続する場合には簡単に仮想マシン側へポートフォワーディングが可能です。親の3306にアクセスすると、仮想マシン側の3306につながります。
図:ポートフォワーディングで一気にパフォーマンス改善
MySQL側の準備
MySQL Serverのインストール
今回、仮想環境上のLinuxやmacOS上に、MySQL Serverをインストールして開発を行っております。今回はmacOSで以下の手順でインストールしております。以前、XAMPPを使っていた関係で、以前のバージョンのMySQLが残っていたため、トラブルになりつつも、無事にサーバを用意する事ができました。
- macOSでのMySQLはHome Brewを使ってのインストールが一般的ですね。まずはターミナルを起動します。
- brew updateでアップデートをまずは実行
- 続けて、brew install mysqlで本体をインストールします。
- 完了したら、mysql_secure_installationコマンドでセットアップを開始するのですが、今回エラーが出ました。「Can't connect to local MySQL server through socket '/tmp/mysql.sock'」
- sudo touch /tmp/mysql.sockでソケットファイルをつくってあげる
- また、作成したソケットの権限を、sudo chown ユーザ名 /tmp/mysql.sockで変更
- mysql_secure_installationコマンドで、セットアップ開始。rootのパスワード設定他、基本的にはYで回答しておけばオッケー。
- ERROR! The server quit without updating PID fileというエラーが出る場合には、以下のコマンドを実行して、mysqlを一度再インストールすると良いです。
1 2 3 4 |
rm -rf /usr/local/mysql rm -rf /Library/StartupItems/MYSQL rm -rf /Library/PreferencePanes/MySQL.prefPane rm -rf /Library/Receipts/mysql-.pkg |
brew uninstall mysqlでアンインストール可能です。macOSでHomeBrewで入るバージョンは、5.7ではなく8.0系なので注意が必要です。
テーブルの定義
今回は以前、Google Apps Scriptにて作成した座席表アプリを、MySQL + Electronで移植をしてみようと思います。MySQL側では、スプレッドシートで構成したシートレイアウトを元に、予めテーブルを作っておきましょう。自分が用意した構成は以下の通りです。
- MySQLに新たなschemeとして「zaseki」を作成
- zasekiスキーマに対して、「ユーザ表」と「シートデータ」のテーブルを準備
- MySQLにログインするユーザアカウントの作成とパスワードの設定を済ませておく
- ユーザ表はuseridとして定義、シートデータはseatとして定義しました。
- 2つのテーブルには3.で作ったアカウントでのアクセス権限は付与しておきましょう。
図:新しいテーブルスキーマを作成
図:ユーザ表のテーブル定義
図:シート表のテーブル定義
MySQLのTimeoutを設定等
MySQLを動かすUbuntu上のサーバでいろいろとKeepaliveの設定を行っていても、MySQLにはMySQLのコネクションの処理に関する設定があります。こちらも設定しておくべきでしょう。
skip-resolve-name
my.cnfもしくはmysqld.cnfに対して、skip-resolve-nameの設定を追記します。これは固定IPで運用する場合に設定するもので、DNSでの逆引きを利用しない設定です。少しだけ早くなると同時に余計なDNS参照をする事がなくなります。単純にskip-resolve-nameを追記するだけでオッケーです。
ただしこの設定を追加すると、localhostでの接続ができなくなります。127.0.0.1での接続は可能ですが、MySQL側で例えば、root@localhostでアカウントを作っている場合、接続出来ません。root@127.0.0.1で作成している場合には接続が可能です。
wait_timeout
MySQLに接続する時に用いられる、接続してからの維持時間がコレです。デフォルトでは8時間(28800秒)で設定されています。正直これは長すぎるので、短く設定してみたいと思います。
これを300秒とする場合には、my.cnfもしくはmysqld.cnfに対して、wait_timeout=300を追記するだけです。
ソースコード
ログイン画面の作成(setting.html)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
<!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> <script src="js/jquery.touch-punch.min.js"></script> <link rel="stylesheet" href="css/jquery-ui.css"> <link rel="stylesheet" href="css/setting.css"> <script src="index.js"></script> <script> $(function() { $( "input[type=submit], a, button" ) .button() .click(function() { }); }); // IPC通信を行う var ipcRenderer = require( 'electron' ).ipcRenderer; window.onload = function () { //受信レンダラーの準備 testAsync(); }; //メインプロセス側からの非同期に通信を受信待機する(1回だけ実行) function testAsync() { //各種雑多なメッセージを受け取る ipcRenderer.on('msg', function(event,arg) { alert(arg); }); //保存済みデータを受け取る ipcRenderer.on('init',function(event,arg){ //受け取ったデータをHTMLのボックスに反映する var array = arg; //配列データを各項目に入れてあげる document.getElementById("server").value = array[0]; document.getElementById("dbname").value = array[1]; document.getElementById("user").value = array[2]; document.getElementById("pass").value = array[3]; }) //DB設定初期値をもらう ipcRenderer.send('async', "dbname"); } //キャンセル時に設定ウィンドウを閉じる function notsetting(){ ipcRenderer.send('closeset', "setting"); } //セッティング項目を保存する function savesetting(){ //入力値のvalidation var validata = ""; var array = []; //サーバーアドレス validata = document.getElementById("server").value; if(validata == ""){ alert("サーバアドレスが入っていませんよ"); document.getElementById("server").focus(); return; }else{ array.push(validata); } //使用DB名 validata = document.getElementById("dbname").value; if(validata == ""){ alert("使用するDB名が入っていませんよ"); document.getElementById("dbname").focus(); return; }else{ array.push(validata); } //ユーザID validata = document.getElementById("user").value; if(validata == ""){ alert("ユーザIDが入っていませんよ"); document.getElementById("user").focus(); return; }else{ array.push(validata); } //パスワード validata = document.getElementById("pass").value; if(validata == ""){ alert("パスワードが入っていませんよ"); document.getElementById("pass").focus(); return; }else{ array.push(validata); } //メインプロセスに処理を送る ipcRenderer.send('keytar', array); } </script> <title>座席表 Plusのセッティング</title> </head> <body> <!-- セッティング項目を表示 --> <form class="contact_form" action="#" method="post" name="contact_form"> <ul> <li> <h2>DB接続設定</h2> <span class="required_notification">*印は、必須入力項目です</span> </li> <li> <label for="name">サーバーアドレス</label> <input type="text" placeholder="192.168.1.100" style="width:100px" id="server" required /> <span class="form_hint">ポートは3306決め打ちなので入力不要</span> </li> <li> <label for="name">使用DB名:</label> <input type="text" name="dbname" placeholder="database名" style="width:80px" id="dbname" required /> <span class="form_hint">接続するデータベース名を入力</span> </li> <li> <label for="uid">ユーザID:</label> <input type="uid" name="uid" placeholder="" style="width:80px" id="user" required /> <span class="form_hint">DB接続用のユーザIDを入力します。"</span> </li> <li> <label for="name">パスワード:</label> <input type="password" name="password" placeholder="********" style="width:100px" id="pass" required /> <span class="form_hint">DB接続用のパスワードを入力します。"</span> </li> </ul> </form> <p> <center> <button onClick='savesetting()' id="saveman" class="action" title='設定を保存する'>設定保存</button> <button onClick='notsetting()' id="cancelman" class="action" title='キャンセル'>キャンセル</button> </center> </p> </body> </html> |
- メイン画面の子Windowとして呼び出されます。macOSの場合は上からにゅっと出てくるようになるのですが、Windowsの場合はモーダルダイアログとして表示されます。
- サーバアドレス、使用DB名、ユーザID、パスワードを入力し、メインプロセスのkeytarにて情報をOS管理の資格情報システムに登録します。また、起動時には資格情報システムから呼び出してロードします。
- 今回の画面のCSSはこちらのサイトのスタイルシートを利用しています。
メイン画面の作成(index.html)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 |
<!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> <script src="js/jquery.touch-punch.min.js"></script> <script src="js/jquery.shapeshift.js"></script> <link rel="stylesheet" href="css/jquery-ui.css"> <style> .container { border: 0px dashed #CCC; position: relative; line-height: 40px; } .container > div { background: #FFFFFF; position: absolute; height: 40px; width: 60px; border-style: solid; border-color: #5882FA; font-size: 10px; font-weight:bold; text-align:center; display: inline-block; } .container > div:hover{ background-color: #f5ffaa; -webkit-transition: all 0.2s ease; -moz-transition: all 0.2s ease; -o-transition: all 0.2s ease; transition: all 0.2s ease; } .container > div:active{ background-color: #ffc9d7; -webkit-transition: all 0.2s ease; -moz-transition: all 0.2s ease; -o-transition: all 0.2s ease; transition: all 0.2s ease; } .container > div[data-ss-colspan="2"] { width: 130px; background:#d3ffd7;} .container > div[nothing="1"] { visibility:hidden; } .container > div[cabinet="1"] { background:#A4A4A4; color:#fff;} .container > .ss-placeholder-child { background: transparent; border: 1px dashed blue; } /* ダイアログの背景画像変更用 */ .ui-widget-overlay { background: #000 url(images/halloween.png) 10% 100% repeat-x; opacity: .50; background-size: 100% 100%; filter: Alpha(Opacity=70); } .box { float: left; width: 180px; text-align: center; } .box2 { float: left; width: 180px; text-align: center; } .boxContainer { overflow: hidden; width:100%; } /* clearfix */ .boxContainer:before, .boxContainer:after { content: ""; display: table; } .boxContainer:after { clear: both; } /* For IE 6/7 (trigger hasLayout) */ .boxContainer { zoom: 1; } </style> <script src="index.js"></script> <script> //クリック時に確保する一時座席ID var tempseat = ""; //ダイアログ表示用 $(function() { $( "#dialog" ).dialog({ autoOpen: false, width:400, height: 270, title: "ログイン", modal: true, show: { effect: "explode", duration: 500 }, hide: { effect: "explode", duration: 500 } }); }); //登録用ダイアログ表示設定 $(function() { $( "#dialog2" ).dialog({ autoOpen: false, closeText: "保存せずに閉じます", width: 400, height: 300, title: "作業内容", modal: true, show: { effect: "clip", duration: 500 }, hide: { effect: "clip", duration: 500 }, position: { of : 'body', at: 'center', my: 'center' } }); }); $(function() { $( "input[type=submit], a, button" ) .button() .click(function() { }); }); // IPC通信を行う var ipcRenderer = require( 'electron' ).ipcRenderer; window.onload = function () { //受信レンダラーの準備 testAsync(); }; //メインプロセス側からの非同期に通信を受信待機させる(1回だけ) function testAsync() { //各種雑多なメッセージを受け取る ipcRenderer.on('message', function(event,arg) { alert(arg); return; }); //シート確保時 ipcRenderer.on('ongetsheet', function(event,arg) { //メッセージ表示 alert(arg); //シートデータを更新 connectSQL(); return; }); //seatデータを取得してHTMLへ反映する ipcRenderer.on('record',function(event,arg){ //JSONデータを取得する var json = arg; //データを分解 var dlength = json.count; var record = json.recman; //データ生成ルーチンへ渡す onSeat(dlength,record); return; }) //シートの空き具合データを取得する ipcRenderer.on('seatok',function(event,arg){ //JSONデータを取得する var json = arg; //データを分解 var record = json.data; //データ生成ルーチンへ渡す onSuccess(record); }) } //シート状況をチャートに反映するコード function onSeat(dlength,record){ //データをパースする var json = record; var length = dlength; //ループを回してシート状況を反映する for(var i = 0;i<length;i++){ try{ //固定フラグがONのデータの場合スルーする if(json[i].koteiflg == true){ //ユーザ名がからの場合 if(json[i].username == null || json[i].username == ""){ //スルーする }else{ //既定値を反映する document.getElementById(json[i].seatname).innerHTML = json[i].username; //既定値のみ背景色を変更 document.getElementById(json[i].seatname).style.background = "#d3f1ff"; } }else{ //データが空の場合には、スルーし、ある場合には反映する if(json[i].username == "" || json[i].username == null){ //データが空なので空として反映する document.getElementById(json[i].seatname).innerHTML = json[i].username; document.getElementById(json[i].seatname).style.background = ""; continue; }else{ //キープ済み document.getElementById(json[i].seatname).innerHTML = json[i].username; //キープ済席の背景色を変更 document.getElementById(json[i].seatname).style.background = "#f6d3ff"; } } }catch(e){ } } } //シートの空き状況を取得して処理をする関数 function onSuccess(data){ var flag = false; //ユーザ名が空ならばflagはtrueとする if(data[0].username == null || data[0].username == ""){ flag = true; } //返り値を元に処理を分岐 if(flag == true){ //シート確保用のダイアログを表示する //ダイアログ表示 $('#dialog').dialog({ title: "席の確保", close : function(){ } }); $( "#dialog" ).dialog( "open" ); $( "#dialog" ).dialog("moveToTop"); document.getElementById("dialog").focus(); //IEに保存されてるIDを呼び出せるなら呼び出す document.getElementById("wasabi").value = getData("mannum"); }else{ alert("シート取られてしまったようです・・・画面をリロードして、やり直してみてください。"); } } //LocalStorageへのデータの挿入 function setData(key, data){ localStorage.setItem(key, data); } //LocalStorageからのデータの取得 function getData(key){ var ret = localStorage.getItem(key); //null値判定 if(ret == null){ return ""; }else{ return ret; } } //シートをキープする処理 function seatkeep(){ //ダイアログ内のIDを取得する var manid = document.getElementById("wasabi").value; if(manid == ""){ alert("社員IDが入っていませんよ"); document.getElementById("wasabi").focus(); return; }else{ //ダイアログ内のIDをLocalStorageへ記憶する setData("mannum",manid); } //GAS側へシート確保処理をなげる ipcRenderer.send('async', "keepsheet",[manid,tempseat]); //ダイアログを閉じる document.getElementById("wasabi").value = ""; $( "#dialog" ).dialog( "close" ); } //シート確保をキャンセルする関数 function seatcancel(){ //ダイアログを閉じる document.getElementById("wasabi").value = ""; $( "#dialog" ).dialog( "close" ); //メッセージ表示 alert("キャンセルされますた。"); } //座席の解放ボタンを押した時のダイアログ表示用処理 function releaseseat(){ //シート確保用のダイアログを表示する //ダイアログ表示 $('#dialog2').dialog({ title: "席の解放", close : function(){ } }); $( "#dialog2" ).dialog( "open" ); $( "#dialog2" ).dialog("moveToTop"); document.getElementById("dialog2").focus(); //IEに保存されてるIDを呼び出せるなら呼び出す document.getElementById("wasabi2").value = getData("mannum"); } //席の解放ボタンクリック時の処理 function release(){ //ダイアログ内のIDを取得する var manid = document.getElementById("wasabi2").value; if(manid == ""){ alert("社員IDが入っていませんよ"); document.getElementById("wasabi2").focus(); return; }else{ //ダイアログ内のIDをLocalStorageへ記憶する setData("mannum",manid); } //シートリリース処理をメインプロセスへ ipcRenderer.send('async', "releaseseat",[manid,tempseat]); //ダイアログを閉じる document.getElementById("wasabi2").value = ""; $( "#dialog2" ).dialog( "close" ); } //シート確保をキャンセルする関数 function seatcancel2(){ //ダイアログを閉じる document.getElementById("wasabi2").value = ""; $( "#dialog2" ).dialog( "close" ); //メッセージ表示 alert("キャンセルされますた。"); } //現在のシートデータをDBから取得する function connectSQL(){ // メインプロセスに引数を送信 ipcRenderer.send('async', "connect"); } //接続設定ウィンドウを表示する function setman(){ //メインプロセスに引数を送信 ipcRenderer.send('closeset', "open"); } //パネルを反映 function shapeshifter(){ $(".container").shapeshift({ enableDrag: false, enableCrossDrop: false, enableResize: false, minColumns: 3, minHeight:50, gutterX:5, gutterY:5, paddingX:5, paddingY:5 }); //パネルのクリックアクションを設定 $('.ss-active-child').on('click', function(){ //クリックした要素のIDを取得 var id = $(this).attr("id"); //一時変数に座席IDを確保 tempseat = id; //DIVの値を取得 var manname = $(this).html(); //取得したID要素を表示 if(id == undefined){ alert("<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="💁" src="https://s.w.org/images/core/emoji/11/svg/1f481.svg">この席は座る事が出来ません!!"); }else{ //名前が入ってるかどうかをチェック if(manname != ""){ //確保されてるのでユーザ情報ダイアログを表示する alert("この席はすでに【" + manname + "】さんが座ってますよ。"); return; }else{ //スプレッドシートを参照して空いてるかチェック //google.script.run.withSuccessHandler(onSuccess).checkseat(id); ipcRenderer.send('async', "checkman",id); } } }); } //座席データのHTML挿入 $(function(){ $("#zasekin").load("zaseki.html", function() { shapeshifter(); connectSQL(); }); }); </script> <title>座席表 Plus</title> </head> <body> <div> <button onClick='connectSQL()' id="releaseman" class="action" title='席をリリースします。'>再読込</button> <button onClick='setman()' id="setwindow" class="action" title='接続設定を表示'>接続設定</button> <button onClick='releaseseat()' id="releaseman" class="action" title='席をリリースします。'>座席の解放</button> <hr> </div> <!-- 座席用HTMLを呼び出す --> <div style="width:1000px"> <div id="zasekin"></div> </div> <!-- 座席位置設定ダイアログ --> <div id="dialog" title="Basic dialog"> <p> 今日はこの席でお仕事しますか?座る場合は、IDを入れて、キープをクリックしてください。 </p> <div align="center"> <b>あなたのID:</b> <input type="text" id="wasabi" size="10" maxlength="9" placeholder="例:R91001"> </div> <p> <div class='boxContainer'> <div class='box2'> <span><button onClick='seatkeep()' id="button1" style="font-size: 16px;vertical-align: middle" class="ponyo" title='席を確保します。'><img src='images/icon_check2.png' /> キープ</button></span> </div> <div class='box'> <span><button onClick='seatcancel()' id="button2" style="font-size: 16px;vertical-align: middle" class="ponyo" title='キャンセルします。'><img src='images/cross.png' /> キャンセル</button></span> </div> </div> </div> <!-- 座席位置解除送信ダイアログ --> <div id="dialog2" title="Basic dialog"> <p> <img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="🍲" src="https://s.w.org/images/core/emoji/11/svg/1f372.svg">今日はもうお帰りですか?座席を解放する場合は、IDを入れて、リリースをクリックしてください。 </p> <div align="center"> <b>あなたのID:</b> <input type="text" id="wasabi2" size="10" maxlength="6" placeholder="例:R91001"> </div> <p></p> <div class='boxContainer'> <div class='box2'> <span><button onClick='release()' id="button3" style="font-size: 16px;vertical-align: middle" class="ponyo" title='席を解放します。'><img src='images/icon_check2.png' /> リリース</button></span> </div> <div class='box'> <span><button onClick='seatcancel2()' id="button4" style="font-size: 16px;vertical-align: middle" class="ponyo" title='キャンセルします。'><img src='images/cross.png' /> キャンセル</button></span> </div> </div> </div> </body> </html> |
- メインの座席表を表示するレンダラープロセスのコードです。
- 座席確保やリリース時に入力するダイアログでの値はLocalstorageに保存されます。画面表示する際には呼び出されます
- 座席表の基本データはzaseki.htmlに記述してあり、起動時にidがzasekinの場所にロードされます。ロードが完了してから、shapeshifterの適用を行い、またMySQLから現在のシート確保状況データをconnectSQLでロードさせています。
- 座席のレイアウトや確保対象外とするパネルはzaseki.html側を編集します。nothingが1のものは、確保対象外のパネル(非表示になります)を意味します。cabinetはそもそも椅子ではない場所を意味します。data-ss-colspanは横長のパネルで役員席などに適用する為のものです(横に2パネル消費します)
- testsync関数では、メインプロセスからの通信を受け取るipcrenderer.onを待機させる為の関数です。各関数はメインプロセスからプッシュされると自動的に応答して処理がなされます。
- メインプロセス側にデータを送る役目をしてるのが、ipcRenderer.sendでメインプロセス側で受け取ると処理が開始されます。
- 着席中の人をクリックして、例えばSkype for Businessを起動させて直で連絡させる、なんて機能を付けたい場合は、Skype for BusinessのURLスキーマを使って、単純に<a>タグでURLスキーマをリンクとして張ればOK(例:<A href="sip:yamato.takeru@hogehoge.com?call">Skypeで連絡</A>)
図:タグだけで挙動を制御できるのがElectronの利点ですね
Node.js側コード(index.js)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 |
'use strict'; //標準モジュールの宣言 const electron = require('electron'); const { app } = require('electron'); const BrowserWindow = electron.BrowserWindow; var fs = require('fs'); var readline = require('readline'); //Node.js側とHTML側で通信をするモジュール const ipcMain = require('electron').ipcMain; //追加モジュールの宣言 const keytar = require('keytar'); const mysql = require('promise-mysql'); const Store = require('electron-store'); const store = new Store(); // メインウィンドウはグローバル宣言 let mainWindow = null; let setWindow = null; app.on('ready', function() { //とりあえず、キャッシュをクリアしておく electron.session.defaultSession.clearCache(() => {}) // メイン画面の表示。ウィンドウの幅、高さを指定できる mainWindow = new BrowserWindow({ 'width': 1030, 'height': 750, 'autoHideMenuBar':true, //nodeIntegrationを有効にしないとrenderProcessでrequireを使えない。v5.0.0ではデフォルトで廃止 webPreferences: { nodeIntegration: true }, 'resizable':false, 'fullscreenable':false, 'fullscreen':false }); //セッティングウィンドウ setWindow = new BrowserWindow({ 'width': 520, 'height': 400, 'autoHideMenuBar':true, //nodeIntegrationを有効にしないとrenderProcessでrequireを使えない。v5.0.0ではデフォルトで廃止 webPreferences: { nodeIntegration: true }, 'resizable':false, 'fullscreenable':false, 'fullscreen':false, 'alwaysOnTop':true, 'modal':true, 'parent':mainWindow, 'show':false }) //初期ページの表示 mainWindow.loadURL('file://' + __dirname + '/index.html'); //キー管理情報が存在しているかどうかで判定 setWindow.loadURL('file://' + __dirname + '/setting.html'); if(store.get("id") == "undefined"){ setWindow.show(); //idがないので最初から表示 } //デベロッパーツールを有効化 //mainWindow.webContents.openDevTools(); mainWindow.on('closed', function() { mainWindow = null; }); setWindow.on('closed', function() { //キャッシュを捨てる electron.session.defaultSession.clearCache(() => {}) setWindow = null; }); }); // 全てのウィンドウが閉じたときの処理 app.on('window-all-closed', () => { // macOSの時以外はアプリケーションを終了させます(osxだとドックに残る) if (process.platform !== 'darwin') { app.quit(); } }); //mysql関係をコントロールする ipcMain.on('async', function( event, args, args2){ //コマンド名によって処理を開始 switch(args){ case "connect": //接続テスト conncloudsql(function (ret){ var json = ret; switch(json.status){ case "NOSET": //接続設定がないため繋がなかった場合の処理 mainWindow.webContents.send('message', "DB接続設定がありませんよ"); break; case "ERR": //エラーが発生した場合の処理 mainWindow.webContents.send('message', json.error); break; case "OK": //カウントデータを取得する event.sender.send('record', json); break; } }); break; case "dbname": //DB接続設定をもらう //レンダラー側に送りつける設定を集める var array = []; array.push(store.get("server")); array.push(store.get("dbname")); array.push(store.get("id")); //idを元にkeyを探索する if(store.get("id") != "undefined"){ var servicename = "zaseki_" + store.get("id"); if(keytar.findPassword(servicename)){ const secret = keytar.getPassword(servicename,store.get("id")); secret.then((result) => { array.push(result); event.sender.send('init', array); return; }); }else{ array.push(""); event.sender.send('init', array); } }else{ array.push(""); event.sender.send('init', array); } break; case "checkman": //シートの空き状況を確認する var seatid = args2; //SQLで空き状況確認 checkseatsql(seatid,function (ret){ var json = ret; //ステータスで判定して処理 switch(json.status){ case "NOSET": //接続設定がないため繋がなかった場合の処理 mainWindow.webContents.send('message', "DB接続設定がありませんよ"); break; case "ERR": //エラーが発生した場合の処理 mainWindow.webContents.send('message', json.error); break; case "OK": //カウントデータを取得する event.sender.send('seatok', json); break; } }); break; case "keepsheet": //シートを確保する var uid = args2[0]; var seat = args2[1]; //ユーザの存在確認 chkhumansql(uid,function (ret){ var json = ret; //ステータスで判定して処理 switch(json.status){ case "NOSET": //接続設定がないため繋がなかった場合の処理 mainWindow.webContents.send('message', "DB接続設定がありませんよ"); break; case "ERR": //エラーが発生した場合の処理 mainWindow.webContents.send('message', json.error); console.log("check"); break; case "OK": //カウントデータを取得する if(json.count == 0){ //ユーザがいませんでした。 event.sender.send('message', "ユーザの登録情報がありませんでした。"); return; } //ユーザ名を取得して席取得処理 var human = json.data[0].name; //シートの重複確保チェックと実際の確保ルーチン keepsheetsql([uid,human,seat],function (ret){ var json = ret; switch(json.status){ case "NOSET": //接続設定がないため繋がなかった場合の処理 mainWindow.webContents.send('message', "DB接続設定がありませんよ"); break; case "ERR": //エラーが発生した場合の処理 mainWindow.webContents.send('message', json.msg); console.log("keep"); break; case "OK": //完了メッセージを送る event.sender.send('ongetsheet', json.msg); break; } }); break; } }); break; case "releaseseat": //確保した席をリリースします //シートを確保する var uid = args2[0]; var seat = args2[1]; //ユーザの存在確認 chkhumansql(uid,function (ret){ var json = ret; //ステータスで判定して処理 switch(json.status){ case "NOSET": //接続設定がないため繋がなかった場合の処理 mainWindow.webContents.send('message', "DB接続設定がありませんよ"); break; case "ERR": //エラーが発生した場合の処理 mainWindow.webContents.send('message', json.error); console.log("check"); break; case "OK": //カウントデータを取得する if(json.count == 0){ //ユーザがいませんでした。 event.sender.send('message', "ユーザの登録情報がありませんでした。"); return; } //ユーザ名を取得して席取得処理 var human = json.data[0].name; //シートの重複確保チェックと実際のリリースルーチン relsheetsql([uid,human,seat],function (ret){ var json = ret; switch(json.status){ case "NOSET": //接続設定がないため繋がなかった場合の処理 mainWindow.webContents.send('message', "DB接続設定がありませんよ"); break; case "ERR": //エラーが発生した場合の処理 mainWindow.webContents.send('message', json.msg); console.log("keep"); break; case "OK": //完了メッセージを送る event.sender.send('ongetsheet', json.msg); break; } }); break; } }); break; default: break; } }); //ウィンドウをコントロールする ipcMain.on('closeset', function( event, args ){ //コマンド名によって処理を開始 switch(args){ case "setting": //セッティングウィンドウを非表示にする setWindow.hide(); break; case "open": //セッティングウィンドウを表示する setWindow.show(); break; default: break; } }); //keytar関係をコントロールする ipcMain.on('keytar', function( event, args ){ //配列データを受け取る var array = args; //サービス名を構築する var servicename = "zaseki_" + array[2]; //キーワードを保存する keytar.setPassword(servicename,array[2],array[3]); //他の情報はelectron-storeで保存する store.set("server",array[0]); store.set("dbname",array[1]); store.set("id",array[2]); //セッティングウィンドウを非表示にする setWindow.hide(); }); //シートをリリースする処理 function relsheetsql(args,callback){ var uid = args[0]; var uname = args[1]; var seatnum = ""; var connection; var retman = {}; var pass = []; var result = ""; var pafu = ""; //サービス名を構築する var servicename = "zaseki_" + store.get("id"); //接続設定があるかないか判定 if(store.get("id") == "undefined" || store.get("id") == null){ //エラーでコールバックさせる retman.status = "NOSET"; callback(retman); return; } var secret = keytar.getPassword(servicename,store.get("id")); secret.then((result) => { //パスワードを取得する pass = result; //MySQLに接続してデータを取得する //createConnectionでは接続が時々切れる mysql.createConnection({ host: store.get("server"), port: 3306, user: store.get("id"), password: pass, database: store.get("dbname") }).then(function(conn){ //seat確保済みかどうかのチェック connection = conn; //該当のIDでの登録数が何件かしらべる var result = connection.query('select * from seat where emp_id = "' + uid + '";'); return result; }).then(function(rows){ //データ件数を取得する var dlength = rows.length; seatnum = rows[0].seatname; //返り値を判定する if(dlength != 0){ //0件じゃないのでクエリを実行 //seatnumをRowsから取得 console.log(seatnum); //シートを確保する var result = connection.query('update seat set username = ?, emp_id = ? where seatname = ?;', ["","",seatnum], (err,result)=> { //エラーが発生した場合 if (err) { console.log("接続エラー"); retman.status = "ERR"; retman.msg = error; connection.end(); callback(retman); return; } //取得データを返す retman.status = "OK"; retman.msg = "該当のシートをリリースしました。"; callback(retman); connection.end(); return; } ); }else{ //確保はされていないのでそのまま返す //取得データを返す retman.status = "OK"; retman.msg = "該当の席は既にリリースされているみたいですよ。"; callback(retman); connection.end(); return; } }).catch(function(error){ if (connection && connection.end) connection.end(); //logs out the error retman.status = "ERR"; retman.msg = "接続エラーですよ。パスワードが違うとかサーバアドレス間違ってるとか、ありませんか?"; callback(retman); return; }); }); } //シートをキープする処理 function keepsheetsql(args,callback){ var uid = args[0]; var uname = args[1]; var seatnum = args[2]; var connection; var retman = {}; var pass = []; var result = ""; var pafu = ""; //サービス名を構築する var servicename = "zaseki_" + store.get("id"); //接続設定があるかないか判定 if(store.get("id") == "undefined" || store.get("id") == null){ //エラーでコールバックさせる retman.status = "NOSET"; callback(retman); return; } var secret = keytar.getPassword(servicename,store.get("id")); secret.then((result) => { //パスワードを取得する pass = result; //MySQLに接続してデータを取得する //createConnectionでは接続が時々切れる mysql.createConnection({ host: store.get("server"), port: 3306, user: store.get("id"), password: pass, database: store.get("dbname") }).then(function(conn){ //seat確保済みかどうかのチェック connection = conn; //該当のIDでの登録数が何件かしらべる var result = connection.query('select * from seat where emp_id = "' + uid + '";'); return result; }).then(function(rows){ //データ件数を取得する var dlength = rows.length; //返り値を判定する if(dlength == 0){ //0件なのでクエリを実行 //シートを確保する var result = connection.query('update seat set username = ?, emp_id = ? where seatname = ?;', [uname,uid,seatnum], (err,result)=> { //エラーが発生した場合 if (err) { console.log("接続エラー"); retman.status = "ERR"; retman.msg = error; connection.end(); callback(retman); return; } //取得データを返す retman.status = "OK"; retman.msg = seatnum + "のシートをID:" + uid + "で確保しました。"; callback(retman); connection.end(); return; } ); }else{ //すでに登録済みなのでメッセージを返す //取得データを返す retman.status = "OK"; retman.msg = "すでに別の席が" + uid + "にて確保されているみたいですよ。"; callback(retman); connection.end(); return; } }).catch(function(error){ if (connection && connection.end) connection.end(); //logs out the error retman.status = "ERR"; retman.msg = "接続エラーですよ。パスワードが違うとかサーバアドレス間違ってるとか、ありませんか?"; callback(retman); return; }); }); } //ユーザがいるかどうかチェック function chkhumansql(args,callback){ var connection; var retman = {}; var pass = []; var result = "" //サービス名を構築する var servicename = "zaseki_" + store.get("id"); //接続設定があるかないか判定 if(store.get("id") == "undefined" || store.get("id") == null){ //エラーでコールバックさせる retman.status = "NOSET"; callback(retman); return; } var secret = keytar.getPassword(servicename,store.get("id")); secret.then((result) => { //パスワードを取得する pass = result; //MySQLに接続してデータを取得する //createConnectionでは接続が時々切れる mysql.createConnection({ host: store.get("server"), port: 3306, user: store.get("id"), password: pass, database: store.get("dbname") }).then(function(conn){ //レコード用変数 var hflg = false; //椅子の空き状況 //クエリの実行 connection = conn; var result = connection.query('select * from userid where userid = "' + args + '";', function (err, rows, fields) { //エラーが発生した場合 if (err) { console.log("接続エラー"); retman.status = "ERR"; retman.error = error; callback(retman); connection.end(); return; } //取得データを返す retman.status = "OK"; retman.data = rows; retman.count = rows.length; console.log("OK"); callback(retman); connection.end(); return; }); }).catch(function(error){ if (connection && connection.end) connection.end(); //logs out the error retman.status = "ERR"; retman.error = "接続エラーですよ。パスワードが違うとかサーバアドレス間違ってるとか、ありませんか?"; callback(retman); return; }); }); } //MySQL Serverに接続してシートの空き状況を確認する function checkseatsql(seatnum,callback){ var connection; var retman = {}; var pass = []; var result = "" //サービス名を構築する var servicename = "zaseki_" + store.get("id"); //接続設定があるかないか判定 if(store.get("id") == "undefined" || store.get("id") == null){ //エラーでコールバックさせる retman.status = "NOSET"; callback(retman); return; } var secret = keytar.getPassword(servicename,store.get("id")); secret.then((result) => { //パスワードを取得する pass = result; //MySQLに接続してデータを取得する //createConnectionでは接続が時々切れる mysql.createConnection({ host: store.get("server"), port: 3306, user: store.get("id"), password: pass, database: store.get("dbname") }).then(function(conn){ //レコード用変数 var checkseat = ""; //椅子の空き状況 //クエリの実行 connection = conn; var result = connection.query('select * from seat where seatname = "' + seatnum + '";', function (err, rows, fields) { //エラーが発生した場合 if (err) { console.log("接続エラー"); retman.status = "ERR"; retman.error = error; callback(retman); connection.end(); return; } //取得データを返す retman.status = "OK"; retman.data = rows console.log("OK"); callback(retman); connection.end(); return; }); }).catch(function(error){ if (connection && connection.end) connection.end(); //logs out the error retman.status = "ERR"; retman.error = "接続エラーですよ。パスワードが違うとかサーバアドレス間違ってるとか、ありませんか?"; callback(retman); return; }); }); } //MySQL Serverに接続してシートデータを取得する関数 function conncloudsql(callback){ var connection; var retman = {}; var pass = []; var result = "" //サービス名を構築する var servicename = "zaseki_" + store.get("id"); //接続設定があるかないか判定 if(store.get("id") == "undefined" || store.get("id") == null){ //エラーでコールバックさせる retman.status = "NOSET"; callback(retman); return; } //パスワードを取得する var secret = keytar.getPassword(servicename,store.get("id")); secret.then((result) => { //パスワードを取得する pass = result; //MySQLに接続してデータを取得する //createConnectionでは接続が時々切れる mysql.createConnection({ host: store.get("server"), port: 3306, user: store.get("id"), password: pass, database: store.get("dbname") }).then(function(conn){ //レコード用変数 var rlength = 0; //レコードの数 var record = "" //レコードデータを格納する //クエリの実行 connection = conn; var result = connection.query('select * from seat;', function (err, rows, fields) { //エラーが発生した場合 if (err) { console.log("接続エラー"); retman.status = "ERR"; retman.error = error; callback(retman); connection.end(); return; } //レコードデータを格納する rlength = rows.length; //取得データを返す retman.status = "OK"; retman.count = rlength; retman.recman = rows; console.log("OK"); callback(retman); connection.end(); return; }); }).catch(function(error){ if (connection && connection.end) connection.end(); //logs out the error retman.status = "ERR"; retman.error = "接続エラーですよ。パスワードが違うとかサーバアドレス間違ってるとか、ありませんか?"; callback(retman); return; }); }); } |
- keytar自体はモジュールが動いてくれる状況であれば、OSに合わせてOSに装備されてる資格情報管理システムへパスワードを記録してくれます。
- 今回、やや冗長なコードの書き方になっています。
- 接続設定用のsetting.htmlおよびメイン表示用のindex.htmlの2つによるマルチウィンドウ仕様ですが、setting.html側には初期ではhideになるようにし、またindex.htmlの子ウィンドウになるようbrowserwindowにオプション設定を追加してあります。
- ipcMain.onにてレンダラープロセス側からの通信を待機させてあります。
- promise-mysqlを利用しているので、同期処理でMySQLへの接続を実現しています。
- MySQL接続とクエリの処理では、通常のSQL文を使った処理と、where条件に?を使うプレースホルダーを使ったケースの2つで記述をしています。プレースホルダーを使ったほうが引数を渡しやすいですね。SQLインジェクション対策も考えたら、プレースホルダと変数のチェック機能をつけるべきでしょう。
- keepsheetとrelsheetの2つでは確保状況の確認と実際に値をセットするSQLの2回を連続実行しています。promiseで順番に実行させられるので、コールバック地獄にならず綺麗に書けます。
- relsheetsqlにてケースによって確保した座席がリリースされない場合があるので修正しました。
機能の追加
2019年8月30日現在、社内向けで作ってる座席表アプリは、更なる機能増強を図り、現在以下のような機能を搭載しています。目的は座ってる場所の把握と、顔と名前が一致しない覚えられないというフリーアドレスならではの問題を解決する為です。
- 座席に座ってる人を検索する為の機能(クリックすると座ってる部屋に切り替わり、対象者に★が付く)
- Canvas使用時の文字がにじむ現象の修正(高解像度対応)
- 座席キープ者をクリック時に、詳細な情報をダイアログで表示する機能(写真表示あり)
- 人員増加によりメインルームも複雑化した為、全面クリッカブルマップ化を行った。
この増強した中で、1.および3.についての知見をここに残しておこうと思います。
他のウィンドウから他のウィンドウを操作する
Electronで生成したBrowserWindowによるウィンドウはメインプロセス側から操作する事は可能でも、他のBrowserWindowで生成したレンダラプロセス側から操作する事は通常できません。しかし、検索用ウィンドウからメインの座席表の検索と表示の切り替えをしたいと思った場合、「メインプロセスを中継して操作する」ようなコマンドを用意する必要があります。
命令を受けるレンダラプロセス側では、メインプロセスから飛んできた命令について処理をするコマンドをipc通信で待機させておき、飛んできたら処理を実行するよう仕組みを用意しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
//遠隔からマップを切り替え実行するコマンド ipcRenderer.on('changemap',function(event,arg){ //argsからmapidを取得 var mapid; //canvasをクリアする clearcanvas(); //IDによって表示マップを切り替えてデータをリロード if(mapid == 0){ //執務室をオープンする //背景画像をチェンジする document.getElementById("backimage").src = "images/mainroom.png"; //イメージマップを設定する onImagemap(); //フロアフラグを設定する floorflg = 0; }else{ //パークエリアをオープンする //背景画像をチェンジする document.getElementById("backimage").src = "images/floormap.png"; //イメージマップを設定する onImagemap2(); //フロアフラグを設定する floorflg = 1; } }) //遠隔からマップに★をつけるコマンド ipcRenderer.on('setstar',function(event,arg){ //argからxとyの座標を取得 var xcoord = arg.xcoord - 10; var ycoord = arg.ycoord; // CanvasとContextを取得する。 var canvas = document.getElementById("canvas4"); var ctx = canvas.getContext("2d"); //テキストのフォントのオプション指定 ctx.font= 'bold 20px sans-serif'; //テキストデータを書き込む ctx.fillStyle = '#fff'; ctx.textBaseline = 'middle'; ctx.fillText("🌟",xcoord,ycoord); }) //遠隔からマップの★だけクリアするコマンド ipcRenderer.on('clearcanvas',function(event,arg){ //canvas4をクリアする var ctx = document.getElementById('canvas4').getContext('2d'); ctx.clearRect(0, 0, 1200, 700); }) //エリアチェンジコマンド ipcRenderer.on('areachange',function(event,arg){ //areaフラグを取得 var mapid = arg; //マップチェンジ mapChange(mapid); }); //マップ切り替え function mapChange(mapid){ //canvasをクリアする clearcanvas(); clearstar(); //IDによって表示マップを切り替えてデータをリロード if(mapid == 0){ //執務室をオープンする //背景画像をチェンジする document.getElementById("backimage").src = "images/mainroom.png"; //イメージマップを設定する onImagemap(); //フロアフラグを設定する floorflg = 0; }else{ //パークエリアをオープンする //背景画像をチェンジする document.getElementById("backimage").src = "images/floormap.png"; //イメージマップを設定する onImagemap2(); //フロアフラグを設定する floorflg = 1; } } |
- 対象者が今見ているマップではない場合、マップチェンジを行ってから🌟の描写を行ってる
- 🌟描写後に、検索窓を閉じたりマップを変更した場合には星をクリアするようにしている。
- 🌟の描写用に4枚目のcanvasを用いています。
図:ユーザ座標検索窓から座席表メインウィンドウを操作する
キープ者の詳細ダイアログ表示
キープされてる座席をクリックすると、座席のタグに書き込まれているフラグを読み取り、対象者の詳細情報をMySQLから取り出し、モーダルダイアログに表示するようにしています。今回利用させて頂いたモーダルのコード自体は、こちらのサイトを参照してみて下さい。非常に優れたモーダルウィンドウです。
このウィンドウの注意点は以下の通り。
- 写真の有無はMySQL側のpict列を参照させ、-1ならば指定のURLからロードする
- 写真は人によって縦横サイズが異なる。このような写真を400x400の写真枠内に収まるようにアスペクト比を考慮してリサイズする。
- CSSな処理として、人名については写真にオーバーレイさせた位置に表示。
- CSSな処理として、社員IDについても、左上にIDとして重ねて表示させる(こちらのCSSを利用させて頂きました)
HTML側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
<!-- ここからモーダルウィンドウ --> <div id="modal-content" style="z-index=21;width:800px;height:500px"> <div id="modal-bar" style="width:100%;heifht:100px;background:#b7ffd5;border:0px solid #a7e;padding 2px">詳細情報</div> <div id="modal-content-innar"> <table > <tr> <th rowspan="5" width="300"> <div id="overman"> <section> <div class="img-container--table-cell"> <img id="usrpict" src="" border="0" width="400px" height="400px"/> </div> </section> <p id="userfullname">社員氏名</p> <span id="idinfo">社員のIDはここ</span> </div> </th> <td id="busyoman">所属部署名はここ</td> </tr> <tr> <td> <div class='boxContainer'> <div class='box3'> <img id="telicon" src="images/tel.png" width="32" height="32" border="0"/> </div> <div class='box4'> <div class="telman" id="telnum">ここに電話番号を記載</div> </div> </div> </td> </tr> <tr> <td> <div class='boxContainer'> <div class='box3'> <img id="pcicon" src="images/pc.png" width="32" height="32" border="0"/> </div> <div class='box4'> <div class="telman" id="pcnum">ここにパソコン名</div> </div> </div> </td> </tr> <tr> <td> <div class='boxContainer'> <div class='box3'> <img id="mailicon" src="images/letter.png" width="32" height="32" border="0"/> </div> <div class='box4'> <div class="telman2"><a href="mailto:armin.arlert@hotmail.com" id="mailman">armin.arlert@hotmail.com</a></div> </div> </div> </td> </tr> <tr> <td> <div class='boxContainer'> <div class='box3'> <img id="teamsicon" src="images/teams.png" width="32" height="32" border="0"/> </div> <div class='box4'> <div class="telman2" id="teams"><a href="msteams://recipient=armin.arlert@hotmail.com">Teamsで連絡</a></div> </div> </div> </td> </tr> </table> <hr> <div align="right"> <p><a id="modal-close" class="button-link">閉じる</a></p> </div> </div> </div> |
CSSコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
/* 氏名を画像の上にオーバーレイさせる */ #overman{ position: relative; } #overman p { position: absolute; color: black; top: 300px; left: 0px; font-size: 30px; width:400px; text-shadow: white 2px 0px, white -2px 0px, white 0px -2px, white 0px 2px, white 2px 2px , white -2px 2px, white 2px -2px, white -2px -2px, white 1px 2px, white -1px 2px, white 1px -2px, white -1px -2px, white 2px 1px, white -2px 1px, white 2px -1px, white -2px -1px; } /* 社員IDを画像の上左端にオーバーレイさせる */ #overman span { position: absolute; top: 0;/*画像の左上に配置*/ left: 0; margin: 0; /*余計な隙間を除く*/ color: black;/*文字を白に*/ background: skyblue;/*背景色*/ font-size: 15px; line-height: 1;/*行高は1に*/ padding: 5px 10px;/*文字周りの余白*/ } /* 画像のアスペクト比を固定したまま表示 */ #usrpict{ object-fit:contain; width:400px; height:400px; background-clor:#aaa; } .img-container--table-cell{ width:400px; height:400px; display:table-cell; vertical-align:middle; text-align:center; border:0px solid darkgray; } |
図:写真データがある場合はその画像をロードします
外部アクセスでプロキシーを経由させる
自宅や特にプロキシーサーバを設定していないような会社の場合には、この設定は不要ですが、それ相応の企業になると外部へのアクセスはプロキシーサーバ経由でなければさせないようになっていると思います。この場合、メインプロセス、レンダラープロセス共に外部へアクセス出来ないので、例えばレンダラプロセス側でCDNなどのライブラリを読みに行っている場合、取ってくる事ができません。
そこで以下のようなコードを追加する事で、必ず外部アクセスはプロキシーサーバを経由するようにする事が可能です。
1 2 3 4 5 |
//プロキシーサーバ直の場合 app.commandLine.appendSwitch('proxy-server', 'http://192.168.1.2:8080'); //プロキシーのPACファイルの場合 app.commandLine.appendSwitch('proxy-pac-url', 'http://192.168.1.2:8080/proxy.pac'); |
図:プロキシー経由させずにDNSエラーが出る
JSONデータをまるっと列に入れ込みたい
MySQL 5.7より、Columnの型に「JSON型」というものが用意されました。予め、テーブルの対象の列の方をJSON型にしておき、入れ込むJSONをJSON.stringifyしておいたものを、普通に入れ込めば入ります。但し、MySQL側で入れることのできるJSONは、いい加減な書式だと弾かれてしまいます。入れることのできるJSONの例は以下の通り
1 |
{"col1":"test", "col2","tomato"} |
列の名前がMySQLの予約語(tableやらbeforeやら)でなければ、JSON.stringify(jsondata)した値をそのまま、insert intoで流し込むことができるようになります。自分の場合、アプリケーションで編集前のデータを取っておき、編集保存時にログテーブルへとこの編集前データを記録。いざという時には、このログからそのまま復元できるようにアプリに作り込んでいます。
図:beforedata列にJSON値が入った
指定時刻に座席を強制リリース
今回のアプリケーションは、誰かが座席を確保したまま、リリースを忘れると永遠に席を確保された状態になってしまいます。それでは翌日皆が困ることになりますね。そこで、この座席についてkoteiflgが0の値(つまりフリーアドレス席全部)について、すべての座席データを強制リリースさせる為の仕組みが必要になります。
この作業もNode.jsでやらせる事が可能です。MySQL Serverの入ってるマシンにNode.jsをインストールして、座席を強制リリースするSQL文を書いたアプリをサービスとして常駐させて置きます。今回は、Ubuntu Linux 18.04をベースにお話を進めます。
事前準備
electronでもNode.jsを利用しましたが、今回のアプリでもNode.jsを利用します。今回追加で利用するモジュールは以下の通り。クライアントアプリも作れ、サーバサイドも同じ言語で作れるNode.jsの美味しいポイントですね。
- cronモジュール - 定期的にプログラムを実行する
- promise-mysqlモジュール - electronでも利用したMySQLへPromiseなやり方で接続する為のモジュール
- keytarモジュール - OSの資格情報システムにパスワードを登録したり取り出したりするモジュール
- prompt-passwordモジュール - パスワード入力時に文字は*で秘匿して表示する為のモジュール
- foreverモジュール - jsファイルをデーモン化して常駐させるモジュール
これらをnpm installでプロジェクトフォルダ内で実行しインストールしておきましょう。foreverはプログラム中で使うのではなく、外部で利用するものなので、-gオプションでグローバルインストールしましょう。
ソースコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
'use strict'; //標準モジュールの宣言 var fs = require('fs'); var readline = require('readline'); var cronJob = require('cron').CronJob; // 毎日21:00分に実行 var cronTime = "00 00 21 * * 1-7"; //CronJobを定義する var job = new cronJob({ //実行したい日時 or crontab書式 cronTime: cronTime //指定時に実行したい関数 , onTick: function() { cleardatabase(); } //ジョブの完了または停止時に実行する関数 , onComplete: function() { console.log( |