ElmでWebフロントエンドの信頼性を高める

公開: 2022-03-11

Webフロントエンドをデバッグしようとして、複雑な一連のイベントを処理するコードに巻き込まれたことは何回ありますか?

jQuery、Backbone.js、またはその他の一般的なJavaScriptフレームワークで構築された多くのコンポーネントを処理するUIのコードをリファクタリングしようとしたことがありますか?

これらのシナリオで最も苦痛なことの1つは、イベントの複数の不確定なシーケンスを追跡し、これらすべての動作を予測して修正しようとすることです。 単に悪夢!

私は常に、Webフロントエンド開発のこの地獄のような側面から逃れる方法を探してきました。 Backbone.jsは、Webフロントエンドに長い間欠けていた構造を与えることで、この点でうまく機能しました。 しかし、最も些細なことのいくつかを実行するために必要な冗長性を考えると、それはそれほど良くはなりませんでした。

ElmでWebフロントエンドの信頼性を高める

それから私はエルムに会いました。

Elmは、Haskellプログラミング言語に基づく静的に型付けされた関数型言語ですが、仕様はより単純です。 コンパイラー(これもHaskellを使用して構築されています)はElmコードを解析し、JavaScriptにコンパイルします。

Elmは元々フロントエンド開発用に構築されましたが、ソフトウェアエンジニアは、サーバー側のプログラミングにも使用する方法を見つけました。

この記事では、ElmがWebフロントエンド開発の考え方をどのように変えることができるかについての概要と、この関数型プログラミング言語の基本について説明します。 このチュートリアルでは、Elmを使用して簡単なショッピングカートのようなアプリケーションを開発します。

なぜニレ?

Elmには多くの利点があり、そのほとんどはクリーンなWebフロントエンドアーキテクチャを実現するのに非常に役立ちます。 Elmは、他の一般的なフレームワーク(React.jsでさえ)よりも優れたHTMLレンダリングパフォーマンスの利点を提供します。 さらに、Elmを使用すると、開発者はコードを記述できます。実際には、JavaScriptなどの動的型付け言語を悩ますランタイム例外はほとんど発生しません。

コンパイラーはタイプを自動的に推測し、わかりやすいエラーを発行するため、開発者は実行前に潜在的な問題を認識できます。

NoRedInkには36,000行のElmがあり、1年以上生産された後も、ランタイム例外は1つも発生していません。 [ソース]

Elmを試すためだけに、既存のJavaScriptアプリケーション全体を変換する必要はありません。 JavaScriptとの優れた相互運用性により、既存のアプリケーションのごく一部を取得して、Elmで書き直すこともできます。

Elmには、提供するものの完全な説明を提供するだけでなく、Elmアーキテクチャに従ってWebフロントエンドを構築するための適切なガイドを提供する優れたドキュメントもあります。これは、モジュール性、コードの再利用、およびテストに最適です。 。

シンプルなカートを作ろう

Elmコードの非常に短いスニペットから始めましょう。

 import List exposing (..) cart = [] item product quantity = { product = product, qty = quantity } product name price = { name = name, price = price } add cart product = if isEmpty (filter (\item -> item.product == product) cart) then append cart [item product 1] else cart subtotal cart = -- we want to calculate cart subtotal sum (map (\item -> item.product.price * toFloat item.qty) cart)

--で始まるテキストは、Elmのコメントです。

ここでは、カートをアイテムのリストとして定義しています。ここで、すべてのアイテムは2つの値(対応する製品と数量)を持つレコードです。 各製品は、名前と価格が記載されたレコードです。

カートに商品を追加するには、その商品がカートにすでに存在するかどうかを確認する必要があります。

もしそうなら、私たちは何もしません。 それ以外の場合は、商品を新しいアイテムとしてカートに追加します。 リストをフィルタリングし、各アイテムを商品と照合し、結果のフィルタリングされたリストが空であるかどうかを確認することにより、商品がカートにすでに存在するかどうかを確認します。

小計を計算するために、カート内のアイテムを繰り返し処理し、対応する製品の数量と価格を見つけて、すべてを合計します。

