ElmとElectronでデスクトップアプリを作ってみた

これは [Elm Advent Calendar 2017](https://qiita.com/advent-calendar/2017/elm) の 15 日目の記事です．

## はじめに
[Elm](http://elm-lang.org/) と [Electron](https://electronjs.org/) を使って Twitter のハッシュタグである [#elm](https://twitter.com/search?q=%23elm) をストリーミングするアプリを作ってみました．完成したものは [GitHub - calmery/elm-advent-calendar-2017](https://github.com/calmery/elm-advent-calendar-2017) で見ることができます．
![スクリーンショット 2017-12-14 23.04.46のコピー.png](https://qiita-image-store.s3.amazonaws.com/0/211005/a9a2bd51-afb3-615c-9255-a3b7054e4413.png)
## 実装
コミット毎にまとめていきます．

### Hello World
[GitHub - calmery/elm-advent-calendar-2017 at 6490533e51c1fb4afea9e03aa9562e4762f52207](https://github.com/calmery/elm-advent-calendar-2017/tree/6490533e51c1fb4afea9e03aa9562e4762f52207)
![スクリーンショット 2017-12-14 23.24.27.png](https://qiita-image-store.s3.amazonaws.com/0/211005/12247c4c-f148-368b-3e15-8a424160b54a.png)

#### Elm が undefined になった
早速というか．Electron から Elm を参照すると undefined となってしまいました．

```javascript
Elm.Main.fullscreen() // Uncaught ReferenceError: Elm is not defined
```
実際に生成されたコードを見ると module.exports が優先されてしまうようです．

```javascript
if (typeof module === "object")
{
  module['exports'] = Elm;
  return;
}

var globalElm = this['Elm'];
if (typeof globalElm === "undefined")
{
  this['Elm'] = Elm;
  return;
}
```
なので module.exports から直接参照するようにしました．

```javascript
const Elm = module.exports

// require を使って読み込むこともできる
const Elm = require( './app.js' )
```
この問題は webpack を使用することで気にならなくなります．

### webpack を追加する
[GitHub - calmery/elm-advent-calendar-2017 at 55e80b6a4e1e11bb8713177fcbbaabc13d90732d](https://github.com/calmery/elm-advent-calendar-2017/tree/55e80b6a4e1e11bb8713177fcbbaabc13d90732d)
[webpackその1 · Elm Tutorial](https://www.elm-tutorial.org/jp/04-starting/03-webpack-1.html) の説明が参考になりました．

### Twitter から取得したツイートをプロセス間通信と Port で送る
[GitHub - calmery/elm-advent-calendar-2017 at 45fff5fea0170ef369d8554a4dbcbe0f020abe81](https://github.com/calmery/elm-advent-calendar-2017/tree/45fff5fea0170ef369d8554a4dbcbe0f020abe81)
Electron はメインプロセスとレンダラプロセスが別れています．なので，その間でやりとりを行うためにプロセス間通信を行う必要があります．ここは [ipcMain | Electron](https://electronjs.org/docs/api/ipc-main) を見るといいかなと思います．

```javascript
// src/entry.js
// In main process.
stream.on( 'data', event => {
  const tweet = event.text
  window.webContents.send( 'newTweet', tweet )
} )
```

```javascript
// src/public/entry.js
// In renderer process.
ipcRenderer.on( 'newTweet', ( _, tweet ) => {
  // do something ...
} )
```

ひとまずツイートの本文だけをレンダラプロセスに渡すようにしました．ここから，さらに受け取ったデータを Elm に Port を使って渡します．ここは [JavaScript · An Introduction to Elm](https://guide.elm-lang.org/interop/javascript.html) の Step 2: Talk to JavaScript とか [ElmのPortでJSを使う。 - Qiita](https://qiita.com/jooex/items/5ff2d3b86563cf5dbd84) が参考になります．

```javascript
// src/public/entry.js
ipcRenderer.on( 'newTweet', ( _, tweet ) => {
  app.ports.newTweet.send( tweet )
} )
```

```elm
-- src/public/elm/Main.elm
port newTweet : (String -> msg) -> Sub msg

type Msg
    = NewTweet String

subscriptions : Model -> Sub Msg
subscriptions model =
    newTweet NewTweet
```

### JSON をデコードする
[GitHub - calmery/elm-advent-calendar-2017 at 39ab6b701fcfbbf29a46ec706286c2f4c76a5f1f](https://github.com/calmery/elm-advent-calendar-2017/tree/39ab6b701fcfbbf29a46ec706286c2f4c76a5f1f)
ツイートの本文だけでは物足りないのでユーザの情報なども一緒に渡すようにしました．

```javascript
// src/entry.js
stream.on( 'data', event => {
  const tweet = {
    text: event.text,
    created_at: event.created_at,
    user: {
      profile_image_url: event.user.profile_image_url,
      name: event.user.name,
      screen_name: event.user.screen_name,
    }
  }

  window.webContents.send( 'newTweet', JSON.stringify( tweet ) )
} )
```
Elm で JSON をデコードします．[JSON · An Introduction to Elm](https://guide.elm-lang.org/interop/json.html) や [[Elm] Decoder a からいろいろ理解ってしまおう - Qiita](https://qiita.com/ymtszw/items/1cabbdbda4273b4c1978) が参考になります．

```elm
-- src/public/elm/Main.elm
type alias Tweet =
    { user : User
    , text : String
    , created_at : String
    }

decodeTweet : String -> Result String Tweet
decodeTweet response =
    decodeString tweetDecoder response

tweetDecoder : Decoder Tweet
tweetDecoder =
    map3 Tweet
        (field "user" userDecoder)
        (field "text" string)
        (field "created_at" string)
```

### 通知する
[GitHub - calmery/elm-advent-calendar-2017 at 5ee82825bd10cd5c3b0705f21b0804ba33455ca3](https://github.com/calmery/elm-advent-calendar-2017/tree/5ee82825bd10cd5c3b0705f21b0804ba33455ca3)
Elm から Port を通して [Notification - Web API インターフェイス | MDN](https://developer.mozilla.org/ja/docs/Web/API/notification) を呼び出すことでツイートを受け取った際に通知を表示するようにしました．

```elm
-- src/public/elm/Main.elm
port notification : String -> Cmd msg

...

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
  ...
    Ok tweet ->
      ( List.append [ tweet ] <| List.take 100 model, notification tweet.text )
```

```javascript
// src/public/entry.js
app.ports.notification.subscribe( message => {
  new Notification( message )
} )
```

### 見た目を整える
[GitHub - calmery/elm-advent-calendar-2017 at c72da54e9bfb6d0aff18446d4313bfd31af246c5](https://github.com/calmery/elm-advent-calendar-2017/tree/c72da54e9bfb6d0aff18446d4313bfd31af246c5)
Elm にも [elm-styled](https://github.com/styled-components/elm-styled) とか [style-elements](https://github.com/mdgriffith/style-elements/) とかあるようですが，今回は普通に CSS を使いました．

## まとめ
躓いたところもいくつかあったけれど，思っていたよりすんなりできたかなと思います．Elm はまだわからないことが多いので色々と作りながら試していけたらと思います．