Calmery.me

みっかぼうずにならないようがんばる

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

これは Elm Advent Calendar 2017Electron Advent Calendar 2017 の 15 日目の記事です.

Qiita にも同じ内容の記事を投稿しています.
ElmとElectronでデスクトップアプリを作ってみた - Qiita

はじめに

ElmElectron を使って Twitterハッシュタグである #elm をストリーミングするアプリを作ってみました.完成したものは GitHub - calmery/elm-advent-calendar-2017 で見ることができます.
f:id:calmery:20171214230543p:plain

実装

コミット毎にまとめていきます.

Hello World

GitHub - calmery/elm-advent-calendar-2017 at 6490533e51c1fb4afea9e03aa9562e4762f52207
f:id:calmery:20171215004356p:plain

Elm が undefined になった

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

Elm.Main.fullscreen() // Uncaught ReferenceError: Elm is not defined

実際に生成されたコードを見ると module.exports が優先されてしまうようです.

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

var globalElm = this['Elm'];
if (typeof globalElm === "undefined")
{
  this['Elm'] = Elm;
  return;
}

なので module.exports から直接参照するようにしました.

const Elm = module.exports

// require を使って読み込むこともできる
const Elm = require( './app.js' )

この問題は webpack を使用することで気にならなくなりました.

Twitter から取得したツイートをプロセス間通信と Port で送る

GitHub - calmery/elm-advent-calendar-2017 at 45fff5fea0170ef369d8554a4dbcbe0f020abe81
Electron はメインプロセスとレンダラプロセスが別れています.なので,その間でやりとりを行うためにプロセス間通信を行う必要があります.ここは ipcMain | Electron を見るといいかなと思います.

// src/entry.js

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

// In renderer process.
ipcRenderer.on( 'newTweet', ( _, tweet ) => {
  // do something ...
} )

ひとまずツイートの本文だけをレンダラプロセスに渡すようにしました.ここから,さらに受け取ったデータを Elm に Port を使って渡します.ここは JavaScript · An Introduction to Elm の Step 2: Talk to JavaScript とか ElmのPortでJSを使う。 - Qiita が参考になります.

ipcRenderer.on( 'newTweet', ( _, tweet ) => {
  app.ports.newTweet.send( tweet )
} )
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
ツイートの本文だけでは物足りないのでユーザの情報なども一緒に渡すようにしました.

// 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[Elm] Decoder a からいろいろ理解ってしまおう - Qiita が参考になります.

-- 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
Elm から Port を通して Notification - Web API インターフェイス | MDN を呼び出すことでツイートを受け取った際に通知を表示するようにしました.

-- 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 )
// src/public/entry.js
app.ports.notification.subscribe( message => {
  new Notification( message )
} )

見た目を整える

GitHub - calmery/elm-advent-calendar-2017 at c72da54e9bfb6d0aff18446d4313bfd31af246c5
Elm にも elm-styled とか style-elements とかあるようですが,今回は普通に CSS を使いました.

まとめ

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