Electronで恋愛・婚活サービス「Pairs」をクライアントアプリ化し、機能拡張をしてみた

Electronのwebviewを使ってみたかったので「Paris」のクライアントアプリ化して、少し機能拡張したので備忘録です。

#やりたいこと
- Electronを使用し、Pairsをクライアントアプリ化する
- webviewの機能を使用した機能拡張（コミュニティ内のソート機能の実装、Web版はコミュニティにソート機能がないため）


以下完成形です。ElectronでPairsを表示。
コミュニティの画面内でのみ画面下部に検索バーを表示し、絞り込みたい年齢を選択。絞り込みをクリックすると範囲外の年齢の方が非表示になります。
（下記gifアニでは26〜30歳に該当しない方は非表示。display:noneにしているだけなので、ロードや画面遷移は発生しません。blur処理がめんどくさく、少ない画像でアニメーションさせているのでわかりにくかったらすいません。。）

![pairs.gif](https://qiita-image-store.s3.amazonaws.com/0/126897/51d3940a-688d-0ca9-51c5-71ba2cfea6fc.gif)

#技術スタック
- Electron
- React.js
- Webpack


#ソースコードについて
必要な方はGithubを参照ください。
[paris-client](https://github.com/TatsuyaNkmura/paris-client)

以下、4ファイルがメインとなります。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
    <head>
      <meta charset="utf-8">
      <title>Pairs Client</title>
      <link rel="stylesheet" href="build/app.css">
    </head>
    <body>
      <webview preload="./build/wv.js" src='https://pairs.lv/' autosize='on'></webview>
      <!--<webview src='https://www.facebook.com/' autosize='on'></webview>-->
      <div id="app"></div>
      <script src="build/app.js"></script>
    </body>
</html>

```


```js:wv.js
/*
 * webview内で実行するJS
 **/

(function(){

    'use strict';

    const {ipcRenderer} = require('electron');
    const PARSE_ELEMENT_CLS_NAME = ".list_view_user_item";

    ipcRenderer.on('ping', (event, ages) => {
        let users = Array.from(document.querySelectorAll(PARSE_ELEMENT_CLS_NAME));

        users.map((user) => {
            var child1 = user.children[1];
            var child2 = child1.children[0];
            var child3 = child2.children[0];
            var age = child3.children[2];

            if(age.innerHTML < ages.underAge && ages.underAge) {
                user.style.display = "none";
            }

            if(age.innerHTML > ages.overAge && ages.overAge) {
                user.style.display = "none";
            }
        });

        ipcRenderer.sendToHost(ages);

    });

}());
```

```js:entry.js
/*
 * Electron側で実行するJS
 **/

import React from 'react'
import {render} from 'react-dom'
import App from './App.jsx'

const webview = document.querySelector('webview');

let isInitialized = false;

const onLoad = ()=> {
  webview.addEventListener('did-stop-loading', function(e){
    let url = webview.getURL();

    if(!isInitialized){
      render( <App webview = {webview} />, document.getElementById('app'));
      isInitialized = true;
    }
  });
}

webview.addEventListener('dom-ready', () => {
  webview.openDevTools();

  // キャッシュ削除用のメソッド
  // webview.reloadIgnoringCache();
  
  onLoad();
});
```

```js:App.js
/*
 * 拡張機能のJS
 **/

import React, {Component} from 'react'
import {render} from 'react-dom'
import ClassNames from 'classnames';
import Select from './components/Select.jsx'

const UNDER_AGE = 18;
const OVER_AGE = 60;

const COMMUNITY_URL = "https://pairs.lv/#/community/view/";

export default class App extends Component {

  constructor(props) {
    super(props);

    this.state = {isCommunity: false};
    this.timerId;
    this.webview = this.props.webview;
    this.webviewLoaded();
    this.handleSubmit.bind(this);
  }

  webviewLoaded(){
    this.webview.addEventListener('did-stop-loading', ()=>{
      let url = this.webview.getURL();
      if(url.indexOf(COMMUNITY_URL) != -1){
        this.setState({ isCommunity: true });
      }else {
        clearInterval(this.timerId);
        this.setState({ isCommunity: false });
      }
    });
  }

  handleSubmit(event) {
    event.preventDefault();
    let params = {};
    params.underAge = document.sort.under_age.value;
    params.overAge = document.sort.over_age.value;
    this.timerId = setInterval(()=> {
      this.webview.send('ping', params);
    }, 2000);
  }

  render() {
    let {isCommunity} = this.state;
    const className = ClassNames({
      "header": true,
      "is-show": isCommunity === true
    });
    return (
      <div className={className}>
        <form className="headerInner" name="sort">
          <div className="selectWrap">
            <Select name="under_age" under_age={UNDER_AGE} over_age={OVER_AGE} />
          </div>
          <span>以上</span>
          <div className="selectWrap">
            <Select name="over_age" under_age={UNDER_AGE} over_age={OVER_AGE} />
          </div>
          <span>未満</span>
        </form>
        <div className="button" onClick={(event) => this.handleSubmit(event)}>絞り込み</div>
      </div>
    )
  }
}
```

#Electronで「Pairs」を表示する

```html:index.html
<webview preload="./build/wv.js" src='https://pairs.lv/' autosize='on'></webview>
```
上記の一行で表示できます。Electronとwebview内は別プロセスとなるため、webview内で実行したいスクリプトをpreload属性に指定します。

<img width="1358" alt="Screenshot 2017-07-10 22.50.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/126897/6bdbdf70-bc20-ee8c-0c41-dbcc9b8ba140.png">


#「Pairs」にログインする


ヘッドレスブラウザを使えばうまくできると思うのですが断念しました。。

```html:index.html
<!--<webview preload="./build/wv.js" src='https://pairs.lv/' autosize='on'></webview> -->
<webview src='https://www.facebook.com/' autosize='on'></webview>
```
webviewで一度Facebookにログインします。

<img width="1347" alt="Screenshot 2017-07-10 22.52.08.png" src="https://qiita-image-store.s3.amazonaws.com/0/126897/673b1d14-d0ed-6aaf-fd26-ba3e804bd02a.png">

<img width="1350" alt="Screenshot 2017-07-10 22.54.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/126897/30b3d397-362d-7720-2945-f1677a484907.png">



```html:index.html
<webview preload="./build/wv.js" src='https://pairs.lv/' autosize='on'></webview>
<!--　<webview src='https://www.facebook.com/' autosize='on'></webview>　-->
```
Facebookにログイン後、ソースコードを元に戻し、Ctrl+Rでリフレッシュします。Pairsにログインできるはずです。ログインまでのフロー、知見のある方はご教示いただけますと幸いです。

<img width="1364" alt="Screenshot 2017-07-10 23.00.15.png" src="https://qiita-image-store.s3.amazonaws.com/0/126897/5ff648ee-69c5-9d54-f2dd-3faf0669423a.png">


#「Pairs」を使ってみる

とりあえず基本機能などは使えます。

<img width="1351" alt="Screenshot 2017-07-12 22.43.13.png" src="https://qiita-image-store.s3.amazonaws.com/0/126897/60f1390f-439e-df96-da7c-0010b94e7ff0.png">

#拡張機能実装の経緯

通常画面では、以下1枚目の赤枠をクリックすると、2枚目のようにポップアップが表示されソートができるのですが、3枚目のコミュニティの画面には条件設定へのリンクがなく、ソートができません。

![Screenshot 2017-07-12 22.55.33.jpg](https://qiita-image-store.s3.amazonaws.com/0/126897/34f05220-2096-f47d-390a-fb01f4d4e867.jpeg)

<img width="1355" alt="Screenshot 2017-07-12 22.49.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/126897/6bcad7c0-f158-e13a-e69e-2e9f9ff41fed.png">

<img width="1348" alt="Screenshot 2017-07-12 23.02.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/126897/2d604a7f-3420-aa2d-3a21-817e948631c7.png">

そこでコミュニティも年齢で絞り込みをかけられるようにしたいと思います。


#拡張機能を実装する（コミュニティからのソート機能）

まず、コミュニティなのかを判断する必要があるため、URLを取得し、コミュニティなら"state.isCommunity:true"に変更して検索バーを表示します。

```js:App.js

  webviewLoaded(){
    this.webview.addEventListener('did-stop-loading', ()=>{
      let url = this.webview.getURL();
      if(url.indexOf(COMMUNITY_URL) != -1){
        this.setState({ isCommunity: true });
      }else {
        clearInterval(this.timerId);
        this.setState({ isCommunity: false });
      }
    });
  }

```


年齢を指定し、「絞り込み」をクリックするとhandleSubmitメソッド内、"this.webview.send('ping', params);"で2秒毎にwebview内にイベントを投げます。

```js:App.js

  handleSubmit(event) {
    event.preventDefault();

    let params = {};
    params.underAge = document.sort.under_age.value;
    params.overAge = document.sort.over_age.value;
    this.timerId = setInterval(()=> {
      this.webview.send('ping', params);
    }, 2000);
  }

```


女性のリストをループして、年齢を取得し、指定した範囲外の年齢の方を非表示にします。

```js:wv.js
(function(){

    'use strict';

    const {ipcRenderer} = require('electron');
    const PARSE_ELEMENT_CLS_NAME = ".list_view_user_item";

    ipcRenderer.on('ping', (event, ages) => {
        let users = Array.from(document.querySelectorAll(PARSE_ELEMENT_CLS_NAME));

        users.map((user) => {
            var child1 = user.children[1];
            var child2 = child1.children[0];
            var child3 = child2.children[0];
            var age = child3.children[2];

            if(age.innerHTML < ages.underAge && ages.underAge) {
                user.style.display = "none";
            }

            if(age.innerHTML > ages.overAge && ages.overAge) {
                user.style.display = "none";
            }
        });

        ipcRenderer.sendToHost(ages);

    });

}());
```

#まとめ
とりあえずやりたいことはできたので、次はもう少しスマートに作りたいというのと、ログイン時間やいいね数、地域からのソートも加えたいと思います。