ElectronとReactでAPI叩いて何か表示するアプリを試してみたメモ

先日、会社の業務外開発イベントに参加してきた。 これは、自分が新卒の頃からやっているイベントの後継で、 テーマ自由でもくもく作業するハッカソン、所謂もくもくそん。

今回は、ずっと昔から気になっていたElectronとReact.jsを使ってみることにした。 大半はJavaScript力を向上したいという思惑がある。

作るもの・使うもの

Redmine上の自分のタスクを、取得してきて表示するだけのアプリ。

ViewのフレームワークReact.jsを利用して、 自社の Redmine Rest APIのRestAPIを叩いて表示。 それらをElectronでアプリ化する。

Vue.jsとどっち試そうか迷ったが、 なんかいろいろ応用が効きそうなReact.jsにしてみることに。

Step0: nvm, node Install

まずnvmをいれる

$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.1/install.sh | bash

そして、とりあえずnodeの安定版いれる

$ nvm install --lts
$ nvm use --lts

Step1: Scaffold

React.jsのテンプレートプロジェクトをcreate-react-appでつくる

こちらの手順を参考させていただいた。 https://qiita.com/chibicode/items/8533dd72f1ebaeb4b614

create-react-appインストール

$ npm install -g create-react-app

プロジェクト生成

$ create-react-app redmine-desktop

できたプロジェクトはすでにgitリポジトリになってる。楽。

Step2: Electron化

こちらの手順を参考にさせていただいた。というよりほぼそのままの手順。

https://medium.com/@impaachu/how-to-build-a-react-based-electron-app-d0f27413f17f

以下パッケージを追加

$ npm install cross-env electron-is-dev

以下パッケージをdev環境に追加

$ npm install -D concurrently electron electron-builder wait-on

以下electron.jsをPublic以下に配置

const electron = require("electron");
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
const path = require("path");
const isDev = require("electron-is-dev");
let mainWindow;

function createWindow() {
  mainWindow = new BrowserWindow({ width: 320, height: 640 });
  mainWindow.loadURL(
    isDev
      ? "http://localhost:3000"
      : `file://${path.join(__dirname, "../build/index.html")}`
  );
  mainWindow.on("closed", () => (mainWindow = null));
}

app.on("ready", createWindow);
app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit();
  }
});

app.on("activate", () => {
  if (mainWindow === null) {
    createWindow();
  }
});

以下をpackage.jsonに追加

...
"description": "",
"author": "Mikito Yoshiya",
"build": {
    "appId": "net.mikinya.redmine-desktop"
},
"main": "public/electron.js",
"homepage": "./",
...

package.jsonのscripts項目を以下に変更

...
"scripts": {
    "react-start": "react-scripts start",
    "react-build": "react-scripts build",
    "react-test": "react-scripts test --env=jsdom",
    "react-eject": "react-scripts eject",
    "electron-build": "electron-builder",
    "release": "yarn react-build && electron-builder --publish=always",
    "build": "yarn react-build && yarn electron-build",
    "start": "concurrently \"cross-env BROWSER=none yarn react-start\" \"wait-on http://localhost:3000 && electron .\""
},
...

この時点で

$ npm run start

でアプリが開発モードで起動するように

$ npm run build

でapp化してdist以下にappと配布用のdmgを出力できるようになった。

Step3: Presentation, React Component構築

ビューのレイヤーを修正

公式のチュートリアル等を参考にごにょごにょいじった。

CSSも適当にbootswatchのcss.minを入れた。

cssはただ配置してApp.jsからimportしただけだけど、これでいいのだろうか。

できたReactのコンポーネントは以下みたいな感じ。

import React from 'react';

class TaskCard extends React.Component {
  render() {
    return (
      <div className="card text-white bg-primary mb-3" >
        <div className="card-header">{this.props.title}</div>
      </div >
    );
  }
}

