Electron を試す 11 – webpack によるビルド

2017年12月20日 0 開発 ,

これまで Web フロントエンドや Electron の JavaScript ビルドには Browserify と Babel を使用してきた。しかし Browserify の開発は停滞している。現在は個人から org 運営へ移管しており browserify/discuss にて開発者の募集や議論もおこなわれているものの、往時の勢いはない。

私としては CLI のみで完結して設定を package.json に集約しやすい点から Bundler の中では Browserify 推しだった。しかし今後のことを考えると webpack に移行したほうがよいと判断せざるをえない。

というわけで Electron プロジェクトの開発環境に webpack を導入してみた。以下はその覚書。

Browserify + Babel によるビルド

移行前に Browserify + Babel でおこなっていたビルド設定は以下。package.json 内で完結している。

{
  "babel": {
    "presets": [
      ["env", {"targets": {"electron": "1.7"}}],
      "react"
    ],
    "env": {
      "development": {
        "presets": [
          "power-assert"
        ]
      }
    }
  },
  "scripts": {
    "build:js-main": "browserify -t [ babelify ] ./src/js/main/Main.js --exclude electron --im --no-detect-globals --node -d | exorcist --base ./src/assets ./src/assets/main.js.map > ./src/assets/main.js",
    "build:js-renderer": "browserify -t [ babelify ] ./src/js/renderer/App.js --exclude electron -d | exorcist --base ./src/assets ./src/assets/renderer.js.map > ./src/assets/renderer.js",

    "watch:js-main": "watchify -v -t [ babelify ] ./src/js/main/Main.js --exclude electron --im --no-detect-globals --node -o \"exorcist --base ./src/assets ./src/assets/main.js.map > ./src/assets/main.js\" -d",
    "watch:js-renderer": "watchify -v -t [ babelify ] ./src/js/renderer/App.js --exclude electron -o \"exorcist --base ./src/assets ./src/assets/renderer.js.map > ./src/assets/renderer.js\" -d",

    "release:js-main": "cross-env NODE_ENV=production browserify -t [ babelify ] ./src/js/main/Main.js --exclude electron --im --no-detect-globals --node | uglifyjs -c warnings=false -m -d DEBUG=false > ./dist/src/assets/main.js",
    "release:js-renderer": "cross-env NODE_ENV=production browserify -t [ babelify ] ./src/js/renderer/App.js --exclude electron | uglifyjs -c warnings=false -m -d DEBUG=false > ./dist/src/assets/renderer.js"
  }
}

Main/Renderer プロセスで個別に Bundle。目的ごとに接頭辞で分類しており、それぞれ

  • build: 開発用ビルド
  • watch: 開発用ビルド、ファイル監視して更新検知したら差分ビルドする
  • release: リリース用ビルド

となる。かなり長くて複雑なオプションを指定しているが、これは Electron や Node のランタイム モジュールを除外するなどの処理が必要なため。詳細は

を参照のこと。webpack では同等かそれ以上の Bundle 処理を目指す。

webpack

まずは必要な npm のインストール。

$ npm i -D webpack babel-loader babel-minify-webpack-plugin
$ npm i -D babel-core babel-preset-env babel-preset-react

それぞれの役割をまとめる。

npm 役割
webpack Bundler。複数の JavaScript を設定に応じて結合する。
babel-loader webpack から Babel を呼び出して Transpile するためのモジュール。
babel-minify-webpack-plugin minify 用モジュール。webpack 的には uglifyjs-webpack-plugin のほうが一般的で標準プラグインでもあるのだが、Bundle 以外の処理は Babel ファミリーで揃えたかったのでこちらを採用。
babel-core Transpiler となる Babel の本体。
babel-preset-env ES.next で実装されたコードを指定された環境用に変換するためのモジュール。例えば Electron v1.7 向けにすると Electron 上でで有効なもの (ES2015 Classes など) はそのままに、不足しているものだけ代替コードに変換してくれる。
babel-preset-react React 関連 (JSX など) を変換してくれるモジュール。

Babel は Bundler から独立しているため、Browserify 時代の設定をそのまま流用可能。

