Electron + TypeScript + Vue.jsでデスクトップアプリ(1) -はじめのはじめ-

はじめに

Electronを使ったデスクトップアプリを作成します.ものすごくシンプルなものですが,基本をおさえるにはよいと思います.

最終的には

できるあがる予定のアプリ

このようなアプリができあがる予定ですが,今回の記事ではとりあえず最小構成で画面が表示されるところまでをやります!

環境

今回の開発環境は以下です.macOSでもLinux系OSでも同じように開発できます.Windows系は未確認です.

  • OS: Ubuntu 17.10 64bit(執筆開始時点では17.04でしたが途中でアップデートしました)
  • メモリ: 7.7GiB
  • npm: 3.10.10
  • node: v6.11.0

npmおよびnodeはすでにインストールされているとします.

Electron

ElectronはJavascript,HTML,CSSといったWebアプリの技術でクロスプラットホームなデスクトップアプリを作成できるフレームワークです.

Githubによって開発されていて,かの有名なクロスプラットホームなエディタAtomのために作られています.

ブラウザ部分にはChromiumが,Javascriptのエンジンにはnodeが使われていてブラウザごとの対応などに頭を悩ませる必要もなく,簡単にアプリ開発できます!

Electron | Build cross platform desktop apps with JavaScript, HTML, and CSS

特徴的なのは,Electronでは,アプリケーション全体を司るメインプロセスと,アプリケーションの画面を司るレンダラプロセスとに処理がわけられているところでしょうか.

これはWebアプリでいうところのサーバ側のプロセス,クライアント側のプロセスに相当すると考えてよいでしょう.ただし,サーバ-クライアント間の通信ではHTTPを使いますが,Electronではプロセス間通信(IPC)を使います.そのためのライブラリも含まれています.

また,nodeベースであることから,多くの部分を(alt)JSで記述できるところもよいところかと思います.

Vue.js

Vue.jsユーザインタフェースを構築するためのクライアントサイドのアプリケーションフレームワークです.以前に紹介したAngularなどと同じく,コンポーネント指向であり,中核は(MV*モデルでいうところの)Viewに重点が置かれていて,少しずつ適用することも可能となっています.

Vue.js

またコンポーネントライブラリとしてVue Materialを利用します.これはMaterial Designに基づく各種コンポーネントを提供するものです.

Introduction – Vue Material

Introduction – Material Design

今回のところは出番はないのですがレンダラ側はVue.jsの利用を前提としてディレクトリ構成・コーディングを行います.

TypeScript

TypeScriptはこれまでにも何度か紹介していますが,Microsoft開発のaltJSです.ざっくりいうとJavascriptに型システムを乗せた感じのものでES6にも対応していることから,安全に効率のよい開発ができます.

TypeScript – JavasSript that scales

Electron,Vue.jsとも,Typescriptに対するサポートがあります.つまり,型定義ファイルがライブラリ自体に含まれるので,面倒な手間なしに開発できます.

その他

それ以外にも諸々のタスクを実行させるためにgulpを,クライアントサイドの開発のためにwebpackを利用します.

gulp.js

webpack

またVue.jsのコンポーネントの見た目(テンプレート)はHTMLで定義しますが,HTML手書きは人間のやることではないのでPugを使って書きます.Vue.jsではデフォルトでPugを使ったテンプレートがサポートされていますので簡単です.

Getting Started – Pug

準備

プロジェクト作成

まずはプロジェクトを作成します.好きなディレクトリに移動して,

npm init

とします.ここではelectron-sample 以下で作業します.

このときいろいろ聞かれますが,とくに変更するものがなければデフォルトでも問題ないでしょう.もしgithub等で公開する予定があるのなら,ライセンスくらいは気にしておいたほうがよいかもしれません.

パッケージインストール

今回必要なパッケージは以下です.

"devDependencies": {
  "css-loader": "^0.28.7",
  "electron-packager": "^9.1.0",
  "gulp": "^3.9.1",
  "gulp-pug": "^3.3.0",
  "gulp-typescript": "^3.2.2",
  "pug-html-loader": "^1.1.5",
  "style-loader": "^0.19.0",
  "ts-loader": "^3.0.3",
  "typescript": "^2.5.3",
  "vue-loader": "^13.3.0",
  "vue-template-compiler": "^2.5.2",
  "webpack": "^3.8.1",
  "webpack-stream": "^4.0.0"
},
"dependencies": {
  "electron": "1.7.5",
  "vue": "^2.5.2",
  "vue-material": "^0.7.5"
}

npm i -S <パッケージ名> あるいはnpm i -D <パッケージ名> としてインストールします.あるいはこのpackage.json にパッケージ名を列挙しておいてnpm install としてもよいです.

各パッケージは最低バージョンだけを指定してありますが,Electronだけは記事執筆時点での最新版1.7.9のインストールがうまくいかなかったので1.7.5を指定しています.

tsconfig.json

こんかいはTypescriptを使いますのでプロジェクトルートにtsconfig.json を配置する必要があります.

