読者です 読者をやめる 読者になる 読者になる

ジンジャー研究室

長めのつぶやき。難しいことは書きません。

elm-conf 2016に行ってきたメモ

elm-conf 2016

アメリカ、セントルイスにて。 以下、終わった後に記憶を頼りに書き起こした雑極まりないメモ。

雰囲気

写真は休憩中だから人少ないけど。

f:id:jinjor:20160915122715j:plain

トーク

Keynote: Code is the easy part (by Evan Czaplicki)

コードを書くのは一番簡単な部分。言語設計として何を書くかが問題。 Pythonは最初の5年ほぼプライベートで、ドキュメントが整うのには10年かかった。Elmはまだ4年。 要求を一つずつコードで実装して解決することはしない。数ある要求を集めてパターンを見つける。そのためにたくさんのフィードバックが欲しい。 0.18に向けて今取り組んでるのがデバッガー。モデルの状態を全部記憶できるし、状態を保存してリロードできたりする。あとサーバーサイドレンダリングとか。細々したタスクは今はあえて無視してる。

Beyond Hello World and Todo Lists (by Ossi Hanhinen)

リアルワールド体験談。割と苦労した話とか。 すべてをコンポーネントにしてはいけない。問題を一般化せずに体当たりしていけ。 パッケージの切り分け方とかルーティングとかデバッグとかのTips紹介。

Compilers as Therapists, or Why Elm is Good for ADHD (by Luke Westby)

会場沸いてたけど、ほとんど聞き取れなかったごめん。 ADHDは注意力(集中力)に難のある障害なんだけど、Elmだと大丈夫だった。という話だったと思う。

LT: Rich Animation (by Matthew Griffith)

同時にアニメーションさせるときに割り込むかキューに積むかとか、真面目に考えると結構難しい。 elm-style-animationだとエレガントにできる。

LT: Functional Data Structures (by Tessa Kelly)

バイナリツリーを4種類の方法で実装して比較してみた。 インデックスとかよりもポインタ(union typeとかで)実装した方が明らかにエレガント。

LT: 0-60 in 15 Minutes: Building a Realtime App With Elm and Horizon (by Abadi Kurniawan)

とにかくサーバサイド書きたくない。なら書かなきゃいいじゃん。 Elmを使ってHorizon.js(RethinkDB)のインターフェイスを実装したら、Elmだけでチャットアプリが作れた。

Rolling Random Romans (by Joël Quenneville)

ローマ人の名前の付け方の法則をモデル化してランダム生成してみた。 文脈によって制約が変わったりしてとても複雑。 純粋な関数型でランダム値を生成するにはSeedが必要で面倒だが、0.17でCmdによる生成が可能になった。 ランダムのパターンを自由に組み立てられるのが便利。ボトムアップに組み立てると良い。

Building an Interactive Storytelling Framework in Elm (by Jeff Schomay)

ユーザ選択によって変わるストーリーを生成する。 会場のリクエストで選択肢を決定してストーリーを進めるデモ。最高の盛り上がり。 3パターンくらい試行錯誤した。union type使うと網羅できるけど、書き方が冗長になるのとフレームワークの場合は具体的な内容を知ることができないので万能ではなかった。最終的にリストを使ったDSLに。

The Clockwork Gardener: Growing an Elm App With Templates (by Jessica Kerr)

AtomエディタでElmの良くあるボイラープレートを生成する。(Atomist) 「〜〜を〜〜で〜〜しろ」みたいな一行のコマンドでどんどんElmコードを生成するテンプレート芸。 だいぶ頭おかしい(良い意味で)。 コードの自動生成が怖い?Elmコンパイラがついてるから全然怖くないよ。

Nightingale.space - Elm and Crowd-Source Music-Making (by Murphy Randle)

Twitter上に楽譜を置いたらキューに積んで片っ端から再生していくアプリ。 Elixir(Phoenix) + Elm + JavaScript。 パースに使ってるelm-combine最高(とても同意)。 会場でリアルタイムにツイートされた楽譜を再生していくデモ。楽しい。

Making Impossible States Impossible (by Richard Feldman)

データ構造がしっかりしていればinvalidな状態を作ることは原理的に不可能。 たとえばCSSの@は順番がある。ユーザにリストで書かせるとバリデーションとかテストが必要。 テストはいいけど、しなくて済むならその方がいいよね。 その他の例としてはHistoryとか。 あとは、シングルコンストラクタのパターンを使ってデータをプライベート化した上で、公開したい操作だけを関数で提供する。

トーク全体の感想

とにかく全員トークのクオリティが高くて、どうやったら面白くなるかが真剣に練られてるのがすごいと思った。見習わないと。 英語はほとんど聞き取れなくてスライドの文字情報と雰囲気で補完した。無理。全然無理。