webpack.config.babel.js

webpack の設定は webpack.config.js というファイルに JavaScript として実装する。この処理を ES2015 以降へ対応させたい場合は Babel をインストールしたうえで webpack.config.babel.js というファイル名にする。今回はこちらでゆく。

import WebPack from 'webpack'
import MinifyPlugin from 'babel-minify-webpack-plugin'

export default (env) => {
  const MAIN = env && env.main
  const PROD = env && env.prod

  return {
    target: MAIN ? 'electron-main' : 'web',
    entry: MAIN ? './src/js/main/App.js' : './src/js/renderer/App.js',
    output: {
      path: PROD ? `${__dirname}/dist/src/assets` : `${__dirname}/src/assets`,
      filename: MAIN ? 'main.js' : 'renderer.js'
    },
    devtool: PROD ? '' : 'source-map',
    node: {
      __dirname: false,
      __filename: false
    },
    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader'
          }
        }
      ]
    },
    plugins: PROD ? [
      new MinifyPlugin({
        replace: {
          'replacements': [
            {
              'identifierName': 'DEBUG',
              'replacement': {
                'type': 'numericLiteral',
                'value': 0
              }
            }
          ]
        }
      }, {}),
      new WebPack.DefinePlugin({
        'process.env.NODE_ENV': JSON.stringify('production')
      })
    ] : [
      // development
    ]
  }
}

内容について解説する。

エントリー ポイントを export default (env) => {...} としているが、これは Environment Variables を使用するためのもの。

Electron は少なくとも Main/Renderer の 2 種類をビルドする必要がある。それらを更に開発用とリリース用にわけるので単純に設定ファイルを定義すると 4 種類もの分岐が発生するうえ、設定や処理の大半は共通。

そのため設定ファイルは共通で外部引数によって処理を分岐する方式を採用した。Environment Variables はこの用途にうってつけの機能である。webpack を CLI から実行する際に

$ webpack --env.prod --env.main

のように --env.XXXX 形式の引数を与えると webpack のエントリー ポイントにした関数の第一引数に展開される。値名だけだと Boolean で true となり --env.XXXX=1 のように明示的に内容を指定することも可能。これを利用して npm-scripts 側は

{
  "scripts": {
    "build:js-main": "webpack --env.main",
    "build:js-renderer": "webpack",
    "watch:js-main": "webpack --env.main --watch",
    "watch:js-renderer": "webpack --watch",
    "release:js-main": "webpack --env.prod --env.main",
    "release:js-renderer": "webpack --env.prod"
  }
}

となる。--env.main により Main/Renderer、--env.prod でリリースと開発版を分岐している。これを受けて webpack 側は

export default (env) => {
  const MAIN = env && env.main
  const PROD = env && env.prod
}

とする。env を直に使わず別の変数に格納している理由は --env.XXXX が未指定だと undefined になるため env.XXXX を判定できないから。参照部分ごとに env の存在をチェックするのは冗長なので事前チェックして変数化しておく。

__dirname__filename

コード中に __dirname__filename が含まれていた場合、標準では webpack がコンテキストにあわせて解決しようとする。これは Node として実行される Main プロセスにおいて問題になる。例えば BrowserWindow.loadURL のパスに指定すると正しくファイルを読み込めない。そのため無効化して Node として処理されるようにする。

{
  node: {
    __dirname: false,
    __filename: false
  }
}

Electron だと __filename を使用することはなさそうだが、いざ参照したくなったときに Node と異なる動作をすると厄介なのでついでに設定しておく。これで開発版だけでなくリソースを asar にパッケージ化したリリース版イメージでも想定どおりパスが処理される。Renderer ではそもそも __dirname などを参照することはないが、使用しないものについて webpack 設定を分岐する意味はないため Main/Renderer 共通の設定としている。

target

webpack の Target 設定はビルド対象の仕向けを指定することで import/require を適切に処理してくれる。Electron 向けとしては

Option Description
electron-main Compile for Electron for main process.
electron-renderer Compile for Electron for renderer process, providing a target using JsonpTemplatePlugin, FunctionModulePlugin for browser environments and NodeTargetPlugin and ExternalsPlugin for CommonJS and Electron built-in modules.

