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に表示してみる。記法やお作法の正しいやりかたは全然不明。
うん。これでとりあえずデモはできる。
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を適用してみたい
- 参考:https://qiita.com/ttiger55/items/50d88e9dbf3039d7ab66
- DIPもDIもしているっぽい!
- Appまでビルドして配布形式までとりあえず1日で走りきれたので満足
- たまには違ったコンテキストの作業は刺激になる
- 割と簡単に改良できそうなので、この後少しいじってみることにする