最後のMaking Impossible States Impossibleは、基本的だけどなかなかはっきり言及されてない部分なので、何気に名トークかも。いつも関数型の人が雑に「型があるから安全」って言ってるけど、実は構造体の定義の話もあって「じゃあJavaも型あるじゃん」とか言われてなかなか伝わらない部分。

Q/Aセッション

スピーカー全員が回答者。言語の開発に関してはEvanだったけど。

Q: プロダクションで客にどう説明してる?
A: リスクは確かにあるけど、そこで議論がストップするの良くない。 それに見合うだけあるいはそれを上回るメリットがあるならリスクは取っていい。

Q: Haskell使えない人がコントリビュートするには?
A: Haskellコードいきなりいじる前にコミュニティのコンセンサスとって。あとドキュメントを直接直してくる人も居るけど、いや待ってこれ君の本じゃないから。

Q: デザイナーと協業するには?
A: 前提としてデザイナーは頭がいい。CSSが使えるんだから。 Elmを理解してもらおう。(ツールについての言及もあったけど聞き取れず)

Q: Effect Managerどうなるの?
A: 現時点でほとんど完成してる(Stablish)とは思うけど、今後の展開によっては変えないといけない部分があるかもしれないから安易に公開OKにできない事情がある。

他にもあったけど忘れた。

Evanに話しかけてみた

アイコン見せたらお前かー!みたいな感じだった。elm-time-travelの存在は認知してたけど見てはいなかったみたいで、デモ見せてみたら「これ今まさに作ってるやつじゃん」って言ってた。モデルの出力どうやってるのか聞かれたので、toStringしたやつをパースしてると白状したら爆笑してた。

Evanめっちゃいい人。 唯一知ってる日本語は「仕事がありません」らしい。

ElmでHTMLパーサを作って公開するまでの手順

ElmでHTMLパーサを作った。

github.com

せっかくなので、ライブラリ制作に着手してから公開するまでのプロセスを書いてみる。Elm 開発の雰囲気を伝えるのが目的なので、特定のトピックが知りたい方はQiitaへどうぞ。(コードが沢山あるけど試してないので動かないかも。あと、途中でテストライブラリをアップデートしたりして実際に踏んだプロセスと違うし、コードも所々違うんだけど、それは無視して最短・最適のパスを踏んだことにする。)

経緯

Excel(とか他の表計算ソフト)からクリップボードにコピーしてWebアプリに貼り付けようとしたところ、フォーマットがHTMLだったのでパースしてデータを取り出したかった。ここで問題発生。

別にJSでパースしてElm側に送り込んでもいいんだけど、それだとなんとなく負けた気がするのでHTMLパーサを書くことにした。

プロジェクトを作る

プロジェクト用のディレクトリと簡単なElmコードを用意する。

elm-html-parser/
  - src/
    - HtmlParser.elm
src/HtmlParser.elm
module HtmlParser exposing (..)

parse : String -> ()
parse s = ()

まだ何も決まってないのでこれでいい。早速コンパイル

$ elm-make src/HtmlParser.elm

初回コンパイル時にelm-package.jsonやらelm-stuffやら色々出来る。

テストを書く

elm-community/elm-testを使う。と言っても、実際にはこのパッケージを手動でインストールする必要はなく、代わりにそのランナーであるnpmパッケージの elm-test をインストールして使う。

$ sudo npm install -g elm-test
$ elm-test init

elm-test initすると、テストに必要なひな形を作ってくれる。

tests/
  - .gitignore
  - elm-package.json
  - Main.elm
  - Tests.elm

Tests.elm を編集。

Tests.elm
module Tests exposing (..)

import Test exposing (..)
import Expect
import HtmlParser

all : Test
all =
  describe "HtmlParser"
    [ test "basic" (\_ -> Expect.equal () (HtmlParser.parse ""))
    ]

ラムダ式になっている(\_ ->)のは、ランダム値テストのため。fuzz関数を使うと与えた範囲でランダムな値を生成できる(CIの時は再現性が欲しいので、seedを固定値で指定する)。今回は使っていない。

elm-testコマンドでテスト実行。

$ elm-test

GitHubとTravisCIのための設定

.gitignore
elm-stuff
documentation.json

elm-stuff フォルダはライブラリの置き場所なので、必ず.gitignoreに入れておく。

続いて Travis CI にもelm-testを叩いてもらうように設定する。

.travis.yml
language: node_js
node_js:
  - "4.2"
before_script:
  - npm install -g elm
  - npm install -g elm-test
  - elm-package install -y
script: elm-test

README.md にバッジを設置。

README.md
# elm-html-parser

