stylesheet

2020-02-09

Electron + Vue.jsでデスクトップアプリを作る

久々のJavaScriptネタは、Electron

ダウンロードしたアプリケーションがコイツを使ったWebアプリだったりするとなかなかに残念な気持ちになる、あのElectron。
一部のセクターで根強いニーズがあるとかないとか。

そんなElectronをVue.jsと合わせて使うメモ。開発環境はUbuntuベースのディストリ。

目次

  1. プロジェクトの作成
  2. ESLintを一時的に無効にする
  3. アイコンを追加
  4. ウィンドウサイズと位置の保存と復元
  5. Same-origin-policyを無効にする
  6. クロスプラットフォームビルド
  7. 参考情報

プロジェクトの作成

vue-cliをインストール。
VUE CLI 3以前のものをインストールしている場合はyarn global remove vue-cliで削除しておく。

$ yarn global add @vue/cli
$ vue --version
@vue/cli 4.1.2

vue createコマンドでプロジェクトを作成。
Manually select featuresを選ぶと色々と聞かれるので、 デスクトップアプリもJavaScriptで作成するような変態さんは、ここで自分の宗派をゴリ押しするとよい。

CSSプリプロセッサはStylus、コーディングスタイルはStandard以外ありえない訳だが...

$ vue create hello-vue-electron-builder


Vue CLI v4.1.2
? Please pick a preset: 
  default (babel, eslint) 
❯ Manually select features 

...

Vue CLI v4.1.2
? Please pick a preset: Manually select features
? Check the features needed for your project: 
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◉ Router
 ◉ Vuex
 ◉ CSS Pre-processors
❯◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing

...

Vue CLI v4.1.2
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Vuex, CSS Pre-processors, Linter
? Use history mode for router? (Requires proper server setup for index fallback in production) No
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Stylus
? Pick a linter / formatter config: Standard
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? (y/N) 

...

📄  Generating README.md...

🎉  Successfully created project hello-vue-electron-builder.
👉  Get started with the following commands:

 $ cd hello-vue-electron-builder
 $ yarn serve

生成されたファイルの一覧。


$ cd hello-vue-electron-builder
$ tree . -I node_modules
.
├── README.md
├── babel.config.js
├── package.json
├── public
│   ├── favicon.ico
│   └── index.html
├── src
│   ├── App.vue
│   ├── assets
│   │   └── logo.png
│   ├── components
│   │   └── HelloWorld.vue
│   ├── main.js
│   ├── router
│   │   └── index.js
│   ├── store
│   │   └── index.js
│   └── views
│       ├── About.vue
│       └── Home.vue
└── yarn.lock

7 directories, 14 files

続いてvue-cli-plugin-electron-builderを追加。

$ vue add electron-builder

📦  Installing vue-cli-plugin-electron-builder...
...

? Choose Electron Version (Use arrow keys)
  ^4.0.0 
  ^5.0.0 
❯ ^6.0.0 


? Choose Electron Version ^6.0.0

🚀  Invoking generator for vue-cli-plugin-electron-builder...
 WARN  Devtools extensions are broken in Electron 6.0.0 and greater
 WARN  Vue Devtools have been disabled, see the comments in your background file for more info
📦  Installing additional dependencies...

Version6を選んだところ、不吉なワーニングが表示されるが...


yarn install v1.21.1
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.2.11: The platform "linux" is incompatible with this module.
info "fsevents@1.2.11" is an optional dependency and failed compatibility check. Excluding it from installation.
info fsevents@2.1.2: The platform "linux" is incompatible with this module.
info "fsevents@2.1.2" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
$ electron-builder install-app-deps
  • electron-builder  version=21.2.0
Done in 5.69s.
⠋  Running completion hooks...error: 'installVueDevtools' is defined but never used (no-unused-vars) at src/background.js:6:3:
  4 | import {
  5 |   createProtocol,
> 6 |   installVueDevtools
    |   ^
  7 | } from 'vue-cli-plugin-electron-builder/lib'
  8 | const isDevelopment = process.env.NODE_ENV !== 'production'
  9 | 


