こんにちは。 Eight でフロントエンドエンジニアをしている鳥山(@pvcresin)です。
最近、事業部長に Slack の絵文字をプレゼントするという実績を解除しました。
早く使われないかなとそわそわしています。
さて今回は、Sass の mixin を集めたライブラリである compass-mixins を消し、Autoprefixer に移行した話をしたいと思います。
Sass の mixin
PC 版 Eight では、スタイリングに CSS のプリプロセッサである Sass(SCSS 記法)を使っており、 webpack で CSS に変換しています。 Eight での実際の使われ方についてはこちらの記事で紹介しています。
Sass には、あらかじめスタイルを定義しておくことで、そのスタイルを使い回すことができる mixin (ミックスイン)という機能があります。 例えば、幅 100px・高さ 50px の赤と青の四角形を描画したい時、SCSS で
@mixin box { width: 100px; height: 50px; } .red-box { @include box; background-color: red; } .blue-box { @include box; background-color: blue; }
のように書くと、以下のような CSS に変換されます。
.red-box { width: 100px; height: 50px; background-color: red; } .blue-box { width: 100px; height: 50px; background-color: blue; }
この例では、2 つの四角形に共通する width と height の記述を @mixin box
に定義し、
それぞれの四角形に対応するブロック内で @include box
と書くことで 2 つの四角形にスタイルを適用しています。
これが mixin の基本的な使い方になります。
この他にも引数をとることができたり、その初期値を設定できたりと、スタイル定義を簡潔に書くための機能が備わっています。
compass-mixins
今回言及する compass-mixins は、一言で言えば先にあげたような mixin の集合体です。
compass-mixins を import することで、既に定義されている便利な mixin を使うことができます。
ちなみに、compass-mixins は Compass という CSS オーサリングツールから派生したものになります。
Compass には Sass を CSS に変換する機能やスプライト画像を生成する機能など、スタイリングを支援する様々な便利機能がありましたが、
2016 年にメンテナンスを終了しています。
compass-mixins はこの Compass に含まれていた mixin を切り出したものになります。
また、Compass は Sass のコンパイルに Ruby Sass を使っていたため、mixin もそれに対応する形でしたが、
compass-mixins ではコンパイルがより高速な C++製の LibSass に対応した形に変更されています。
Eight における compass-mixins
Eight では主に CSS プロパティに Vendor Prefix(ベンダープレフィックス) を付与するという目的で compass-mixins を使用してきました。
例えば user-select: none;
を指定したいとして、
.box { @include user-select(none); }
と書くと、以下のように Vendor Prefix を付与した CSS を出力してくれます。
.box { -webkit-user-select: none; -ms-user-select: none; user-select: none; }
確かに複数の Vendor Prefix を書かなくて良いので便利です。 Eight でも長年愛用されており、私が入社した 2019 年においても現役という状況でした。 しかし、開発をしているうちに、compass-mixins は必要ないのではないかと考えるようになりました。
compass-mixins をやめる理由
compass-mixins が必要ないのではないかと考えた理由は以下の 4 つになります。
- エディタの補完が弱い
- 更新が 2016 年で止まっている
- Vendor Prefix の必要性の低下
- Autoprefixer の存在
時系列順に並んでいるので、1 つひとつ見ていきます。
1. エディタの補完が弱い
最近のエディタでは純粋な CSS の書き味はとても良いです。 プロパティ名や値を補完してくれるので、スムーズにコーディングを進めることができます。 一方で、node_modules 下にある compass-mixins の mixin を使用する時は基本的にあまり補完が効かず、いつも不便に感じていました。 もちろん私の開発環境が悪かった、またはカスタマイズが足りなかった可能性もあります。 しかし、これがきっかけとなり、compass-mixins の必要性について考えるようになりました。
2. 開発が 2016 年で止まっている
調べてみると、compass-mixins の開発は派生元の Compass と同じく 2016 年で止まっていました 😇 開発の止まったライブラリに依存したコードが残っているのはリスクになるので、 身動きがとれるうちに別の何かに移行するのが賢明です。 このあたりから本腰を入れて compass-mixins を消すことを考え始めます。
3. Vendor Prefix の必要性の低下
まずは、現状把握から着手しました。
compass-mixins がどのように使われているかを調査した結果、
使用されているほとんどの mixin が Vendor Prefix の付与のためのものであることがわかりました。
そもそも Vendor Prefix とは、ブラウザの提供元(ベンダー)が標準化前の機能を先行実装した際に、
その機能を有効化するためにプロパティや値の前につける接頭辞(-webkit-
や -ms-
など)のことを指します。
Vendor Prefix については、下記の MDN のページに概要が載っています。
https://developer.mozilla.org/ja/docs/Glossary/Vendor_Prefix
注目する点として、このページには以下の記述がありました。
ブラウザーベンダーは、実験的な機能にベンダー接頭辞をつけることをやめるようになってきています。
どうやら実験的機能を開発者が乱用した結果、ベンダーが機能を追加することが困難になってしまい、 現在は標準化前の機能の制御方法として Vendor Prefix を使わない方向に進んでいるようです。 また、ブラウザの開発が進んだこともあり、Vendor Prefix を付けなくとも大丈夫な状況になってきています。
4. Autoprefixer の存在
とはいえ、Vendor Prefix がまったく必要なくなったわけではないので、依然として付ける必要があります。
かと言って、いちいち Vendor Prefix が必要かどうかを調べるのも面倒なので、自動付与したい。
そこで登場するのが Autoprefixer です。
Autoprefixer は CSS 加工ツールである PostCSS のプラグインです。
各ブラウザがサポートする機能をまとめている Can I use の情報を元に、必要な Vendor Prefix を自動で付与してくれます。
Can I use は頻繁に更新があり最新の情報を反映しているため、更新が止まっている compass-mixins より信用できそうです。
また、サポートするブラウザの指定には Browserslist というツールが使えます。
package.json や .browserslistrc ファイルに設定を書くと、Autoprefixer がそれを読み取り、
サポートするブラウザに応じた出力を行ってくれます。
例えば、世界での使用率 5 %以上のブラウザのバージョンをサポートしたかった場合、browserslist の設定を > 5%
とすると、
.box { display: flex; }
という入力に対し、Autoprefixer は
.box { display: flex; }
のようにそのまま出力します。
これはつまり、サポートするブラウザでは Vendor Prefix は必要なかったことを意味します。
次に、追加で IE10 をサポートしようとした場合、設定を > 5%, IE 10
とすると、
.box { display: flex; }
は、
.box { display: -ms-flexbox; display: flex; }
のように出力されます。 IE10 では Vendor Prefix を付けなければならないことがわかります。 これらのツールにより、必要最低限の Vendor Prefix のみ付与するという形が実現できます。
以上のような 4 つの理由から、compass-mixins をやめ、Autoprefixer に移行することを決定しました。
移行手順
Autoprefixer への移行は以下の順で進めていきました。
- Autoprefixer の導入
- 指定した mixin の使用を禁止する簡易 Linter の作成
- Vendor Prefix 目的の mixin 使用部分を純粋な CSS に置換
- Vendor Prefix 目的以外の mixin の移植
- compass-mixins の削除
1 つひとつ詳しく見ていきます。
1. Autoprefixer の導入
まずはじめに移行先となる Autoprefixer を導入していきました。
Eight では元々 CSS を Minify する目的で、cssnano という PostCSS のプラグインを使っていたため、
PostCSS のプラグイン設定で cssnano に続けて Autoprefixer を追記しました。
具体的には、webpack.config.js の postcss-loader 部分で
{ loader: 'postcss-loader', options: { /* 省略 */ postcssOptions: { plugins: [ require('cssnano')({ preset: 'default' }), + require('autoprefixer'), ], }, }, },
のようにプラグイン設定を追記しました。
さらに、package.json の browserslist 部分に
{ "browserslist": [ /* ブラウザの指定 */ ] }
サポートするブラウザの指定を配列形式で記述しました。
これで Autoprefixer とその Peer Dependencies の PostCSS をインストールすれば Vendor Prefix がつくようになります。
package.json の browserslist の記述でサポートするブラウザの対象範囲を広げた場合に、出力がしっかり変化するところまで確認して導入作業は完了です。
あとは、目視で確認したり、E2E(End-To-End)テストを回したりして、スタイル崩れなどが起きていないかを注意深くチェックしました。
特に問題もなく、Autoprefixer の導入が完了しました。
余談ですが、PostCSS にはモダンな CSS を古いブラウザでも理解できる形に変換する PostCSS Preset Env というプラグインがあります。
PostCSS Preset Env は多数のプラグインをまとめたものであり、この中に Autoprefixer も含まれているため、こちらを使うという選択肢もありました。
しかし、今回は Vendor Prefix をつけることのみが目的であったため採用を見送りました。
2. 指定した mixin の使用を禁止する簡易 Linter の作成
移行は compass-mixins の mixin の使用部分を純粋な CSS に徐々に置換して進めていきます。
移行済みの mixin が新たに使われることを防ぐため、使用を禁止する仕組みが必要になります。
まず始めに考えたのは既存の Linter(静的解析ツール)の使用です。
Linter の設定で実現できれば、エディタでの可視化や pre-commit hook でエラーを出すことが手軽に実現できると考えました。
Eight では stylelint や stylelint-scss を使って SCSS ファイルでの Lint を行っています。
そのため、指定した mixin の使用を禁止するものはないかと探したのですが、残念ながらピッタリのものは見つかりませんでした 🥺
stylelint-scss に新しい Lint ルールを追加する PR(プルリクエスト)を送っても良かったのですが、
実装で考慮することが増える上、マージされる保証もなく、仮にマージされたとしてもリリースまでに時間がかかる可能性がありました。
それらの点を考慮すると、compass-mixins を消すまでの一時的な仕組みという観点ではオーバーエンジニアリングであると判断しました。
次の手として、自作のスクリプトで CLI ツールを作成し、簡易的な Linter を実現する方法を考えました。
こちらは、かなり作り込まないと既存の Linter ほどリッチな制御はできませんが、
pre-commit hook でエラーを出す程度の目的を達成するためには必要十分だと感じたため、こちらを採用することにしました。
以下がスクリプトになります。
// compass-mixins-check.js const fs = require("fs"); const path = require("path"); const { EOL } = require("os"); const forbiddenMixins = ["display-flex", "flex-wrap"]; const forbiddenTexts = forbiddenMixins.map( (forbiddenMixin) => `@include ${forbiddenMixin}` ); if (process.argv.length < 3) { console.error("Error: No file specified."); process.exit(1); } let foundErrorFile = false; const inputFilePaths = process.argv.slice(2); inputFilePaths.forEach((inputFilePath) => { let isErrorFile = false; const absolutePath = path.resolve(process.cwd(), inputFilePath); const lines = fs.readFileSync(absolutePath, { encoding: "utf8" }).split(EOL); lines.forEach((line, lineNum) => { forbiddenTexts.forEach((forbiddenText) => { const index = line.search(forbiddenText); if (index === -1) return; if (!foundErrorFile) { foundErrorFile = true; } if (!isErrorFile) { isErrorFile = true; console.error(`\n${inputFilePath}`); } const lineNumText = (lineNum + 1).toString().padStart(3, " "); const indexText = index.toString().padEnd(3, " "); console.error( `${lineNumText}:${indexText} ✖ Use usual CSS property instead of '${forbiddenText}'. compass-mixins is deprecated.` ); }); }); }); if (foundErrorFile) { console.error(""); process.exit(1); }
6 行目にある forbiddenMixins
に移行済みの使用を禁止したい mixin の名前を書き、node compass-mixins-check.js foo.scss bar.scss
のように実行します。
すると、入力したファイルの各行を @include
で始まる mixin の文字列で検索し、使用が見つかった場合には該当部分と共にエラーにするといったものになります。
Node.js 覚えたてのような非常に原始的かつ色々な考慮がされていない雑なスクリプトではありますが、目的に対しては十分だったと感じています。
簡易 Linter が完成したので、SCSS ファイルをコミットしようとした場合に pre-commit hook でこれを呼び出すようにします。
これには husky と lint-staged を使いました。
package.json にコマンドを追加します。
{ "scripts": { + "compassmixins:check": "node script/compass-mixins-check.js" }, "husky": { "hooks": { "pre-commit": "lint-staged --allow-empty" } }, "lint-staged": { "**/*.{css,scss}": [ "stylelint --fix", + "yarn run compassmixins:check" ] }, }
禁止した mixin を使用した SCSS ファイルをコミットしようとした場合に以下のようにエラーが出ます。
✖ yarn run compassmixins:check: /absolute/path/to/foo.scss 5:4 ✖ Use usual CSS property instead of '@include display-flex'. compass-mixins is deprecated. /absolute/path/to/bar.scss 3:4 ✖ Use usual CSS property instead of '@include display-flex'. compass-mixins is deprecated. 4:4 ✖ Use usual CSS property instead of '@include flex-wrap'. compass-mixins is deprecated.
エラーの場所と修正方法と原因が書いてあるので、これで伝わるはず...!
ちなみに、CI でのチェックは今回行いませんでした。
雑なスクリプトなのでパフォーマンスなどはまったく考えられておらず、CI で実行すればファイルの Lint に時間がかかりそうだと判断したためです。
3. Vendor Prefix のための mixin 使用部分を純粋な CSS に置換
compass-mixins の使用箇所を少なくするため、
.box { @include user-select(none); }
という記述があったとき、これを
.box { user-select: none; }
のように純粋な CSS に置換していきます。
置換前後の出力される CSS をチェックしながら慎重に進めていきます。
CSS の出力が変わらなければ、まったく問題ありません。
もし出力が変わったとしても、サポートするブラウザで必要な Vendor Prefix がついていれば、こちらも問題ありません。
CSS の宣言が減った場合は無駄な Vendor Prefix がついていたということになります。
いきなりすべてを置換したのでは 1 つの PR における差分が大変な量になってしまうので、何回かに分けて置換する PR を出していきました。
ちなみに、チームメンバーに教えて頂いたのですが、Sass v3 からは変数名や mixin 名などにおいてハイフン(-
)とアンダースコア(_
)が 同一とみなされる そうです。
@include display-flex
をチェックするときは @include display_flex
もチェックしたほうが良いですね。
4. Vendor Prefix 目的以外の mixin の移植
最初に Eight では compass-mixins を主に Vendor Prefix を付与する目的で使用してきたと書きました。 しかし、compass-mixins にはそれ以外にもスタイルを定義した便利な mixin があります。 Eight では、そのような mixin を 2 つ使っていたため、 こちらは compass-mixins のリポジトリ からローカルに移植して対応しました。
5. compass-mixins の削除
ここまでで、移行自体は完了しました。 最後に、compass-mixins をリポジトリから削除し、簡易 Linter のスクリプトを削除して作業は完了です。
まとめ
今回は、Sass のライブラリである compass-mixins を消し、Autoprefixer に移行した話をしました。
正直、はじめは Vendor Prefix 書くの面倒だな、純粋な CSS 書きたいなぐらいのモチベーションがぼんやりとあっただけで、compass-mixins を完全にリポジトリから消せるとは思ってもみませんでした。
しかし、実際に作業してみると毎日 PR を出して 1 週間程度でサクッと終えることができました。
Web フロントエンド開発を行うすべてのエンジニアの開発者体験の向上に繋がるため、非常にコスパが良かったなと感じています。
自作スクリプトによる簡易 Linter では AST(抽象構文木)を使うようなものではなく、非常に雑な仕様に倒したことも、作業をスムーズに進めることができた要因だと考えています。
「Linter って用途によってはこんな雑な実装でもいいんだ」と新しい発見をすることができました。
ひとまず、補完がしっかりと効く純粋な CSS を書くことができる喜びを噛み締めて開発していきたいと思います。