の 2 種類をサポートしている。Main プロセスをビルドする場合、そのまま Node/Electron のモジュールを Bundle すると参照エラーになる。よって electron-main を指定して実行時に参照解決されるようにする。

Renderer プロセスについては electron-rendererweb を指定する。前者だと webpack プラグインと組み合わせることで Node/Electron モジュールの参照を適切に処理してくれる。しかし私は Renderer 側で Node/Electron モジュールを使用しない主義であり、Main プロセスとの通信に必須の ipcRenderer に限定している。

Node/Electron の機能が必要ならば Main プロセスにリクエストすればよい。処理結果も Main プロセスから Renderer プロセス側へ返せる。よって Renderer の targetweb とした。

{
  target: MAIN ? 'electron-main' : 'web`
}

Renderer の require について。ipcRenderer を使用するために require が必要となる。しかしこれは直に使用せず

const ipc = window.require('electron').ipcRenderer
ipc.send('message', 'Hello world!!')

というように window に属するものとして明示的に呼び出す。こうすると Web フロントエンドとしての require or import と Node/Electron 由来の参照を明確に区別できる。そのため targetweb でも問題ない。これは Borwserify 時代からの対応だが、設計面でも Bundler 処理的にも好ましいと考えており、実際に webpack でもそのまま維持できた。

minify と環境変数 production

使用してる npm の項でも触れたが、今回のビルド設定では minify に babel-minify-webpack-plugin を使用している。これは babel-minify の wrapper である。これはかつて babili と呼ばれており、本ブログでも

で紹介したことがある。Babel ファミリーのツールだけあって ES.next を考慮した minify を実行してくれる。

Browserify と組み合わせると Babel (babelify) から先に実行されるため、Bundler 部分のコードが minify されない。単独 CLI だと Babel 処理に組み込めない。…といった問題があってイマイチだったのだが webpack だと Bundler 処理の一部として実行されるため、問題がすべて解決されている。

webpack の設定としては

{
  plugins: PROD ? [
    new MinifyPlugin({
      replace: {
        'replacements': [
          {
            'identifierName': 'DEBUG',
            'replacement': {
              'type': 'numericLiteral',
              'value': 0
            }
          }
        ]
      }
    }, {}),
    new WebPack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production')
    })
  ] : [
    // development
  ]
}

となる。第一引数に babel-minify のパラメーター、第二引数でプラグイン独自のパラメーターを指定する。今回はデバッグ時の if-def 変数 DEBUG を無効化するようにした。この変数については

で言及しているので詳細はそちらを参照のこと。簡単に要約すると

if (DEBUG) {
  console.log('DEBUG MODE!!!')
}

みたいにデバッグ専用の処理を定義しておき、リリース用ビルドで処理をまるごと除去するためのもの。古式ゆかしいコンパイル分岐である。

リリース用ビルドにおける production 環境変数について。

npm によっては process.env.NODE_ENVdevelopmentproduction であることを判定してコンパイル分岐してる。本来ならさきほど紹介した例もこの方法で実装すべきだったが、それは将来の課題ということで。

process.env.NODE_ENV を変更する場合、CLI 実行であれば cross-env か webpack 処理で代入することになるのだが、標準プラグイン DefinePlugin で対応するほうがよいだろう。

DefinePlugin にしておけば他に環境変数を設定したいときも、ここへ局所化できる。webpack 公式でも Production の Specify the Environment でこの方法を紹介している。

これらの設定で minify すると適切に圧縮され、mangle による副産物としての難読化もおこなわれていることが確認できるはず。

original-fs

音楽プレーヤーのサンプルで Electron の original-fs を参照していたのだが、webpack の targetelectron-main にしていてもこれがエラーになる。

ERROR in ./src/js/main/model/MusicMetadataReader.js
Module not found: Error: Can't resolve 'original-fs' in '.../examples-electron/audio-player/src/js/main/model'

