Rails
sprockets
webpack

Rails5.1.3でsassとjsの管理をSprocketsからWebpackerに移行した

追記(2017.12.12)

webpacker 3系に対応した記事 を書きました :muscle:

Webpacker2.0からデフォルトでcss(sass)のビルドも出来るようになってて且つ、digestの付与とかも問題なくいけるっぽいので、現在制作中でSPAではなくまだjavascriptの規模が大きくないアプリのjavascriptとsassをwebpackerに移行してみました。

モチベーションとしてはjavascriptはES6で書きたい+viewライブラリ使いたい、sassはPostCSS使いたいとかよくある感じの奴です。

インストール

Gemfileにwebpackerを追加します。

Gemfile
gem "webpacker", "~> 2.0"

webpackerの設定を追加します。

$ bin/rails webpacker:install

上記コマンドでwebpacker(webpack)の設定とか諸々追加されます。ちょっと特徴的なのがwebpackでコンパイル対象となるディレクトリがapp/javascript/packs以下のファイル全てがentryになるところでしょうか。

構成

私自身が普段基盤システムやDevOps系のことをやっててフロントエンド周りにそこまで詳しくないので、今回は一旦assets以下の構造をapp/javascript以下に移行する形でやってみようと思います。

app/javascript
│
├── javascripts
│   ├── application.js
│   ├── ...
│
├── packs
│   └── application.js
│
└── stylesheets
    ├── application.scss
    ├── ...

packs/application.jsではjavascripts/application.jsstylesheets/application.scssをimportするだけにして、それぞれをこれまでのsprocketsと同じように管理できるようにしてみます。

packs/application.js
import '../javascripts/application';
import '../stylesheets/application';

app/javascript/javascriptsというディレクトリがだいぶアレな感じですが、webpackの設定を変えればいいだけなので一旦動作するところまではそのままでいっちゃいましょう。:innocent:

起動設定

はじめに、簡単に動作確認できるようにforemanでwebpack-dev-serverを起動する設定をやっておきます。

Gemfile
group :development do
  gem "foreman"
  #...
end
Procfile
web: bin/rails s
webpacker: bin/webpack-dev-server
$ bundle install
$ bundle binstubs foreman

これで、 bin/foreman startで開発用のサーバーが立ち上がります。ただ、foreman経由で起動した場合にRailsのポートが5000になるので、http://localhost:5000 でアクセスしましょう。

Sass

まずはじめにsassをwebpacker管理に移行します。app/assets/stylesheetsディレクトリ以下のファイルをまるっとapp/javascript/stylesheetsディレクトリにコピーしちゃいましょう。

次に、webpackのsass-loaderはglobでのimport (@import "layout/*";みたいな奴) に対応していないので、import-glob-loaderとその設定を追加します。

$ yarn add import-glob-loader
config/webpack/loaders/sass.js
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const { env } = require('../configuration.js')

module.exports = {
  test: /\.(scss|sass|css)$/i,
  use: ExtractTextPlugin.extract({
    fallback: 'style-loader',
    use: [
      { loader: 'css-loader', options: { minimize: env.NODE_ENV === 'production' } },
      { loader: 'postcss-loader', options: { sourceMap: true } },
      'resolve-url-loader',
      { loader: 'sass-loader', options: { sourceMap: true } },
      'import-glob-loader', // ここに追加
    ]
  })
}

これで、globでのimportの解決ができるようになります。

次に、app/views/layouts/application.html.hamlに以下の行を追記します。

application.html.haml
    -# = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
    = stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'

後はwebpackerのデフォルト設定でsassやpostcssの設定が幾つか含まれているので、gem系のassetsやimage-urlなどのhelperを使ってなければ多分そのまま動きます。:tada:

sprokectsのsass helperを使っている場合には このへん を参考に泣きながら修正しましょう。

gemでfont-awesomeやbootstrap-sassを使っている場合には若干の変更が必要です。基本的には対応するnpmパッケージをインストールしてパスを書き換えるだけなので、さくっとやっちゃいましょう。

font-awesome

yarnでパッケージを追加します。

$ yarn add font-awesome

次に、sassでのimportを以下に変更します。

@import "~font-awesome/scss/font-awesome";

ちなみに、gem "font-awesome"を使ってる場合、fa_iconなどのhelperはそのまま使い続けても問題ないので無理に外す必要は無いです。

