JavaScript
reactjs
webpack
Electron
redux

Electron + React + Redux でボイラープレート作ってみた

More than 1 year has passed since last update.

先日、React+Redux入門 - Qiita という記事を書きました。さて、React+Reduxと同じように流行っているイケているフレームワークと言えば、Electronですよね。Electron + React + Reduxのボイラープレートを作ってみましたので、今日はその解説をいたします。

内容

Visual Studio Codeでなるべくいい感じに開発出来るように目指したもので、内容としては単純なカウントアップのサンプルコードになっています。Electronの弱点である配布ファイルがでかくなりすぎる問題の対応のためファイル結合をwebpackで行っています。webpackではSassやJSONなど様々なファイルを結合することができます。

gulpなどは使用しておらず、watchやbuildの為のJSスクリプトが付属します。

構成

  • 言語・ライブラリ・フレームワーク
    • ES2015/JSX
    • Electron
    • React + Redux
  • 開発・ビルド環境
    • webpack
    • electron-connect
    • babel
    • eslint
    • jsbeautify
    • editorconfig
    • power-assert + mocha
    • electron-packager

動かし方

導入

$ mkdir <プロジェクトの名前>
$ cd <プロジェクトの名前>
$ git init
$ git remote add upstream git@github.com:erukiti/tpl-electron-react-redux.git
$ git pull upstream master
$ npm i

開発用の確認

$ npm start

webpackのwatchモードが起動してElectronのアプリケーションが立ち上がります。ファイルを編集したらアプリケーションがリスタートします。

テスト

$ npm test

ユニットテストが走ります。power-assert + mocha に対応しています。

配布用パッケージ作成

$ npm run build

release ディレクトリ下に配布パッケージが作成されます。

  • MacOS 64bit
  • Windows 32bit
  • Windows 64bit

解説

前述しましたが、gulpなどは使わず、watchやbuild用のスクリプトが付属していて、JSのコードとしてwatch/buildが走ります。

watch

webpackをrequireしてソースファイルが更新されたらelectron-connectを使って再起動をするようにしたスクリプトです。gulpを使って実現する場合に比べて再コンパイルが速いです。

'use strict'

const webpack = require('webpack')
const conf = require('./webpack.config.js')

const compiler = webpack(conf)

let electron = null

compiler.watch({}, (err, stats) => {
    if (err) {
        // FIXME
        console.dir(err)
        return
    }

    if (stats.hasWarnings()) {
        stats.compilation.warnings.forEach(warning => {
            // FIXME
            console.dir(warning)
        })
    }

    if (stats.hasErrors()) {
        stats.compilation.errors.forEach(error => {
            console.log(error.error.toString())
            if (error.error.codeFrame) {
                console.log(error.error.codeFrame)
            }
        })
        return
    }

    if (!electron) {
        electron = require('electron-connect').server.create({ path: 'build/' })
        electron.start()
        electron.on('quit', () => process.exit(0))
    } else {
        electron.restart()
    }
})

build

watchの時とは違いrequireしたwebackでrunをして一回のみ実行します。webpackが通ったあとは、electron-packagerでDarwin/Win32向けにそれぞれパッケージングと7zで圧縮をかけます。(zipが良い場合はオプションをいじればできるはず)

リファクタリングしろよ感はあります…。

'use strict'

const webpack = require('webpack')
const conf = require('./webpack.config.js')
const packager = require('electron-packager')
const Zip = require('node-7z')
const fs = require('fs')

const compiler = webpack(conf)

const confPackage = JSON.parse(fs.readFileSync('./package.json'))

compiler.run((err, stats) => {
    if (err) {
        // FIXME
        console.dir(err)
        return
    }

    if (stats.hasWarnings()) {
        stats.compilation.warnings.forEach(warning => {
            // FIXME
            console.dir(warning)
        })
    }

    if (stats.hasErrors()) {
        stats.compilation.errors.forEach(error => {
            console.log(error.error.toString())
            if (error.error.codeFrame) {
                console.log(error.error.codeFrame)
            }
        })
        return
    }

    const packagerConfDarwin = {
        dir: 'build',
        out: 'release/',
        name: confPackage.name,
        arch: 'x64',
        asar: true,
        platform: 'darwin',
        version: confPackage.dependencies['electron-prebuilt'],
        icon: 'src/app.icns',
        overwrite: true
    }

    if (process.env.ELECTRON_SIGN_DARWIN) {
        packagerConfDarwin['sign'] = process.env.ELECTRON_SIGN_DARWIN
    }

    packager(packagerConfDarwin, (err2, path) => {
        let archive = new Zip()
        archive.add(`release/${confPackage.name}-darwin-${confPackage.version}.7z`, `release/${confPackage.name}-darwin-x64/`, {
            m0: '=BCJ',
            m1: '=LZMA:d=21'
        }).then(() => {

        }).catch(err3 => console.error(err3))
    })

    const packagerConfWin32 = {
        dir: 'build',
        out: 'release/',
        name: confPackage.name,
        arch: ['ia32', 'x64'],
        asar: true,
        platform: 'win32',
        version: confPackage.dependencies['electron-prebuilt'],
        icon: 'tmp/app.ico',
        overwrite: true
    }

    packager(packagerConfWin32, (err2, pathes) => {
        pathes.forEach((path) => {
            const a = path.split('-')
            const platform = a[1]
            const arch = a[2]

            let archive = new Zip()
            archive.add(`release/${confPackage.name}-${platform}-${arch}-${confPackage.version}.7z`, path, {
                m0: '=BCJ',
                m1: '=LZMA:d=21'
            }).then(() => {

            }).catch(err3 => console.error(err3))

        })

    })

})

まとめ