昨年アメリカのベイエリアに引っ越し・転職して入った会社が、ちょうどElmを本格的に本番運用し始めようとしているところでした。それから1年半弱たった今、フロントエンドで新規に書くコードはほぼすべてElm、総行数はテストを含め5万行に達し、その過程で運用がスムーズにいかない場面も経験しました。そこで、
- Elm、よさそうだけど実戦で使うためのツールは揃ってるの?
- 実戦で使い始めたけど他の人はこの辺どうしてるのか知りたい
という読者を想定して、自動テスト・デプロイ・チーム内コミュニケーションに使っているツールややってることを簡単に紹介します。
目次
- 前提条件や環境など
- コーディングスタイルの統一
- テスト
- 依存パッケージの管理
- アセット管理
- CIビルドの高速化のための小技
- コード分析
- 人事採用
前提条件や環境など
- 自社プロダクトを作っている会社です
- 1つのページもしくは機能ごとに1つのElmアプリという構成です (サイトの機能全部が1つのアプリに入っている、いわゆるSPAではありません)
- 現在全部で32個
- Elm 0.16で動いているページとElm 0.18で動いているページが半々くらいの割合です
- バックエンドはRuby on Railsです
コーディングスタイルの統一
コードを自動でフォーマットする elm-format
問題
コーディング規約の中でも、インデントや改行の仕方などソースコードのレイアウトに関するものは自動化したいものです。エディタが整形してくれる場合もありますが、実装や設定によって微妙に結果が違ってきたりもします。
解決
elm-formatは、単一のバイナリ + 各種エディタ用プラグインという形をとった自動コード整形ツールです。
これをCIビルド時にバリデーションモードで実行し、成功しなければビルドを失敗させます。
elm-format --validate --elm-version=0.18 src tests
また、開発時にテストを実行するためのスクリプト内で自動的に全てのElmコードを整形しています。
bin/elm-format-$ELM_FORMAT_VERSION --elm-version=0.18 src --yes > /dev/null
bin/elm-format-$ELM_FORMAT_VERSION --elm-version=0.18 tests --yes > /dev/null
モジュールの命名規則を明文化する
問題
何をどう分割してどういうディレクトリ配置にするか、については言語仕様上何の制限もないので、散らかりがちです。
解決
普通ですがガイドラインを決めて共有しています。
ディレクトリ配置に限っていえば、今はこんな感じです:
-
src/Page/
- 各ページ用のアプリたち -
src/Nri/
- 使い回せるコンポーネント群(使い回せるといっても自社プロダクト内での使い回しに限る)。「Nri」は自社プロダクトの名前の略です -
src/Data/
- 使い回せる型定義と関連した便利関数など -
src/
- オープンソース化してしまえそうなくらい使い回しのきくモジュール
使い回せるコンポーネントを一覧できるページを作る
問題
コードをざっと眺めただけでは、特定のコンポーネントがどういう見た目に対応しているのかを即座に判断できないかもしれません。今作っているページのこのアイコンはもう誰かが実装済みなのかを簡単に知るには?
解決
使い回しのきくコンポーネントを一覧できる内部向けページをRailsアプリに追加しました。ここにあると役立つ何かを書いたときには手動で追加します。
中身は標準的なElmアプリです。エッセンスを抜きだすとこんな感じ:
type alias Model = ...
{-| 1つのコンポーネントをあらわすレコード -}
type alias StyleModule =
{ filename : String -- どのファイルか
, importing : String -- コピペしてすぐ使えるインポート文
, migration : Maybe String -- Elm 0.16 版のソースコードはどこにあるか
, content : List (Html Msg) -- このコンポーネントを色々な状態でレンダリング
}
styleModules : Model -> List StyleModule
styleModules model =
...
view : Model -> Html Msg
view model =
List.map viewStyleModule (styleModules model)
テスト
単体テストフレームワークelm-testと周辺パッケージ
問題
静的型チェックのおかげで、「elm-make
が通れば動く」という安心感がElmにはありますが、その動作が意図通りかは保証してくれないので、やはりテストを書かないと枕を高くして寝られません。
解決
単体テストを書くための基本的なパッケージは揃っていると思います。(最近フロントエンドよりインフラ寄りなので、断言は差し控えます!)
- elm-test - 単体テストフレームワーク。入力値を自動生成するファズテストもできる
- elm-html-test - ビュー関数から返ってきたDOMに対してアサートが書ける
- elm-testable - アップデート関数から返ってきたCmdなどに対してアサートが書ける
- elm-doc-test - 関数のドキュメンテーション内にテストを書ける
自家APIとの整合性をチェックするための JSON Schema
問題
Elm内にいる限り型チェックのおかげで何もびっくりするようなことは起こりませんが、外部世界とのやりとりとなるとびっくりするようなこと(== エラー)が起こる可能性があります。
とくに、バックエンドとフロントエンド間で期待しているデータ形式にずれが発生するタイミングはたくさんあります。それぞれコードを書きおこす人が違っていたり、後からバックエンドだけちょちょっと修正したり。そのための統合テストではあるのですが、実行速度に難があります。
解決
リクエストとレスポンスの形式をJSON Schemaで記述し、Elm側が生成するリクエスト、バックエンド側が生成する初期データおよびレスポンスの形式がスキーマに合致するかを単体テストで検証しています。
バックエンド側で使っているバリデーターはjson-schema、フロントエンド側はajvをNativeコードでラップしてelm-testでテストしています。
こんな感じ:
after do
expect(assigns(:page_data).to_json).
to match_json_schema "page/learn/page-data.schema.yaml"
end
assertMatchesJsonSchema validator "page/learn/answer.schema.yaml" answerData
(JSONは手で書くには冗長なので、YAMLで書いたものをJSONに変換してからバリデーターに食わせてます。)
今後
JSON Schemaだけではどんな内部APIがあるのか一覧しにくかったり冗長だったりするので、ramlか?Haskell + servant-swaggerかelm-exportか?などと悩んでいます。
依存パッケージの管理
依存パッケージのバージョンを固定する
依存パッケージの新規インストール時にはelm-packageが自動でelm-package.json
に追加してくれますが、許容範囲が "5.0.0 <= v < 6.0.0"
のように「メジャーバージョンが同じ最新のものを」という指定になります。
elm-package publish
の仕様上、メジャーバージョンが同じであればコンパイルは通ることが保証されていますが、期待通りの動作をするかまでは保証の範囲外です。「CI時点では問題なかったけど、デプロイまでの間にちょうどバージョンが上がって不具合が出た」などの予想外の事態を避けるため、依存パッケージのバージョンは "5.0.0 <= v <= 5.0.0"
のように固定し、CI時にバージョンが固定されているか自動でチェックしています。
テストコード用依存ライブラリのバージョンずれをチェックする elm_deps_check.py と、バージョンを同期する elm_deps_sync.py
elm-ops-toolingの一部です。
問題
elm-packageの仕様上、テストコード用のelm-package.json
をメインのelm-package.json
とは別に保守する必要があります。手動管理は間違いのもとです。
解決
CIビルド時に、elm_deps_check.py
でテストコードの依存パッケージ群とメインの依存パッケージ群のバージョンが一致するかを確認しています。一致しなければビルドが失敗します。
elm-ops-tooling/elm_deps_check.py \
src/elm-stuff/exact-dependencies.json \
tests/elm-stuff/exact-dependencies.json \
--exact --quiet
また、開発時にテストを実行するためのスクリプト内で、自動的にテストコードの依存パッケージを更新するようにしています。
elm-ops-tooling/elm_deps_sync.py src/elm-package.json tests/elm-package.json
野良Nativeモジュール入りパッケージをインストールする native_package_install.py
elm-ops-toolingの一部です。
問題
Nativeモジュール1を含むパッケージはElm 0.17以降、公式パッケージレジストリには新規登録できなくなりました。「Elmの発展のためにはElmパッケージは原則としてElmで書かれているべきである。足りないところはElmが公式にAPIを提供する」という方針になったためです。
そうはいっても、ブラウザとは関係の薄い領域(テストで便利なnodeモジュールなど)や、Nativeを使わないとコードが冗長になる(metaタグの内容を読みたいとか)など、Nativeモジュールがほしい場面はゼロではありません。自社コード内でNativeモジュールを実装すれば済みますが、再利用性の高いものはオープンソースで公開したいものです。
解決
native_package_install.py
で野良Nativeパッケージをインストールできます。パッケージとそのバージョンを指定したJSONファイルを作り、
{
"NoRedInk/elm-asset-path" : "2.0.0"
}
ダウンロード先を指定してインストールします。
elm-ops-tooling/native_package_install.py elm-native-package.json \
--elm-config elm-package.json \
--vendor vendor/
結果、ダウンロードしたモジュールのディレクトリが各々--elm-config
で指定したelm-package.json
の"source-directories"
に追加され、Elmアプリから利用できるようになります。
通信が不安定でも確実にパッケージをインストールする with_retry.rb
elm-ops-toolingの一部です。
問題
elm-packageはGitHubから各パッケージのzipファイルをダウンロードしてインストールしますが、ダウンロードに失敗することがたまにありました。何度もelm-package install
をおこなうCI環境では、みんなのビルドがランダムに失敗する原因になるため、とくに頭痛の原因になります。
解決
バグ報告しつつ、引数にあたえられたコマンドを成功するまで実行しなおすスクリプトを通してelm-package install
を実行するようにしました。
elm-ops-tooling/with_retry.rb node_modules/.bin/elm-package install --yes
今後
elm-packageが利用しているHTTPライブラリで関係がありそうなバグが解決されたことなどから、現在 (Elm 0.17以降) では必要ないかもしれません。近々with_retry.rb
を外してCIをぶんまわしてみる予定です。
アセット管理
画像ファイルその他をCDNにのせやすくする elm-assets-loader
問題
CDN上にキャッシュされているファイルを更新したいタイミングで更新する手法として、ファイル名もしくはクエリ文字列にファイルのバージョンを示す文字列を入れ込むというものがあります。Elmで普通に画像のパスを指定してコンパイルするだけではそうしたことはやってくれません。
解決
webpack + elm-webpack-loaderでElmコードをコンパイルした後にelm-assets-loader + file-loaderで画像のファイル名にファイルの中身のハッシュ値を入れ込む処理をさせています。
さらにelm-asset-pathをかませてRails側からCDNホストを指定することで、
img [ src (AssetPath.url <| AssetPath "/assets/star.png") ] []
こんなElmコードを書くと
<img src="https://0xcdasdf.cloudfront.amazonaws.com/assets/star-038a1253d7a9e4682deb72cd68c3a328.png"/>
こんなURLが生成されます。
またCI時に、CDNにのっていない画像パスを検知したらビルドが失敗するようにしています。
CIビルドの高速化のための小技
テスト用ディレクトリにメインのディレクトリから依存パッケージをコピーする
elm-test用elm-package.json
の依存パッケージ群は、メインの依存パッケージ群にテスト用パッケージを追加したものなので、大部分が重複します。rsync
すればGitHubから同じものをダウンロードしなくてすみます。
rsync -a ../elm-stuff .
Travis向け: build-artifactsディレクトリをキャッシュする
Travis CIには、ビルド中にインストールしたライブラリなどをキャッシュし、次のビルドで自動的に使ってくれる機能があります。elm-make
の中間ファイル置き場であるelm-stuff/build-artifacts
をキャッシュさせるとビルド時間を短縮できます。
cache:
directories:
- tests/elm-stuff/build-artifacts
CIに限らない高速化: elm-makeを走らせる前に全Elmファイルをバッチコンパイルする
20ファイル単位でelm-makeにかけるようにしたところ、コンパイル時間が半分ほどになりました(2016年4月時点)。これはelm-makeの並列処理の仕方に関わると思われますが、具体的な理由はつかめていません。
疑似コードで書くとこんな感じ:
Main.elm以外のElmファイルを20個ごとにelm-make --yes --output /dev/null
Main.elmを1つずつelm-make
コード分析
SlackからElmコードについてクエリを投げられる slack-today-i-did
問題
「Elm 0.16から0.17への移行状況を簡単に確認したい」「このページはどれくらい簡単に0.17に移行できるか確認したい」「chatopsいいよね」という気運が高まりつつありました。
解決
メインのGitレポジトリを手元にクローンして、ちょっとした分析結果を答えてくれるSlackボットを運用しています。
Elm 0.16で書かれたTabsコンポーネントをElm最新バージョンに対応させるのはどれくらい大変か2を聞いてみた例:
@today-i-did how-hard-to-port Tabs
We have found the following filenames:
Here's the breakdown for the:
file Tabs.elm: total hardness 14
Ports and signals : 3 | Html stuff : 11
人事採用
とくにElmの経験を必須としない
問題
マイナーな言語を採用すると、プロジェクトに新規に関われる人がまったく見つからなくなってしまうのではないか?
現状
新卒はもちろん、中途でもとくにElmの経験を必須条件とはしていません。「Elmやりたい!」という意欲があるかだけ確認しています。
そしてElmはまだマイナーではあれど、とっつきにくい言語ではありません。「はじめてのElm」というお題で面接でペアプロすることもありますが、みんな書けてしまうので適格不適格の情報を得にくく、お題としてはダメなのではないかという声さえあります。
以上です。参考になれば幸いです。「自分はここはこうしてる」などあればコメントでぜひ。