Electron は 2 種類の fs を提供しており original-fsasar をサポートしない Node 本来のもの。その処理を実装していた当時は Electron 版 fs だとなにか問題が起きると勘違いして明示的に original-fs を使用したほうがよいと考えていた。

しかし asar に関する処理をしないのであれば fs でもよいため、そのように修正した。

original-fs が本当に必要となったときは困りそうだから、後で webpack の issues を探してこの問題が報告されていなければレポートを挙げるかもしれない。

まとめ

webpack を使用しはじめたのは最近であること、Electron アプリで Bundler 処理する場合は Node と Web フロントエンドの両方を考慮しなければならないことから、かなり不安があった。

しかし Browserify 関連のツール チェインで要件を洗い出せていたのは大きかった。この種のツールに戸惑うのは要件が定まっていないからである。先に目的を明確にしていれば利用や代替はさほど難しくない。必要最小でよいのだし。今回もそんな感じでわりとスムーズに移行できた。

あと webpack の Environment Variables 機能のおかげで想像よりもずっとシンプルな設定にできて嬉しい。webpack を npm-scripts から実行する派ならファイル分岐を減らすのに効果抜群なのでかなりオススメ。

そういえば Electron サンプルとして examples-electron を公開してるけど、ずっと 3 プロジェクトなのもさみしいので簡易ファイラーを追加したいと考えてる。前から自分用に画像ビューアーがほしいと思ってて、その習作としてもよいのでは?というのがその理由。

ただ examples-electron は他にも

  • Flux を material-flux から reduxalmin に移行
    • 機能的に不満はないが Browserify から webpack へ移行したように一般的で勢いのあるものにしたい
    • redux は既に一般教養となってるから好みとは別に触れておくべきだろう
    • redux を class base にしたような almin も興味ある、なにしろ material-flux と同じ作者だ
  • AltCSS を Stylus 以外へ移行
    • AltCSS 系では Stylus が最も気に入っているけど開発側の熱は失われかけているため移行を検討したい
    • 2017/12 時点だと postcss が有力候補だろうか?
    • ただし Stylus 単体で実現していた機能を postcss のプラグインをかき集めて実現するのは面倒そうだ
    • 大きな問題が起きるまでは Stylus でよい、という考えもある
  • Test Runner を ava へ移行
    • 現在は mocha だが、これと組み合わせてる power-assert を ava が内部利用してるので出力機能としては同等
    • ava の機能としては並列実行に惹かれる
    • mocha も mocha.parallel があるけど ava だと単体で多機能なのでツール チェインを削減できる

といった課題がある。足回りを先にしたほうが生産性あがるのでサンプル追加より環境面を優先するかもしれない。その場合 redux/alumin 移行が作業と学習量として重く時間かかりそう。

Electron を試す 10 – Main プロセスのデバッグ

2017年8月23日 0 開発 ,

このシリーズ第一回node-inspector を利用した Main プロセスのステップ実行を紹介した。しかし node-inspector は現在、非推奨である。Node v6 でサポートされた --inspect を使用することになっている。

Node 同様に Electron も v1.7.2--inspect が追加された。つまり Node/Electron 共に単体でステップ実行を利用できる。現時点の latest version は v1.7.5 なので開発者としても正式にこの機能を採用する時がきた。

というわけで、Electron が提供するデバッグ機能についてまとめる。

サンプル プロジェクトは以下。

Chrome

Electron 公式サイトに Main プロセスをデバッグするための方法が掲載されている。

Debugging the Main Process | Electron

Electron CLI を実行する際に --inspect=[port] オプションを指定することで、Electron の Main プロセスと外部デバッガーを連携できる。例えば

electron --inspect=5858 your/app

のようにする。package.json の npm-scripts に定義する場合は

{
  "scripts": {
    "app": "electron --inspect=5858 src/"
  }
}

こんな感じになる。実際に Terminal から実行すると

$ npm run app

> electron-audio-player@1.4.0 app .../examples-electron/audio-player
> electron --inspect=5858 src/

Debugger listening on port 5858.
Warning: This is an experimental feature and could change at any time.
To start debugging, open the following URL in Chrome:
    chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:5858/ee7b65f0-dc0d-46f7-9d9e-997441981f52

