blog.unresolved.xyz

[Rails5.1.3] Sass(SCSS)からwebpacker + PostCSSに移行してみるか

Tue Oct 10 2017

    僕は結構Asset Pipelineの仕組みが好きなんで特にやめちゃいたいみたいな意識はないんですが、 個人プロジェクトだし新しいものを実際に使ってみる経験っていうのも大切かなと。

    あと、僕のプロジェクトは依存性の解決だけをyarnがやっていて、yarnで落としている3rd Party系アセットをAsset Pipelineで運用していくやり方に限界を感じていました。

    CSSとその他フォントファイル等が共存しているパッケージとか。

    移行前の状態

    現状はそれなりにRailsの基本的な状態です。僕はあんまり色んなものをゴテゴテと入れないタイプなので。

    • rails (5.1.3)
    • アセット系はすべてAsset Pipelineで処理
    • 依存性に関してはyarnでやりたかったのでwebpacker:installは済んでる
    • でもwebpackerは使ってない

    目指すところ

    「移行すること」を目的にしちゃうのはあまりにもナンセンスなので、移行後に目指すところと守るべきラインを決めます。

    そもそも、現状の環境に困ってるわけではないですし・・・。

    • .scssで書いてるやつを全部移行して、PostCSS on webpackerで処理できるようにする
    • scss-lintをやめて、stylelintに移行する
    • なんかAutoprefixerは今でも使うのが常識らしいので入れる(なんか勝手にいらないものと思ってた・・・。)

    Set Up Your Build Tools  |  Tools for Web Developers  |  Google Developers

    守るべきライン

    CSSに関する部分だけなので、とりあえず以下だけにしました。

    • ファイルサイズは極端に増やさない(Autoprefixerなしの状態で)
    • 処理時間は極端に増やさない(Asset Pipelineは移行後も生きるので、webpackの頑張り分が多少増える気がする)
    • FontAwesome等の3rd Party系アセットも、きちんとダイジェスト付きで配信する
    • 開発環境は悪化させない(主にwebpack-dev-serverまわりのこと)
    • Roadieはちゃんと動くようにする(HTMLメールを使ってるので)

    これが守れないならmasterブランチにマージしません。

    僕はCSSがでっかくなるのがすごく嫌なタイプなので・・・。とはいえ少しくらい増えるのはOKとします。 10KBとか増えるならNGで。

    移行前作業 - 現状での出力されるファイルサイズと処理時間を計測

    そんなでかいプロジェクトでもないので、言うほどでもないかと。 FontAwesomeが幅取ってるかもですね・・・。

    ビルド

    1$ time rails assets:precompile RAILS_ENV=production 2yarn install v1.2.0 3[1/4] Resolving packages... 4success Already up-to-date. 5Done in 1.12s. 6I, [2017-10-10T08:36:05.289046 #5] INFO -- : Writing /myapp/public/assets/bg-b811dc3d5d616bfc91c7be908927f5797db3878ef5eed28e0277ed40f5c2a9ba.png 7 : 8 : 9 : 10I, [2017-10-10T08:36:14.803537 #5] INFO -- : Writing /myapp/public/assets/express/lib/application-489ef282d160b38a75de19f711472bb48c4eca65cecd6e1ed83fceba74dcee35.js.gz 11Webpacker is installed 🎉 🍰 12Using /myapp/config/webpacker.yml file for setting up webpack paths 13Compiling… 14Compiled all packs in /myapp/public/packs 157.71user 2.51system 0:33.19elapsed 30%CPU (0avgtext+0avgdata 125336maxresident)k 160inputs+0outputs (1major+177016minor)pagefaults 0swaps

    ファイルサイズ

    1$ ll -ltrh public/assets/ 2total 6832 3-rw-r--r-- 1 daisuketsuji staff 12K Oct 8 17:17 application-b190eef5e6c0c69209dc27227adc2168bfe95ae722ed7cc4a011fb96bd1b11ee.css.gz 4-rw-r--r-- 1 daisuketsuji staff 60K Oct 8 17:17 application-b190eef5e6c0c69209dc27227adc2168bfe95ae722ed7cc4a011fb96bd1b11ee.css

    処理時間の計測はこうでいいんでしょうか。こういうの詳しくないので誰か教えて・・・。

    移行作業その1 - PostCSSが動くようにする

    webpackerは標準設定があるので、まずそれを把握するためにとりあえずREADMEに目を通します。

    rails/webpacker: Use Webpack to manage app-like JavaScript modules in Rails

    あれ!PostCSS - Auto-Prefixerって書いてあるな・・・。Autoprefixer、デフォルトで有効ってこと・・・?

    追記: cssnextに標準でAutoprefixerが入ってるからです。

    application.cssを作成

    こういうののディレクトリ構成はデファクトみたいなのがあると思うんですが、僕はこうしました。

    1. 2├── app 3│   ├── assets 4│   ├── helpers 5│   └── javascript 6│       ├── packs 7│       └── stylesheets

    application.jsはこう。

    1import '../stylesheets/application'

    CSSの中身は移行したものではなく、PostCSSを使っていない暫定の記述だけです。とりあえず疎通が見たかったので。

    HTMLからの呼び出しはstylesheet_pack_tagを使えばいけるっぽいんですが、この記事に書いてもしょうがない作業なんで割愛します。

    PostCSSがちゃんと動くかを見る

    application.cssの中身をPostCSSぽくして、ちゃんと動くかを見てみたいので、 まず、webpackerのCSSに対するローダーがデフォルトでどのようになっているかを確認してみます。

    設定はここですかね。

    webpacker/style.js at master · rails/webpacker

    逆から読むんでしたっけ?これは以下の順番で処理してくれるということなんでしょうか。webpackの設定ファイルの見方がいまいち・・・。

    • sass-loader
    • postcss-loader
    • css-loader

    あれ、sassも読めるのかな・・・。まあどちらにせよ、PostCSSのローダーは入っているようなのでこのまま行ってみましょう。

    PostCSSの設定はルートに.postcssrc.ymlを置けばいいみたいですね。

    1const postcssConfigPath = path.resolve(process.cwd(), '.postcssrc.yml')

    webpackerのインストール直後はこんな感じで配置されるようです。

    1plugins: 2 postcss-smart-import: {} 3 postcss-cssnext: {}

    smart-importが入ってるので、importが使えるかを見てみます。

    app/javascript/stylesheets/modulesディレクトリを作って、test.cssを作ってみます。

    1.teststyle { 2 color: #eceff1; 3}

    application.cssはこんな感じ。

    1@import "modules/test";

    bin/webpackを実行すると・・・、

    1$ cat public/packs/application-3cb0874e28a4e33e875112e4732c6ddd.css 2.teststyle { 3 color: #eceff1; 4}

    行けてそう!

    移行作業その2 - とりあえずCSSを移行して処理してみる

    とりあえずどかっとファイル移動しただけでどうなるかを見てみます。SCSSの拡張子もそのままです。

    1ERROR in ./node_modules/css-loader?{"minimize":false}!./node_modules/postcss-loader/lib?{"sourceMap":true,"config":{"path":"/myapp/.postcssrc.yml"}}!./node_modules/resolve-url-loader!./node_modules/sass-loader/lib/loader.js?{"sourceMap":true}!./app/javascript/stylesheets/application.scss 2Module build failed: 3@import "modules/*";

    あー、まあエラーしたので、章をわけて1個ずつ対応します。

    smart-importでglob展開はできない

    globでのimportは出来ないんですね。過去にpostcss-importプラグインがサポートしていたようですが、外されたようです。

    Remove glob support · postcss/postcss-import@1fbeca6

    postcss-easy-importというプラグインでできるようですが、まあここで増やすのもな・・・という気もするので、1個1個importするように書き換えました。

    TrySound/postcss-easy-import: PostCSS plugin to inline @import rules content with extra features

    この対応だけしたら、普通にbin/webpackは通るようになりました。

    FontAwesomeを読めるようにする

    これまではGemで読んでたので、ダイジェストもいい感じにやってくれてました。 webpacker経由でうまいことダイジェストも解決する方法を知らないので、ここで解決しちゃいます。

    1npm i font-awesome --save

    application.cssには

    1@import "~font-awesome/css/font-awesome";

    CSSだとフォントファイルにダイジェストがつかなかったりするかな?と思ったら、なんか読み込むだけでうまくいきました・・・。

    1$ head -n 20 public/packs/application-b619b7134a47c7bc1d3df67c6b135f76.css 2@charset "UTF-8"; 3 4/*! 5 * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 6 * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 7 */ 8 9/* FONT PATH 10 * -------------------------- */ 11 12@font-face { 13 font-family: 'FontAwesome'; 14 src: url(/packs/_/_/node_modules/font-awesome/fonts/fontawesome-webfont-674f50d287a8c48dc19ba404d20fe713.eot); 15 src: url(/packs/_/_/node_modules/font-awesome/fonts/fontawesome-webfont-674f50d287a8c48dc19ba404d20fe713.eot) format("embedded-opentype"), url(/packs/_/_/node_modules/font-awesome/fonts/fontawesome-webfont-af7ae505a9eed503f8b8e6982036873e.woff2) format("woff2"), url(/packs/_/_/node_modules/font-awesome/fonts/fontawesome-webfont-fee66e712a8a08eef5805a46892932ad.woff) format("woff"), url(/packs/_/_/node_modules/font-awesome/fonts/fontawesome-webfont-b06871f281fee6b241d60582ae9369b9.ttf) format("truetype"), url(/packs/_/_/node_modules/font-awesome/fonts/fontawesome-webfont-912ec66d7572ff821749319396470bde.svg) format("svg"); 16 font-weight: normal; 17 font-style: normal; 18} 19 20.fa { 21 display: inline-block;

    これはなんかのローダーがやってくれてるんでしょうね。ちょっとその辺は追ってませんが、すごいね・・・。

    移行間作業 - 出力されるファイルサイズと処理時間を計測

    とりあえずビルドは通ったので、ここでちょっと計測してみます。 変数展開とかが全然できてないはずなので、まだ完成じゃないですが・・・。

    ビルド

    1$ time rails assets:precompile RAILS_ENV=production 2yarn install v1.2.0 3[1/4] Resolving packages... 4success Already up-to-date. 5Done in 1.06s. 6I, [2017-10-10T08:45:22.160159 #6] INFO -- : Writing /myapp/public/assets/bg-b811dc3d5d616bfc91c7be908927f5797db3878ef5eed28e0277ed40f5c2a9ba.png 7 : 8 : 9 : 10I, [2017-10-10T08:45:32.132392 #6] INFO -- : Writing /myapp/public/assets/express/lib/application-489ef282d160b38a75de19f711472bb48c4eca65cecd6e1ed83fceba74dcee35.js.gz 11Webpacker is installed 🎉 🍰 12Using /myapp/config/webpacker.yml file for setting up webpack paths 13Compiling… 14Compiled all packs in /myapp/public/packs 1519.57user 11.70system 2:20.05elapsed 22%CPU (0avgtext+0avgdata 238252maxresident)k 160inputs+0outputs (16major+276827minor)pagefaults 0swaps

    ファイルサイズ

    1$ ll -ltrh public/packs/ 2total 296 3-rw-r--r-- 1 daisuketsuji staff 125B Oct 8 17:47 application-d90f6dc35a6c073d4a45066d53e72dd6.css.map 4-rw-r--r-- 1 daisuketsuji staff 18K Oct 8 17:47 application-d90f6dc35a6c073d4a45066d53e72dd6.css.gz 5-rw-r--r-- 1 daisuketsuji staff 102K Oct 8 17:47 application-d90f6dc35a6c073d4a45066d53e72dd6.css 6drwxr-xr-x 3 daisuketsuji staff 102B Oct 8 17:47 _/

    比較

    before   7.71user 2.51system 0:33.19elapsed 30%CPU (0avgtext+0avgdata 125336maxresident)k
    after  19.57user 11.70system 2:20.05elapsed 22%CPU (0avgtext+0avgdata 238252maxresident)k
    

    あれー!致命的なほど遅くなった!source mapも出しちゃってるからかな・・・。

    移行作業その3 - webpackerの設定をちゃんとする & 足りないプラグインを入れる

    このままでは失敗に終わってしまう!足りないプラグインを足しながら設定を見直します。

    全然知識がないのでとりあえず調べてみる。

    build performance

    devtool: "eval" has the best performance, but it only maps to compiled source code per module. In many cases this is good enough. (Hint: combine it with output.pathinfo: true.)

    ふむふむ。source mapは一旦いっか。

    source mapを出さないようにしてみる

    1$ time rails assets:precompile RAILS_ENV=production 2yarn install v1.2.0 3[1/4] Resolving packages... 4success Already up-to-date. 5Done in 0.92s. 6I, [2017-10-10T12:34:02.467861 #6] INFO -- : Writing /myapp/public/assets/bg-b811dc3d5d616bfc91c7be908927f5797db3878ef5eed28e0277ed40f5c2a9ba.png 7 : 8 : 9 : 10I, [2017-10-10T12:34:12.881463 #6] INFO -- : Writing /myapp/public/assets/express/lib/application-489ef282d160b38a75de19f711472bb48c4eca65cecd6e1ed83fceba74dcee35.js.gz 11Webpacker is installed 🎉 🍰 12Using /myapp/config/webpacker.yml file for setting up webpack paths 13Compiling… 14Compiled all packs in /myapp/public/packs 1521.31user 10.99system 2:34.00elapsed 20%CPU (0avgtext+0avgdata 216200maxresident)k 160inputs+0outputs (16major+279629minor)pagefaults 0swaps

    む、むしろ遅くなった・・・。

    SCSSをやめる

    source mapの設定はいったんデフォルトに戻して、違う原因を探します。

    application.scssを削っていったら改善されたので、どうやら@importの数か、純粋に処理してるファイル量に応じて遅くなっているらしい・・・。 SCSSをそのまま移行したのがだめだったのだろうか?

    1$ bin/webpack 2Hash: 3a5ad03cf822d8759fb0 3Version: webpack 3.6.0 4Time: 90548ms

    90548ms!

    とりあえずSCSSをCSSに変えてみよう。 拡張子を変えて、処理できるようにいくつかプラグインを追加します。

    1npm i postcss-simple-vars --save // 変数宣言を$でやれるようにする 2npm i postcss-mixins --save // mixin機能 3npm i postcss-extend --save // extend機能 4npm i postcss-nested --save // ネスト記法

    .postcssrc.ymlにも。

    1plugins: 2 postcss-smart-import: {} 3 postcss-mixins: {} 4 postcss-extend: {} 5 postcss-nested: {} 6 postcss-simple-vars: {}

    .postcssrc.ymlは記述の順番がシビアみたいです。とりあえず上の状態では動きました・・・。

    mixinについてはちょっと記述の修正が必要でした。

    1@define-mixin mixin-name $arg1: 1rem { 2 size: $arg1; 3} 4 5.test-class { 6 @mixin mixin-name 2rem; 7}

    これで実行してみると・・・。

    1$ time bin/webpack RAILS_ENV=production 2Hash: efdeaedf276a411fa522 3Version: webpack 3.6.0 4Time: 9356ms

    お!10分の1に!

    assets:precompileは・・・。

    1$ time rails assets:precompile RAILS_ENV=production 2yarn install v1.2.0 3[1/4] Resolving packages... 4success Already up-to-date. 5Done in 0.91s. 6I, [2017-10-11T01:19:52.825045 #5] INFO -- : Writing /myapp/public/assets/bg-b811dc3d5d616bfc91c7be908927f5797db3878ef5eed28e0277ed40f5c2a9ba.png 7 : 8 : 9 : 10I, [2017-10-11T01:19:53.612881 #5] INFO -- : Writing /myapp/public/assets/application-e7c2d6880942eb7309f8eb703c4db21e2d3a26d5b1f7a3895fb5593afe618704.js.gz 11Webpacker is installed 🎉 🍰 12Using /myapp/config/webpacker.yml file for setting up webpack paths 13Compiling… 14Compiled all packs in /myapp/public/packs 1513.00user 4.80system 0:58.08elapsed 30%CPU (0avgtext+0avgdata 222560maxresident)k 160inputs+0outputs (17major+259560minor)pagefaults 0swaps

    比較

    before   7.71user 2.51system 0:33.19elapsed 30%CPU (0avgtext+0avgdata 125336maxresident)k
    after   13.00user 4.80system 0:58.08elapsed 30%CPU (0avgtext+0avgdata 222560maxresident)k
    

    まだ移行前より遅いけど、だいぶよくなった!もうちょっと頑張ればこえられるかな・・・。一旦は許容範囲ということにしましょう。

    ちなみにsource mapを出さないようにしてみると・・・?

    1$ time rails assets:precompile RAILS_ENV=production 2yarn install v1.2.0 3[1/4] Resolving packages... 4success Already up-to-date. 5Done in 0.89s. 6I, [2017-10-11T01:25:08.843047 #5] INFO -- : Writing /myapp/public/assets/bg-b811dc3d5d616bfc91c7be908927f5797db3878ef5eed28e0277ed40f5c2a9ba.png 7 : 8 : 9 : 10I, [2017-10-11T01:25:09.646634 #5] INFO -- : Writing /myapp/public/assets/application-e7c2d6880942eb7309f8eb703c4db21e2d3a26d5b1f7a3895fb5593afe618704.js.gz 11Webpacker is installed 🎉 🍰 12Using /myapp/config/webpacker.yml file for setting up webpack paths 13Compiling… 14Compiled all packs in /myapp/public/packs 1513.90user 4.23system 0:58.81elapsed 30%CPU (0avgtext+0avgdata 228544maxresident)k 160inputs+0outputs (17major+258170minor)pagefaults 0swaps

    全然はやくならん!やり方違うのかな・・・。

    ファイルサイズはかわってなさげ。これはいいね。

    1-rw-r--r-- 1 daisuketsuji staff 12K Oct 9 10:26 application-c22dd87ef356a17cda53b66b25a442e0.css.gz 2-rw-r--r-- 1 daisuketsuji staff 60K Oct 9 10:26 application-c22dd87ef356a17cda53b66b25a442e0.css

    他に参考にした資料。

    Optimising build performance, initial: 40s, incremental: 6s · Issue #1574 · webpack/webpack

    移行作業その4 - 開発環境でのスタイルシートの更新監視

    webpackでCSSを処理するようになったので、これまでのようにrails sしてれば一緒に処理される〜ってことがなくなりました。

    (追記 : これ、処理されるみたいです。ただ常駐しないので毎回webpackが立ち上がって・・・って感じの挙動で遅かったです。)

    bin/webpack-dev-serverrails sと並行して起動する必要があるので、公式も推奨しているForemanを使ってプロセス管理をします。

    開発環境はシンプルに保ちたいので、この工程が入るなら導入しなくてもいいかなと思うくらいのハードルでした・・・。

    webpacker/env.md at master · rails/webpacker

    とりあえずProcfileを作ります。ローカル用に分けたかったので、Procfile.localにしました。

    1rails: bundle exec rails s -p 3005 -b '0.0.0.0' 2webpack: bin/webpack-dev-server

    Foreman自体も入れましょう。

    1group :development do 2 : 3 : 4 gem 'foreman' 5end 6

    あとは実行するだけ。

    1foreman start -f Procfile.local

    移行後作業 - プラグインの設定とめぼしいプラグインを入れる

    さて、ここらへんで今後の運用も考えて、PostCSSにはどんなプラグインがあって、どんなのを入れたいかを考えてみようと思います。 導入しないにしても、知っておく必要はあると思うので。

    とりあえず移行した!だけだとただのやってみた記事になるし、そもそももったいないので。

    以下から探せるんですが、

    PostCSS.parts | A searchable catalog of PostCSS plugins

    一覧性が悪かったので、以下をざーっと眺めてみました。

    postcss/plugins.md at master · postcss/postcss

    CSS4のやつとかは見てて面白いですね。

    Autoprefixer

    まずは目的の1つであったAutoprefixerから。

    npm i autoprefixer --save
    

    なんか、どのブラウザを対象にするかのジャッジがちょっとむずかしいらしいですね。 デフォルトだと結構バサッと切り捨てるようなので、一旦そのままにしてあとでちゃんとドキュメントを読んでみようと思います。

    hexrgba

    これ、僕普段すごく使うので入れなきゃいけませんでした。忘れてた。

    seaneking/postcss-hexrgba: PostCSS plugin that adds shorthand hex methods to rgba() values

    rgba(0, 0, 0, .5)みたいなのを、rgba(#000, .5)みたいに書けるようにするやつ。

    npm i postcss-hexrgba --save
    

    lazyimagecss

    画像のサイズを自動的にwidth、heightに設定してくれるもの。これはすごいですね!

    Jeff2Ma/postcss-lazyimagecss: A PostCSS plugin that generates images's CSS width & height properties automatically.

    導入はしなかったけど覚えておこう。

    移行後作業 - stylelintを入れる

    stylelint

    これはどちらにしろ入れる方針だったので、最後にやっちゃいます。

    1npm i stylelint --save

    なんかCLIでも動くようにできるみたいですが、せっかくだしPostCSSの処理に組み込んじゃいます。

    僕はCSSのlintはsmacssのソート順になっていることだけをチェックできてれば満足なので、以下の設定を使います。

    cahamilton/stylelint-config-property-sort-order-smacss: Stylelint config for Property Sort Ordering based on the SMACSS methodology

    1npm i stylelint-config-property-sort-order-smacss --save

    .stylelintrc.ymlはこんな。

    1extends: stylelint-config-property-sort-order-smacss

    stylefmtとやらを使えばauto correctもできるみたいなので、今度やってみよう。

    まとめ

    長くなったので整理します。

    移行前後の処理時間

    約30秒も遅くなっちゃいました。悔しいけど妥協します。

    17.71user 2.51system 0:33.19elapsed 30%CPU (0avgtext+0avgdata 125336maxresident)k 213.67user 7.65system 1:00.65elapsed 35%CPU (0avgtext+0avgdata 226164maxresident)k

    移行前後のファイルサイズ

    これはAutoprefixerも入った状態のものです。1KB増えましたね。許容範囲です。

    1-rw-r--r-- 1 daisuketsuji staff 60K Oct 8 17:17 application-b190eef5e6c0c69209dc27227adc2168bfe95ae722ed7cc4a011fb96bd1b11ee.css 2-rw-r--r-- 1 daisuketsuji staff 61K Oct 9 14:43 application-a390b9b8bda3b3036f4273c37d529aba.css

    最終的なpackage.json

    1{ 2 "name": "myapp", 3 "private": true, 4 "dependencies": { 5 "@rails/webpacker": "^3.0.2", 6 "autoprefixer": "^7.1.5", 7 "font-awesome": "^4.7.0", 8 "postcss-extend": "^1.0.5", 9 "postcss-hexrgba": "^1.0.0", 10 "postcss-mixins": "^6.1.1", 11 "postcss-nested": "^2.1.2", 12 "postcss-simple-vars": "^4.1.0", 13 "stylelint": "^8.2.0", 14 "stylelint-config-property-sort-order-smacss": "^2.0.0" 15 }, 16 "devDependencies": { 17 "webpack-dev-server": "^2.9.1" 18 } 19}

    最終的な.postcssrc.yml

    1plugins: 2 postcss-smart-import: {} 3 postcss-mixins: {} 4 postcss-nested: {} 5 postcss-extend: {} 6 postcss-simple-vars: {} 7 postcss-hexrgba: {} 8 autoprefixer: {}

    stylelintはこのプロセスに入れちゃうとnode_modulesが処理されちゃって除外の仕方がわからなかったので、CLIでやるようにしました・・。

    エディタ上では効くので、まあCIで弾ければいいかなと。

    書かなかったけど詰まったところ

    • @import "any/styles.css"のように拡張子をつけないと変数周りの処理がうまくいかなかった(css以外の処理が入っちゃう?)
    • @define-mixin、@mixinの引数には括弧を付けちゃだめみたい(エラーしてめちゃくちゃ詰まった!)
    • postcss-extendではプレースホルダーセレクタの中でカレント(&)が参照できなかったので、mixinにしました
    • 画像を参照したいときは、url(asset_path())で持ってきたところを普通にurl()にすればみれるようになる。パスは~images/xxx.jpgみたいなかんじ。

    今後

    せっかくなので、JSと画像系アセットもwebpack側に寄せようかなと。 そしたらAsset Pipelineがいらなくなるので、少しは早くなるかな・・・。