1 error found.

やっぱり出た。
'installVueDevtools' is defined but never used (no-unused-vars)とのことなので、Linterが未使用の変数をエラーにしているようだ。 上のほうで壊れていると警告されているDevtools拡張関連なので、該当の記述を削除で問題ないだろう。

// src/background.js

'use strict'

import { app, protocol, BrowserWindow } from 'electron'
import {
  createProtocol,
  // installVueDevtools
} from 'vue-cli-plugin-electron-builder/lib'
const isDevelopment = process.env.NODE_ENV !== 'production'

...

installVueDevtools使用箇所はコメントアウトされていたので、単純に消し忘れではなかろうか。
コンパイルエラーは修正されたはずなので、開発サーバーを起動。

$ yarn electron:serve

特に問題なさそうなので、リリースビルドを実行。Electronアプリケーションを生成する。

$ yarn electron:build
$ tree . -I node_modules
.
├── README.md
├── babel.config.js
├── dist_electron
...
│   ├── hello-vue-electron-builder-0.1.0.AppImage
│   ├── hello-vue-electron-builder_0.1.0_amd64.snap
│   ├── index.js
│   ├── linux-unpacked
│   │   ├── LICENSE.electron.txt
│   │   ├── LICENSES.chromium.html
│   │   ├── chrome-sandbox
...

dist_electron以下にアプリケーションが生成された。
AppImageファイルは直接実行できる。

$ ./dist_electron/hello-vue-electron-builder-0.1.0.AppImage &

ESLintを一時的に無効にする

上記でもエラーで止まっていたが、保守や機能追加程度ならまだしも、新規開発中に未使用変数如きでコンパイルエラーにされるのはさすがに効率が悪い。
ルールを変えずに手っ取り早くLinterチェックをスキップするには、.eslintignoreファイルを使用するのが良さそう。

中身は.gitignore等と同じようにglobパターンで指定できる。
/node_modules/*bower_components/*は特に記載しなくても無視してくれるようだ。


// .eslitignore

**/*  // 全ファイル
**/*.js  // jsファイル
**/*.vue // vueファイル

.eslintignoreファイルまでバージョン管理されていることは稀だろうから、比較的行いやすい方法だとおもう。
コミット前にはチェックを忘れずに。

アイコンを追加

いろんなサイズやフォーマットを準備しなければいけないので結構な手間。アイコン生成ツールを使うとまとめて生成できる。

$ yarn add electron-icon-builder --dev

package.jsonへアイコン生成スクリプトを追加。

// package.json

{
  "name": "hello-vue-cli-v3",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    ...
    "electron:generate-icons": "electron-icon-builder --input=./public/icon.png --output=build --flatten"
  },
  ...

public/icon.pngへ画像ファイルを配置(1024x1024以上の大きさの正方形画像が推奨されている)してスクリプトを実行。

$ yarn electron:generate-icons
$ tree build/icons
build/icons
├── 1024x1024.png
├── 128x128.png
├── 16x16.png
├── 24x24.png
├── 256x256.png
├── 32x32.png
├── 48x48.png
├── 512x512.png
├── 64x64.png
├── icon.icns
└── icon.ico

0 directories, 11 files

vue.config.jsファイルを作成して、Electron Builderへのオプションを指定。

// vue.config.js

module.exports = {
  pluginOptions: {
    electronBuilder: {
      builderOptions: {
        // options placed here will be merged with default configuration and passed to electron-builder
        linux: {
          target: ["AppImage"],
          icon: "build/icons/"
        },
        win: {
          target: ["zip"],
          icon: "build/icons/icon.ico"
        }
      }
    }
  }
}

タスクトレイアイコンの指定はbackground.jsへ記述。new BrowserWindowiconオプションを渡す。