外部デバッガーとして使用する Chrome 用の URL が出力される。これを Chrome のアドレス バーに入力してページを表示すれば DevTools が割り当てられる。

--inspect と Chrome

あとは普段 DevTools を利用しているように Sources タブからブレーク ポイントを貼ってステップ実行したり、変数の内容を参照するなどのデバッグが可能となる。

なお --inspect=[port]=[port] は省略可能。その場合は自動的にポート 5858 が選択されたものとして扱われる。省略するかはお好みで。開発環境によってはポート番号が競合するかもしれないので、変更可能であることを明示するため規定値でもそのまま指定しておくのもよいだろう。私はそうしている。

Visual Studio Code

Atom と並んで Web アプリ開発者に人気のテキスト エディター Visual Studio Code (以下、vscode) にはデバッグ機能が実装されている。これは vscode の大きな特徴であり、エディターにも関わらず IDE のようなデバッグを可能とする。

これは汎用的に設計してあり、様々な言語へ対応できるようになっている。Microsoft 謹製の TypeScript や C# は当然として Python、Ruby、PHP といった言語から Node (JavaScript) まで幅広くサポート。もちろん Electron からも利用可能。

Main プロセスを素の Node で実装している場合は上記の公式資料にある設定だけで使い始められる。しかし Babel などの Transpiler、Browserifywebpack といった Bundler を利用して JavaScript を変換しているならば対象となるコードを指定するために工夫が要る。

vscode のデバッグ機能と設定ファイルの詳細は以下を参照のこと。

launch.json

はじめに vscode 用の設定ファイルを定義する。vscode で読み込むプロジェクトのルートに .vscode フォルダーを作成し、その中へ launch.json というファイルを作成。内容は以下。

{
  // Use IntelliSense to learn about possible Node.js debug attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Main Process",
      "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
      "windows": {
        "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd"
      },
      "program": "${workspaceRoot}/src/js/main/Main.js",
      "outFiles": [
        "${workspaceRoot}/src/assets/main.js"
      ]
    }
  ]
}

configurations 内に設定を定義してゆく。

type はデバッグ実行のプラットフォームを表す。Electron 用の設定ではなく Node として実行する。request は新たにプロセスを起動する launch と起動されているものを利用する attach のいずれかを指定する。基本、launch でいい。name は役割で命名しておく。

${workspaceRoot} は特別な変数で、プロジェクトのルート フォルダーをあらわす。基本的に設定はこのフォルダー内で完結するだろうから、パスはこれで開始しておけばよい。

runtimeExecutable はデバッグ実行で使用する Electron のパスになる。Windows だけ起動方法が変わる (cmd.exe 経由になる) ため windows 内へ個別に定義が必要。

program はデバッグ実行のエントリー ポイントになるコードを指定する。Transpiler を利用しているなら変換元のエントリー ポイントになるファイルを指定する。変換先を指定しても動作するが、Bundler で複数ファイルを単一化しているとブレーク ポイントを貼りにくくなる。

outFilesprogram と関連付けるファイル一覧を定義。Transpiler を利用しているなら変換先のエントリー ポイントになるファイルを指定する。以前は sourceMapstrue に指定する必要もあったが、現在はデフォルトで有効になっている。Transpiler/Bundler と Source Maps を利用した最小の設定としては今回のもので十分。

他にもデフォルトが変更されて省略可能なものがあったり、outDir が deprecated で outFiles が代替になっているなどの変化がある。この記事ですら古びるかもしれないので、自分で設定する際は前述の公式資料を必ず確認すること。

Source Maps の注意点

私の環境は Babel + Browserify になる。元のコードは ES.next (例えば ES2015 の Modules など) で書いて、Babel により Electron 向けに変換している。これらのタスクは package.json の npm-scripts で組んでおり、デバッグに関連する定義は以下となる。