通常のアプリケーションの場合とelectronアプリの場合とでは少し異なるのでやや注意が必要かもしれません.

{
    "compilerOptions": {
        "target": "ES6",
        "module": "commonjs",
        "moduleResolution": "node",
        "sourceMap": true,
        "noImplicitThis": true,
        "outDir": "./dist/main",
        "types": [
            "node"
        ]
    },
    "include": [
        "./src/main/*.ts"
    ],
    "exclude": [
        "node_modules",
        "**/node_modules/*",
        "./src/main/models/*"
    ]
}

targetES6(ES2015)に,modulecommonjsに,moduleResolutionnodeにそれぞれ指定します.

またtypesnodeを追加します.それ以外のオプションはお好みで問題ないと思います.

メインプロセス側

src/main 以下にメインプロセス側のコードを配置することにします.

コード自体はいたってシンプルです.

import { app, BrowserWindow } from 'electron';

class MyApp {
    mainWindow: Electron.BrowserWindow | null = null;

    constructor(public app: Electron.App) {
        this.app.on('window-all-closed', () => {
            if (process.platform != 'darwin') {
                setTimeout(() => { this.app.quit(); }, 50);
            }
        });

        this.app.on('ready', () => {
            this.mainWindow = new BrowserWindow({
                width: 800,
                height: 545,
                minWidth: 80,
                minHeight: 45
            });
       
            this.mainWindow.on('closed', (event: Electron.Event) => {
                this.mainWindow = null;
            });

            this.mainWindow.loadURL(`file://${__dirname}/../renderer/index.html`);

            // this.mainWindow.webContents.openDevTools();
        });
    }
}

const myapp = new MyApp(app);

コンストラクタで起動時,終了時の処理を記述します.起動時にはレンダラプロセス側のエントリポイントとなるindex.html をロードします.開発時点ではこのファイルはindex.pug ですが,処理されてHTMLになります.読み込むときは変換後のファイル名を記述しないといけないことに注意しましょう.

このときコメントアウトされているopenDevTools() を有効にすると,起動時に開発ツールが表示されるので必要に応じて役立てましょう(アプリにメニューを定義しない場合,デフォルトのメニューが追加されますが,そこで開発ツールを開くこともできます).

レンダラプロセス側

レンダラプロセス側のコードはsrc/renderer 以下に配置することにします.

エントリポイントとなるindex.pug はこれだけです.

<!DOCTYPE html>
html(lang="ja")
    head
        meta(charset="UTF-8")
        meta(name="viewport", content="width=device-width, initial-scale=1.0")
        meta(http-equiv="X-UA-Compatible", content="ie=edge")
        link(rel="stylesheet", href="./iconfont/material-icons.css")
        title Electronアプリサンプル
    body
        div#app
        script(type="text/javascript" src="./index.js")

div#app にアプリケーションのメインのコンポーネントが読み込まれます.

body で読み込んでいるindex.js は同じディレクトリに配置したindex.ts がトランスパイルされてできるものです.中身は以下です.

import Vue from 'vue';
import App from './App.vue';

import * as VueMaterial from 'vue-material';
import 'vue-material/dist/vue-material.css';

Vue.use(VueMaterial);

new Vue({
    el: '#app',
    template: '<App/>',
    components: {App}
});

アプリケーション全体を表すコンポーネントApp を定義しています.またこのコンポーネントApp は同じディレクトリのApp.vue に定義します.

import Vue from 'vue';

export default Vue.extend({
    name: 'app',
    props: {
    },
    components: { },
    data() {
    },
    methods: {
    },
    created() {
    },
});


<template lang="pug">
div#app Hello Electron-Vue-Typescript!!
</template>

<style>
</style>

<script> にコンポーネントの動作を定義するスクリプト(基本はJavascriptですが,lang=”ts” を指定することでTypeScriptでの記述が可能です)を,<template> に論理構造(基本はHTMLですが,lang=”pug” を指定することでやはりPugが利用可能です),<style> でスタイル(基本はCSSですが,lang=”stylus” を指定して,Stylusが利用可能です.またscoped を指定することで,このコンポーネントだけに適用させることも可能です.)を記述していきます.

ここではとくに処理をせず,Hello Electron-Vue-Typescript!! という文字列を表示するだけです.

望むのであれば,これらをそれぞれ別のファイルにすることも可能です.その方法は次回説明しましょう.

このように1つのファイルで1つのコンポーネントを定義したものをSingle File Component(SFC)といいます.Typescriptを使ってSFCの開発をするときimport App from ‘./App.vue’; の部分で型に関するエラーが出てしまいます.

[ts] Cannot find module './App.vue'.

解決策としては

declare module "*.vue" {
    import Vue from 'vue';
    export default Vue;
}

と記述したファイルsfc.d.ts (ファイル名はなんでもよいですが,拡張子は*.d.tsでなければなりません)をどこかに配置します.読み込まれるパスに配置すればどこでもよいようですが,ここではわかりやすくsrc/renderer 以下に配置しましょう.

ビルドスクリプト等々

さて,ここまででメインプロセス側,レンダラプロセス側ともコードの準備はできました.ファイルの配置は以下のようになっているはずです.

electron-sample
├── package.json
├── tsconfig.json
├── src
     ├── main
     │   └── main.ts
     └── renderer
        ├── App.vue
        ├── index.pug
        ├── index.ts
        └── sfc.d.ts

これらをビルドしたものをelectron-sample/dist 以下に出力したいのでそのようにスクリプトを書きましょう.

レンダラプロセス側はwebpackを利用して1つのファイルにまとめ上げ,メイン側は単にtsc コマンドを使います.そしてそれらをgulpによって処理してもらいます.

まずはレンダラプロセス側のファイルを処理させるwebpackの設定ファイルwebpack.config.js です.

module.exports = {
    entry: './src/renderer/index.ts',
    output: {
        filename: './renderer/index.js'
    },
    resolve: {
        extensions: ['.webpack.js', '.ts', '.js', '.css'],
        alias: {
            'vue$': 'vue/dist/vue.esm.js'
        }
    },
    target: 'electron',
    module: {
        rules: [
            { test: /\.ts$/, loader: 'ts-loader', options:{appendTsSuffixTo: [/\.vue$/]} },
            { test: /\.vue$/, loader: 'vue-loader' },
            { test: /\.(pug|jade)$/, loader: ["raw-loader", "pug-html-loader"] },
            { test: /\.css$/, loader: ["style-loader", "css-loader"] }
        ]
    }
}

*.ts ,*.vue ,*.pug に関してはプラグイン(ローダ)をインストールしてありますのでそのルールを記述します.

いっぽう,プロジェクトのタスク全体を処理させるgulpの設定ファイルgulpfile.js は以下です.

'use strict';

const gulp = require("gulp");
const ts = require('gulp-typescript');
const pug = require('gulp-pug');
const webpackStream = require("webpack-stream");
const webpack = require("webpack");
const ps = require('child_process').execSync;
const fs = require('fs');

const webpackConfig = require("./webpack.config");

// メインプロセス側
const tsProject = ts.createProject('tsconfig.json', () => {
    typescript: require('typescript')
});

gulp.task('compile-ts', () => {
    const options = {
    };

    return gulp.src('src/main/**/*.ts')
        .pipe(tsProject())
        .pipe(gulp.dest('dist/main'));
});

gulp.task('compile-pug', () => {
    return gulp.src('src/renderer/*.pug')
        .pipe(pug())
        .pipe(gulp.dest('dist/renderer'));
});

gulp.task('compile-renderer', () => {
    return webpackStream(webpackConfig, webpack).pipe(gulp.dest("dist"));
});

gulp.task("build", ['compile-ts', 'compile-pug', 'compile-renderer'], () => {
});

gulp.task("run", ["build"], () => {
    return ps('./node_modules/.bin/electron .');
});

gulp.task("default", ['build'], () => {
});

レンダラプロセス側はwebpackを使って,メインプロセス側はプロジェクトのtsconfig.json を使って処理するようにしています.

それ以外はとくに変わったところはないでしょう.

最終的にはこのような構成になりました.

electron-sample
├── gulpfile.js
├── package.json
├── src
│   ├── main
│   │   └── main.ts
│   └── renderer
│       ├── App.vue
│       ├── index.pug
│       ├── index.ts
│       └── sfc.d.ts
├── tsconfig.json
└── webpack.config.js

とりあえず実行

ビルドするだけならgulp とするだけでよいです.

ビルド後に実行までするならgulp run とします.

やってみましょう.

gulp run

とすると……

[00:59:38] Using gulpfile ~/dev/samples/electron-test/gulpfile.js
[00:59:38] Starting 'compile-ts'...
[00:59:38] Starting 'compile-pug'...
[00:59:38] Starting 'compile-renderer'...
[00:59:40] Finished 'compile-ts' after 2.26 s
[00:59:40] Finished 'compile-pug' after 2.27 s
[00:59:47] Version: webpack 3.8.1
              Asset    Size  Chunks                    Chunk Names
./renderer/index.js  697 kB       0  [emitted]  [big]  main
[00:59:47] Finished 'compile-renderer' after 9.21 s
[00:59:47] Starting 'build'...
[00:59:47] Finished 'build' after 6.24 μs
[00:59:47] Starting 'run'...

といった出力があり,

はじめてのElectronアプリ

できました!!(Ubuntu版)

おわりに

さて,Electron + Typescriptで最初のアプリケーションが作成できました!

実際に書かなくてはならないコードの量はかなり少なく(ただしパッケージングすると,node_modules をバンドルする必要性から百数十MiB程度のサイズになってしまいますが……),またmacOS,Linux系,Windowsでも動作させられるのでなかなか魅力的だと思います.またWebアプリをまるごとElectronアプリにパックする,というようなことも可能なようで,いろいろできそうですよね.

次回は今回登場させられなかったVue.jsを使って画面を作っていきたいと思います!

お楽しみに!では.