これはカートとその関連機能が得ることができるのと同じくらいミニマルです。 このコードから始めて、段階的に改善し、完全なWebコンポーネント、またはElmの用語でプログラムにします。

プログラムのさまざまな識別子に型を追加することから始めましょう。 Elmはそれ自体で型を推測できますが、Elmとそのコンパイラを最大限に活用するには、型を明示的に示すことをお勧めします。

 module Cart1 exposing ( Cart, Item, Product , add, subtotal , itemSubtotal ) -- This is module and its API definition {-| We build an easy shopping cart. @docs Cart, Item, Product, add, subtotal, itemSubtotal -} import List exposing (..) -- we need list manipulation functions {-| Cart is a list of items. -} type alias Cart = List Item {-| Item is a record of product and quantity. -} type alias Item = { product : Product, qty : Int } {-| Product is a record with name and price -} type alias Product = { name : String, price : Float } {-| We want to add stuff to a cart. This is a function definition, it takes a cart, a product to add and returns new cart -} add : Cart -> Product -> Cart {-| This is an implementation of the 'add' function. Just append product item to the cart if there is no such product in the cart listed. Do nothing if the product exists. -} add cart product = if isEmpty (filter (\item -> item.product == product) cart) then append cart [Item product 1] else cart {-| I need to calculate cart subtotal. The function takes a cart and returns float. -} subtotal : Cart -> Float {-| It's easy -- just sum subtotal of all items. -} subtotal cart = sum (map itemSubtotal cart) {-| Item subtotal takes item and return the subtotal float. -} itemSubtotal : Item -> Float {-| Subtotal is based on product's price and quantity. -} itemSubtotal item = item.product.price * toFloat item.qty

型アノテーションを使用すると、コンパイラーは、実行時例外が発生する可能性のある問題をキャッチできるようになりました。

しかし、エルムはそれだけではありません。 Elmアーキテクチャは、Webフロントエンドを構造化するための単純なパターンを通じて開発者をガイドします。これは、ほとんどの開発者がすでに精通している概念を通じて行われます。

  • モデル:モデルはプログラムの状態を保持します。
  • ビュー:ビューは状態を視覚的に表したものです。
  • 更新:更新は、状態を変更する方法です。

更新を処理するコードの部分をコントローラーと考えると、古き良きModel-View-Controller(MVC)パラダイムに非常によく似たものがあります。

Elmは純粋な関数型プログラミング言語であるため、すべてのデータは不変です。つまり、モデルを変更することはできません。 代わりに、更新関数を使用して、前のモデルに基づいて新しいモデルを作成できます。

なぜそんなに素晴らしいのですか?

不変のデータを使用すると、関数に副作用が発生することはなくなります。 これにより、エルムのタイムトラベルデバッガーなど、可能性の世界が開かれます。これについては、後ほど説明します。

モデルの変更でビューの変更が必要になるたびにビューがレンダリングされ、モデル内の同じデータに対して常に同じ結果が得られます。これは、純粋関数が常に同じ結果を返すのとほぼ同じ方法です。同じ入力引数。

主な機能から始める

先に進んで、カートアプリケーションのHTMLビューを実装しましょう。

Reactに精通している場合、これはきっと喜ばれることです。Elmには、HTML要素を定義するためのパッケージがあります。 これにより、外部のテンプレート言語に依存することなく、Elmを使用してビューを実装できます。

HTML要素のラッパーは、 Htmlパッケージで入手できます。

 import Html exposing (Html, button, table, caption, thead, tbody, tfoot, tr, td, th, text, section, p, h1)

すべてのElmプログラムは、main関数を実行することから始まります。

 type alias Stock = List Product type alias Model = { cart : Cart, stock : Stock } main = Html.beginnerProgram { model = Model [] [ Product "Bicycle" 100.50 , Product "Rocket" 15.36 , Product "Biscuit" 21.15 ] , view = view , update = update }

ここで、main関数は、いくつかのモデル、ビュー、および更新関数を使用してElmプログラムを初期化します。 いくつかの種類の製品とその価格を定義しました。 簡単にするために、製品の数に制限がないことを前提としています。

シンプルなアップデート機能

