styled-componentsへの最近の感想
今の職場で既に組まれたシステムが既にstyled-componentsにべったり依存していて、別に積極的に入れ替える理由もないので普通に使っているけれども、やっぱりこれ微妙だなと思った話。
そもそもとして、ビルドシステムへの介入が多くて不必要にロックインになったり、提示されてる手法がだいたいイマイチで普通にCSSとかSCSSを書く以上の意義が見出せないので、僕は基本的に「JS中にCSSを書いたり、ES Moduleのimportを使ってCSSを読み込むタイプのアプローチ(以下CSS-in-JS)」がそんなに好きではない・大人しくCSS書く方が好きではあるんだけど。色々あった理由はこちら:
- https://saneyukis.hatenablog.com/entry/2019/02/28/022750
- https://saneyukis.hatenablog.com/entry/2019/03/14/023446
感想
上のリンク先にも色々書いてる微妙に感じる点を全部見なかった事にしても、やっぱり微妙だと思う。
- 何を作るにもstyled-componentsで包まれた(ファクトリ)コンポーネントを作る事になるので、ラムダ(匿名関数)を書いて済ませたい箇所で、古のJavaのCallbackクラスを書くのを強制される気持ちになる
- 具体的には調整用のdivやspanをぺろっと置きたいだけなのに、わざわざ書く事になる
- 結果、包まれた(ファクトリ)コンポーネントは変数定義順の問題があり、function宣言のhoistingなどに頼ることができないので、 BEMのelement定義してからBlock書く感じになる。なので、抽象化されたコンポーネントを定義したいのに、まず具象化された細部から書いていく必要があるし、既存のコードもそのように書かれる
- ここら辺、人によって書き方変わるし、lintなどのしばり方次第で如何様にも書けるだろうけど、temporal dead zoneを考えると宣言してない変数を使うのはありえないのは共通認識とする
- 末節・BEMでいうelement側の定義をファイルを分けてimportすれば解決する問題だが、繰り返すようにラムダ(匿名関数)を書いて済ませたい箇所でCallbackクラスを定義するような事になる
- そもそもテンプレートリテラルで文字列として記述しているのが、非常に中途半端に見える
- JSX記法がDSLとしてセマンティクスを持つ事による教訓が活かされていないのは釈然としない
イマイチだと思ったのはここら辺。他にも色々あるけれども。
かつてのCoffeeScriptを持て囃す論説への拒否感と似ているものの、そんなことをいうとCoffeeScriptに失礼だな。
それとstyled-componentsに限った話題ではないけれども、いわゆる「GUIのTheme実現が可能」みたいな売り文句は実際には十中八九使わないタイプの機能だと思っていて、なぜかというと
- 現実にはダークモード+alphaくらいしかテーマを作らない
- そもそもテーマ機能を真面目に用意してるプロダクトを見た数の方が少ない
- ダークモード対応なら
prefers-color-scheme
で解決する
- ダークモード対応なら
- そもそもテーマ機能を真面目に用意してるプロダクトを見た数の方が少ない
- 仮に実装するとしても、起こり得るテーマの変更はCSS custom propertiesで使って色やサイズや
background-image
などの変数定義で解決するのが大半なので、標準化された方法に依存した方が良い - もっと複雑なことをやりたい場合は、大抵はGUIの実装部分が別になるので「一つのファイル・コードで複数のテーマを実現可能」にお世話になることは(あんまり)ない
- ユーザー定義スタイルを許容する場合、尚更public interfaceとしてclass属性などは必須だし.
から。 ちなみに自分の好きなアプローチは上の方に貼ったリンク先に全部書いてあって、それを超えるものではなかったかな。
Facebookのリデザインに見る、CSS-in-JSの良さそうな方向
色々ぐだぐだ書いているけれども、 https://engineering.fb.com/web/facebook-redesign/ に載っているサンプルコードの方向性は、良さそうに見える。実際にこの通りに書かれてるかはわからないけどね。
なぜかというと、
Update my status
I'll begin my new job from this May after short leave from this industry. Thank you for worrying about me. See you again in the community!
My next job will be began from WFH. Its start may be a bit hard for me. Interesting.
BlinkとWebKitの違い(大雑把)
「〜がChromiumベースに!」なことが起こるたびに「Chromium/BlinkはWebKitを源流とするエンジンでしかじか」みたいな話が出てきて、「実質WebKitだから同じだね」という反応が出てくるのが恒例行事っぽくなってるけど、結構モニョモニョする。
先祖が同じなら子孫も同じ、ってそんな単純な話じゃない。
fork前、BlinkがChromium WebKitというかWebKit Chromium portと呼ばれていた頃でさえ、Chromium portとApple portの2つが同じエンジンと呼べる箇所って、layoutとかdomとかstyleとかブラウザエンジンのコア部分だけで、他はV8とJSCとか、SkiaとCore Graphicsとか、そもそもプロセス分けてる方法も違うし、呉越同舟というか寄り合い所帯感だった。composition周りだってApple portはCore Animationにべっとり依存するような実装じゃなかったけ。
それで、数少ない共有部なコアでさえ、fork後に色々変わっている。自分の知っている・思い出せる範囲で大きいやつだと
- layout
- style
- DOMのオブジェクト管理
とかとか。細かいあれこれを挙げだすとキリがない。
GeckoやEdgeHTMLと比較した場合、WebKitとBlinkは近縁種だと思うけど相対的なものでしかなくて、それぞれが呉越同舟していた時代に追加された仕様未定義な挙動に箇所に関するテストケースや、同祖であることに由来する箇所くらいしか、同じエンジンとは言えない。ましてや最近実装されたものは以下略。
リポジトリを境界としたコードベースとしては、WebKitは変わらずライブラリ指向だけど、Chromium/Blinkはプラットフォーム志向で、リポジトリの成長の方向性としてはFirefox/Geckoの方が近くなってきている
でも、そう変わるのも当然で、当代で世界トップクラスにお金を持っている二社が別々の方針で(数十人とか数百人のオーダーで)エンジニアを7年も張り付けていて、しかもそれぞれプロジェクトが実現したいWebに対するスタンスは結構違っているのに色々変わらない訳が無い。
でも、一方でWebGLの実装とかはWebKit(のApple port)もANGLE使おうとしてて、このままだとGecko/Blink/WebKitのどれもANGLEになろうとしているんだから、面白い。
JSConf 2019 Postmortem
JSConf 2019で Your benchmark may not guide real application performance という話をしてきました。 寒い中お越しいただいた皆様。ありがとうございました。
話さなかったこと
「そうは言ってもベンチマークを組み込むのつらいよね」な話
自分の体験談、身の回りの同僚知人各位の体験談も合わせていくとそれだけの一本のコンテンツにできますが、今回は単純にスコープ外かつ30分に収まらないので諦めました。私の力量の問題もあります
後からベンチマーク足そうと思うと本当に大変。まあ最初からやっても大変だとは思うけども、ゼロからやる方が"ちょっとだけ"簡単だと思う。
詰まるところカルチャーの問題なので、うまくチームや会社を巻き込まないといけない。「ソフトウェアパフォーマンスは究極的には組織文化である(要約)」というJoe Duffyの主張はとても正しい。「パフォーマンス専任担当者・チーム」が、どこからともなくやってきて、ベンチマーク足して帰って行ってもあんまりうまくいかない。文化として根付いていないからだ。
プロダクトを主に書いているチームの人間が自分たちで取り組まない限り、途中から現状維持でさえキツくなったり、ベンチマークスコアがどんどん遅くなるのを眺めて何もできないままとなっていくみたいな話。「遅い変更は強制revert」ルールとかも、ちゃんと(評価など含めて)カルチャー化してないと、ひたすら疲弊するだけですし。
再現性は手軽さも重要みたいな話
動かすだけで一苦労みたいな、手軽に再現できないベンチマークは誰も試さなくなる。「一回動かすのにセットアップで1人/週かかって、実行に5時間かかります」みたいなのは絶対に続かない。
わかりやすいボトルネックが本当に見当たらなくて「全体的に遅い」みたいなことになってる問題に対処する話
わかりやすいボトルネックを1~2つ潰したらこうなることが多い気がする。最初からそうなってることも、それなりにある(気がする)。
原因はクリティカルパスが隠れている以外にも、「どうしようもなく全体が遅い」「全体を速くしないと速くならない」みたいなこともあって、それはプログラミング慣習の問題だったり、ソフトウェアアーキテクチャだったり、そもそもの機能要求に起因したり、まあ色々。徹底して分析するしかないと思っている。
この手の話で好きなのはAndroidの豪傑Dianne HackbornがGoogle+で書いていた話なのだけど、もう消えちゃったんだよねえ.....(と思ったらアーカイブが残っていた)。これは少々極端だしOSやフレームワーク層の意見で、アプリケーション層とはまた少し違うと思うけれども、態度として敬意を払いたい。
「単純なボトルネックなんて早々に消え失せてしまった」系の話だとMozillaのQuantum Flowの時の話とかも個人的には好きです。
そもそもコアバリューってなんだよ・ちゃんと自分たちのコアバリュー考えて仕事してるんだっけ話
複数の友人から指摘されたけど、これ言い始めるともう少しメタな話になるので省きました。指摘をくれた友人各位には「俺よりもお前の方が適任だと思うぞ」と返信していたので、彼らがやってくれるかもしれません。
Q&A形式のまとめ
友人に勧められたのですが、(英語力の問題で)面白いやりとりが思い浮かばなかったor身も蓋もなさすぎるのでボツになりました。
よかったこと
話が刺さってくだすった観客が何人かいたこと。 その後、直接感想をくださった皆様、ありがとうございます。楽しんでもらえらた人がいるだけで何よりです。
やっちまったなと思うこと
動画が公開されるかもしれませんので、現時点で気づいたことを予め懺悔しておきます
- 観客の目を見て喋る機会が少なかった
- 「下ばっかり見て何だコイツ」と思われた方もいると思います。申し訳ありません。カンペ読んでました
- しゃべるの速すぎる
- すいません。練習が足りませんでした。
- 序盤、「自分たちでコントロールできない指標」のくだりでしどろもどろになってしまったこと
- うっかり口が滑ってしまった
- 多分意味不明なこと言ってます。本当に申し訳ない。
- ベンチマークは指標に過ぎないという話が弱かったかもしれない
- あくまでも実現したいコアバリュー・目標としての速度があって、ベンチマークはそこに向かって進むためのテストケースに過ぎないという話です
- Causal Profilingの話への理解が微妙に甘い。たぶん変なこと言ってる気がする
- 理解が甘いのに無理に言及してしまった
- 詳しく知りたい方は元論文を参照してください
謝辞
運営の皆様、二日間開催お疲れ様でした。来年はUDXカンファレンスや大田区産業プラザPioでの開催となれば駅近なので嬉しいです。途中で帰ってしまったため、もしアンケートフォームなどありましたら公式Twitterでシェアいただければ幸いです。
当日お越しいただいた皆様、ありがとうございました。楽しんでいただけたなら幸いです。
観客の皆さんは金と時間を使って観に来てくださっているわけで、やはり楽しんでもらえないことには自分が発表する意味はないわけです。無闇に客に媚びれば良いというものでもありませんが、ちゃんと一人でもいいから需要のある話を提供できないといけないわけです。そうした中で何かしら刺さってくれる人がいたのであれば、登壇者冥利に尽きます。
資料のレビューやアイデア出しに参加いただいた友人各位、本当にありがとうございました。無事形になりました。皆さんのおかげです。本当にありがとうございました。
Leave my work
I was fired 😫
Sorry, this is a bit clickbait phrase. My contract was ended due to various reasons. This primary reason is that I could not pass the interview by an industrial doctor and I ended over the period my employer allows me to rest.
Thank you for my colleagues.
By the way, I'm finding my next job. I don't have any concrete next plan. I consider all options about my next position since I can no longer to work for CyberAgent Inc. If you have an opportunity which I may suite for, I’ll appreciate to let me knows about it. Please contact me via my email address which I listed to GitHub.
I'd like to work to continue software engineer and I'm interested in to challenge to shape a platform service provides high availability, performance, and reliability.
However, this my hope might too long jump if you know my career. But I'd like to challenge it.
Finally, I appreciate my colleagues. I’m looking forward to see you again. Bye.
css-modulesを止めようとしている話(具体的な解決編)
BEMでいいじゃん話の続きその2にして, 具体的な解決編.
多分ここが気になる人が多いと思うのでなるべく箇条書きで済ませることにする.
背景
前回書いてた内容をまとめると以下のようになる.
無駄話が多いので前回は読まなくてもいいです.
追記: 前回読んでもらった方がいい気がしてきた. 時間が無い方はいったん飛ばしてくれて構わないのは変わらず.
状況
- サービスの初期からwebpackのcss-loader(を用いたcss-modules)を使用していた
- サービス開始時の鉄火場の中で, cascadingに基づく暗黙的なスタイルの継承を用いて, UI component treeにおいて祖先側が子孫側のスタイルを上書きしている箇所が多々あった
起きた問題
- リファクタリングの一環でディレクトリ構造を転置し, 併せてimportの順序も変更・整列した結果, css-loaderによるモジュール間依存グラフの構築順序が変わり,
最終成果物たるCSSのcascading順序が変わり, 先述の暗黙的な上書きと相まって, ES Module間の依存構造の先の先に位置するような,
ほぼ全く予期せぬ箇所のスタイルがいきなり崩れるような事故が発生し始めた
- この時点でreftest(visual regression test)は無い
- reftestがあったとしてもいきなり全く関係ない箇所のスタイルが壊れるコードはメンテがしんどい
- リファクタリングを妨げるような道具立ては長期運用するプロダクトにおいて選択する理由がない
- webpackの上にCSSのビルドまで依存させた結果, ビルドパイプラインが複雑化の一途を辿っていた.
解決方法
要求事項
- 最終的に生成されるCSSが予測可能(透過的)なアプローチであること
- 他のツール群(Lintなど)との相互運用性が取れていること
- できればwebpackに依存しないこと
- css-loaderベースの既存コードからのmigrationが容易であること
- css-loaderベースの既存コードのメリットである「UIコンポーネントのコードとそれに関連するCSSファイルの物理的距離が近い(siblingである)」ことが保たれること
具体的な解決法
importの方法
元々, 以下のようなファイル構成になっていた
/ ├ featA/ │ ├ ComponentA.css │ └ ComponentA.jsx └ featB/ ├ ComponentB.css └ ComponentB.jsx
ComponentA.jsx
のスタイル指定は原則的にはComponentA.css
に記述され,
css-loaderを用いることでwebpackでのbuild時にimport styles form './ComponentA.css';
のように解決される.
これを以下のようにする
/ root.css ├ featA/ │ ├ featA.css │ ├ ComponentA.css │ └ ComponentA.jsx └ featB/ ├ featB.css ├ ComponentB.css └ ComponentB.jsx
/root.css
は, featA.css
とfeatB.css
を任意の順序で@import
経由でimportし, featA.css
はComponentA.css
を同様にimportする. featB.css
も同じ.
これにより, 既存の記述方式からの緩やかな移行が可能になりつつ, ディレクトリ単位での順序関係の定義がCSS wayで明確に可能になった.
また, 現実的にはUIコンポーネントは粒度の細かいものから大きなものに沿って, 祖先方向(caller)なUIコンポーネントが子孫方向(callee)となり, 従属関係があるように設計されるため, セレクタ名さえユニークであればconflictすることはないし, ディレクトリ構造を少しいじった程度で規則が変わるという問題もない.
これは元々css-loaderを採用していた時から「理想的・本来的にはそうなっているはず」だったのだけど, 現実にはcascading順序が先述の理由から偶発的に変わることがあり, 厳密な保証を取りにくい状況にあった.
それをroot.css
でのimport順序に用いてシンプルなcascading順序解決にすることで最終的なコードにおける記述順序が透過的な形式で記述できるようにできた.
そのため, 万が一に順序の問題が起きたとしても(そしてそれはレガシーコードとの釣り合いの問題や, css-loaderからの移行, リファクタリングの中間過程においては頻繁に発生しうるが), 以前よりは順序問題として容易にコントロールが可能になる.
尚、コードはわかりやすくするとこんな感じになる.
/* desktop/mobileで切り替え可能になっている */ @import './variables.css'; @import './component/atom.css'; @import './component/mole.css'; @import './component/org.css'; /* template */ @import './featA/featA.css'; @import './featB/featB.css'; /* page */ @import './page1/page1.css'; @import './page2/page2.css';
これにより
はなんとかなった.
セレクタの命名規則
前項の前提を達成するためにセレクタをユニークにするにはBEMを下敷きに以下のように設定した.
.アプリケーションドメイン-コンポーネント名__コンポーネント内の要素--修飾子
アプリケーションドメイン の箇所は, 当該cssが配置されているディレクトリまでのパスが-
で繋がれる形式になることが多い.
一方で汎用コンポーネントは.com-a
や.com-m
のように簡略表記を許容していた.
同時に, あとでgrepで検索しやすくするため, アプリケーションドメイン-コンポーネント名
の部分をJSコードなどで動的に構築するのは(事実上)禁止することにした.
これは将来的にセレクタ名をrenameするのを容易にするための方針. そもそもcssファイルのsiblingに位置するjsファイルの中でしか使われないので必ずしも重要ではないのだけれども, 原理上, 上位(粒度の粗い側)のコンポーネントからセレクタ経由でdirty hackとして上書きされる可能性を許容してしまっているため, それを「やらざるを得なくなった」場合への対処の余地も含んでいる(現実にはコードレビューで弾かれることの方が多いので存在はしないだろうと思っているが).
結果(中間報告)
一番最初にも述べたけど最終的な移行完了を自分は見届けたわけではないので中間報告になるけれども.
- postcssベースで実現
- node-sassを使わなかったのは, Node.jsのmajor version upへのnode-sassの対応が他に比べて遅れやすく, Node.jsの更新の妨げになるケースが過去多々あったために避けた
- 要求事項は達成
- コード上の使用箇所とスタイル定義が紐づくため, 不要コードの削除も比較的容易なcss-loaderのメリットを引き継げたのは非常に大きい
- 何よりも生成コードの予測がつきやすくなった
- easyではないがsimpleは達成できた
ので結果は上々(と言えると思っている).
Performance Impacts
- minifyしないことで相対的なファイルサイズの増加は起こり得る
- SpeedCurveでの計測を見ていると移行による大規模なperf regressionは起きていない
- 長期的にはむしろ通常の機能追加に基づくコードの増加の影響の方が多い....
未解決または諦めた問題たち
- class属性のmangling
- critical rendering pathに必要なCSSだけの読み込み
- css-loaderべったりの時から, 最終成果物は独立したCSSファイルにしていたため, これ自体はregressionではない
- 将来的にこれが問題になった時に改めて検討する
- plainなCSSにした結果, 必要なスタイル定義だけを切り出してhtml中に埋め込むなどの最適化の余地が広がっているんじゃないかと思っているものの, その調査と最適化を必要とする段階には至っていないので未確認.
- 現代の多くのwebサイト・アプリケーションでは, この問題が現実の問題になるよりも先に, initialで読み込むJSのサイズの方がよっぽどperformance bugになると推測しているし, このとあるアプリケーションA(仮)では事実そうだった
- renameがめんどい
- 変更する箇所が多い
- これはある程度固まったプロダクトでは問題ない
- 立ち上げ期の試行錯誤の続くコードベースではめんどくさいかもしれない
- grep一発で解決可能ではあるが...
余談
- 元々CSS-in-JS嫌いな自分が面白半分で実験していたものを, regression鎮火のために, UIコード周りのオーナーと煮詰め直してworkaroundとして急遽投入したものだけど, 案外うまくいった(と思っている).
- easyではないがcascading style sheetの記述としてsimpleな所に落ち着いたと思うし, 自分としてはBEMで問題が解決していたという屁理屈が実証を伴ったという認識.
- いくつかのunresolved question含め、そのうち同僚の誰かがもう少し詳細に話してくれる....はず. 多分.
教訓・まとめ
- class属性を上書きしてどうにかするのは忙しくても止めた方が良い
- reftestは早期からあると良い
- 壊れたことを検知するには当然テストですよ
- CSSはcascading style sheetsでありES Moduleとは諸々の解決規則が違うので, 道具としては別物として考えるべき
追記: ちょっとした返答・反論とか
CSS Moduleを止めようとしている話(具体的な解決編) - saneyuki_s log“cascadingに基づく暗黙的なスタイルの継承を用いて, UI component treeにおいて祖先側が子孫側のスタイルを上書きしている” コレを食い止めなられなかったことが致命的であって、BEMかCSS Modulesか、という話はあんまり関係な
2019/03/15 03:32
根本的かつ理想的にはそうです. それで解決できていたならば何を採用しても苦労しません.
しかし残念ながら,
- レビューの際の見逃しやby designや時間的な制約による場当たり的な対応という意味の事故も含め, そのような問題が容易に発生しうるのがCSS Scopingのない(WebComponentsに頼れない)世界のCascading Style Sheetsの限界である
- そうした問題が発生していないのであれば, そもそもBEMだstyled-componentsだのという論争も存在しない.
- 現実にそれが当該アプリケーションのコードベースでは発生していた
- 今後の運用の過程で継承や上書きに頼る必要のある箇所が(偶発的であっても)現れるのが予見しうる状況にあったし, 仮にゼロから理想的な設計かつ負債の無いコードベースで構築したとしても, プロダクションにおけるコーナーケースも考慮すると, どこかの層で原理的に担保しない限り「発生しない」という前提に立つのは難しい.
- CSSの継承に頼らず, 関連するpropertyを個別に
unset
またはinitial
で埋めつつ作業をするのも解法としてあり得るが現実的ではない. - そして本文の繰り返しになりますが, リファクタリングなどに際してそこの依存が思わぬ箇所に出てくる結果となっていた. また, css-modulesの枠内で対処を試みると, 取り扱っている対象と記法のセマンティクスのミスマッチから却ってmonkey patchを増やすばかりであった
ため, それらの課題に対して
- そもそも暗黙的な上書きをしうるケースを減らす方向にコーディングスタイルやミクロなUIツリーの設計を変えるようにした
- 最終的に生成されるCSSコードを透過的に扱えるようにすることで, 以下の課題への対処を容易にする
- 1の結果発生しうるコーナーケースの緩和
- 1を志向してもどうしてもうっかり発生しうる将来の問題へのworkaround
- すでに負債と化していたコードベースで発生していた, css-module依存のcascadingに依存したコードからの移行
というのが, 当該アプリケーションで採用した解決案であり, 本エントリで主に言及していたのは2になります.
CSS Moduleを止めようとしている話(具体的な解決編) - saneyuki_s log無秩序なCSS Modulesの使用から、秩序あるBEMにしたら、秩序を得た。CSS Modulesはなにも問題ないのでは?
2019/03/15 20:27
改めて説明すると,
- css-module時代のコードはそれはそれで秩序はあった.
- 付け加えていうのであれば
- リファクタリングを妨げるような道具立ては長期運用するプロダクトにおいて選択する理由がない
- 最終的にcascading style sheetsのセマンティクスで表現されるものを, ESModule(ないしwebpackのloaderセマンティクス)で構築するような, 異なるセマンティクスを道具で構築するのはやり方が悪い
- 最終的に規約や秩序や運用でカバーといった言葉で解決するしかないのであれば, よりsimpleな運用で済む手段で良い
- とりあえずこちら と, 本エントリを改めて読んでいただければと思います
色々書いてなかったこと
Twitterでやりとりした中で、やっぱり書いたほうがいいなと思った話。
込み入りすぎててうまく説明できる気がしなかった初稿で説明していなかったが, className
地獄への門の一端として, 現状の課題たるコーナーケースも色々あった(全部覚えていない + 全部列挙する前にCSS Module路線を諦めた)。例えば「本来であれば:nth-child()
擬似クラスで解決するのが綺麗なはずだが、実際には適用したい対象がReact Componentかつ孫よりも下位に位置するため, 結局何かしらの上書きが必要になっている」など。この辺りについては「そもそものUI ComponentのAPI設計・粒度がおかしい」と断じてしまえばそれまでなのだが, 目の前にあるコードをmigrationしようとするには避けて通れない問題であった。
また、CSS custom propertiesなどはレガシー環境を考慮するとコンパイル時に解決するものになってしまう。つまり, 実行時制約を転化には処理系でのネイティブサポートが必須になるもののIE11などのサポートを含めている以上はそちらを前提にした設計に切り替えることは難しい(丸々IE11向けとそれ以外のコードベースを用意するか、そこも含めてサポートする互換レイヤーを何かしらで用意する必要があった)。
また、WebComponents一式が使えるようになったとしても, ShadowDOM境界を跨ぐ前提に立つと諸々の設計パターンは変わる可能性があり, WebComponents周りが未だ発展途上気味であるため(form control周りなどは最たる例だし, 残念ながら「今年はWebComponents元年」と年初に叫ぶはもはや年中行事になりつつある), より「モダン」なapproachへ将来的に移行したいことや最も強固なエコシステムであるWeb標準のなるべく近くに位置しておきたいことを考慮すると, メンテナブルかつ保守的(Web標準の近くに寄る)手段を採用するほうが長期的には払うコストが少ないだろうと予想し. ゆえに, シンプルに振ることになった.
ゼロから全てを描き直せるのであれば, おそらく強固な規約を浸透させるなどの運用戦術を取りつつ, 別の手段を考慮する可能性もあるだろう. しかし, 我々にそのような富豪的な選択肢はありえないし, 仮に書き直せるとしても真っ先にレガシー環境を切り捨てる方向に体力を注ぐだろうし, 一連の記事で述べている過程は「書き直しやすくする」ための前段階としての措置であった. そもそも頻繁に書き直しできるのであれば, 長期的にメンテナブルにすべきかなどといった問題はない. 困ったら書き直せばいいのだから.
それとstyled-components/CSS-in-JSに関する話については, 「最終的にCSSというシステムに落ちるのだから」自分は否定的にすぎない(あれで本当に多くのcascadingの課題を難なく解決できるのであればそちらで全く問題ない). ゆえに, React Nativeのように最終形がAndroidやiOSのスタイルシステムに落ちるフレームワークに関しては意見するつもりはない.そっちは今の所よく知らないし.
css-modulesを止めようとしている話(長々とした状況説明編)
BEMでいいじゃん話の続きその1にして, とあるアプリケーションが困っていた話.
書き味は最良ではないけれど, 設計思想を持った上で長期メンテを考えると結構いい感じだと思っているアプローチに至った. しかしながら, 放っておくと誰も試行錯誤の過程を書かないままになってしまいそうだし, それを書きそうな同僚も(多忙のあまり)書きそうな雰囲気はなさそうだし, もしかすると永遠に出てこない気さえするので書いてみる.
多分完遂の暁にはどこかで話として総括が出てくると思うけど, 中間報告ということで.
経緯
とあるアプリケーションは, 元々はCSS Moduleをcssのビルドシステムに用いてAtomic Designとして作っていた. CSS Moduleを選定した理由は自分はよく知らないが, ものの試しで使ってみようと思ったんだと思う. ここまでは別に間違ってはいない. (自分は選ばないが)styled-componentsに比べれば自分の好みではあるし, そこまでガミガミと言うつもりはない.
しかし, 肝心のコードがやらかしていた.
セレクタの黒魔術こそ殆どなかったものの, いたるところに存在するReact製のUI componentがclass属性の上書きを許容する形になっていた. この時点で開かれすぎたcomponent指向なので色々破綻しているのだが, おそらく汎用的なcomponentを作ろうとする麻疹の一種だったのだろうと推察している. そしてそれは同時に, initial release前の炎上の鉄火場のなかでダクトテープを貼り付けてデザインレイアウトとの不整合をどうにか鎮火しようとするプログラマーのメンタルモデルとハマってしまったのだろう(その時自分はその場にいないので、真相は藪の中だけど). 結果, アプリケーションサービスがリリースされて1年以上が経過した頃(自分が関わるようになった頃)には, いたることで上位のcomponentが思い思いにclass属性を上書きすることで帳尻を合わせるコードが散見されるようになっていた.
複雑なセレクタの黒魔術による上書きではなく, class属性の上書きで対処しようとした理由はわからない. だが、おそらくはCSS Moduleを使っている関係上, production buildでclass属性がmangleされてしまい, そのmangle結果もコードの成長に応じて必ずしも一定しないためにセレクタでの解決が不可能だったのではないかと推測する. とはいえセレクタを使っていても問題は後述の問題は回避できなかったと思うけど.
さて, そのような不安定なコードベースの現状に対して, アプリケーションの属するサービスは順調に成長を重ねていた. もし「実は伸びないのでcloseします」みたいなサービスであったら、ガタガタでも何の問題も無かっただろう. どうせ死ぬんだし. だが, それとは真逆でむしろ伸ばす方向にするにはどうするかを考える必要があった. そのような状況で, やばいなーと思いつつもガタガタになっているとはつゆも知らない自分(と同僚たち)はうっかりパンドラの箱を開けてしまうことになる.
発端はコードベース内のディレクトリ構造をリファクタリングしているときに起こった. そもそもディレクトリ構造をリファクタリングしていたのはコードやモジュールの責務境界を明瞭にするために実施したのだが, その過程でES Moduleのimport文の順序関係や格納されるパスのアルファベット順を変えてしまうケースがあった. 変えてもアプリケーションはちゃんと動いているし読み込む側の順序に依存するようなことはないだろうと思っていた.
読み込む側での順序を変えた結果, アプリケーション内で読み込まれる側のモジュールの依存関係に基づくDAG上でトラバースされ評価されるタイミングは変わることになる. しかもその変わった対象は変更したモジュールから遠く離れた箇所であったりもするため, changesetから読み取ることも難しい. これがJSの世界に閉じただけならば問題なかったのだが, 変更前後でグラフ構造の比較が必要になる上, 「変更箇所から10モジュールくらい跨いだ先のJSファイルが読み込んでいるCSSファイルの評価順序が変わった」のを上手く検知しながら他を壊さないように直す方法を探す必要がある. さらに悪いことに, その評価順序の変更の適用される対象が「最終的にはフラットな構造を上から下まで読み込んだ順序に評価する」cascading style sheetsなのであった.
結果を先に述べると, 上の方で述べた「上位のcomponentがclass属性を介して下位のcomponentのclass属性を上書きして微妙なレイアウトを調整する」ために必要なcascading順序が大きく破綻して, 「注意して確認すると壊れている」「大きく破綻するケースもあれば小さくずれているだけも両方ありうる」状況になってしまった.
それでもreftestがあれば早期に問題には気づけたであろうが, 当時の自分たちはreftestを持っておらず, しかも悪いことにリファクタリング作業が半分以上進んだ段階でその問題が発覚したために(それまで気づくことができなかった), 今更ディレクトリ構造の変更を中断して全てをrollbackするのも難しい状態になっていた.
では仮にreftestを持っていた場合は早期に問題が検知できて堅実に進めることができたのだろうか? その場合は先にも述べたようにモジュール間の依存グラフ構造を黒ひげ危機一発的におっかなびっくりと壊さないように変更を進めることになる. それが嫌であれば、コード行数10~15万行の中から, まず安易な上書きをしているところをすべて取り除く必要があり,それは縦横無尽に鎮火を前提として書かれたhackを全て取り除くだけのチームの体力が要求される. どちらにせよ, コードの責務分界点を分けるという目的に到達するまでに年単位の労力が必要になる. そして繰り返すが当時の自分たちにはreftestはない. 事実上, この路線のままでは軽微なcleanupにも等しいリファクタリングさえもロクにできない状況に追い込まれていた. リファクタリングできないコードベースとか敗北宣告にも等しかった.
解決に向けて
話を戻す, 今更rollbackして後にも引けない, かといって放っておくと前進すら難しい自分たちは以下の強行着陸を採用することになる..
- 可能な限りモジュール依存グラフの評価順序を変えないために, JSのimport文のsortを諦める
- CSS Module by css-loaderを使うのではなく, 素のcssをconcatすることでcascadingの評価順序を壊さないようにする
- class属性の上書き順序を保持またはコントロール可能にする
- postcss-loaderを用いる.
1は, 評価順序を変えなければ最終成果物が変わらないという保証が取れたので, 2で払うべきコストを先送りしてディレクトリ構造の変換を継続するための手段. 2は, すでに壊れてしまったものを制御可能な状態に戻すための処置. 手間は増えるしダルいものの, 明示的にcascadingの順序を制御できるために, もっともシンプルな解決方法となる.
この2つを, 最終的に2に持っていく合意のもとに壊れた箇所を修正しながら適用することになった. そもそもclass属性上書きしているのを直すべきなのだが, 影響範囲が大きすぎる + regressionの鎮火が優先であるため, 次のフェーズの課題とした.
結果的に多数のregressionは鎮火し, その後細々と変え続けた結果, 開始から数カ月(機能実装などで一時中断していたので実働としてはもっと短い)ののちにディレクトリ構造の変更は完了したのであった(CSS Module撤廃はside work的に現在も進行中)..
......同僚各位、その節は本当にご迷惑をおかけしました....
なんか話が長くなってきたので具体的にどうしたかは次のエントリで書くことにする.