スポンサーリンク

Elm0.19でFirestore利用のTODOアプリ(1)ElmでJSONを扱う(FirestoreからElmへデータを送る)

2019年9月28日

以前、localStorageを利用したTODOアプリを作成しました。

この次段階として、ElmでFirebase Firestoreを用いたTODOアプリを作りたいのですが、うまくいきません。

JSONについて、学ぶ必要があると感じました。
以下の本のp132を参考に、いろいろ写経してみたいと思います。

スポンサードリンク

Elm0.19でFirestore利用のTODOアプリ目次

開発環境

Windows 10 Pro
Chrome
VisualStudioCode 1.32.3
git version 2.20.1.windows.1
nvm 1.1.7
node 10.2.0
npm 6.4.1
elm 0.19.0-bugfix6
elm-format 0.8.1
VisualStudioCodeの拡張機能でelmをインストールして、settings.jsonに以下のようにelmを設定。
(『Alt + Shift + F』と『Ctrl + S』を使用。)

    "[elm]": {
        "editor.formatOnSave": true
    },

JSONって何?

例えば、以下のようなデータの記法のことです。

{ "name": "Tanaka", "age": 26 }

{ … } の中に、
  『ダブルクォーテーション(“)で囲んだ変数名』

  『値』
をセミコロン(:)でつなげたペアを記載します。

また、変数名と値のペアは、カンマ(,)で区切ります。

異なる言語間(例:JavaScriptとElm)、異なる場所(例:クライアントとサーバ)で、データのやり取りをするときに、この記法を用いることが多いです。

以下の解説が非常にわかりやすいです。

http://www.tohoho-web.com/ex/json.html

JSONデータはただの『文字列』であり、それ以上でもそれ以下でもないようです。

JSONはJavaScript Object Notationの略語とはいえ、

JSONとJavaScriptは全く別のもの

として考えた方がよさそうです。

参考: https://www.sejuku.net/blog/79911
JavaScriptのjson stringifyを完全に理解しよう!
かい
2018/11/27

JavaScriptの『オブジェクト』とElmの『レコード』

連想配列っぽいものを、

  • JavaScriptでは『オブジェクト』
  • Elmでは『レコード』

と呼びます。微妙に書き方や使用方法が異なります。

引用元: https://qiita.com/boiyaa/items/7050eb93106a93a8dfb8
@boiyaa
2017年02月27日に更新
ElmとJavaScriptのシンタックス比較表

FirebaseからElmにデータを送るときのおおまかな流れ

Elmアプリ(をJavaScriptにコンパイルしたファイル)からは直接、Firebaseとデータのやり取りをすることができません。

そのため、src/Main.elmとFirebaseプロジェクトの間を、index.html(ここにJavaScriptのコードを記載)に仲介させます。

Firebase →(JSON)→ index.html →(portまたはflag)→ src/Main.elm

  • FirebaseからJSONデータ(文字列)をindex.html(JavaScript)に送る
  • index.html(JavaScript) は受け取ったJSON(文字列)を、JavaScriptのobjectオブジェクトに変換して理解する
  • index.html(JavaScript) は 、上記のobjectオブジェクト をport(またはflags)を介してsrc/Main.elm(をコンパイルしたJavascriptファイルであるmain.js)に送る
  • src/Main.elmは、index.html(JavaScript)から受けとったobjectオブジェクトを、Json.Decodeモジュールに定義されたデコーダーを用いて『デコード』する。具体的には、field関数やmap2関数を用いて、JavaScriptの『オブジェクト』をElmの『レコード』に変換する。(←この文章は間違っているかもしれませんが、正しい文章思いつくことができません。20190523追記)

JSON(文字列)をElmのレコードにデコードする

基礎からわかるElmをp134付近を写経して、JSONを、Elmのレコードにデコードすることに慣れてみたいと思います。

コマンドプロンプトで以下を入力し、elm replにelmのコードを入力していきます。

