総行数57,000の巨大CSS群をLessに書き換えた軌跡
こんにちは!フロントエンド闇祓いの Kuniwak です。
この投稿はmixiグループ Advent Calendar 2015の20日目の記事です。
今年の9月に、スマートフォン Web ブラウザ版 mixi「mixi Touch」の巨大 CSS を Less (CSS プリプロセッサー)でビルドする環境へと移行しました。 書き換えた CSS の行数は、なんと 56,725行 です。😵
ということで、今回は弊社の大規模 CSS → Less 移行事例についてお話しします。
背景
スマートフォン版 mixi は、2010年5月に始まりました。 この頃のスマートフォンは、iPhone 端末であれば iPhone 3GS、Android 端末であれば Nexus One という時期です。 また、スマートフォンの世界では、Webkit ベースのブラウザーが席巻していた時代ということになります。
そのため、当時の CSS は Webkit ベースのブラウザーのみを考慮した書き方が多く見られました。たとえば、次のような -webkit-
プレフィックスを使った書き方です:
/* 非標準の CSS プロパティ fuga を使う場合の書き方 */
.hoge {
-webkit-fuga: moge;
fuga: hoge;
}
この -webkit-
プレフィックスは非標準(または実験段階)の機能などを先行して利用するためのものです。 ただし、実験段階の機能ということだけあって、標準化される過程で CSS プロパティの名前が変わったり、別仕様に取り込まれてしまったりするリスクがあります。
そのため、-webkit-
のようなベンダープレフィックスを使うためには、変更されたときの影響を小さくしたり、書き換えを楽にするような工夫が必要とされます。
しかし、当時の弊社の CSS では、次のような悪い書き方が散見されました:
/* 潜在的な問題を抱えたコード */
.something {
-webkit-background-size: 8px 8px;
}
このコードは、当時のモバイルブラウザーであれば意図通り表示されることでしょう。 しかし、この書き方には3つの潜在的な問題があります。
- 非 Webkit ベースのブラウザーを考慮していない
-webkit-
つきのプロパティが標準になったときの書き換えが大変-webkit-
つきのプロパティが非標準になったときの書き換えが大変
当時から問題は認識されていたものの、問題への対処は見送られていました。
そして、5年経った今になって、この3つの問題が顕在化したのです。
この問題の解決のために、HTML コーダー(マークアップエンジニア)2名 + 技術支援1名 + テスター1名の体制で対応作業を始めました。
問題1: 非 Webkit ベースのブラウザーを考慮していない
-webkit-
プレフィックスをつけたプロパティは、非 Webkit ベースのモバイルブラウザー(Android 版 Firefox や Windows Phone の IE など)では解釈されません。 しかし、これらのシェアが小さいためか、-webkit-
プレフィックスのみの CSS が世に溢れています。
そこで動き出したのが Mozilla です。 Mozilla には WebCompat Team なる部署があり、-webkit-
のみの CSS を使っている Web サイトへの啓蒙活動をしているそうです(Mozilla の方による記事 WebCompat)。 mixi に対しても Mozilla からのコンタクトがありましたが、あまりの -webkit-
プレフィックスの多さから見送られた、苦い記憶があります。😢
また、啓蒙活動とは別に、Firefox のコード側にも動きがありました。 それは、特定のドメインの CSS では、-webkit-
プレフィックスを外して解釈するというものです。 つまり、ドメインによって CSS の解釈が変わることになります。 この動きについては Mozilla 側の対応チケットを見ると何がおこなわれたのかわかります。
実は、この Mozilla 側での動きが、Less 化の強い動機になりました。 これは、弊社のステージング用の環境では専用のドメインが使われていることに起因します。 そのため、本番環境とステージング環境での CSS 解釈に差が出てしまうのです。 これではステージング環境の意味がありません。
つまり、非 Webkit なブラウザーに注意を払わなかった結果、ブラウザーベンダーがお怒りになり大変なことになったのでした。
問題2: -webkit-
つきのプロパティが標準になったときの書き換えが大変
ベンダープレフィックスつきのプロパティは、そのまま標準化されることがあります。 この場合、-webkit-
をつけたままのものは廃止予定(deprecated)になります。 そのため、「最後にプレフィックスなしのプロパティを付け加える」ことで、標準化されたものを優先的に利用する書き方が推奨されています。
.something {
-webkit-background-size: 8px 8px;
background-size: 8px 8px;
}
しかし、弊社の CSS には、プレフィックスなしのプロパティを併記しないものが多く見られました。 このような CSS がおよそ 57,000 行もあるのですから、推奨されている書き方に直すのも大変です。
つまり、推奨される書き方に従わないままコードが育っていった結果、推奨の状態に戻すことが困難になっていました。
問題3: -webkit-
つきのプロパティが非標準になったときの書き換えが大変
ベンダープレフィックスつきの機能が標準化される過程で、CSS プロパティの指定方法が変わったり、機能自体が削除されることがあります。
たとえば、mixi の CSS には、相当前に非標準となった box 仕様のプロパティが使われていました。 この件は Mozilla でも問題になっていたらしく、「mixi.jp Mobile version is using the old-old CSS webkit flexbox syntax」というチケットがつくられており、個人的にはとても恥ずかしい気持ちでいっぱいでした。
このような機能は廃止予定になることが多く、何かの拍子に取り除かれてしまっても文句は言えません。 そのため、廃止予定になった時点で書き換える必要があります。 しかし、既に行数が膨らんでしまった CSS を仕様変更の都度に書き直すのは大変です。
まとめると、非標準な機能を多用すると、非標準の機能が消されることに怯える日々を過ごすことになることがあります。
問題への対処の軌跡
救世主 Autoprefixer
これらの問題に対処するために、Autoprefixer というツールを使うことにしました。 Autoprefixer は、指定した環境に合ったベンダープレフィックスを自動的につけてくれるツールです。
/* Autoprefixer 適用前 */
.example {
background-image: linear-gradient(#e87451, #c42f02);
}
/* Autoprefixer 適用後 */
.example {
background-image: -webkit-gradient(linear, left top, left bottom, from(#e87451), to(#c42f02));
background-image: -webkit-linear-gradient(#e87451, #c42f02);
background-image: -moz-linear-gradient(#e87451, #c42f02);
background-image: linear-gradient(#e87451, #c42f02);
}
この Autoprefixer を使うと、問題の1と2を解消できます。 問題3についても、Autoprefixer が可能な限り標準の書き方へと変換してくれます。
ただし、Autoprefixer は既にベンダープレフィックスがついているプロパティに対しては何もしません。 そのため、前作業として CSS のベンダープレフィックスを外すように書き換える作業が必要になります。
CSS:fixme で Autporefixer の下ごしらえ
前述の通り、Autoprefixer の下ごしらえとして、既存の CSS からベンダープレフィックスを取り除く必要があります。
このために、CSS:fixme というツールを使いました。 このツールは Mozilla の方が開発しているオープンソースのツールで、ベンダープレフィックスつきの古い記法を、Autoprefixer にかけられるような記法へと変換してくれます。
ただし、巨大なファイルで作業するにはコマンドラインによる変換の方が便利だったため、魔改造をおこないました(魔改造後のコードは MPL ライセンスにしたがって公開しています)。
また、CSS:fixme で変換されないものもありました。 変換されない理由は、未標準だからか、変換対象から漏れていたかのどちらかです。 この部分は、マークアップエンジニアの方と細かく結果を確認しながら作業を進めました。
このベンダープレフィックスを外す作業が、全体のおおよそ半分を占めています。
Less 導入
さて、救世主 Autoprefixer を使うためには、CSS をビルドする必要が生じます。 そして、どうせビルドするならということで、CSS の表現力を補う CSS プリプロセッサー Less を導入しました。 つまり、Less 移行は、Autoprefixer 導入のついでという側面が強かったのです。😜
さて、CSS から Less へ移行すると変数が使えるようになります。 これによって、保守性を格段に向上させられます。
たとえば、カラーレギュレーションの色を変数にしておくだけでも、管理しやすいことは明らかです:
//- `@basicTextColor`: 基本のテキストカラー
@basicTextColor: #333;
//- `@subTextColor`: 基本よりすこし弱め
@subTextColor: #666;
// ...
カラーレギュレーション以外にも、アイコンフォントのコードポイントを変数によって集中管理しています。また、私個人としては z-index についても変数で管理することを目指しています。
さて、Less の他にも Sass という有名な CSS プリプロセッサーがあります。ここで Sass ではなく Less を採用した決め手は、それぞれの提供されているエコシステムにあります。 Sass は Ruby のエコシステムで提供されていますが、Less は Node.js のエコシステムで提供されています。 そして、Node.js の方が、Perl と同居させるのに都合がよかったのです。
-webkit-box
との格闘
これで平和が訪れるかと思いきや、-webkit-box
という厄介な問題が立ちはだかりました。
この -webkit-box
は、横並び/縦並びの要素の表示制御に関する機能です。 これと同等の機能を実現するためには多大な労力が必要なため、非標準だった当時から重宝されていました。
ただし、この仕様は box(2009年)→ flexbox(2011年)→ flex(現在の最終草案の仕様)と 3 度大きく変更されました(仕様の変遷については flexboxの旧仕様、改定仕様、現行仕様の一覧 « LINE Engineers' Blog という素晴らしいまとめ記事があります)。 そして、-webkit-box
はこのうち最も古い仕様で、Autoprefixer が適用できる状態にするには大幅な書き換えが必要です。
たとえば、CSS:fixme は可能な限り -webkit-box
を最新の仕様である flex
に書き換えてくれます。 しかし、display: -webkit-box
があたっている要素の子要素の width
に問題が生じます。
-webkit-box
仕様では、width
が指定されていれば flex-basis
指定と同等の効果がありました。 しかし、flex
を使う時には明示的に flex-basis
を記述しなければなりません。
すると、CSS:fixme が width
プロパティに遭遇したとき、これが flex-basis
に相当するのか、それとも通常の width
なのかを区別する必要があります。 このような書き換えは、セレクタの対象の親子関係を考慮できなければ実現できません。
/* ... */
.moge {
/*
* この width の意味は、親要素に display: -webkit-box が
* あたっているかどうかで変わってしまう
*/
width: 10px;
}
/* ... */
つまり、BEM のような命名規則で CSS から親子関係を推測できない限り、機械的な検証は困難です。 この問題は、2人がかりでのコードレビューと、凄腕テスターによるステージング環境での動作確認で洗い出しました。
このような QC(品質管理)作業に、残り半分の作業時間が費やされています。 この甲斐あって、57,000 行に渡る書き換えにもかかわらず、不具合報告はきていません。
Less 化における QC への技術的支援
これまでの作業で、Autoprefixer による問題解決が実現できました。 しかし、このままでは、まだ Less の機能を活用できていません。 宝の持ち腐れの状態になっています。
そこで、次のフェーズとして、Less の機能を利用するようにリファクタするフェーズがはじまりました。
このフェーズでは、生成される CSS が変わらないことを期待できます。すると、CSS の AST(抽象構文木)による比較が可能になります。AST による比較では、CSS 内の改行位置や空白の取り方に左右されないため、よりブラウザーの CSS 解釈に近しい結果がえられます。この AST がリファクタ前後で変化していなければ、表示に差異がでないことが期待できます。
ただし、Less のような CSS プリプロセッサーを使えば、AST に差は出てしまうものです。 この AST の差で重要なのは、セレクタ・プロパティの過不足と、ルールの記述順序の2点です。
ここで重要なのは、この2点の深刻度に違いがあることです。 たとえば、セレクタ・プロパティの過不足は、記述漏れの可能性を強く示唆します。 これに対して、ルールの記述順序の差は、スタイルの上書きの可能性を示唆するものの、影響はごく軽微です。 この2つを混同してしまうと、深刻度の高い警告が深刻度の低い警告に埋もれてしまうといった問題が生じてしまいます。 ようするに、この2つは分解して観察することが重要なのです。
そして、既存の CSS の AST 比較ツールには、プロパティの過不足とルールの記述順序を分解して比較できるツールが存在しませんでした。 そこで、css-semdiff というツールを開発しました。
たとえば、css-semdiff を使うと、次のようにプロパティの過不足が発見できます。 このプロパティの過不足は、ルールの記述順序に影響されません。
$ css-astdiff a.css b.css --verbose
extra:
#header ul {
display: -webkit-box;
display: -webkit-flex;
display: -moz-box;
display: flex;
}
missing:
#header ul {
display: -webkit-box;
display: -webkit-flex;
display: flex;
}
23 extra rules and 23 missing rules between a.css and b.css
また、ルールの記述順序の差異のみを発見することもできます:
$ css-orderdiff a.css b.css --verbose
order changed: #footDisplay
become to be higher than:
.error #mainDisplay,
.webView01 #mainDisplay
order changed: .error #mainDisplay
become to be lower than:
#footDisplay
このフェーズでは css-semdiff を活用した結果、複数の不具合を未然に防止できました。 この css-semdiff は、MIT ライセンスで公開しています。 ぜひ、QC 作業にお役立てください!
後日談
この話は、私が新卒だったころに諦めた Firefox 対応を、1年越しに達成した話でもあります。それだけに、個人的にも思い入れの深い仕事です。
ちなみに、この作業を終えたところ、Mozilla の方からお礼のお返事 をいただきました。
This is fixed. \o/
Thanks to Mixi team and their hard work.With a lot of love from Mozilla.
この仕事に携われてよかったと思えた瞬間です。😝
まとめ
弊社のスマートフォンページでは次の3つの問題が生じていました。
- 非 Webkit ベースのブラウザーを考慮していない
-webkit-
つきのプロパティが標準になったときの書き換えが大変-webkit-
つきのプロパティが非標準になったときの書き換えが大変
そして、これらの問題を次のようなツール/活動で解決したという話でした。
- Autoprefixer
- CSS:fixme
- コードレビューなどの QC 活動
- css-semdiff
次は esugita さんの「Find Job!っていう求人メディアに関わってるので、採用とかとか。」です。
宣伝
ミクシィでは、技術を駆使して負債に立ち向かう闇祓いエンジニアをいつでも歓迎します! 闇祓いスキルをつけておくと、光環境/闇環境を選ばない技術力がつくことでしょう。
ともに闇祓い、してみませんか?