[Electron] 文字列カウンタのソースコードの解説

# はじめに

先日、[Electron](http://electron.atom.io)を使って、[LCC](https://github.com/cotrpepe/LCC)という簡単な文字列カウンタを作りました。この記事では、このアプリのソースコードについて、簡単に解説します。


# Electronとは

Electronとは、Web技術（Javascript + HTML + CSS）を使って、クロスプラットフォーム（Mac/Windows/Linux）なデスクトップアプリを開発できる実行環境で、ChromiumやNode.jsをベースに作られています。Electronを使ったアプリとしては、[Atomエディタ](https://atom.io)や[Visual Studio Code](https://code.visualstudio.com)などがあります。


# 開発したアプリ

[LCC (Lightweight Character Counter)](https://github.com/cotrpepe/LCC)


# 対応OS

Mac/Windows


# バイナリ

以下のページからダウンロードできます。
↓
https://github.com/cotrpepe/LCC/releases/tag/v0.1.0


# ソースコード

ソースコードは、Githubにアップしました。
↓
https://github.com/cotrpepe/LCC


# アプリの操作方法

1. カウントしたい文字列をクリップボードにコピーする
2. 文字数が表示される

![example.png](https://qiita-image-store.s3.amazonaws.com/0/46434/131885aa-73c1-8989-b47f-25227764f0e1.png)


# ソースコードの解説

## LCCのバージョン

v0.1.0のコードを基に解説していきます。


## 開発環境

* Mac OS X 10.10.5
* node.js v0.12.7
* electron-prebuilt 0.31.0
* electron-packager 5.0.2
* mocha 2.2.5



## ディレクトリの構成

Electronアプリは、以下の２種類のプロセスから成ります。

|プロセス名|担当|
|:--|:--|
|mainプロセス|全てのWebページとrendererプロセスの管理、ネイティブGUI APIとのやり取りを担当する|
|rendererプロセス|個々のWebページを担当する|

そのため、mainプロセスで動作するコードと、rendererプロセスで動作するコードとで、ディレクトリを分けました。

```
LCC/
└── src/
     ├── main/
     └── renderer/
```


## アプリの起動

`package.json`の`main`で、一番最初に起動するスクリプトを指定します。

```js:LCC/package.js
  ...
  "main": "./src/main/main.js",
  ...
```

アプリ起動直後に、以下のメソッドが一番始めに実行されます。

```js:LCC/src/main/main.js
app.on('ready', function() {
	...
});
```


## HTMLファイルのロード

`main-window-init.js`の`createMainWindow()`の中で、メインウィンドウのためのHTMLファイルをロードします。

```js:LCC/src/main/main.js
app.on('ready', function() {
	mainWindow = require('./main-window-init.js').createMainWindow();
	...
});
```

HTMLファイルをロードするには、`browser-window`モジュールの`loadUrl()`を使います。

```js:LCC/src/main/main-window-init.js
var path = require('path');
var BrowserWindow = require('browser-window');

module.exports.createMainWindow = function() {

	...

	var mainWindow = new BrowserWindow({
		...
	});

	...

	var htmlPath = path.join(__dirname, '/../../main.html');
	mainWindow.loadUrl('file://' + htmlPath); // HTMLファイルのロード

	...
};
```


## メニューの作成（Mac only）

Macの場合にのみ、メニューを作成します。

```js:LCC/src/main/main.js
app.on('ready', function() {
	...

	if (process.platform == 'darwin') {
		require('./menu.js').createMenu();
	}

	...
});
```

`Menu.buildFromTemplate()`を使ってメニューを作成します。`label`にメニュー名を、`submenu`に入れ子にするメニューを指定します。

また、`setApplicationMenu()`を使って、作成したメニューを登録します。

```js:LCC/src/main/menu.js
var Menu = require('menu');
var menuItem = require('./menu-item.js');

module.exports.createMenu = function() {
	var menu = Menu.buildFromTemplate([ // メニューの作成
		{
			label: 'File',
			submenu: [
				menuItem.aboutWindowMenu(),
				...
			],
		},
		...
	]);
	Menu.setApplicationMenu(menu); // メニューの登録
};
```

`menu-item.js`でメニュー項目を作成しています。`label`にメニュー名を、`click`にクリックしたときの処理を書きます。

```js:LCC/src/main/menu-item.js
var app = require('app');
...

module.exports.aboutWindowMenu = function() {
	return {
		label: 'About ' + app.getName(),
		click: function () {
			require('./about-window.js').openAboutWindow();
		}
	};
};

...
```

`about-window.js`の`openAboutWindow()`を実行すると、以下のようなダイアログが表示されます。

![スクリーンショット 2015-08-23 12.03.35.png](https://qiita-image-store.s3.amazonaws.com/0/46434/e369b82c-ef5d-26df-c782-aa85d00774d8.png)

ダイアログを表示するには、`dialog`モジュールの`showMessageBox()`を使います。

```js:LCC/src/main/about-window.js
var app = require('app');
var dialog = require('dialog');
var pjson = require('../../package.json');

module.exports.openAboutWindow = function() {
	dialog.showMessageBox({
		type: 'info',
		buttons: ['ok'],
		title: 'About ' + app.getName(),
		message: app.getName(),
		detail: pjson.description + '\nv' + app.getVersion()
	});
};
```



## トレイアイコンの作成

```js:LCC/src/main/main.js
app.on('ready', function() {
	...
	trayIcon = require('./tray.js').createTray();
	...
});
```

トレイアイコンを作成する際も、`Menu.buildFromTemplate()`を使ってメニューを作成します。トレイアイコンにメニューを登録するには、`tray`モジュールの`setContextMenu()`を使います。

```js:LCC/src/main/tray.js
var app = require('app');
var path = require('path');
var Tray = require('tray');
var Menu = require('menu');
var menuItem = require('./menu-item.js');

module.exports.createTray = function() {
	var iconPath = path.join(__dirname, '/../../image/tray_icon.png');
	var trayIcon = new Tray(iconPath);

	var contextMenu = Menu.buildFromTemplate([ // メニューの作成
		menuItem.aboutWindowMenu(),
		menuItem.helpMenu(),
		{ type: 'separator' },
		menuItem.quitMenu()
	]);

	...

	trayIcon.setContextMenu(contextMenu); // メニューの登録

	return trayIcon;
};
```

## ショートカットの登録

```js:LCC/src/main/main.js
var shortcut = require('./shortcut.js');
...

app.on('ready', function() {
	...
	shortcut.registerQuit();
});
```

ショートカットを登録するには、`global-shortcut`モジュールの`register()`を使います。

```js:LCC/src/main/shortcut.js
var app = require('app');
var globalShortcut = require('global-shortcut');

module.exports.registerQuit = function() {
	var shortcut = process.platform == 'darwin' ? 'Command+Q' : 'Ctrl+Q'; // Mac向けとWindows向けでショートカットを切り替える
	var ret = globalShortcut.register(shortcut, function() { // ショートカットの登録
		app.quit();
	});
	...
};
```


## Rendererプロセスの開始

Rendererプロセスでは、`renderer-main.js`が一番最初に呼び出されます。

```html:LCC/main.html
<!DOCTYPE html>
<html>
	...
  <body>
		...
		<script>require('./src/renderer/renderer-main.js');</script>
  </body>

</html>
```


## クリップボードの監視

`renderer-main.js`の中で、500msec毎にクリップボードを監視しています。クリップボードの内容が変更された場合は、文字数をカウントして表示します。

```js:LCC/src/renderer/renderer-main.js
var clipboard = require('clipboard');
...

var prevLen = string.getLengthInCrossPlatform(clipboard.readText());
...

setInterval(function() { // 500msec毎に繰り返し
  var curLen = string.getLengthInCrossPlatform(clipboard.readText());

  if (prevLen != curLen) { // クリップボードが更新されていた場合
    prevLen = curLen;
    mainWindow.update(curLen); // 表示される文字数のアップデート
  }
}, 500);
```

## 数字の桁区切り

文字数を表示する際に、３桁ごとに区切り文字（`123456789 → 123,456,789`）を入れるために、以下の正規表現を使っています。

```js:LCC/src/renderer/string-style.js
module.exports.addThousandsSeparator = function(number) {
	...
	return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
```

この正規表現は、Stack Overflowの[Elias Zamariaさんの回答](http://stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascrip)を参考にしています。

### 補足）上記の正規表現の意味

ここで例として、123456789（123,456,789）という数字を取り上げます。

`(\d{3})+(?!\d)`の部分は、「数字が後ろに続かない、３の倍数分連続している数字」を表しており、これは`789`と`456789`と`123456789`にマッチします。

```
                       ↓↓↓        ↓↓↓↓↓↓     ↓↓↓↓↓↓↓↓↓
(\d{3})+(?!\d) → 123456789 and 123456789 and 123456789
```

つまり、`\B(?=(\d{3})+(?!\d))`は、`\B(?=789)`と`\B(?=456789)``\B(?=123456789)`になります。

```
\B(?=(\d{3})+(?!\d)) → \B(?=789) and \B(?=456789) and \B(?=123456789)
```

`\B(?=789)`は、「後ろに`789`がある、\B（単語内の文字と文字の間 = 今回の場合、数字と数字の間）」なので、`6`と`7`の間にマッチします。

```
                  ↓
\B(?=789) → 123456 789
```

上記のマッチを`,`で置換するので、以下のようになります。

```
      ↓
123456 789 → 123456,789
```

同様に、`\B(?=456789)`は、「後ろに`456789`がある、\B（今回の場合、数字と数字の間）」なので、`3`と`4`の間にマッチします。

```
                  ↓
\B(?=456789) → 123 456789
```

上記のマッチを`,`で置換するので、以下のようになります。

```
   ↓
123 456789 → 123,456789
```

また、`\B(?=123456789)`は、「後ろに`123456789`がある、\B（今回の場合、数字と数字の間）」を表しますが、これにマッチする箇所は存在しません。

```
\B(?=123456789) → マッチする箇所なし
```

従って、最終的に`123,456,789`という文字列が出来ます。


## 改行の取り扱い

Windowsでは、改行コードが`\r\n`です。これを１文字としてカウントするため、文字列長を求める前に、`\r\n`を`\n`に変換しています。

```js:LCC/src/renderer/string.js
module.exports.getLengthInCrossPlatform = function(str) {
  return str.replace(/\r?\n/g, '\n').length;
};
```


## テストコード

一部のコードについては、[mocha](http://mochajs.org)というnode.js用のテスティングフレームワークを使って、単体テストを行いました。

テストコードは以下のようになります。

```js:LCC/test/string-style-test.js
var assert = require('assert');
var ss = require('../src/renderer/string-style.js');

describe('string-style.js', function() {
  describe('addThousandsSeparator()', function() {
    it('shouldn\'t add the thousands separator to zero', function() {
      assert.equal('0', ss.addThousandsSeparator(0));
    });

    ...

  });

  describe('getScaledFontSize()', function () {
    it('should return scaled font size', function () {
        ...
    });

    ...

  });
});
```

|記述|意味|
|:--|:--|
|```it(...)```|テストケース|
|```describe(...)```|テストケースをまとめたテストスイート|