[![Build Status](https://travis-ci.org/jinjor/elm-html-parser.svg)]
(https://travis-ci.org/jinjor/elm-html-parser)

あとは Travis CI で該当リポジトリをテストするように設定する。これで、プッシュする度にテストが回る環境が整った。

LISENCEは特に理由がなければBSD3が適当。

パーサを実装する

$ elm-package install Bogdanp/elm-combine

Bogdanp/elm-combine はパーサコンビネータのライブラリ。別のもあるけどこれが一番速い。elm-packageは--saveとか書かなくてもデフォルトでelm-package.jsonに追記してくれる。

まずは、AST(抽象構文木)の定義。

src/HtmlParser.elm
type Node
  = Text String
  | Element String Attributes (List Node)
  | Comment String


parse : String -> List Node
parse s = [] -- TODO 実装する

適当にテストを書いて失敗させる。

tests/Tests.elm
testParse : String -> List Node -> (() -> Expectation)
testParse s ast = _ ->
  Expect.equal ast (HtmlParser.parse s)


all : Test
all =
  describe "HtmlParser"
    [ test "basic" (testParse "1" [Text "1"])
    , test "basic" (testParse "<a></a>" [Element "a" [] []])
    ]

次に、テストが通るまで頑張って実装。

src/HtmlParser.elm
parse : String -> List Node
parse s =
  case fst (Combine.parse node s) of
    Ok x -> [x]
    Err _ -> []


node : Parser Node
node =
  element `or` text


text : Parser Node
text =
  (\s -> Text s)
  `map` regex "[^<]*"


element : Parser Node
element =
  (\name _ -> Element name [] [])
  `map` startTag
  `andMap` endTag


tagName : Parser String
tagName =
  regex "[a-z][a-z0-9\\-]*"


startTag : Parser String
startTag =
  between (string "<") (string ">") tagName


endTag : Parser String
endTag =
  between (string "</") (string ">") tagName

これでテストが通る。あとはテスト増やす⇨実装する、の繰り返し。elm-combineの作者に教えてもらったトリビアとしては、Char型をなるべく使わずにString型とregexを使うと速くなる。

ドキュメントを書く

ライブラリが完成したらすぐに公開したいところだけど、公開するすべての型と関数にドキュメントを書くまで公にできない。

src/HtmlParser.elm
{-| Parse HTML.

`` `elm
parse "text" == [ Text "text" ]
`` `
-}
parse : String -> List Node
parse s = ...

見た目をプレビューするには、以下のコマンドを打って出てきたJSONファイルをここで読み込む。

$ elm-make --docs=documentation.json

上のサイトはちょっとバグってるが気にしない。

パッケージを公開する

elm-package.json をいい感じに書き直す。公開前に確認するのはだいたい以下。

elm-package.json
    "repository": "https://github.com/jinjor/elm-html-parser.git",
    "source-directories": [
        "src"
    ],
    "exposed-modules": [
        "HtmlParser",
        "HtmlParser.Util"
    ],

exposed-modules以外のモジュールは公開されないので、もし内部でのみ使うモジュールがあればHtmlParser.Internalのようにしておくと良い。こうするとテストのためだけに関数を公開できたりして便利。

Gitのタグをつけて公開(公開されたパッケージ)。リンクするURLは/latestにしておかないと古いドキュメントを参照してしまうという罠があるので気をつける。

$ git add -A
$ git commit -a -m "implement something"
$ git tag -a 1.0.0 -m "first release"
$ git push origin master
$ git push origin --tags
$ elm-package publish

パッケージの公開に関して詳しくはuehajさんの記事に良くまとまってます。

デモページ

GitHubリポジトリdocsフォルダを使って公開する。docsフォルダに色々突っ込んでもリポジトリの言語のバーには反映されないっぽくて助かる。生成されたJavaScriptを入れると真っ黄色になるので。

ElmにまともなEditorライブラリがないのでace.jsを使う。Elmの port 機能を使うと外界のJavaScriptと会話できるので、aceエディタの文字を送り込んでパースさせる。

<script src="./script.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.0/ace.js"></script>
<script type="text/javascript">
  var app = Elm.Demo.fullscreen();
  setTimeout(function() {
    var editor = ace.edit('editor');
    editor.setTheme("ace/theme/monokai");
    editor.getSession().setMode("ace/mode/html");
    app.ports.init.send(editor.getValue());
    window.addEventListener('keydown', function(e) {
      // Ctrl + S
      if(e.ctrlKey && e.keyCode == 83) {
        e.preventDefault();
        app.ports.parse.send(editor.getValue());
      }
    }, true);
  });
  </script>

バージョンアップして再公開

デモページで色んなHTMLを突っ込んでみたらたまに失敗してたので、パッチバージョンを当てることにした。

まず失敗するテストケースを追加。

tests/Tests.elm
+    , test "basic" (testParse """<input data-foo2="a">""" [Element "input" [("data-foo2", "a")] []])

実装を修正する。

src/HtmlParser.elm
-  map String.toLower (regex "[a-zA-Z][a-zA-Z:\\-]*")
+  map String.toLower (regex "[a-zA-Z][a-zA-Z0-9:\\-]*")

次のコマンドを打つとバージョンを上げてくれて、elm-package.json も更新される。

$ elm-package bump

バージョンアップの種類(MAJOR/MINOR/PATCH)は型を見て勝手に判定してくれる。今回はAPIを破壊していないし機能追加もしていないので、PATCH。いわゆるセマンティックバージョニングというやつ。

この時点では公開されていないので、先ほどと同じステップで公開する。

$ git add -A
$ git commit -a -m "fix something"
$ git tag -a 1.0.1 -m "second release"
$ git push origin master
$ git push origin --tags
$ elm-package publish

宣伝する

Slack とか Twitter とか elm-discuss を使う。Slackは初心者の質問も常に受け付けているので、詰まったら聞いてみると答えが得られたりする。

まとめ

Elmでテスト駆動開発しつつパッケージを公開するまでの流れを紹介してみた。そんなにハマりどころはないと思う。

再利用可能なコンポーネントはアンチパターン

言いたいこと

Webフロントエンド界隈で「コンポーネント」という言葉が蔓延していて、「再利用可能になるように設計すべきだ」という論調があるが、実際には本当に再利用可能である必要性があるまで、極力考えないほうが良い。YAGNIとも言う。

以下、現時点での考え。

ビューの階層化自体はOK

ここはReactの恩恵と言っても良い気がしていて、それまであんまり明言されて来なかった「ビューの階層化」について公式で説明している点がとても良くて、開発者全員がビューはツリーになってるよねというマインドで統一できた功績は大きいと思う。

再利用可能なコンポーネント

ビューはツリーでいいんだけど、それをコンポーネントと呼んでいるのでなんとなくDatePickerとかTextEditorみたいな汎用的なものを想像して、「アプリケーションの事情を知っていてはいけない」という気持ちになって疎結合に作りたくなってしまう。そうすると、次の2つの点で引数の数が増大する。

  • グローバルな「Model」を知っていてはいけないので、それまでmodelを渡せば良かったところが、model.isEditMode, model.saveEnabled, model.flags.colors, model.users, ... のようにどんどん増えていく。

  • グローバルな「Action」を知っていてはいけないので、onInputChange, onSubmit, onUserSelect, ... のような引数がどんどん増えて行く。

React界隈では以前からこれを「バケツリレー」と呼んでいて、辛いので避けるべきものだとされてきたが、とはいえやっぱり疎結合にした方が良いのではというジレンマが生まれる。しかし、この記事ではそれに真っ向から反対したい。

アプリケーションを知る・知らない

コンポーネントは粒度によって大まかに「アプリケーションを知っている(=再利用不可能)」「アプリケーションを知らない(=再利用可能)」に分けられる。個人的な感覚では、普通にアプリケーションを作ると前者が圧倒的に多い。YAGNIの一言で済むんだけど、再利用は幻想なのでやめるべき。

状態を持つ・持たない

「アプリケーションを知らない(再利用可能)」となると、副次的に導入されるのが「コンポーネントの状態」だ。例えば、DatePickerだと「いま何月何日が選択されているか」などという状態をアプリケーションに知られることなく管理して、必要に応じてイベントなりなんなりで引っ張り出して使ってもらうという設計になる。これもまた結果的に複雑化の原因になる。

疎結合のようで密結合

仮に理想的で疎結合なDatePickerが完成したとして、じゃあアプリケーションからそれを使いましょうとなった時に、問題が生じてくる。実は「権限に応じて選択できる部分とできない部分を変えましょう」とか、「ユーザーに応じて表示するカレンダーを変えましょう」という話になって、isSelectableとかformatDateみたいな引数が増える。ここまでは「バケツリレー」でお馴染みの辛さなのだが、もっと進むと「クリックした時にその日の予定一覧を取得してポップアップしましょう」とかいう話になる。いや、もちろんそれは汎用的に型付けして設計することは出来るので見た目は疎結合なんだけど、もうほとんど特定のアプリケーションのために存在しているだろう、という話になる。

重複するデータ

先ほどの例で、DatePickerがユーザの予定をキャッシュしていたとすると、他でもその予定を使いたいとなった時にデータが重複する。「ユーザの予定はDatePickerに保存されているはずだから、そこから取ってきて渡す」というのは明らかにおかしいし、グローバルに持っておくべきデータのはずだ。逆に、DatePickerが状態を持っていなかったとすると、今度は「DatePickerコンポーネントの使い方として、onFetchイベントが呼ばれたら外部で必ずキャッシュを保持してください」みたいな約束事を作ることになって、それも苦しい。

なぜ「コンポーネント」を作りたくなるのか

部分的なモデルとビューが「ほとんど」同じ関心を持っていることが多いのが原因だと思う。UIデザインとして、似たようなデータは似たような場所に表示するのがユーザとしてもわかりやすい。おそらく8割くらいモデルとビューが一致する。だから一緒にしてコンポーネントにするのだが、残り2割のために疎結合という前提が崩壊して辛い結果を生む。

共通化したいのは「見た目」

ほとんどの場合、共通化のモチベーションとしては「見た目を統一したい」だと思うので、振る舞いは二の次に考えよう。そうすると、与えたデータを表示するだけの「シンプルな関数」としてのビューが良いということになる。例えば、デフォルトボタンとプライマリボタンを共通化しておくとか、カレンダーも2画面以上で使うなら、ただカレンダーを描画する関数だけを用意しておく。

モデルは分割・アクションはグローバル

ただし、やはり「関心の分離」はしたい。そこで、もう少し具体的に考えると「引数に渡すのはある程度絞ったモデルで、アクションはグローバルなものを発行して良い」というルールが、今のところ一番バランス良く収まる気がしている。最初はアクションをツリー状にしてFooAction Foo.Clickみたいな感じにしていたのだが、結局その結果実行されるロジックはフラットだったので、本質的にはあまり意味がなかった。だからもう、アクションが30個並ぼうが50個並ぼうが気にしなくていい。本来そういう性質のアプリケーションなんだから、そういうものだと思えということ。「大きくなったから」という理由だけでアクションを分割したりモデルを分割したりすると、その部分の見た目は綺麗になるけど別の部分にしわ寄せが来るぞと。

型に任せてあとは禅の精神で書く

そもそもこれを書こうと思ったきっかけが、以下の記事。

medium.com

「デカいけどだから何?落ち着けよ」とのこと。自分も最近巨大なモデルとアクションに嫌気がさしてリファクタリングを試みたんだけど、上に挙げたような諸々の理由で失敗していた矢先にこの記事に出会ったので「ですよね」としか言えなかった。特にElmの場合は強力な型がついているので、見た目がおぞましくても意外と何とかなるし、後でリファクタリングしてもほぼバグらないので安心していて大丈夫。Elm Architectureがモジュラリティに優れていて素晴らしいという話と矛盾するようだけど、あれは本当にモジュラリティが必要な時のために取っておけばOK。

まとめ

再利用可能なコンポーネントは幻想であり、多くの場合アンチパターン。巨大なモデルやアクションはそれ自体悪いことではないので、恐れずそのまま突き進もう。あんまり賢くやることを考えすぎず、アプリケーションの本質を考えることに時間を費やそう。

以上。

inline style で :hover を再現した

CSS in JS(Elm)方式で、:hoverが出来ない問題を解決した。

github.com

ElmだけどJavaScriptでもたぶん出来る。

使い方

通常、次のように書いている部分を

li [] [ text "Hello" ]

次のように書く。

hover [("background", "#456")] li [] [ text "Hello" ]

すると、ホバー時のみ追加のスタイルが適用される。

仕組み

onmouseenteronmouseleaveの2属性にスクリプトを自動的に突っ込む。

onmouseenter時に、元々DOMに付与されていたスタイルをdata-hover-style-nameに退避して、ホバー時のスタイルを適用する(キーはキャメルケースに変換する)。onmouseleave時に退避していたスタイルを元に戻す。自分の要素しかいじっていないので、Virtual DOM的にも問題ない。

詳しい実装はソースで。

気付き

onmouseenteronmouseleaveイベントはマウスが動いたときには発火するんだけど、要素の側が動いてマウスと重なったりする場合に発火されないことに気付いた。詰んだと思ったんだけど、調べてみたら実は:hoverも同じ挙動をしていた問題なし。

検証用: https://jsfiddle.net/86vc591j/

海外エンジニアが話題にしていて「なるほど」と思ったプログラミングに関する考え方3つ

f:id:jinjor:20160603011250p:plain

プログラミングに関する格言みたいなのは昔から結構あって、例えばYAGNIみたいに日本でも十分浸透してるのは多いんだけど、やっぱり新しい概念はどんどん生まれていくので追いかけていると面白い。

というわけで、最近知った中でもっと日本でも言及されても良いと思ったやつを3つ紹介。

Simple Made Easy

Rich Hickey(Clojure言語の作者)による講演(2011年)のタイトル。全文はここで読める。英語しんどくてPOSTDに投げたんだけど音沙汰がない。まだ全部見てないから和訳欲しい。

内容としては、みんな安易に「簡単」なものを選びがちだけど「シンプル」なものの方が価値あるぜ、というもの。曰く、「シンプル」は絶対的・客観的な指標だけど「簡単」は相対的・主観的なもの。例えば英語の話者にとってドイツ語は難しいが、それは自分にとって「遠い」存在であるだけで悪いものじゃない。

「慣れているか」よりも「シンプルか」に目を向けよう、という教訓。

Magic vs. Boilerplate

上と似た系統の話題なんだけど、「マジック」と「ボイラープレート」はトレードオフの関係にあるという話。Phil Webb氏(Spring Frameworkのコミッター)下のツイートが発端らしい。

http://www.johndcook.com/blog/2016/03/07/the-magic-vs-boilerplate-tradeoff/

直接どっちが良いかという答えは書かれてないけど、周りの反応を見ている限りでは「マジックよりもボイラープレートを取れ」という主旨で良さそう。冗長な記述を省略するために抽象化を進めるのは正しいんだけど、行き過ぎるとマジックになって追うのが困難になる。これについて言及しているブログは、例えばこことか。

SSCCE

Short, Self Contained, Correct (Compilable), Exampleの頭文字をとったもの。公式サイト?らしきところに詳しい解説がある。

何かというと、バグ報告の時に「このケースで上手く動作しない」と言って数百行のコードを渡されたらウンザリするよね、ということで例はなるべく小さくしろという話。そのバグが再現する条件を満たす最小限のコードであることが理想で、報告された側がすぐに試せるように無駄な依存関係も全部切って即座にコンパイル可能な状態で渡すと喜ばれる。

この単語をよく見かけるのが主にElmコミュニティなので、言語とか小さいライブラリとかに向いていて大規模に環境を用意する必要があると厳しい感じはするんだけど、心がけとしては持っておくと良いと思う。

まとめ

何かあった時に自分の言葉で一から説明するよりも、こういうのをさっと引用すると説得力が増しそうな気がするので便利に使っていきたい。

「Webアプリ」の解釈が広すぎる話

最近Webフレームワーク周りで無駄に摩擦が生まれてるなー、と思うことを詩的に書いてみる。

そもそも何が作りたいのか

古くはjQueryから始まって、最近だとReact(+Redux)とかAngular2とか色々あるわけだけれども、そもそもそれらを使って作ろうとしてるものはみんな一緒なの?っていうのがあって、色んな話を聞いているとかみ合ってない感がすごい。以下の分類は別に細かくちゃんと定義しましょうとか言っているわけではなくて、「例えばこういうのがあるんじゃないの?」という一例。いま自分が関わっているのは主に3と4なので、その他で間違ってたら指摘して欲しいんだけど、この前提を共有していないために「複雑すぎる」とか言ってるんじゃないかという仮説がある。

1.Webサイト

基本的に静的なWebサイトで画面遷移するんだけど、ところどころ動きがあったりするのでフレームワークが必要。SEOが重要なのでサーバーサイドレンダリングしたい。初期レンダリングの速度がそのまま売上につながる。モバイルも考えてファイルサイズは極力小さくしたい。

2.Webシステム/サービス

SNSとか動画サイトとか。チャットがあったりタグシステムがあったり、一画面で色々したい。SEOについては上と同じ。初期レンダリングは若干遅くても何とかなるが、UX向上のためにやはり速くしておきたい。

3.業務Webシステム

閉じたWeb。SEO検索エンジンを付けたときの利便性のためでオプションだが、特定の情報にリンクするためのURLは生成したい。初期レンダリングの速度はUX向上のために必要だが、直接的には売上に貢献しないかもしれない。デザインは全体で統一感さえ取れていればなんとかなる。

4.GUI / エディタ

表計算ソフトとかテキストエディタとかIDEとか。SEO関係ない。セキュリティ要件をクリアしつつ、GUIを構築する手段としてWebを使いたい。コピー/ペースト・Undo/Redoコンテキストメニューなどは当たり前の世界。状態をたくさん持つし、コンポーネントもネストしまくる。

5.ゲーム

Canvas用のフレームワークもあるが、DOM用のフレームワークを便利に使いたい。SEOはトップページで必要(投稿できるタイプなら各ページに必要かもしれない)。これも状態をたくさん持つ。

「SPA」は本当に1ページなのか

「Yes」という答えがあるのかもしれないが、個人的にはそういうのはあんまり見たことがなくて、全く別の内容を扱っている場合は普通にページを分けている(ログインページとかは大体分けるし)。その場合は「Router」の役割はそこまで重要じゃなくて、せいぜい検索クエリが最初からセットされてるくらいの機能で良い。だから「Routerとか大げさじゃね?」という意見にも頷ける。

どれだけ未来を見ているか

これも人によって大きく異なると思っていて、明日使うために便利な道具を探す人もいれば、理想の開発を求めて将来に投資する人もいる。自分は後者に寄っているけれども、見方によっては馬鹿げてるとは思う。人によってこの距離感が違うのが、すれ違いの要因のひとつであるように思う。「俺的未来仕様」を考えすぎじゃないかっていうフレームワークもあるので、批判があるのも分かる。

どうすべきか

ただ感じだことを書いただけなので、別にどうでもいい。たまに自分とは全然違う角度から切り込んでくるブコメとかがあって「そういう観点あるなー」とためになったりするので(感謝!)、異文化が混ざってるのは必ずしも悪くない。ただ、他人のブログとかを読むときに「はぁ何考えてるの?」と思ったりする前に立ち止まって考えようと思った。そういう話でした。

CSS in JS(Elm)したら想像以上に良かった

f:id:jinjor:20160530165006p:plain

React界隈では結構前からCSS in JS」と言って、雑に言うと「CSSはイケてないからJSでインラインスタイルを書いてしまえ」という話がある。(ちゃんと知りたい人はこちら

自分も前々からCSSは変数が使えないとか名前が被るとか諸々イケてないのは同意してたんだけど、じゃあJSで書くのが良いかと言われたら「いや流石にロジック汚れるんじゃね?」とか「CSSの便利機能を捨てて平気なの?」とか色々と懐疑的だったんだけど、1~2か月書いてみたら想像以上に良かったので感想を書くことにした。

まず一番に主張したい部分を先に言うと、こう。

(誤解)JSのコードがスタイル記述で汚れる
(正解)JSのコードがスタイル記述から解放される

前提

  • 実際に書いたのはJavaScriptではなくElmなので以下は全てElmコードで書くんだけど、本質は変わらないはずなのでJavaScriptに置き換えて読んでください。
  • デザイナと協業したりプロダクションで実際に使ったわけではないので、その辺のノウハウはないです。
  • パフォーマンスの話はないです。遅くなったら考えようと思ったけど遅くならなかったので。
  • CSSに対する問題意識はある程度共有できていて、React的な書き方をしているWebアプリが対象。

というわけで、ここからが感想というかやってみて分かったこと。

スタイルは今まで通り分離できる

まず誤解されているかもしれないところを解消する。「CSS in JS」と言っても、メインのロジック中にベタベタスタイルを記述していくわけではなくて、スタイルは普通に分離して書くということ。基本的には今までstyle.cssに書いていた内容をstyle.jsに書くだけ。

Elmではこんな感じで「Styles.elm」を記述した。雰囲気を見てもらうのが目的なので詳細な内容は追わなくてOK。

module Styles exposing (..)

zIndex =
  { subView = "600"
  , messageBar = "700"
  , contextMenu = "800"
  }

messageBar =
    [ ("position", "absolute")
    , ("color", "#fff")
    , ("z-index", zIndex.messageBar)
    , ("padding", "5px 10px")
    , ("transition", "height opacity 0.8s linear")
    ]

successBar =
  messageBar ++
    [ ("background-color", "#4c5")
    , ("opacity", "1")
    ]

errorBar =
  messageBar ++
    [ ("background-color", "#d45")
    , ("opacity", "1")
    ]

noneBar =
  messageBar ++
    [ ("opacity", "0")
    , ("pointer-events", "none")
    , ("background-color", "#4c5")
    ]

CSSを書いてるのと何ら変わらない。使用する側のコードは以下。

successView msg =
  div [ style Styles.successBar ] [ text msg ]

errorView message =
  div [ style Styles.errorBar ] [ text message ]

noneView =
  div [ style Styles.noneBar ] [ ]

今までclass属性でスタイル指定していた部分をstyle属性に直すだけ。スタイルはStyle.elmに書いてあるから、使用する側のコードは全く汚れない。今まで通り普通に分離できる。

便利なCSSはそのまま使える

「スタイルをJS」でと聞くと、今までのCSSの知見をすべて捨てて何から何まで自力で座標計算から何からやらなきゃいけないような気がしてくるが、そんなことは全くない。上の例に示したように、positionz-indexpointer-eventsのようなほぼすべてのCSSの便利プロパティは健在だ。Virtual DOMを使っていてもtransitionが動作しないなどのバグも特に無かった。

スタイルの合成が可能

これは「CSS in JS」のそもそもの動機なので言うまでもないのだが、使いまわせるスタイルは変数にしておくことで簡単に合成できる。上の例でいうと、successBarmessageBarに成功時特有のスタイル(緑色とか)を足し合わせることで定義できる。

OOCSS(Bootstrapのような)でも合成は出来るのだが、必要なclassを全て書き連ねる必要があるのでHTMLがどんどん汚れていってしまう(class="message-bar message-bar-success")。スタイル定義側で合成すればHTMLはクリーンに保てるし、「HTMLにはセマンティクスを記述してスタイルはCSS側で~」という昔ながらの価値を取り戻すことが出来る。

スタイルを関心別にまとめて管理

変数を使うことで、よりよいスタイルの管理も可能だ。例えば「色は全部まとめて一つのファイルに定義しておく」とか「z-indexは一か所にまとめて管理する」のようなニーズにも自由自在に対応できる。

動的な値を使った計算が可能

CSS(Sassなどのプリプロセッサを含む)にどうしても出来ないことの一つに「実行時に動的に変化する値を参照」というものがある。「画面サイズ」がその典型例で、「メインコンテンツの高さは画面全体の高さからヘッダの高さを引いた値」とか「コンテクストメニューが画面からはみ出ないように位置を調整する」みたいなことが出来ない。仕方がないので今までそういうロジックは例外的にJavaScript内に書いていたのだが、「CSS in JS」では動的に引数を与えることが出来る。

以下は引数を用いたスタイル定義の例(Elm)。

-- 画面の高さを引数に取る
mainView windowHeight =
  ( flex ++
      [ ("height", px (windowHeight - headerHeight))
      , ("position", "relative")
      ]
  )

数値だけではなく、フラグ的なものを突っ込むこともできる。タブなどを選択したときのactive状態などが典型例。実際のコードからの抜粋。

-- 数値(インデックス)と真偽値の2つを引数に取る
subViewTab index active =
    [ ("position", "absolute")
    , ("top", px (10 + index * 130))
    , ("left", "-30px")
    , ("width", "30px")
    , ("height", "120px")
    , ("padding-left", "6px")
    , ("line-height", "135px")
    , ("background-color", "#eee")
    , ("z-index", zIndex.subView)
    , ("cursor", "pointer")
    , ("border-radius", "8px 0 0 8px")
    , ("box-shadow", if active then "" else "inset -4px 0 4px rgba(0,0,0,0.03)")
    , ("box-sizing", "border-box")
    ]

今まではそれが出来なかったのでHTMLやJavaScriptに書かれていたが、本来このような記述は普通にCSSに書かれるべきもののはずだ。もちろん何でもかんでもロジックを突っ込んでしまうと責務をオーバーしてしまうので、そこは節度を守ってやる。

CSSを使った方が良い場面もある

CSS in JS」だからと言って、別にCSSを使うことを禁止する理由もないので必要ならCSSを使う

まず、インラインスタイルで難しいこととして最初に思い浮かぶのが:hover。流石にこれはCSSでやらないと面倒だろう。JavaScriptでも出来なくはないが、enter/leaveイベントを自力でハンドルする必要があるし、じゃあそのホバーしている状態がロジックとして必要になるケースがあるかと言ったら、まぁない。だから:hoverは諦めて普通にclass属性を使うことにした。

次に、reset系CSS。これもCSSでやっておかないと面倒なことになる。やらない場合「全てのスタイルはこのベースのスタイルを継承してください」のような厄介なルールを作ることになる。嫌な予感しかしないので最初から避けるのが良さそうだ。

弱点に見える部分もほぼ代替手段がある

その他、気付いた点。

  • animationはうまく出来ない。JavaScriptでやる。※未検証
  • ::before/::after疑似要素は普通にJavaScript/HTMLに書けば問題なし。
  • メディアクエリはおそらくフラグで出来る。※未検証
  • ブラウザのツールで同じスタイルを一括変更してプレビューできない。例えば、<li>要素の最初の一つをブラウザ上で変更しても残りには反映されない。JavaScriptのスタイル定義を書き直して画面をリフレッシュする必要がある。
  • BootstrapのようなCSSフレームワークが使えない。それの問題を解決したいので仕方がない。「CSS in JS」用の良いフレームワークが出てくるのを待つか、自分で作る。

自分が納得のできるメリットを見つける

CSSの弱点」とかで調べるとモジュラリティがどうとかいう話がよく出てくるのが、ぶっちゃけよく分からなかった。具体的にどういうコードだとモジュラリティが上がった状態で何が解決するのかよく分からない。

だから「z-indexがー」でも「BEMがめんどくさい」でも「書いてて快感物質が出る」でもなんでもいいけど、理屈はさておき自分の実感できる理由で採用すれば良いんじゃないかなと。逆に実感できないなら主張してもあんまり意味がない。たとえば名前空間の問題も理屈上そうなんだろうけど、自分がそれで困るプロジェクトに関わってなくてよく分からなかったので、ここには書かなかった。あと、CSSではなくてJSのモジュールとして書けるのでエコシステム的にも良いと思っていて、本当はそこも言いたいんだけど試す機会がなかったので誰か知見を持っていれば聞きたい。

まとめ

最初に感じた「スタイルが分離できなくなるんじゃないか」という懸念とは真逆で、むしろ今までJSでゴリゴリ書いていたスタイルに関するロジックが、ちゃんとあるべき場所に収まった

レッツトライ。

自分自身の反省でもあるんだけど、固定観念は良くないと思いました。

以上。