{
  "babel": {
    "presets": [
      ["env", {"targets": {"electron": "1.7"}}],
      "react"
    ]
  },
  "scripts": {
    "watch:js-main": "watchify -v -t [ babelify ] ./src/js/main/Main.js --exclude electron --im --no-detect-globals --node -o \"exorcist --base ./src/assets ./src/assets/main.js.map > ./src/assets/main.js\" -d"
  },
  "devDependencies": {
    "babel-preset-env": "^1.6.0",
    "babel-preset-power-assert": "^1.0.0",
    "babel-preset-react": "^6.24.1",
    "babelify": "^7.3.0",
    "browserify": "^14.4.0",
    "electron": "^1.7.5",
    "exorcist": "^0.4.0",
    "watchify": "^3.9.0"
  },
}

watch:js-main が browserify (watchify) によるファイル監視つきの JavaScript 変換になる。

browserify により変換された内容を exorcist に渡して Source Maps となる main.js.map を生成し、その後に変換結果となる main.js を出力している。

これらのうち、vscode のデバッグで重要なのは Source Maps にあたる map ファイルである。map ファイルは変換元ファイルのパスとコード行などが定義されている。vscode のデバッグ機能で map ファイルを使用する場合、変換元ファイルのパスは map からの相対にしなければならない。

exorcist の README によれば、オプションを何も指定しないと変換元ファイルのパスは absolute path (絶対パス) になる。実際には exorcist を実行したフォルダーをルートにした相対パスになるようだが、どちらにせよ map ファイルから見た変換元ファイルへのパスにはならない。

この状態で vscode のデバッグを開始すると変換元を見つけられず program に指定されたファイルをそのまま解釈してしまう。デバッガーは Node として動作するため、現時点で未対応の import 構文などを見つけると Syntax Error 例外でプロセスが中断される。

これを防ぐため exorcist の --base オプションに map と変換されたファイルの出力先フォルダーを指定。変換元ファイルへのパスがそこから見た相対となるようにした。

exorcist --base ./src/assets ./src/assets/main.js.map > ./src/assets/main.js

--base [PATH] の有無で map ファイルの sources が変化することを確認できる。

デバッグ実行

すべての設定が正しく定義されていれば vscode のデバッグ機能を利用できる。JavaScript 変換 & map ファイル生成された状態で vscode のメニューから「デバッグ」、「デバッグの開始」を選ぶとアプリが起動されるはず。

次に vscode でブレーク ポイントを貼りたいファイルを開き、対象行の行番号の左側をクリックすると赤い丸印アイコンが表示される。

ブレーク ポイント

この状態でその場所を通るような操作をアプリから実行すると、その箇所でプロセスが一時停止 (ブレーク) する。このとき vscode のサイドバーからデバッグを開いていれば、変数なども確認できる。

デバッグ実行

コード編集に使用しているエディターとデバッガーがひとつになっていることの便利さ (IDE 感) を実感。これはいい。

userData のパス問題

Electron でアプリ固有のデータを保存する場合は app.getPath('userData') で得られたフォルダーを利用することが多いだろう。このパスは Electron の Web Storage や IndexedDB などを保存する場所で、OS のユーザー単位かつアプリ単位で作成される。

userData には

The directory for storing your app’s configuration files, which by default it is the appData directory appended with your app’s name.

とある。この説明なら package.jsonname が参照されることを期待するけど、実際にはアプリの実行ファイル名が使用されるようだ。例えば AudioPlayer.appAudioPlayer.exe
であれば AudioPlayer というフォルダーになる。

Chrome によるデバッグでは Electron プロセスを単体実行して Chrome と関連付けているだけなので、userData は通常のアプリと同じ場所になる。しかし vscode のデバッグ実行だとフォルダー名は Electron 一択となる。

そのため複数のアプリでデバッグ実行している場合、データが競合するかもしれない。これに気付いたのは、これまで --inspect を利用して IndexedDB などに保存していたデータが vscode のデバッグだと読み込めていなかったから。userData のパスを調べたら、どのプロジェクトでも Electron になっていた。app.getName()Electron を返す。

かなり致命的な問題と思うのだけど、ざっと調べたかぎり vscode 公式のデバッグ関連資料や GitHub issues にはこれについての言及が見つからなかった。後で issue あげるかも。

