Electron + Express + React で Web・デスクトップ共通のアプリを作る

Electron + Express.js + React を使ってWebアプリとデスクトップアプリを共通のコードで作ってみたいと思います。

今回作成したコードは [github](https://github.com/ryojiysd/react-express-electron-sample) に置いてあります。

---
## 参考にしたサイト

- [Electron + Express で デスクトップアプリを作る](https://www.kabanoki.net/1184/)

---

## 想定しているターゲット

あまり無いとは思いますが，普段，サーバに繋がる環境ではブラウザからアクセスしてアプリを実行するけど，稀にサーバに繋がらないスタンドアロンの環境でもアプリを実行するようなケースを想定しています。

---

## 動作確認環境

- OS macOS Mojave
- Electron 4.0.6
- Express.js 4.16.0
- React 16.8.6
- Node.js v11.6.0

---

それでは早速ですが，アプリの雛形生成に express-generator を使用するので、インストールを行います。

```sh
$ npm install -g express-generator
```

"sample-app" という名前でアプリケーションを作成します。

```sh
$ express sample-app
$ cd sample-app
```

必要なパッケージをインストールします。

```sh
$ npm install
```

この時点で動作確認してみます。以下のコマンドを実行して

```sh
$ npm start
```


ブラウザで `localhost:3000` にアクセスすると以下のような画面が表示されるはずです。

<img width="624" alt="01.png" src="https://qiita-image-store.s3.amazonaws.com/0/90675/32e8a4e0-1063-5ed0-eeb7-f947eaace759.png">


### Electron のセットアップ

以下のコマンドで Electron をインストールします。

```sh
$ npm install --save-dev electron
```

続いて、app.js と同じ場所に main.js を作成します。

```javascript: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 に以下の一行を追加します。

```json
  "main": "main.js",
```

以下のコマンドを実行して動作確認します。

```sh
$ npx electron .
```

以下のような画面が表示されれば成功です。

<img width="612" alt="02.png" src="https://qiita-image-store.s3.amazonaws.com/0/90675/0787fa3e-b807-685e-9b7b-061094482913.png">


## npm-run-all で Web と Electron を同時に起動する

必須ではありませんが，一つのコマンドで Web サーバと Electron のアプリを同時に起動できると便利な事が多いのでここで設定しておきます。

`npm-run-all` という複数のタスクを同時に起動するパッケージを使用して、Web と Electron を同時に起動できるようにします。

まず以下のコマンドで `npm-run-all` をインストールします。

```bash
$ npm install --save-dev npm-run-all
```

次に `package.json` を以下のように修正します。

```json: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 アプリが同時に起動できるようになりました。

```sh
$ npm run dev
```

## React の追加

下記のコマンドで React のパッケージをインストールします。

```
npm install --save react react-dom
```

Jade は使用しないので削除しておきます。

```
npm uninstall --save jade
```

まずは Hello World を作成してみます。

```html:public/index.html
<!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 のコンポーネントを作成します。

```jsx:src/index.js
import React from 'react'
import ReactDOM from 'react-dom'

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('root')
)
```

これでソース自体の作成は完了です。後は実行するための設定を行います。

### 不要な処理・ファイルの削除

"express-generator" が作成したファイル・コードの中で不要になったものを削除します。

```sh
$ rm routes/*.js
$ rm -rf views
```

app.js の以下の行を削除します。

```js:app.js
var indexRouter = require('./routes/index')
var usersRouter = require('./routes/users')
```

```js
// view engine setup
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'jade')
```

```js
app.use('/', indexRouter)
app.use('/users', usersRouter)
```

### Babel と Webpack の設定

JSX から JavaScript への変換に使用する Babel のインストールと設定行います。

```sh
$ npm install --save-dev babel babel-core babel-loader@7 babel-preset-env babel-preset-react
```

.babelrc に以下の内容を記述します。

```json:.babelrc
{
  "presets": ["env", "react"]
}
```

Webpack のインストール

```sh
$ npm install --save-dev webpack webpack-cli
```

```sh
$ npm install --save-dev babel-loader css-loader style-loader
```

```json:package.json
  "build": "webpack -d"
```


```js:wabpack.config.js
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']
      }
    ]
  }
}
```

この時点で以下のコマンドを実行すると

```sh
$ npm run build
$ npm run dev
```

以下のような画面が表示され、またブラウザで localhost にアクセスすると同様な画面が表示されると思います。

<img width="625" alt="hello_world.png" src="https://qiita-image-store.s3.amazonaws.com/0/90675/9683186c-fde0-23a8-0e5c-3b04e75a8844.png">


## React と Express の連携

月並みですが，サーバ側（Express）でおみくじの結果を生成して画面に表示する処理を実装してみたいと思います。

クライアントからサーバへのAjax通信には "SuperAgent" を使うのではじめにインストールしておきます。

```sh
$ nom install --save superagent
```

### サーバ側の実装

"/api/kuji" にアクセスされると "{result: '大吉'}" のような JSON データを返す API を実装します。

```js:routes/kuji.js
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
```

```js:app.js
var kuji = require('./routes/kuji')

app.use('/api/kuji', kuji)
```

この時点で，`npm start` でサーバを起動してブラウザから `http://localhost:3000/api/kuji` へアクセスすると以下のような JSON のデータが表示されるはずです。

<img width="646" alt="api.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90675/131d7d3c-2a12-f462-a19d-1494e7809376.png">

### クライアント側の実装

画面上の「ひく」ボタンをクリックすると，おみくじの結果が画面に表示されるコンポーネントを作成します。

```sh:/src/index.js
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')
)
```

この時点で以下のコマンドを実行して

```sh
$ npm run build
$ npm run dev
```

画面上の「ひく」ボタンをクリックすると以下のような画面が表示されると思います。

<img width="578" alt="kuji.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90675/e46b73d1-b9ed-afb8-7366-fbd1ee8b71ea.png">

以上で終わりです。通常の Express, React でアプリを書いたコードがそのままネイティブアプリ化できるのは便利なケースもあるのではないかと思います。