LoginSignup
8
4

More than 5 years have passed since last update.

ElmとJavaScriptの会話(Port)

Last updated at Posted at 2017-12-19

 (追記)もうすこし基本的なPortの記事も書きました ==> Portを通したElm Record と JavaScript Object の互換性 - Qiita

 一般的に、Elmだけで完全なアプリを作る場合もあれば、JavaScriptプログラムと組み合わせてアプリを作成する場合もあります。Elmに無いライブラリを使いたい場合などは、当然後者を選択します。ElmにはJavaScriptと会話するためのPortという仕組みがあります。今回は以下のサイトに従って勉強しましたが、その備忘録です。ソースコードは多少修正してあります。
http://codeloveandboards.com/blog/2017/05/15/elm-and-the-outer-world/

 Elmは一番基本のModel-View-Updateパタンの場合だと、汚れ仕事であるイベントの管理やDOM表示はElm Realtimeが行ってくれます。美しいパタンですが、汚れ仕事はほとんどできません。そこでCmd/Subを導入して、それ以外のWebアプリではよくある汚れ仕事も行えるようになりました。Httpや乱数などです。しかしそれも限定的で、JavaScriptでなければやれないこともあります。Portはこれを解決すべく、任意のJavaScriptプログラムと交信できる仕組みとして導入されたと考えられます。
Elmで乱数を扱う(Cmd) - Qiita
Elmでサーバと通信する(Cmd) - Qiita
Building custom DOM event handlers in Elm

1.開発環境について

 今回はElmプログラムからJavaScriptのFileReaderオブジェクトを使う例を考えます。今回のプログラムは3つのファイルから構成されます。3つのプログラムファイルはプロジェクトディレクトリ直下のsrcディレクトリにあるとします。
・index.html -- ここにJavaScriptプログラムを展開します
・Main.elm -- elmのMain moduleです。index.htmlのJavaScriptと会話します
・Ports.elm -- Portsを定義したelm moduleです。

 まず必要なパッケージをインストールします。

$ elm-package install elm-lang/http

 以下のコマンドでコンパイルします。

$ elm make --warn --debug src/Main.elm --output src/main.js

 httpサーバを立ち上げ、ブラウザでアクセスするとアプリが起動します。

$ elm-reactor -a=www.xxxxxx.jp -p=3030

2.プログラムの処理の流れ

 大雑把に説明すると以下の流れになります。

画像ファイル名を選択(Elm)  --> 画像ファイルの中身を読む(JavaScript)  --> 画像を表示(Elm)

 もう少し詳細な流れは以下のようになります。

1.ファイル選択 <input type="file">の画面を表示する(Elm)
2.ファイルを選択しイベントを発生させる(Elm)
3.イベント処理で選択されたファイル名をJavaScriptに送る(Elm)
4.ファイル名を受け取り、FileReaderで中身を読み込む(JavaScript)
5.読み込んだファイルデータをElmに送信する(JavaScript)
6.ファイルデータを受け取り、画面に表示する(Elm)

3.port module(src/Ports.elm)

 Elmで以下のように名前と型だけの関数を用意し、Cmd/Subとして実行します。

src/Ports.elm
port module Ports exposing (fileSelected, fileLoaded)

import Json.Decode as JD

-- Out ports
port fileSelected : JD.Value -> Cmd msg

-- In ports
port fileLoaded : (String -> msg) -> Sub msg

 まずmodule宣言ですが、以下のように先頭にportを付ける必要があります。

port module Ports exposing (fileSelected, fileLoaded)

 続けて2つの関数(fileSelected、fileLoaded)の名前と型だけを記述します。これだけを宣言するとElmプログラムとJavaScriptプログラムで以下のように使うことができるようになります。

 以下が選択されたファイル名を送るコードです(Elm --> JavaScript)。 ファイル選択のイベントはElm内で起こり、それをJavaScriptに伝えるべくupdate関数でCmdを発行します(1)。するとJavaScript側で対応する関数が起動されます(2)。

1. Cmd.batch [ fileSelected event ]  @ Elm
2. app.ports.fileSelected.subscribe(function (e) {...}); @ JavaScript

 以下が読み込んだファイルコンテンツを送るコードです(JavaScript --> Elm)。上の2のJavaScriptのcallbackの中でファイルを読み込み、終了したらElmに伝えます(3)。Elm側では(4)で登録済みのSubで受取り、update関数を呼びます。

3. app.ports.fileLoaded.send(base64encoded); @ JavaScript
4. Sub.batch [ fileLoaded FileLoaded ] @ Elm

4.カスタム イベント リスナーの作成

 このカスタムイベントの部分は少しわかりにくいので、細かく見ていこうと思います。まずは以下のview関数を見てください。

view関数
type Msg
    = FileSelected JD.Value
    | FileLoaded String

view model =
    section
        [ sectionStyles ]
        [ imageView model.imageData
        , input
            [ type_ "file"
            , accept "image/*"
            , on "change" (JD.map FileSelected JD.value)
            ]
            []
        ]

 注目してほしいのはon関数ですが、これはHtml.Eventsで定義されている関数でイベントリスナーを登録してくれます。まずは型を見てください。
http://package.elm-lang.org/packages/elm-lang/html/latest/Html-Events#on

on : String -> Decoder msg -> Attribute msg
map : (a -> value) -> Decoder a -> Decoder value

 イベントが起こった時に、decoderはeventオブジェクトを Elmの値に変換してくれます。その値はupdate関数に送られます。型を考慮して実際のコードをみます。