C:/elm/elm-json/ フォルダを新規作成し、そのフォルダをVisualStudioCodeで開き、Ctrl+@でターミナル画面を開き、以下を入力します。(2回とも、何か聞かれたらそのままEnterを押します。)

elm init
elm install elm/json

elmの対話モードelm replを使用するために、以下を入力します。

elm repl

objectから1つのフィールドを取得してみます。(フィールドという言葉は調べましたが、いまいちはっきりとしませんでした。)

例:{ “x”: 3, “y”: 4 } からkey(x)の値value(3)を取得する

Json.DedodeモジュールのdecodeString関数と、
Json.Decodeモジュールのfield関数 を用います。

decodeString : Decoder a -> String -> Result Error a
field: String -> Decoder a -> Decoder a

以下のように入力します

import Json.Decode exposing (..)
field "x" int
decodeString (field "x" int) """{ "x": 3, "y": 4 }"""

ちなみに、

(“””)(ダブルクォーテーション3つ)で囲むと、 『コード上で見たままの文字列を作る』

ことになります。JavaScriptの(`)で囲むのと似ている気がします。( 基礎からわかるElm p35)

うまく、取得できたようです。

次に、2つのフィールド(x, y)を同時に取得してみます。
map2関数を用いるそうです。

map2: (a -> b -> value) -> Decoder a -> Decoder b -> Decoder value

とりあえず、 ( 基礎からわかるElm p35) を写経してみます。

フィールドが3つ、4つと増えた場合は、map3, map4関数を用いるとのことです。
Json.Decodeモジュールはmap8関数までの提供とのことで、それ以上のオブジェクトを扱うのであれば、

NoRedInk/elm-json-decode-pipeline パッケージ

を使用する必要があるとのことです。

ちょっと似たようなことを試してみます。

type alias Model = { newTodo : String , todoList : List String }

modelDecoder = map2 Model (field "newTodo" string) (field "todoList" (list string))

decodeString modelDecoder """{ "newTodo" : "abc" , "todoList" : [ "朝ごはん",  "昼ごはん",  "dinner", "お風呂" ] }"""

Json.Decodeモジュールのat関数(ネストしたオブジェクトのフィールドをデコードする)

import Json.Decoder exposing (..)
json = """{ "person": { "name": "tom", "age": 42 } }"""

decodeString (at ["person", "name"] string) json

decodeString (at ["person", "name"] int) json

Elmの値からJavaScriptのオブジェクトを作成し、さらにJSONにエンコードする

Json.Encodeモジュールを用います。

1. Elmの値から、Json.Encode.Value型の値(JavaScriptのオブジェクト)を作る
2. encode関数を用いて、 Json.Encode.Value型の値を、『文字列化』する。(JSONに変換する)

Ctrl+C => y + Enter で、elm replを終了しておきます。

flagsにJSONデータを入れてElmで表示


https://github.com/adash333/elm-todo-localstorage2 を少し変更して、初期データをindex.html内のJSONデータとし、src/Main.elmで表示してみました。

https://ellie-app.com/5BCGQJzBK68a1

Firebaseプロジェクトの作成とデータ入力

https;//console.firebase.google.com/ に接続して、こちらの前半を参考に、新規Firebaseプロジェクトを作成し、DatabaseとしてFirestoreで、以下のように入力しておきます。

なお、今回は、データベースのルールは、一時的に、だれもが読み書き可としておきます。

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write;
    }
  }
}

index.htmlの新規作成

index.htmlを新規作成して、以下のように入力します。FirebaseConfigのところは、ご自身のものを入れてください。

なお、この時点でindex.htmlをChromeで開いたのち、Ctrl+Shift+Enterでconsole画面を読むと、以下のように、Firestoreから以下のようなオブジェクトを受け取っていることがわかります。

{
    "newTodo" : "",
    "todoList" : ["333", "123", "a", "11111", "asdfa" ] 
}

src/Main.elmの新規作成

ごちゃごちゃやっていたのですが、index.html(JavaScript)からsrc/Main.elmへFirebaseのデータをflagsやportを通して送る方法として、以下の2通りが考えられます。

  • JavaScriptのobjectを送る(elmで受け取るときはJson.Decode.Value)
  • JSON(文字列)を送る(elmで受け取るときはString)

上記のうち、JSONで送る方法でやることにしました。JSONで送ると、Elmの方で、いちいちJson.Decode.decodeString関数を用いてデコードしなければならないのですが、objectとして送った場合のelmでの受け取る方法が思いつかなかったので、以下のようなコードとしました。( https://github.com/evancz/elm-todomvc を見ると、objectとしてindex.htmlからsrc/Main.elmへ送る方がコードがすっきりしそうなのですが、、、)

今回のポイントとしては、以下となります。

index.html側で、Firestoreから受け取ったデータをJSON.stringifyで文字列に変換して、ports.read.send(JSON.stringify(doc.data())); で、readという名前のポートを通してデータを送ります。

const DB = firebase.firestore();
var docRef = DB.collection('foo').doc('1111111');
    docRef
        .get()
        .then(doc => {
            app.ports.read.send(JSON.stringify(doc.data()));
        });

Elm側では、port read : (String -> Msg) -> Sub msg で文字列としてデータをindex.htmlから受け取ります。よく分かりませんが、subscriptionsの書き方は以下のようになるようです。(本当はちゃんと理解したい!)

subscriptions : Model -> Sub Msg
subscriptions model =
    read Read

port read : (String -> msg) -> Sub msg

そして、UPDATEのtype Msgの一つとして Read String を定義します。

『文字列』をElmの『レコードrecord (連想配列のようなもの)』に変換(デコード)するために、fromJsonModelなる関数を定義しておき、

fromJsonModel : Decoder Model
fromJsonModel =
    map2 Model
        (field "newTodo" string)
        (field "todoList" (list string))

これを用いて、Readは以下のように定義します。

    Read s ->
            ( case decodeString fromJsonModel s of
                Err e ->
                    model

                Ok newModel ->
                    { newModel
                        | newTodo = model.newTodo
                    }
            , Cmd.none
            )

これで、やっと、Firestoreからindex.htmlを介して、src/Main.elmにデータを送り、表示することができました。

しかし、この方法だと、Firestoreのデータを追加したり削除したりしても、Elmの方のデータはリアルタイムには更新されませんでした。原因は分かりません。。。ぐはっ。

Firestoreで変更発生時の通知

以下を見ると、『onSnapshot()メソッドでリスナをセットすることで、ドキュメントの変更時にコールバックを受けることが出来る。』そうです。。。
https://qiita.com/subaru44k/items/a88e638333b8d5cc29f2#%E5%A4%89%E6%9B%B4%E7%99%BA%E7%94%9F%E6%99%82%E3%81%AE%E9%80%9A%E7%9F%A5

      const DB = firebase.firestore();

      var docRef = DB.collection('foo').doc('1111111');
      docRef.onSnapshot(function(doc) {
    console.log(doc.data());
    console.log(typeof doc.data());
    console.log(typeof JSON.stringify(doc.data()));   
    app.ports.read.send(JSON.stringify(doc.data()));
    });

これで、Firestoreのデータを削除したり追加したりすると、自動でMain.elmの方のデータも書き換わり、リアルタイムデータベースらしくなりました。

たったこんなことをするだけなのに、1か月くらいかかった気がします。。。Elmを扱うためには、素のJavaScriptの勉強をしっかりやらないと、、、(当たり前なことだとはわかっていますが、、、)

参考: https://guide.elm-lang.jp/effects/json.html
Elm公式ガイド 日本語版 JSON

http://nexus1.hatenablog.com/entry/2017/06/19/224848
関数型よく知らない人のelm入門(5) – Jsonパース実践サンプル集

スポンサーリンク