Rails5.1.3 + Webpacker 3.1でsassとjsの管理をSprocketsからWebpackerに移行する

https://qiita.com/daichirata/items/1b99519c5436cf98d44b

こちらの、Webpacker 3.1対応版です。

ほとんど差分はないですが、上記の記事からの変更点としては

* webpacker 3系の設定ファイルの書き方に対応
* bootstrap v4.0.0-beta.2 に対応
* vue.jsを使った場合のwebpackのresolve.alias周りの設定
* 実際に開発を進めた後のディレクトリ構成の変遷

となってます。上の記事を既に見ている人は、その辺だけ見てもらう形でも問題無かなと思われます。:ok_hand:

## インストール

Gemfileにwebpackerを追加します。

```rb:Gemfile
gem "webpacker", "~> 3.0"
```

webpackerの設定を追加します。

```shell-session
$ bin/rails webpacker:install
```

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


## 構成

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

最後の方に、開発が進んでfrontendのスタンダードな構成に寄せていった奴も記載しておきました。

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

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

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

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

## 起動設定

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

```rb:Gemfile
group :development do
  gem "foreman"
  #...
end
```

```:Procfile
web: bin/rails s
webpacker: bin/webpack-dev-server
```

```sh
$ 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
```

```js:config/webpack/environment.js
const { environment } = require('@rails/webpacker')

environment.loaders.get('sass').use.push('import-glob-loader')
```

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

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

```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を使っている場合には [このへん](https://github.com/rails/webpacker#link-sprocket-assets) を参考に泣きながら修正しましょう。

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

### font-awesome

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

```
$ yarn add font-awesome
```

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

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

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


### bootstrap

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

```
$ yarn add bootstrap
```

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

```sass
@import 'bootstrap/scss/bootstrap';
```

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

```sass
/*!
 * Bootstrap v4.0.0-beta (https://getbootstrap.com)
 * Copyright 2011-2017 The Bootstrap Authors
 * Copyright 2011-2017 Twitter, Inc.
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 */

@import "~bootstrap/scss/functions";
@import "~bootstrap/scss/variables";
@import "~bootstrap/scss/mixins";
@import "~bootstrap/scss/print";
@import "~bootstrap/scss/reboot";
@import "~bootstrap/scss/type";
@import "~bootstrap/scss/images";
@import "~bootstrap/scss/code";
@import "~bootstrap/scss/grid";
@import "~bootstrap/scss/tables";
@import "~bootstrap/scss/forms";
@import "~bootstrap/scss/buttons";
@import "~bootstrap/scss/transitions";
@import "~bootstrap/scss/dropdown";
@import "~bootstrap/scss/button-group";
@import "~bootstrap/scss/input-group";
@import "~bootstrap/scss/custom-forms";
@import "~bootstrap/scss/nav";
@import "~bootstrap/scss/navbar";
@import "~bootstrap/scss/card";
@import "~bootstrap/scss/breadcrumb";
@import "~bootstrap/scss/pagination";
@import "~bootstrap/scss/badge";
@import "~bootstrap/scss/jumbotron";
@import "~bootstrap/scss/alert";
@import "~bootstrap/scss/progress";
@import "~bootstrap/scss/media";
@import "~bootstrap/scss/list-group";
@import "~bootstrap/scss/close";
@import "~bootstrap/scss/modal";
@import "~bootstrap/scss/tooltip";
@import "~bootstrap/scss/popover";
@import "~bootstrap/scss/carousel";
@import "~bootstrap/scss/utilities";
```

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

## javascript (coffeescript)

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

``` shell
$ yarn add coffeescript
```

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

// ...
```

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

### bootstrap (+ jQuery)

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

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

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

```js:app/javascript/javascripts/application.js
import 'bootstrap';
```

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

```js:config/webpack/environment.js
const { environment } = require('@rails/webpacker')
const webpack = require('webpack')

environment.loaders.get('sass').use.push('import-glob-loader')

environment.plugins.set(
  'Provide',
  new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery',
    jquery: 'jquery',
  })
)

module.exports = environment
```

### Vue.js

私はVueを使っているのですが、vue-cliの `import XX from '@/components';` のように `@` でパスを解決できるのが結構便利なのでその辺も追加しちゃいましょう。

```js:config/webpack/environment.js

// ...

// vue-cliっぽく書きたいですよね
environment.config.merge({
  resolve: {
    alias: {
      '@': 'javascripts',
      vue: 'vue/dist/vue.js',
    }
  }
})

// ...

```

### rails-ujs + turbolinks

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

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

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


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

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

## 現在の構成

前の記事を書いてから結構開発が進み、vue.jsを使っているということもあってvue-cliによせて今現在はこんな感じになってます。

```
app/frontend
│
├── packs
│   └── application.js
│
└── src
    ├── assets
    │   ├── images
    │   └── stylesheets
    ├── components
    │   └── ...
    ├── initializers
    │   └── ...
    ├── lib
    │   └── ...
    ├── main.js
    ├── mixins
    │   └── ...
    └── store
        └── ...
```

```yaml:config/webpacker.yml
# Note: You must restart bin/webpack-dev-server for changes to take effect

default: &default
  source_path: app/frontend
  source_entry_path: packs
  public_output_path: assets/packs
  cache_path: tmp/cache/webpacker

  # Additional paths webpack should lookup modules
  # ['app/assets', 'engine/foo/app/assets']
  resolved_paths: []

  # Reload manifest.json on all requests so we reload latest compiled packs
  cache_manifest: false

  extensions:
    - .coffee
    - .erb
    - .js
    - .jsx
    - .ts
    - .vue
    - .sass
    - .scss
    - .css
    - .png
    - .svg
    - .gif
    - .jpeg
    - .jpg

development:
  <<: *default
  compile: true

  # Reference: https://webpack.js.org/configuration/dev-server/
  dev_server:
    https: false
    host: localhost
    port: 3035
    public: localhost:3035
    hmr: false
    # Inline should be set to true if using HMR
    inline: true
    overlay: true
    compress: true
    disable_host_check: true
    use_local_ip: false
    quiet: false
    headers:
      'Access-Control-Allow-Origin': '*'
    watch_options:
      ignored: /node_modules/


test:
  <<: *default
  compile: true

  # Compile test packs to a separate directory
  public_output_path: packs-test

production:
  <<: *default

  # Production depends on precompilation of packs prior to booting for performance.
  compile: false

  # Cache manifest.json for performance
  cache_manifest: true
```


## 終わり

前の記事とあまり変わらないですが、webpackerの設定周りは出力されるファイルがガラッと変わっているのでその辺りで結構修正が必要なので頑張ってください :muscle: