Electron Hands On

MURAKAMI Masahiko edited this page Nov 6, 2015 · 5 revisions

Electronハンズオン資料

目的

  • Electron アプリ開発を体験してみる。

ハンズオンでやること

  1. Electronアプリの開発環境セットアップ
  2. Electronアプリのプロジェクト作成
  3. MarkdownをHTMLにしてElectronのウインドウに表示する
  4. ウインドウに表示したHTMLをPDFに変換する
  5. Electronアプリのビルド

事前準備

次が使える環境を作っておく。

  • Node.js
  • NPM
$ node -v
v4.2.1
$ npm -v
2.14.7

となっていればOK

Electronアプリの開発環境セットアップ

公式サイトにあるように次のコマンドを実行。

npm install electron-prebuilt -g

プロジェクトの作成

公式サイトの Quick Start をやってもいいんだけど、yo つかって雛形から作ったほうが楽なので。 yo 使います。そんなにいっぱいファイルは生成されないので、生成されたファイルを眺めてもいいと思う。

$ npm install yo generator-electron -g
$ mkdir electron-md2pdf && cd electron-md2pdf
$ yo electron

アプリを起動する

生成したプロジェクトのアプリを次のコマンドで起動してみてください。

$ npm start

次のようなウインドウが表示されればOKです。

Electron boilerplate

package.json を見ると一目瞭然ですが electron . を実行して起動しています。

作成されたファイル

作成されたプロジェクトのファイルでElectronアプリで重要なファイルは次のとおりです。 アプリを起動するとindex.jsが実行されます。

  • package.json - Nodeモジュールのpackage.json。
  • index.html - ウインドウに表示される内容のHTMLファイル
  • index.js - メインプロセスで実行されるJavaScriptファイル。

MarkdownをHTMLにしてElectronのウインドウに表示する

雛形を作成して起動できることが確認できたので、次はMarkdownをプレビューする機能を追加します。 プレビューするMarkdownmファイルはファイル選択ダイアログを表示して選ぶようにします。

次の順に実装していきます。

  1. Markdownファイルを開くメニューを追加する
  2. メニューがクリックされたらファイル選択ダイアログを表示する
  3. 選択されたファイルをHTMLに変換する
  4. 変換したHTMLを表示する
  5. 表示しているHTMLをPDFで保存する

Markdownファイルを開くメニューを追加する

まず、Markdownファイルを開くメニューを追加します。Electronでメニューを追加するにはMenuクラスとMenuItemクラスを使います。 直接これらのクラスのコンストラクタ関数を使ってメニューを動的に構築することができます。 ある程度固定のメニューならテンプートとなるオブジェクトを指定して一気にメニューを作成できます。

今回は、後者の方法でメニューを作ってみます。 以下の内容をmenu.jsという名前で作って下さい。

'use strict';

const Menu = require('menu');

const template = [
  {
    label: 'View',
    submenu: [
      {
        label: 'Reload',
        accelerator: 'CmdOrCtrl+R',
        click: function(item, focusedWindow) {
          if (focusedWindow) {
            focusedWindow.reload();
          }
        }
      },
      {
        label: 'Toggle Full Screen',
        accelerator: (function() {
          if (process.platform == 'darwin') {
            return 'Ctrl+Command+F';
          } else {
            return 'F11';
          }
        })(),
        click: function(item, focusedWindow) {
          if (focusedWindow) {
            focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
          }
        }
      },
      {
        label: 'Toggle Developer Tools',
        accelerator: (function() {
          if (process.platform == 'darwin') {
            return 'Alt+Command+I';
          } else {
            return 'Ctrl+Shift+I';
          }
        })(),
        click: function(item, focusedWindow) {
          if (focusedWindow) {
            focusedWindow.toggleDevTools();
          }
        }
      },
    ]
  },
  {
    label: 'Window',
    role: 'window',
    submenu: [
      {
        label: 'Minimize',
        accelerator: 'CmdOrCtrl+M',
        role: 'minimize'
      },
      {
        label: 'Close',
        accelerator: 'CmdOrCtrl+W',
        role: 'close'
      },
    ]
  },
  {
    label: 'Help',
    role: 'help',
    submenu: [
      {
        label: 'Learn More',
        click: function() { require('shell').openExternal('http://electron.atom.io') }
      },
    ]
  },
];

if (process.platform == 'darwin') {
  const app = require('app');
  const name = app.getName();
  template.unshift({
    label: name,
    submenu: [
      {
        label: 'About ' + name,
        role: 'about'
      },
      {
        type: 'separator'
      },
      {
        label: 'Services',
        role: 'services',
        submenu: []
      },
      {
        type: 'separator'
      },
      {
        label: 'Hide ' + name,
        accelerator: 'Command+H',
        role: 'hide'
      },
      {
        label: 'Hide Others',
        accelerator: 'Command+Shift+H',
        role: 'hideothers'
      },
      {
        label: 'Show All',
        role: 'unhide'
      },
      {
        type: 'separator'
      },
      {
        label: 'Quit',
        accelerator: 'Command+Q',
        click: function() { app.quit(); }
      },
    ]
  });
  // Window menu.
  template[3].submenu.push(
    {
      type: 'separator'
    },
    {
      label: 'Bring All to Front',
      role: 'front'
    }
  );
}

const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);

次にmenu.jsを呼び出すコードをindex.jsに追加します。

app.on('ready', () => {
	mainWindow = createMainWindow();
	require('./menu'); // この行を追加する
});

追加できたら、アプリを起動して定義したメニューが表示されるか確認してみてください。 メニューを設定しない場合(デフォルト)の場合との違いを比べてみるといいと思います。

メニューのテンプレートに指定するプロパティで作成するメニューの構造やメニューアイテムの種類を指定できます。メニューのテンプレートに指定できるプロパティの詳細はMenuItemのコンストラクタ関数のドキュメントに書かれています。 ここでは、今回のアプリに関係するものだけ紹介します。

  • label - メニューアイテムに表示される文字列です。
  • click - メニューアイテムがクリックされた時に呼び出される関数です。呼び出される関数はclick(menuItem, BrowserWindow)です。
  • role - 事前に定義されたアクションを指定します。
  • submenu - ネストしたメニューを定義します。

では、Markdownファイルを選択するダイアログ表示するメニューをテンプレートに追加していきます。 menu.jstemplate配列の先頭に次にオプジェクトを追加します。

{
  label: 'File',
  submenu: [
    {
      label: 'Open',
      accelerator: 'Cmd+O',
      click: function(item, focusedWindow) {
        console.log('open markdown file.');
      }
    }
  ]
},

追加したら、アプリを起動してFileメニューとOpenメニューアイテムが表示されるか確認してください。 Openメニューアイテムをクリックするとコンソールにopen markdown file.とログが出力されるかも確認してください。 Fileを開くメニュー

ファイルを開くダイアログを表示する

確認できたら、メニューアイテムがクリックされた時のコールバック関数でファイル選択ダイアログを表示する処理を追加してきます。 ダイアログの表示にはdialogを使います。

menu.jstemplate配列の直前くらいに次のコードを追加します。 dialog.showOpenDialog関数でファイルを選択するダイアログを表示します。戻り値は選択されたファイルのパスです。 キャンセルされた場合はundefinedが返ります。

const dialog = require('dialog');

function showFileOpenDialog() {
  return dialog.showOpenDialog(
    {
      properties: ['openFile'],
      filters: [
        { name: 'Markdown', extensions: ['md'] },
        { name: 'All Files', extensions: ['*'] }
      ]
    }
  );
}

上で定義したshowFileOpenDialog関数をOpenメニューアイテムがクリックされた時に呼び出すために、click関数を次のように修正します。

click: function(item, focusedWindow) {
  console.log('open markdown file.');
  const file = showFileOpenDialog();
  if (file) {
    console.log('opne ' + file);
  }
}

選択されたMarkdownファイルをHTMLに変換して表示する

選択されたファイルのパスを取得するまでできたので、次は、Markdownファイルを開いてHTMLファイルに変換します。 今回はMarkdownファイルをHTMLに変換する処理にmarkedを使います。 markedが出力するHTMLにはhtmlタグなどは含まれていないので、テンプレードとなるHTMLにmarkedが出力するHTMLを埋め込んでHTMLファイルを作成します。 今回、テンプレートエンジンにはECTを使います。

次のコマンドでmarkedECTをインストールします。

$ npm install marked ect --save

次の内容をmd2html.jsとして作成します。Electronとは直接関係しない部分なので詳細は省きます。

'use strict';

const fs = require('fs');
const path = require('path');
const os = require('os');
const marked = require('marked');
const ECT = require('ect');

function md2html(filePath, cb) {
  fs.readFile(filePath, 'utf8', (err, mdString) => {
    if (err) {
      cb(err);
      return;
    }
    const content = marked(mdString);
    const renderer = ECT({ root : __dirname });
    const data = { title: filePath, content : content };
    const html = renderer.render('template.ect', data);
    cb(null, html);
  });
}

function toHtmlFile(mdFilePath, cb) {
  md2html(mdFilePath, (err, html) => {
    if (err) {
      cb(err);
      return;
    }
    const name = path.basename(mdFilePath, path.extname(mdFilePath))
    const htmlPath = path.join(os.tmpdir(), name + '.html');
    fs.writeFile(htmlPath, html, (err) => {
      cb(err, htmlPath)
    });
  });
}

exports.toHtmlFile = toHtmlFile;

次の内容をtemplate.ectとして作成します。

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title><%= @title %></title>
  </head>
  <body>
    <%- @content %>
  </body>
</html>

上で定義したtoHtmFile関数をファイルが選択された場合に呼び出すようにします。

click: function(item, focusedWindow) {
  const file = showFileOpenDialog();
  if (file) {
    require('./md2html').toHtmlFile(file[0], (err, htmlFilePath) => {
      if (err) {
        throw err;
      }
      focusedWindow.loadUrl(`file://` + htmlFilePath);
    });
  }
}

コールバック関数の引数に変換後のHTMLファイルのパスが指定されるので、ウインドウに表示するようにします。 click関数の引数にfocusedWindowBrowserWindowオブジェクトがしてされるのでwin.loadUrl()メソッドを使ってファイルの内容を表示します。

修正が全て終わったら、今作っているElectronアプリのreadme.mdを表示してみましょう。 きっと、次のように表示されるはずです。

readme.mdを表示

ウインドウに表示したHTMLをPDFに変換する

それでは、表示されたHTMLをPDFとして保存できるようにしましょう。 まずは、メニューにPDFファイル保存のメニューアイテムを追加します。

menu.jstemplateオブジェクトのOpenメニューアイテムのあとに次のオブジェクトを追加します。

メニューアイテムがクリックされた時に呼び出されるclick関数ではshowFileSaveDialog()関数でファイル保存ダイアログを表示して入力されたファイルパスを取得します。そのあと、saveAsPDF()関数で指定したファイルパスに表示しているHTMLをPDFとして保存します。

{
  label: 'SaveAsPDF',
  accelerator: 'Shift+Cmd+O',
  click: function(item, focusedWindow) {
    const file = showFileSaveDialog();
    if (file) {
      saveAsPDF(focusedWindow, file, (err) => {
        if (err) {
          dialog.showErrorBox('Save Error', err.toString());
        }
      })
    }
  }
}

それではmenu.jsshowFileSaveDialog()関数を追加します。 この関数はElectronのdialog.showSaveDialog関数呼び出して選択されたファイルを返します。

function showFileSaveDialog() {
  return dialog.showSaveDialog(
    {
      filters: [
        { name: 'PDF', extensions: ['pdf'] },
        { name: 'All Files', extensions: ['*'] }
      ]
    }
  );
}

さらにmenu.jsshowFileSaveDialog()関数を追加します。 この関数ではWebContents.printToPDF()を呼び出してPDFに変換します。変換されたPDFのデータはコールバック関数の引数に指定されるのでfs.writeFile()関数で指定のパスにファイルとして保存します。 WebContentsはWebページのレンダリングやコントロールを行うオブジェクトです。

function saveAsPDF(win, path, cb) {
  win.webContents.printToPDF({}, function(error, data) {
    if (error) {
      cb(error);
      return;
    }
    require('fs').writeFile(path, data, cb);
  })
}

PDFで保存する処理を追加したので、Markdownファイルを表示してPDFとして保存してみましょう。PDFとして保存できたらPDFのビュアーで表示してHTMLの内容が反映されているか確認してみてください。

Electronアプリの配布

Electronアプリが完成したら配布できるようにします。ElectronアプリはOS X、Windows、Linuxで実行できます。配布する形式のファイルを作成する作業の内容は公式ドキュメントに記載されていますが、手作業では結構面倒です。そこでこれらの作業をやってくれるelectron-packagerというツールを使ってアプリを配布できるようにします。 generator-electronで作成したプロジェクトにはすでにelectron-packagerがインストールされているので次のコマンドを実行するだけです。

$ npm run-script build

コマンドの実行が完了するとdistディレクトリに各プラットフォーム向けの実行ファイル(app, exe, ELF)を含むディレクトリが作成されます。 作成されたディレクトリ内の実行ファイルを実行してアプリを起動させてみましょう。環境が用意できる場合はビルドしたOSとは別のOS向けのものがちゃんと動作するかも見てみましょう。

npm run-script buildでどんなコマンドが実行されるかはpackage.jsonscripts.buildのコマンドを見てみると分かります。 指定されているオプションを調べてみるとビルド内容を変えたい時に役にたつと思います。

以上で今回のハンズオンの内容は終了です。

ここまでのハンズオンの完成版は こちら です。 参考にしてください。

時間のあまった人はやってみて

  • PDF出力時のオプションを変えてみる。
  • CSSを適用してみる(github-markdown-cssとか)。
  • 画像などが含まれたMarkdownファイルをプレビューできるようにしてみる(ここまでの実装だと表示できないです)。
  • 複数のMarkdownファイルを結合して一度に表示してみる。

参考資料

Clone this wiki locally
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.