on "change" (JD.map FileSelected JD.value)

 この一行で、(1)JavaScript NativeのDOM eventsオブジェクトの取得、(2)DOM eventsのDecode、(3)Taggingの処理を行っています。

 ここで(JD.map FileSelected JD.value)がdecorderになります。

 JDはJson.Decodeで、JSON値をElm値に変換してくれるmoduleです。
http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode

 JD.valueはJavaScriptのJSONオブジェクトをそのままの形でElmの世界に取り入れるための、偽装の変換を行うdecorderです。実際に値そのものの変更は行っていませんが、形だけはElmの値となっています。これは後でportによってそのままJavaScriptに渡されるものです。Elmの世界ではこれ以上この値の中身に触ることはできません。ここではDOM eventオブジェクトをそのままElm値に変換しています。

 またJD.valueで偽装変換された値は、タグ(FileSelected)を付けたMsgでupdate関数に渡されます。タグ付けはJD.mapで行なわれます。結論的に、update関数にはFileSelected eventというMsgが送られます。

5.JavaScriptプログラム(src/index.html)

 JavaScriptプログラムは以下のようにindex.htmlに埋め込んでいます。

src/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Elm FileReader</title>
  </head>

  <body>
    <div id="elm-area"></div>
    <script src="main.js"></script>
    <script>
        const app = Elm.Main.embed(document.getElementById("elm-area"));

        app.ports.fileSelected.subscribe(function (e) {
          const input = e.target;
          const file = input.files[0];
          const reader = new FileReader();

          reader.onload = (function (event) {
            const base64encoded = event.target.result;
            app.ports.fileLoaded.send(base64encoded);
          });

          reader.onerror = (function (err) {
            console.error(err);
          });

          reader.readAsDataURL(file);
        });
    </script>
  </body>
</html>

 Elmアプリもindex.htmlで起動していますが、以下のコードで行っています。

    <div id="elm-area"></div>
    <script src="main.js"></script>
    <script>
        const app = Elm.Main.embed(document.getElementById("elm-area"));

 上以外はJavaScriptのプログラムになります。まずElm側からファイル名が伝えられます。
 eは、Elm側からfileSelected eventというコマンドで渡されたeventオブジェクトで、Elmのカスタムイベント(on "change")で拾ったJavaScriptのeventを、そのまま渡してくれたものです。つまりJavaScriptのeventです。

        app.ports.fileSelected.subscribe(function (e) {

 次にそのファイルをreadします。

          reader.readAsDataURL(file);

 readが終わったら、データをElm側に渡します。

          reader.onload = (function (event) {
            const base64encoded = event.target.result;
            app.ports.fileLoaded.send(base64encoded);
          });

 以上がJavaScriptプログラムの処理になります。

6.Elmプログラム(src/Main.elm)

 以下がElmプログラムです。

src/Main.elm
module Main exposing (..)

import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (on)
import Json.Decode as JD
import Ports exposing (fileSelected)
import Ports exposing (fileLoaded)

init : ( Model, Cmd Msg )
init =
    initialModel ! []

main : Program Never Model Msg
main =
    Html.program
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }

type Msg
    = FileSelected JD.Value
    | FileLoaded String

type ImageData e s
    = NotLoaded
    | Loading
    | Error e
    | Success s

type alias Model =
    { imageData : ImageData String String }

initialModel : Model
initialModel =
    { imageData = NotLoaded }

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        FileSelected event ->
            ( { model | imageData = Loading }, Cmd.batch [ fileSelected event ] )
        FileLoaded binary ->
            ( { model | imageData = Success binary },  Cmd.batch [] )

view : Model -> Html Msg
view model =
    section
        []
        [ imageView model.imageData
        , input
            [ type_ "file"
            , accept "image/*"
            , on "change" (JD.map FileSelected JD.value)
            ]
            []
        ]

imageView : ImageData String String -> Html Msg
imageView imageData =
    case imageData of
        NotLoaded ->
            p
                []
                [ text "Choose an image file using the selector below..." ]

        Loading ->
            p
                []
                [ text "Loading..." ]

        Error error ->
            p
                []
                [ text error ]

        Success binary ->
            img
                [ style
                    [ ( "max-height", "300px" )
                    , ( "margin-bottom", "3rem" )
                    ]
                , src binary
                ]
                []

subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch [ fileLoaded FileLoaded ]

 今回のテーマで、まだ説明していないのはupdateです。以下に説明していきたいと思います。

update関数
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        FileSelected event ->
            ( { model | imageData = Loading }, Cmd.batch [ fileSelected event ] )
        FileLoaded binary ->
            ( { model | imageData = Success binary },  Cmd.batch [] )

 まず FileSelected event の場合は前に見た通り、以下のイベントに対応しています。

view関数の一部
            , on "change" (JD.map FileSelected JD.value)

 次に FileLoaded binary の場合は、以下のsubscriptionsに対応しています。

subscriptions関数
subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch [ fileLoaded FileLoaded ]

 subscriptionsは、必要なmodel更新を行うべく、任意のイベントソースをリッスンするためのものです。イベントが生じたときにupdateが呼ばれ、modelの更新が行われます。
https://guide.elm-lang.org/architecture/effects/

 このsubscriptionsはJavaScript側の、以下の実行をリッスンしています。

javascriptプログラムの一部
            app.ports.fileLoaded.send(base64encoded);

 今回は以上です。

8
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
4