export default TaskCard;
import React from 'react';
import ReactDOM from 'react-dom';
import './bootstrap.min.css'
import TaskCard from './TaskCard'
import IssueUseCase from './useCase/IssueUseCase'

class App extends React.Component {
  constructor(props) {
    super(props);
    this.useCase = new IssueUseCase();
  }

  render() {
    this.useCase.getOwnIssues(issues => {
      var list = [];
      for (var i in issues) {
        list.push(<TaskCard key={issues[i].id} title={issues[i].subject} />);
      }
      ReactDOM.render(list, document.getElementById('issues'));
    });

    return (
      <div className="container">
        <div className="row">
          <div className="col-lg-12">
            <h2>Tasks</h2>
          </div>
        </div>
        <div className="row">
          <div className="col-lg-4">
            <div className="bs-component">
              <div id="issues"></div>
            </div>
          </div>
        </div>
      </div>
    );
  }
}

export default App;

この時点ではRestAPIからcurlで叩いたjsonをファイルで保存したデータをモックデータとして、Viewに表示してみる。記法やお作法の正しいやりかたは全然不明。

うん。これでとりあえずデモはできる。

f:id:miki05:20191113010021p:plain

Step4: DataStore, API実装

実際にAPIを叩いてサーバーのデータを取ってくるようにする。 通信と非同期の処理にaxiosをつかってみる。 XMLHttpRequstのラッパーでPromiseでイベントの制御ができるらしい。

$ npm install axios

通信部分はとりあえずトークンとかクエリとかベタがき。 あとでUI上から設定できるようにしよう...

import axiosbase from 'axios'

class RedmineApi {
  constructor() {
    this.axios = axiosbase.create({
      baseURL: 'https://url_to_redmine',
      headers: {
        'Content-Type': 'application/json',
        'X-Redmine-API-Key': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
      },
      responseType: 'json',
    })
  }

  getIssues() {
    return this.axios
      .get('/issues.json?limit=100&tracker_id=4&status_id=1&assigned_to_id=270')
  }
}

export default RedmineApi;

ここで二つの問題がでた。 一つめはCORS(Cross-Origin Resource Sharing)で、異なるドメインへの通信を制限するJavaScriptフロントのよくあるやつ。 現状の開発状態でもlocalhostで立ち上げてるぽいし、うん確かにそうなるわな。 でもAppにバンドルした場合はどうなるんだ? これは、以下のElectron側の設定で解決できた。

electron.jsのnew BrowserWindow();のパラメータに以下を追加

...
  mainWindow = new BrowserWindow({
    width: 320, height: 640,
    webPreferences: {
      webSecurity: false
    }
  });
...

参考:https://qiita.com/yasuflatland-lf/items/4f57b6dd311d4918e8f5

もう一つは会社で運用しているRedmineが自己証明書で、ERR_CERT_AUTHORITY_INVALIDエラーがでて、 XMLHttpRequst系ではそれを無視できる設定にできなかったこと。 curlではkオプションを追加するだけだが。

こちらも同様に、以下のElectron側の設定で解決。 electron.jsに以下を追加

...
app.on('certificate-error', function (event, webContents, url, error, certificate, callback) {
  event.preventDefault();
  callback(true);
});
...

参考:https://qiita.com/yuya-oc/items/2764bf7a33c751498858

完成

とりあえず動くものができた。 APIトークン埋め込み、ユーザーID埋め込みなので、完全個人しか使えない。 全然時間足りなかった。

所感

  • Electreon便利, React良さそう
  • JSの作法もっと知りたい
  • webpackとかなんちゃらとか、いろいろ覚えることある
  • TypeScript覚えたい
  • Rxを導入してみたい
  • JSでテストどう書くんだ
  • Electreon上のプロジェクトでClearn Archtectureを適用してみたい
  • Appまでビルドして配布形式までとりあえず1日で走りきれたので満足
  • たまには違ったコンテキストの作業は刺激になる
  • 割と簡単に改良できそうなので、この後少しいじってみることにする