// src/background.js

...

import path from 'path'

...

function createWindow () {
  // Create the browser window.
  win = new BrowserWindow({ width: 800,
    height: 600,
    icon: path.join(__static, 'icon.png'),
    webPreferences: {
      nodeIntegration: true
    } })

  ...
}

ウィンドウサイズと位置の保存と復元

どうもそういったAPIはないようなので自前で実装する。ストレージ用のライブラリを導入するとある程度、楽できるかもしれない。

自前で実装する場合は、以下を参考に。

  • ウィンドウのサイズと位置はwin.getBounds()で取得できる。ウィンドウを閉じたタイミングでこの値をファイルに書き出しておく。
  • ウィンドウサイズと位置の指定は、BrowserWindowのコンストラクタかBrowserWindow#setBoundsメソッドで行える。
  • 設定ファイルは、app.getPath('userData')で取得できる場所へ保存するのが無難。環境に応じたパスを返してくれる。

getBounds()ではwidthheightxyが取れる。

> win.getBounds()
{ width: 800, height: 600, x: 10, y: 10 }

大雑把な実装は以下の通り。
実製品の場合は、クラス化したうえで、外部入力となるので値チェックもしっかり行うべき。

...

import path from 'path'
import {readFileSync, writeFileSync} from 'fs'

const config = path.join(app.getPath('userData'), 'config.json')

function createWindow () {
  let bounds
  try {
    bounds = JSON.parse(readFileSync(config))
  } catch (e) {
    bounds = {width:800, height:600}
  }
  
  win = new BrowserWindow({
    ...bounds,
    webPreferences: {
      nodeIntegration: true,
      webSecurity: true
    }
  })

  ...

  win.on('close', () => {
    writeFileSync(config, JSON.parse(win.getBounds()))
  }

  win.on('closed', () => {
  
  ...

Same-origin-policyを無効にする

CORSヘッダーでリクエストが許可されずにエラーとなる場合、ウィンドウのwebSecurityオプションで無効にできる。

win = new BrowserWindow({
  webPreferences: {webSecurity: true}
})

あえてセキュリティホールを作る行為。ご利用は慎重に。

クロスプラットフォームビルド

Linux上でWindows用のアプリケーションをビルドする。

Windows用のアプリ生成にはwinemonoが必要。 公式ではdockerを勧めているが、コンテナ起動の引数にゲンナリしたのでローカルインストール。

$ sudo apt install wine64 mono-devel

--win引数を追加してWindowアプリのビルド。

$ yarn electron:build --win
$ tree dist_electron
dist_electron
...
├── hello-vue-electron-builder-0.1.0-win.zip
├── hello-vue-electron-builder-0.1.0.AppImage
...
└── win-unpacked
    ├── LICENSE.electron.txt
    ├── LICENSES.chromium.html
    ├── chrome_100_percent.pak
    ├── chrome_200_percent.pak
    ├── d3dcompiler_47.dll
    ├── ffmpeg.dll
    ├── hello-vue-electron-builder.exe
    ├── icudtl.dat
    ├── libEGL.dll
    ├── libGLESv2.dll
    ├── locales
...
    ├── natives_blob.bin
    ├── resources
    │   ├── app.asar
    │   └── electron.asar
    ├── resources.pak
    ├── snapshot_blob.bin
    ├── swiftshader
    │   ├── libEGL.dll
    │   └── libGLESv2.dll
    └── v8_context_snapshot.bin

wineexeファイルを起動してみる。

$ wine dist_electron/win-unpacked/hello-vue-electron-builder.exe &

非効率で馬鹿げてる?
部屋の電気をつけるのに西海岸クラウドサービスと通信する時代だよ。そもそも、Electron自体...(ry

ポンコツ感はあるが一応起動。
本物のWindowsならちゃんと表示されるはず...

以上、私用に一本アプリを作成した時のメモでした。

参考情報