突然ですが、スマートフォンの保有率って知ってますか?総務省の調べによると2017年時点でモバイル端末全体が日本の94.8% その内の75.1%がスマートフォンらしいです。開発者が主体であるGitHubでさえもモバイル化が決定したのが今年の大きなニュースの一つかと思います。僕自身もSlack通知や簡単なPRレビューはスマホで済ませてしまうことが多いです。SPAが当たり前で、これからPWAの技術なども発達していくことを考えると、もはやモバイルファーストに開発すると言ってもおかしくない時代になってきました。そこで、Elmを使ってモバイルなWebアプリケーションを作るのはどのような感じなのか試してみました。今回題材にするのは、Googleの検索サジェスト画面です。しかしさすがGoogle これだけシンプルに見えて、機能が凄まじく おそらく 1/3 ぐらいの機能量とクオリティしか出せませんでした・・・。
こちらが、デモとソースコードになります。デモは是非、スマホ端末からお試しください。
PCとモバイルWebの大きな違い
PC WEBでは、UXを高めようとすると、もしかしたらアプリケーションで使いまわしたいような状態を持つコンポーネントを作りたいというシーンがあるのかもしれません。実際、私はB向けのシステムを普段業務でAngularを用いて開発をしていてリッチな機能を持つコンポーネント無しでは厳しかったな(それでもメンテナンスコストは非常に高いなとも思いますが)と思うシーンが多々あります。しかし、モバイルの場合、装飾を少しリッチにしたいという事は多くても、スマホ向けのUXを考えた何かを作ろうとするとWEBに比べてコンポーネントの重要度が下がるように見えます。例えば、今回のGoogleの検索サジェストに似せたサンプルでは細々としたコンポーネントの集合ではなく、画面をフルに使った体験をどうするかということが主眼になりコレクションを主軸としたロジックや自分で1から書くCSSの方が重要度が高いように思えました。つまりCSSフレームワークやコンポーネントの組み合わせでUXを担保できる時代では無くなってきているのです。
Elmで書くモバイルWeb
Elmでは、JSフレームワークでは当たり前のように使われているコンポーネントという概念がありません。(詳しくはこちらの記事をご覧ください。) 代わりに単純なView関数があるだけです。そしてElmのアーキテクチャがそれを内包し支えます。1画面をフルに使うモバイルだからこそ、今回はElmが非常に刺さると実感しました。
View関数
単なる関数による抽象化の凄さを実感してみましょう。アプリケーションの画像を再掲して簡単に掻い摘んで説明をすると、上部の検索ワードを入れるinput部分searchView
と下部のsearchSuggestions
に二分されます。これがview関数本体です。searchSuggestions
関数を見てみると、検索履歴historySuggestions
と検索ヒントhintSuggestions
の二つから検索サジェストが成り立っています。検索ワードがない場合は検索履歴のみを出したり、全体で8個の検索結果を出すのですが、検索履歴の数から差し引いたりなかなか細かなコレクション操作が求められますが、ElmはImmutableかつ優れた関数が標準で豊富に取り揃えられているので苦なく書くことができました。これは常々Elmを使うときに感じている利点です。ここまででコンポーネント指向無しに、ほとんどHTMLを意識することなく綺麗に抽象化が出来ていることがわかります。
view : Model -> Html Msg
view model =
div [ class "container" ]
[ searchView model.searchWord
, searchSuggestions model
]
searchSuggestions : Model -> Html Msg
searchSuggestions model =
let
{ searchWord } =
model
in
div [ class "UUbT9" ]
[ ul [ class "aajZCb" ]
(let
historySuggestions =
model.historyWords
|> List.filter (String.startsWith searchWord)
|> List.take
limitSuggestion
|> List.map
(historySuggestion searchWord)
hintSuggestions =
Set.diff (Set.fromList model.hintWords) (Set.fromList model.historyWords)
|> Set.toList
|> List.filter (String.startsWith searchWord)
|> List.map (hintSuggestion searchWord)
in
if String.isEmpty searchWord then
historySuggestions
else
historySuggestions ++ List.take (limitSuggestion - List.length historySuggestions) hintSuggestions
)
]
Model
Elmはシングルステートで、この画面に対して必要なデータ構造と意識すべき状態は何かというのが一目瞭然かと思います。アプリケーションがユーザに与えたい体験とそれを実現するデータが何なのかが簡単に考えられるのが良いですね。View関数で説明した通り、豊富なコレクション関数が用意されているデータ構造になります。
type alias Model =
{ searchWord : String
, historyWords : List String
, hintWords : List String
}
init : () -> ( Model, Cmd Msg )
init _ =
( { searchWord = ""
, historyWords = []
, hintWords = [ "elm guide", "elm package", "elm-spa", "elm-jp", "google", "amazon", "apple" ] ++ someWords
}
, Cmd.none
)
Msgとupdate関数
抽象化されたView関数の中で呼び出されるアプリケーションのアクション(Msg)とそれによってModelを更新するupdate関数について説明をします。これも今までと同じように1つの画面で何がユーザが出来て、それがどのように振舞うのかをひとまとめ見れるのはとても良いです。Elmを始めた方がupdateが肥大化してしまうことをよく懸念として挙げられますが、これは分岐が増える ではなく、ユーザのできるアクションの一覧が羅列されるだけでプログラムの複雑性が上がるわけではなく網羅性もコンパイラで担保されるためバグの温床になることは一切ありません。今回のアプリケーションでは、検索ワードを打ち込むことができる。履歴を削除することができる。検索したワードを履歴に保存することができる。つまりここを見るだけでアプリケーションが何をしているかの説明となってしまいます。コードから仕様が読み取れるのは非常に素晴らしいことですね。
type Msg
= UpdateSearchWord String
| DeleteHistory String
| SaveHistory String
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UpdateSearchWord searchWord ->
( { model | searchWord = searchWord }, Cmd.none )
SaveHistory historyWord ->
( { model | historyWords = historyWord :: model.historyWords }, Cmd.none )
DeleteHistory historyWord ->
( { model | historyWords = List.filter (not << (==) historyWord) model.historyWords }, Cmd.none )
まとめ
これから求められることが多くなっていくモバイルWEBですが、基本に忠実にやっていく分にはElmは非常に簡潔で強力な武器になるということが実感できたのではないでしょうか? また小手先のテクニックでは太刀打ちできず、愚直にCSSの力やコレクション操作などの基礎ロジックを組み立てる力が、なんだかんだ価値を発揮するのがこれまでとこれからの開発だと思っています。ですが悲観することなく楽しく開発していきましょう!それでは良いElmライフを!