更新機能は、私たちのアプリケーションが生き返る場所です。

メッセージを受け取り、メッセージの内容に基づいて状態を更新します。 これを、2つのパラメーター(メッセージと現在のモデル)を受け取り、新しいモデルを返す関数として定義します。

 type Msg = Add Product update : Msg -> Model -> Model update msg model = case msg of Add product -> { model | cart = add model.cart product }

今のところ、メッセージがAdd productである場合、単一のケースを処理しています。ここでは、 productとともにcartaddメソッドを呼び出します。

Elmプログラムの複雑さが増すにつれて、更新機能も増します。

ビュー機能の実装

次に、カートのビューを定義します。

ビューは、モデルをHTML表現に変換する関数です。 ただし、これは静的HTMLだけではありません。 HTMLジェネレーターは、さまざまなユーザーの操作やイベントに基づいて、アプリケーションにメッセージを返すことができます。

 view : Model -> Html Msg view model = section [style [("margin", "10px")]] [ stockView model.stock , cartView model.cart ] stockView : Stock -> Html Msg stockView stock = table [] [ caption [] [ h1 [] [ text "Stock" ] ] , thead [] [ tr [] [ th [align "left", width 100] [ text "Name" ] , th [align "right", width 100] [ text "Price" ] , th [width 100] [] ] ] , tbody [] (map stockProductView stock) ] stockProductView : Product -> Html Msg stockProductView product = tr [] [ td [] [ text product.name ] , td [align "right"] [ text ("\t$" ++ toString product.price) ] , td [] [ button [ onClick (Add product) ] [ text "Add to Cart" ] ] ]

Htmlパッケージは、よく使用されるすべての要素のラッパーを、おなじみの名前の関数として提供します(たとえば、関数section<section>要素を生成します)。

Html.Attributesパッケージの一部であるstyle関数は、結果の要素にstyle属性を設定するためにsection関数に渡すことができるオブジェクトを生成します。

再利用性を高めるために、ビューを個別の関数に分割することをお勧めします。

簡単にするために、CSSといくつかのレイアウト属性をビューコードに直接埋め込みました。 ただし、ElmコードからHTML要素をスタイリングするプロセスを合理化するライブラリが存在します。

スニペットの終わり近くにあるbuttonと、ボタンのクリックイベントにAdd productするというメッセージがどのように関連付けられているかに注目してください。

Elmは、コールバック関数を実際のイベントにバインドするために必要なすべてのコードを生成し、関連するパラメーターを使用して更新関数を生成および呼び出す処理を行います。

最後に、ビューの最後のビットを実装する必要があります。

 cartView : Cart -> Html Msg cartView cart = if isEmpty cart then p [] [ text "Cart is empty" ] else table [] [ caption [] [ h1 [] [ text "Cart" ]] , thead [] [ tr [] [ th [ align "left", width 100 ] [ text "Name" ] , th [ align "right", width 100 ] [ text "Price" ] , th [ align "center", width 50 ] [ text "Qty" ] , th [ align "right", width 100 ] [ text "Subtotal" ] ] ] , tbody [] (map cartProductView cart) , tfoot [] [ tr [style [("font-weight", "bold")]] [ td [ align "right", colspan 4 ] [ text ("$" ++ toString (subtotal cart)) ] ] ] ] cartProductView : Item -> Html Msg cartProductView item = tr [] [ td [] [ text item.product.name ] , td [ align "right" ] [ text ("$" ++ toString item.product.price) ] , td [ align "center" ] [ text (toString item.qty) ] , td [ align "right" ] [ text ("$" ++ toString (itemSubtotal item)) ] ]

ここでは、カートの内容をレンダリングするビューの他の部分を定義しました。 ビュー関数はメッセージを出力しませんが、ビューとしての資格を得るには、リターンタイプHtml Msgが必要です。

ビューには、カートの内容が一覧表示されるだけでなく、カートの内容に基づいて小計が計算およびレンダリングされます。

このElmプログラムの完全なコードはここにあります。

今すぐElmプログラムを実行すると、次のように表示されます。

それはすべてどのように機能しますか?

私たちのプログラムは、 main関数からのかなり空の状態、つまりハードコードされた製品がいくつか入った空のカートから始まります。

「カートに追加」ボタンがクリックされるたびに、メッセージが更新機能に送信され、更新機能はそれに応じてカートを更新し、新しいモデルを作成します。 モデルが更新されるたびに、ビュー関数がElmによって呼び出され、HTMLツリーが再生成されます。

ElmはReactと同様の仮想DOMアプローチを使用しているため、UIの変更は必要な場合にのみ実行され、迅速なパフォーマンスが保証されます。

タイプチェッカーだけではありません

Elmは静的に型付けされていますが、コンパイラーは型だけでなく多くのことをチェックできます。

Msgタイプに変更を加えて、コンパイラがそれにどのように反応するかを見てみましょう。

 type Msg = Add Product | ChangeQty Product String

別の種類のメッセージを定義しました。カート内の商品の数量を変更するものです。 ただし、更新機能でこのメッセージを処理せずにプログラムを再実行しようとすると、次のエラーが発生します。

より機能的なカートに向けて

前のセクションでは、数量値のタイプとして文字列を使用したことに注意してください。 これは、値が文字列型の<input>要素から取得されるためです。

Cartモジュールに新しい関数changeQtyを追加しましょう。 モジュールAPIを変更せずに、必要に応じて後で変更できるように、モジュール内に実装を保持することをお勧めします。

 {-| Change quantity of the product in the cart. Look at the result of the function. It uses Result type. The Result type has two parameters: for bad and for good result. So the result will be Error "msg" or a Cart with updated product quantity. -} changeQty : Cart -> Product -> Int -> Result String Cart {-| If the quantity parameter is zero the product will be removed completely from the cart. If the quantity parameter is greater then zero the quantity of the product will be updated. Otherwise (qty < 0) the error will be returned. -} changeQty cart product qty = if qty == 0 then Ok (filter (\item -> item.product /= product) cart) else if qty > 0 then Result.Ok (map (\item -> if item.product == product then { item | qty = qty } else item) cart) else Result.Err ("Wrong negative quantity used: " ++ (toString qty))

関数がどのように使用されるかについては、何も想定しないでください。 パラメータqtyにはIntが含まれると確信できますが、値は何でもかまいません。 したがって、値を確認し、無効な場合はエラーを報告します。

それに応じて、 update機能も更新します。

 update msg model = case msg of Add product -> { model | cart = add model.cart product } ChangeQty product str -> case toInt str of Ok qty -> case changeQty model.cart product qty of Ok cart -> { model | cart = cart } Err msg -> model -- do nothing, the wrong input Err msg -> model -- do nothing, the wrong quantity

文字列quantityパラメータをメッセージから数値に変換してから使用します。 文字列に無効な数値が含まれている場合は、エラーとして報告されます。

ここでは、エラーが発生したときにモデルを変更せずに保持します。 または、ユーザーが確認できるように、ビューにメッセージとしてエラーを報告する方法でモデルを更新することもできます。

 type alias Model = { cart : Cart, stock : Stock, error : Maybe String } main = Html.beginnerProgram { model = Model [] -- empty cart [ Product "Bicycle" 100.50 -- stock , Product "Rocket" 15.36 , Product "Bisquit" 21.15 ] Nothing -- error (no error at beginning) , view = view , update = update } update msg model = case msg of Add product -> { model | cart = add model.cart product } ChangeQty product str -> case toInt str of Ok qty -> case changeQty model.cart product qty of Ok cart -> { model | cart = cart, error = Nothing } Err msg -> { model | error = Just msg } Err msg -> { model | error = Just msg }

モデルのエラー属性には、 Maybe Stringタイプを使用します。 たぶん、 Nothingまたは特定のタイプの値を保持できる別のタイプです。

ビュー機能を次のように更新した後:

 view model = section [style [("margin", "10px")]] [ stockView model.stock , cartView model.cart , errorView model.error ] errorView : Maybe String -> Html Msg errorView error = case error of Just msg -> p [style [("color", "red")]] [ text msg ] Nothing -> p [] []

あなたはこれを見るはずです:

数値以外の値(「1a」など)を入力しようとすると、上のスクリーンショットに示すようなエラーメッセージが表示されます。

パッケージの世界

Elmには、オープンソースパッケージの独自のリポジトリがあります。 Elmのパッケージマネージャーを使用すると、このパッケージのプールを簡単に活用できます。 リポジトリのサイズは、PythonやPHPなどの他の成熟したプログラミング言語に匹敵するものではありませんが、Elmコミュニティは毎日より多くのパッケージを実装するために懸命に取り組んでいます。

ビューに表示される価格の小数点以下の桁数に一貫性がないことに注意してください。

toStringの素朴な使用を、リポジトリからのより良いもの、numeric-elmに置き換えましょう。

 cartProductView item = tr [] [ td [] [ text item.product.name ] , td [ align "right" ] [ text (formatPrice item.product.price) ] , td [ align "center" ] [ input [ value (toString item.qty) , onInput (ChangeQty item.product) , size 3 --, type' "number" ] [] ] , td [ align "right" ] [ text (formatPrice (itemSubtotal item)) ] ] formatPrice : Float -> String formatPrice price = format "$0,0.00" price

ここでは、Numeralパッケージのformat関数を使用しています。 これにより、通常の通貨のフォーマット方法で数値がフォーマットされます。

 100.5 -> $100.50 15.36 -> $15.36 21.15 -> $21.15

自動ドキュメント生成

パッケージをElmリポジトリに公開すると、コード内のコメントに基づいてドキュメントが自動的に生成されます。 Cartモジュールのドキュメントをここで確認すると、実際の動作を確認できます。 これらはすべて、このファイル(Cart.elm)にあるコメントから生成されました。

フロントエンド用の真のデバッガー

最も明白な問題は、コンパイラ自体によって検出および報告されます。 ただし、論理エラーから安全なアプリケーションはありません。

Elmのすべてのデータは不変であり、すべてが更新関数に渡されるメッセージを介して行われるため、Elmプログラムのフロー全体を一連のモデル変更として表すことができます。 デバッガーにとって、Elmはターン制ストラテジーゲームのようなものです。 これにより、デバッガーは、時間の移動など、いくつかの本当に驚くべき偉業を実行できます。 プログラムの存続期間中に発生したさまざまなモデル変更間をジャンプすることにより、プログラムのフローを前後に移動できます。

デバッガーについて詳しくは、こちらをご覧ください。

バックエンドとの相互作用

それで、あなたは言う、私たちは素敵なおもちゃを作りました、しかしエルムは何か深刻なことに使われることができますか? 絶対。

カートのフロントエンドを非同期のバックエンドに接続してみましょう。 それを面白くするために、特別なものを実装します。 すべてのカートとその内容をリアルタイムで検査したいとします。 実際には、このアプローチを使用して、オンラインショップやマーケットプレイスに追加のマーケティング/販売機能を導入したり、ユーザーに提案を行ったり、必要な在庫リソースを見積もったりすることができます。

そのため、カートをクライアント側に保存し、サーバーに各カートについてリアルタイムで通知します。

簡単にするために、Pythonを使用してバックエンドを実装します。 バックエンドの完全なコードはここにあります。

これは、WebSocketを使用し、カートの内容をメモリ内で追跡する単純なWebサーバーです。 簡単にするために、他のすべてのカートを同じページに表示します。 これは、別のページで、または別のElmプログラムとして簡単に実装できます。 今のところ、すべてのユーザーが他のユーザーのカートの概要を見ることができます。

バックエンドを配置したら、Elmアプリを更新して、サーバーとカートの更新を送受信する必要があります。 JSONを使用してペイロードをエンコードします。これに対して、Elmは優れたサポートを提供します。

CartEncoder.elm

ElmデータモデルをJSON文字列表現に変換するエンコーダーを実装します。 そのためには、Json.Encodeライブラリを使用する必要があります。

 module CartEncoder exposing (cart) import Cart2 exposing (Cart, Item, Product) import List exposing (map) import Json.Encode exposing (..) product : Product -> Value product product = object [ ("name", string product.name) , ("price", float product.price) ] item : Item -> Value item item = object [ ("product", product item.product) , ("qty", int item.qty) ] cart : Cart -> Value cart cart = list (map item cart)

ライブラリは、Elmオブジェクトを取得してJSONでエンコードされた文字列に変換するいくつかの関数( stringintfloatobjectなど)を提供します。

CartDecoder.elm

すべてのElmデータにはタイプがあり、どのJSON値をどのタイプに変換する必要があるかを定義する必要があるため、デコーダーの実装は少し注意が必要です。

 module CartDecoder exposing (cart) import Cart2 exposing (Cart, Item, Product) -- decoding for Cart import Json.Decode exposing (..) -- will decode cart from string cart : Decoder (Cart) cart = list item -- decoder for cart is a list of items item : Decoder (Item) item = object2 Item -- decoder for item is an object with two properties: ("product" := product) -- 1) "product" of product ("qty" := int) -- 2) "qty" of int product : Decoder (Product) product = object2 Product -- decoder for product also an object with two properties: ("name" := string) -- 1) "name" ("price" := float) -- 2) "price"