いちおう app.setPath('userData', path) でパスの上書きは可能だが app.getName()Electron なのでアプリ名を抽象化しきれず、ハード コードが発生する。そもそも開発環境の都合でアプリ側のコードが左右されるのも好ましくない。

この件について情報をお持ちの方は、コメント欄や Twitter などで知らせていただきたい。

Electron を試す 9 – Babel 変換を最小におさえつつ minify

2017年6月5日 0 開発 , ,

小ネタ。

Electron が採用している Chromium は ECMAScript 対応がかなり進んでいる。よって Babel を使用しつつも変換を最小におさえたくなる。

この点について以前 babel-preset-env と minify という記事を書いたのだが、uglify-js の ES2015 以降への対応が暫定版なため、よりよい選択肢として babel-preset-babili を試してみた。その記事のコメントで mysticatea さんが提案されているように Browserify へ -g オプションをつければ node_modules 部分も含めて minify 可能だが、それでも Browserify の Bundle 処理は minify されない。

よって uglify-js harmony 版が正式リリースされるのを待っていたところ、uglify-es が提供されたので試してみる。

uglify-es

従来 uglify-js で harmony と呼ばれていた ES2015 以降へ対応する仕向け。README によれば uglify-js@3.x 系に対して API と CLI 互換がある。CLI 名も uglifyjs のまま。よって最新の uglify-js を使用しているなら、特に処理を変えず移行可能。

移行は package.jsondevDependencies で uglify-js を uglify-es へ置き換えればよい。uglify-js@3.x から提供となるので、バージョン系は 3.x 以降となる。安全のため npm un -D uglify-js してから npm i -D uglify-es するのがよいだろう。

余談だが Support UglifyJS 3 と関連 issue にて webpack の uglify-es 対応が検討されている。難航しているようだ。

babel-preset-env

uglify-es により ES2015 以降を解析可能となるため、Babel 変換も最小にする。babel-preset-env へ開発で使用している Electron のバージョンを指定。以下は package.json の例。.babelrc なら "babel" プロパティの値をルートにする。

"babel": {
  "presets": [
    [
      "env",
      {
        "targets": {
          "electron": "1.6"
        }
      }
    ]
  ]
}

この設定で akabekobeko/examples-electron のプロジェクトをビルドして classconstlet といった ES2015 以降の機能はそのままに uglify-es で minify されることを確認できた。

問題点

uglify-es は ES2015 なら対応しているけれど async/await を含むコードはエラーになる。試しに async function – JavaScript | MDN のサンプルを含めてみたところ、

Parse error at 0:2932,6
async function add1(x) {
      ^
ERROR: Unexpected token: keyword (function)
    at JS_Parse_Error.get (eval at <anonymous> (.../examples-electron/audio-player/node_modules/uglify-es/tools/node.js:21:1), <anonymous>:86:23)
    at fatal (.../examples-electron/audio-player/node_modules/uglify-es/bin/uglifyjs:286:52)
    at run (.../examples-electron/audio-player/node_modules/uglify-es/bin/uglifyjs:241:9)
    at Socket.<anonymous> (.../examples-electron/audio-player/node_modules/uglify-es/bin/uglifyjs:179:9)
    at emitNone (events.js:110:20)
    at Socket.emit (events.js:207:7)
    at endReadableNT (_stream_readable.js:1045:12)
    at _combinedTickCallback (internal/process/next_tick.js:102:11)
    at process._tickCallback (internal/process/next_tick.js:161:9)

となった。ちなみに Browserify も以前は es6 async class function fails to parse? という問題があったが修正済み。uglify-es の issue [ES8] async/await not supported also in harmony を読むに、近く修正されそうな気がする。

まとめ

async/await 対応の問題はあるものの、babel-preset-env + uglify-es の組み合わせで Electron 向けの Babel 変換を最小におさえられた。

Chrome Canary 60 はフラグ付きで ES Modules が有効になるそうで、これが Electron に採用されたら Bundle と minify 事情も変わりそう。しかし node_modules も含めたサイズ圧縮の観点から Bundle と minify はしばらく必要な処理なので、今後も動向は継続的にチェックしたい。