bootstrap-sass

yarnでパッケージを追加します。

$ yarn add bootstrap-sass

次に、sassでのimportを以下に変更します。

$icon-font-path: '~bootstrap-sass/assets/fonts/bootstrap/';
@import '~bootstrap-sass/assets/stylesheets/bootstrap';

あるいは個別にカスタマイズしているのであれば、

$icon-font-path: '~bootstrap-sass/assets/fonts/bootstrap/';

// Bootstrap v3.3.7 (http://getbootstrap.com)

// Core variables and mixins
@import "~bootstrap-sass/assets/stylesheets/bootstrap/variables";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/mixins";

// Reset and dependencies
@import "~bootstrap-sass/assets/stylesheets/bootstrap/normalize";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/print";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/glyphicons";

// Core CSS
@import "~bootstrap-sass/assets/stylesheets/bootstrap/scaffolding";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/type";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/code";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/grid";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/tables";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/forms";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/buttons";

// Components
@import "~bootstrap-sass/assets/stylesheets/bootstrap/component-animations";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/dropdowns";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/button-groups";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/input-groups";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/navs";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/navbar";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/breadcrumbs";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/pagination";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/pager";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/labels";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/badges";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/jumbotron";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/thumbnails";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/alerts";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/progress-bars";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/media";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/list-group";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/panels";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/responsive-embed";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/wells";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/close";

// Components w/ JavaScript
@import "~bootstrap-sass/assets/stylesheets/bootstrap/modals";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/tooltip";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/popovers";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/carousel";

// Utility classes
@import "~bootstrap-sass/assets/stylesheets/bootstrap/utilities";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/responsive-utilities";

その他のパッケージも同じようなノリで書き換えちゃっていきましょう。

javascript (coffeescript)

javascriptの場合はsprocketsのrequireをes6 styleのimportに書き換える必要があるので、ちょっとづつ移行していきます。coffee-loaderもデフォルトで含まれているので、一旦は.coffeeのままコピーしても問題無いです。

app/javascript/javascripts/application.js
// TODO
import './xxxxx.coffee';
import './yyyyy.coffee';

// ...

gem系のassetsに関しても同じようにnpmから同様のバージョンを追加していきます。

bootstrap-sass (+ jQuery)

yarnでパッケージを追加します。

$ yarn add bootstrap-sass # sassの時に追加してたら要らないです
$ yarn add jquery

次に、jsのimportを追加します。

app/javascript/javascripts/application.js
import 'bootstrap-sass';

jQueryに関しては、$jQueryがグローバルにエクスポートされている必要があるので、webpackの設定の方を変更します。

config/webpack/shared.js
// Note: You must restart bin/webpack-dev-server for changes to take effect

//...

  plugins: [
    new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(env))),
    new ExtractTextPlugin(env.NODE_ENV === 'production' ? '[name]-[hash].css' : '[name].css'),
    new ManifestPlugin({
      publicPath: output.publicPath,
      writeToFileEmit: true
    }),
    new webpack.ProvidePlugin({ // ここに追加
      $: 'jquery',
      jQuery: 'jquery'
    })
  ],

rails-ujs + turbolinks

ただ、以下のライブラリは一旦gemそのままで使うことにしたため、こちらはsprocketsと併用しています。

app/assets/javascripts/application.js
//= require rails-ujs
//= require turbolinks

どちらもnpmにパッケージがあるので移行すること自体は問題ないですが、rails-ujsに関してはRailsのバージョン変更の際の考慮漏れになりそうだったのと、turbolinksはredirect_toに幾つかの処理を追加するような感じになっていたので今回はそのままにすることにしました。ただ、これもそのうち全部移行するかもしれません。

最後に、app/views/layouts/application.html.hamlに以下の行を追記します。

application.html.haml
    = javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
    = javascript_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'

終わり

これで、今のところ既存からの変更をあまり意識せずいい感じに動いています。ただ、本番運用してないのでもしかしたらもう少し問題が出るかもしれないのでなんかツッコミあったらよろしくお願いします:pray:

webpackerに関しても、フロントエンドに明るくない側からするとある程度デフォルトで色々入ってるのは結構助かるかなーと。デフォルト設定がイケてないとかあるっぽいですが、まあ別にインストールした後自分で変更しちゃえばいいんじゃないですかね。