更新されたElmアプリケーション

最終的なElmコードは少し長いので、ここで見つけることができます。 フロントエンドアプリケーションに加えられた変更の概要は次のとおりです。

元のupdate関数を、カートが更新されるたびにカートの内容の変更をバックエンドに送信する関数でラップしました。

 updateOnServer msg model = let (newModel, have_to_send) = update msg model in case have_to_send of True -> -- send updated cart to server (!) newModel [ WebSocket.send server (encode 0 (CartEncoder.cart newModel.cart)) ] False -> -- do nothing (newModel, Cmd.none)

また、サーバーから更新を受信し、それに応じてローカルモデルを更新するために、 ConsumerCarts Stringのメッセージタイプを追加しました。

ビューが更新され、 consumersCartsView関数を使用して他のカートの概要が表示されるようになりました。

他のカートへの変更をリッスンするためにバックエンドにサブスクライブするために、WebSocket接続が確立されました。

 subscriptions : Model -> Sub Msg subscriptions model = WebSocket.listen server ConsumerCarts server = "ws://127.0.0.1:8765"

また、メイン機能も更新しました。 ここで、追加のinitおよびsubscriptionsパラメーターを指定してHtml.programを使用します。 initはプログラムの初期モデルを指定し、 subscriptionはサブスクリプションのリストを指定します。

サブスクリプションは、特定のチャネルで変更をリッスンし、それらのメッセージをupdate機能に転送するようにElmに指示する方法です。

 main = Html.program { init = init , view = view , update = updateOnServer , subscriptions = subscriptions } init = ( Model [] -- empty cart [ Product "Bicycle" 100.50 -- stock , Product "Rocket" 15.36 , Product "Bisquit" 21.15 ] Nothing -- error (no error at beginning) [] -- consumer carts list is empty , Cmd.none)

最後に、サーバーから受信したConsumerCartsメッセージをデコードする方法を処理しました。 これにより、外部ソースから受け取ったデータがアプリケーションを壊さないことが保証されます。

 ConsumerCarts message -> case decodeString (Json.Decode.list CartDecoder.cart) message of Ok carts -> ({ model | consumer_carts = carts }, False) Err msg -> ({ model | error = Just msg, consumer_carts = [] }, False)

フロントエンドを正気に保つ

ニレは違います。 開発者は別の考え方をする必要があります。

JavaScriptや同様の言語の領域から来た人は誰でも、Elmのやり方を学ぼうとしていることに気付くでしょう。

しかし、最終的には、Elmは、他のフレームワーク(最も人気のあるフレームワークでさえ)がしばしば苦労する何かを提供します。 つまり、巨大な冗長コードに巻き込まれることなく、堅牢なフロントエンドアプリケーションを構築する手段を提供します。

Elmはまた、スマートコンパイラと強力なデバッガを組み合わせることで、JavaScriptがもたらす多くの問題を抽象化します。

Elmは、フロントエンドの開発者が長い間待ち望んでいたものです。 これで実際の動作を確認できたので、自分で試してみて、Elmで次のWebプロジェクトを構築してメリットを享受してください。