Electron + Express.js + React を使ってWebアプリとデスクトップアプリを共通のコードで作ってみたいと思います。
今回作成したコードは github に置いてあります。
参考にしたサイト
想定しているターゲット
あまり無いとは思いますが,普段,サーバに繋がる環境ではブラウザからアクセスしてアプリを実行するけど,稀にサーバに繋がらないスタンドアロンの環境でもアプリを実行するようなケースを想定しています。
動作確認環境
- OS macOS Mojave
- Electron 4.0.6
- Express.js 4.16.0
- React 16.8.6
- Node.js v11.6.0
それでは早速ですが,アプリの雛形生成に express-generator を使用するので、インストールを行います。
$ npm install -g express-generator
"sample-app" という名前でアプリケーションを作成します。
$ express sample-app
$ cd sample-app
必要なパッケージをインストールします。
$ npm install
この時点で動作確認してみます。以下のコマンドを実行して
$ npm start
ブラウザで localhost:3000
にアクセスすると以下のような画面が表示されるはずです。
Electron のセットアップ
以下のコマンドで Electron をインストールします。
$ npm install --save-dev electron
続いて、app.js と同じ場所に main.js を作成します。
// Start Express
const expressApp = require('./app')
expressApp.listen(3000, '127.0.0.1')
// Electronのモジュール
const electron = require('electron')
// アプリケーションをコントロールするモジュール
const app = electron.app
// ウィンドウを作成するモジュール
const BrowserWindow = electron.BrowserWindow
// メインウィンドウはGCされないようにグローバル宣言
let mainWindow
// 全てのウィンドウが閉じたら終了
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
// Electronの初期化完了後に実行
app.on('ready', () => {
// メイン画面の表示。ウィンドウの幅、高さを指定できる
mainWindow = new BrowserWindow({ width: 800, height: 600 })
mainWindow.loadURL('http://127.0.0.1:3000')
// ウィンドウが閉じられたらアプリも終了
mainWindow.on('closed', () => {
mainWindow = null
})
})
package.json に以下の一行を追加します。
"main": "main.js",
以下のコマンドを実行して動作確認します。
$ npx electron .
以下のような画面が表示されれば成功です。
npm-run-all で Web と Electron を同時に起動する
必須ではありませんが,一つのコマンドで Web サーバと Electron のアプリを同時に起動できると便利な事が多いのでここで設定しておきます。
npm-run-all
という複数のタスクを同時に起動するパッケージを使用して、Web と Electron を同時に起動できるようにします。
まず以下のコマンドで npm-run-all
をインストールします。
$ npm install --save-dev npm-run-all
次に package.json
を以下のように修正します。
"scripts": {
"start": "node ./bin/www",
"electron": "electron .",
"dev": "npm-run-all --parallel electron start",
"package": "npx electron-packager . sample-app --platform=darwin --arch=x64 --overwrite"
}
これで以下のコマンドで Web サーバと Electron アプリが同時に起動できるようになりました。
$ npm run dev
React の追加
下記のコマンドで React のパッケージをインストールします。
npm install --save react react-dom
Jade は使用しないので削除しておきます。
npm uninstall --save jade
まずは Hello World を作成してみます。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>sample-app</title>
</head>
<body>
<div id="root"></div>
<script type="text/javascript" src="javascripts/app.js" charset="utf-8"></script>
</body>
</html>
"Hello, world" を返す React のコンポーネントを作成します。
import React from 'react'
import ReactDOM from 'react-dom'
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
)
これでソース自体の作成は完了です。後は実行するための設定を行います。
不要な処理・ファイルの削除
"express-generator" が作成したファイル・コードの中で不要になったものを削除します。
$ rm routes/*.js
$ rm -rf views
app.js の以下の行を削除します。
var indexRouter = require('./routes/index')
var usersRouter = require('./routes/users')
// view engine setup
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'jade')
app.use('/', indexRouter)
app.use('/users', usersRouter)
Babel と Webpack の設定
JSX から JavaScript への変換に使用する Babel のインストールと設定行います。
$ npm install --save-dev babel babel-core babel-loader@7 babel-preset-env babel-preset-react
.babelrc に以下の内容を記述します。
{
"presets": ["env", "react"]
}
Webpack のインストール
$ npm install --save-dev webpack webpack-cli
$ npm install --save-dev babel-loader css-loader style-loader
"build": "webpack -d"
const path = require('path')
module.exports = {
mode: 'development',
entry: {
app: path.join(__dirname, 'src/index.js')
},
output: {
path: path.join(__dirname, 'public/javascripts'),
filename: '[name].js'
},
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader'
}, {
test: /\.css$/,
loader: ['css-loader', 'style-loader']
}
]
}
}
この時点で以下のコマンドを実行すると
$ npm run build
$ npm run dev
以下のような画面が表示され、またブラウザで localhost にアクセスすると同様な画面が表示されると思います。
React と Express の連携
月並みですが,サーバ側(Express)でおみくじの結果を生成して画面に表示する処理を実装してみたいと思います。
クライアントからサーバへのAjax通信には "SuperAgent" を使うのではじめにインストールしておきます。
$ nom install --save superagent
サーバ側の実装
"/api/kuji" にアクセスされると "{result: '大吉'}" のような JSON データを返す API を実装します。
var express = require('express')
var router = express.Router()
const kuji = ['大吉', '吉', '凶']
const get_kuji = () => {
const rnd = Math.floor(Math.random() * kuji.length)
return kuji[rnd]
}
// GET /api/kuji で呼ばれる関数
router.get('/', (req, res) => {
res.json({result: get_kuji()})
})
module.exports = router
var kuji = require('./routes/kuji')
app.use('/api/kuji', kuji)
この時点で,npm start
でサーバを起動してブラウザから http://localhost:3000/api/kuji
へアクセスすると以下のような JSON のデータが表示されるはずです。
クライアント側の実装
画面上の「ひく」ボタンをクリックすると,おみくじの結果が画面に表示されるコンポーネントを作成します。
import React from 'react'
import ReactDOM from 'react-dom'
import request from 'superagent'
class Kuji extends React.Component {
constructor(props) {
super(props)
// デフォルトの state を設定
this.state = { result: '' }
// api() の中で this にアクセスできるよう bind しておく
this.api = this.api.bind(this)
}
// サーバ側にアクセス(GET /api/kuji)して結果を取得して state に格納する
api () {
request.get('api/kuji').end((err, res) => {
if (err) {
console.log(err)
return
}
const result = res.body.result
this.setState({ result: result })
})
}
render () {
return (
<div>
<p>{this.state.result}</p>
<button onClick={ e => this.api() }>ひく</button>
</div>
)
}
}
ReactDOM.render(
<Kuji />,
document.getElementById('root')
)
この時点で以下のコマンドを実行して
$ npm run build
$ npm run dev
画面上の「ひく」ボタンをクリックすると以下のような画面が表示されると思います。
以上で終わりです。通常の Express, React でアプリを書いたコードがそのままネイティブアプリ化できるのは便利なケースもあるのではないかと思います。