Rails7にあげるためにWebpackerを剥がしてみた

こんにちは、エージェンシー事業で開発業務を担当しているリードアプリケーションエンジニアの花田です。
娘が生まれて4ヶ月となり、笑顔が増えてきて毎日癒やされながら仕事をしています。
今回はWebpackerをやめるまでに発生した出来事を書いていこうと思います。

背景

担当サービスのRailsを6 → 7に上げたく調査を行ったところ、Rails7ではWebpackerが廃止されることが分かりました。
元々Webpackerがよしなにやりすぎていて良い印象ではなかったので、これを機にWebpackerを削除することにしました。

システム紹介

担当サービスで使用している言語やフレームワーク等のバージョン一覧です。
フロント周りのバージョンが低いですね。。

また、このサービスでは、RailsがViewを持っておりフロントと密結合になっています。

Webpackerとは?

Webpackerは、汎用的なwebpackビルドシステムのRailsラッパーであり、標準的なwebpackの設定と合理的なデフォルト設定を提供します。

<出典:Railsガイド 1 Webpackerとは >

Webpackerはgemパッケージの1つで、Railsでwebpackを利用する時はとりあえずWebpackerを入れておけば手軽に実装できます。

webpackとは?

webpackなどのフロントエンドビルドシステムの目的は、開発者にとって使いやすい方法でフロントエンドのコードを書き、そのコードをブラウザで利用しやすい方法でパッケージ化することです。
webpackは「JavaScript」「CSS」「画像やフォント」といった静的アセットを管理できます。
webpackを使うと、「JavaScriptコードの記述」「アプリケーション内の他のコードの参照」「コードの変換(トランスパイル)や結合」をダウンロードしやすいpackにまとめられます。

<出典:Railsガイド 1.1 Webpackとは >

webpackはNode.jsのモジュールの1つで、CSS、JavaScript、画像などを1つのファイルとしてまとめるためのモジュールバンドラーです。

Webpackerを削除するには?

Webpackerの後継であるShakapackerを利用するか、Rails7が推薦しているjsbundling-railsとwebpackを利用すればよいみたいです。
ShakapackerはWebpackerイズムを継承しているので不採用にしました。
なので今回はjsbundling-railsとwebpackを用いてWebpackerを削除したいと思います。

Webpackerからjsbundling-rails + webpackに移行する方法

jsbundling-railsのGitHubにバッチリ移行手順が書いてあります。
Switch from Webpacker 5 to jsbundling-rails with webpack
この通り行えば移行は簡単です。

とはならなかったのでエラーになった箇所をつらつら書き出したいと思います。
作業はdevelopment環境で動作確認を行ったあと、staging環境でも動作確認を行いました。
またwebpackのバージョンも3系と古かったので一気に5系まで上げて作業を行っています。

詰まった箇所(webpack編)

まずはGitHubの移行手順を元になんとなく設定を行いました。
以下のコマンドでテストが行えるので、エラーを出力して潰していく方法で作業を進めています。

$ yarn build --progress --color

Module not found: Error: Can't resolve 'CCC/DDD' in '/var/www/web/app/javascript/Views/AAAA/BBBB'

これはwebpackがbuildした際にVue.jsのimportがうまくできないときのエラーです。

エラーの対象コード

import DDD from 'CCC/DDD'

Webpackerの設定ファイルには参照パスを指定していたので、同様にwebpackにも設定する必要があります。

config/webpacker.yml

default: &default
  source_path: app/javascript

webpackの設定では以下のようにしました。

config/webpack.config.js

  resolve: {
    alias: {
      'javascript': path.resolve(__dirname, '..', 'app/javascript'),
      'vue$': 'vue/dist/vue.esm.js'
    },
    extensions: ['.js', '.vue', '.json', '.scss', '.css', '.svg'],
  }

Webpackerのように参照パスの指定を行えずaliasを設定しないといけません。
この影響でVue.jsのimport箇所全てに「javascript」と追加する必要がありましたので、以下のような修正を行いました。

修正前

import HogeComponent from `path/to/HogeComponent`

修正後

import HogeComponent from `javascript/path/to/HogeComponent`

Module not found: Error: Can't resolve 'css-loader' in '/var/www/web/app/javascript/Views/AAAA/DDD'

これはcss-loaderが必要なので必要なパッケージを一緒にインストールしました。

$ yarn add css-loader sass sass-loader mini-css-extract-plugin webpack-remove-empty-scripts

Error loading PostCSS config: Loading PostCSS Plugin failed: Cannot find module 'postcss-import'

これはpostcss-importが無いときのエラーでpostcss-importをインストールしました。

$ yarn add postcss-import

Error loading PostCSS config: Invalid PostCSS Plugin found: [0]

またしてもpostcssのエラーが発生しました。 しかもエラー情報が少なく原因がすぐにはわかりませんでした。。。
調査していくとこちらに似たような現象を発見し、 原因はどうもpostcssのバージョンが8だと駄目みたいです。
postcss-importの最新をインストールすると依存関係でpostcss8が入るので、postcss-import15 → postcss-import13に下げることでエラーを解消しました。

Error loading PostCSS config: Loading PostCSS Plugin failed: Cannot find module 'postcss-cssnext'

こちらもpostcss-cssnextが無いときのエラーなのでpostcss-cssnextをインストールしました。

$ yarn add postcss-cssnext

Module build failed (from ./node_modules/css-loader/dist/cjs.js): ValidationError: Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.

なんとも分かりづらいエラーなのですがsass-loaderが原因です。
エラーにはcss-loaderって書いてあるのでかなり詰まりました。。
sass-loader13 → sass-loader7.1.0に変更することでエラーを解消しました。

$ yarn add sass-loader@7.1.0

7.1.0というバージョンが重要で、sass-loader7の最新ではエラーが発生します。

Error: Cannot find module 'node-sass'

こちらもnode-sassが無いときのエラーなのでnode-sassをインストールしました。

$ yarn add node-sass

Module build failed (from ./node_modules/sass-loader/lib/loader.js): Error: Node Sass version 7.0.3 is incompatible with ^4.0.0.

色々調べるとnode-sassのバージョンが原因でした。
Node.js14だとnode-sass4系しか対応しておらずエラーになります。
なのでnode-sassのバージョンを下げました。

$ yarn add node-sass@4.14.1

Module build failed (from ./node_modules/css-loader/dist/cjs.js): ValidationError: Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.

また同じエラーが発生しました。。。
これはエラーの通りcss-loaderが原因で、css-loader6系 → 5系にバージョンを下げたらエラーがなくなりました。
前に発生した際はcss-loaderのバージョンを下げても解決しなかったので、sass-loaderをバージョンを下げてからcss-loaderを下げる必要がありそうです。

ERROR in ./app/javascript/img/AAA.svg 1:0 Module parse failed: Unexpected token (1:0)

これはsvgが読み込めないのが原因です。
Webpack5からfile-loaderは非推奨なのですが、エラー解決のためにパッケージをインストールしました。

$ yarn add file-loader

webpackのbuild成功

上記のエラーを全て解決したらテストが通りました。
元々Node.jsやwebpackのバージョンが古いのでバージョン関連のエラーに悩まされました。
フロントのバージョンが新しい場合は、もしかしたら簡単にテストを通すことができるかもしれません。

詰まった箇所(Rails, Vue.js編)

Railsを起動するとさっそくエラーが出たので解決したことを書いていこうと思います。

The asset "application.css" is not present in the asset pipeline.

RailsがCSSを読み込めないエラーです。
gemが足りなかったので以下を追加しました。

$ vim Gemfile

cssbundling-rails

しかしこれだけではエラーが解決できず、webpackの設定ファイルを変更することで解決しました。

  test: /\.(png|jpe?g|gif|eot|woff2|woff|ttf|svg)$/i,
  use: [
    {
      loader: 'file-loader',
      options: {
        esModule: false,
        name: '[name].[ext]'
      }
    }
  ]

esModule: falseが重要でES ModulesではなくCommonJSにする必要がありました。
フロントのバージョンが古いので、もしかしたらバージョンが新しいとエラーが出ない可能性がありそうです。

function (a, b, c, d) { return createElement$1(vm, a, b, c, d, true); }

Railsを起動すると画面が真っ白で何も表示されず、ブラウザのデバッグツールに上記のエラーが表示されていました。
これはVue.jsのコンポーネントをうまく呼び出せないのが原因です。
webpackのバージョンを上げた影響なのか、以下のコードだと読み込めませんでした。

修正前

<template lang='pug'/>

修正後

<template lang='pug'>

Rails起動に成功

上記のエラーを解決したら無事管理画面が表示されました。
あとはstaging環境で動作確認できれば終わりです。

詰まった箇所(staging環境編)

staging環境では動作確認だけだと思っていたのですが、色々詰まったのでエラー箇所を書き出したいと思います。

The asset "application.js" is not present in the asset pipeline.

staging環境だと何故かJSが読み込めずエラーが発生しました。
Railsの環境ごとに設定が違うので調査を行うと、以下の違いがありました。

development環境

config.assets.compile = true

staging環境

config.assets.compile = false

これはassetsを自動コンパイルするか決める設定で、development環境では動的にassetsが作られます。
なのでstaging環境ではassetsを手動で作って上げればエラーは解決します。

$ RAILS_ENV=staging  bundle exec rails assets:precompile

assetsで作成されたファイルがブラウザで読み込めないエラー

こちらも画面が真っ白になる現象でmanifest.jsが正しく作られていないのが原因でした。
jsbundling-railsで以下のコマンドを実行するとmanifest.jsに記述が追加されます。

$ bundle exec rails javascript:install:webpack

しかしstaging環境にmanifest.jsファイルが存在していなかったので、以下のように新しく作成することで解決しました。

$ vim app/assets/config/manifest.js

//= link_tree ../builds

faviconとロゴが表示されないエラー

JS,CSSは読み込めたのですが、faviconとロゴだけは表示されない現象が起きました。
これはfaviconとロゴがwebpackのbuild対象に存在しないことが原因です。
faviconとロゴは/public以下に起き、nginxのlocale設定をすることで解決しました。

staging環境でRails起動に成功

webpack/assets/Vue.jsの仕組みをなんとなくでしか理解していなかったので色々苦戦しましたが、無事staging環境でもRailsを起動することができました。

今回学んだこと

Webpackerを削除したことで、JS,CSSのプリコンパイルの流れが少しだけ理解できました。
簡単にですが、理解できたことを以下に書いて行こうと思います。

JS,CSSのプリコンパイルの流れ

まずはassetsのプリコンパイルを行います。

bundle exec rails assets:precompile

このときwebpackとSprocketsも動いており、以下のような仕事を行っています。

webpack ・app/javascript/application.jsとapp/javascript/styles/global.scssファイルをバンドルして、app/assets/buildsに格納する
Sprockets ・Sprocketsはapp/assets/config/manifest.js を読み込み対象ファイルを判断する
・app/assets/builds配下のファイルをフィンガープリント(digest)を付与して、public/assets配下に格納する

※development環境ではpublic/assetsにファイルは置かれず、ブラウザにアクセスした時点でコンパイルします。

webpackのバンドル対象と格納場所は設定ファイルで指定でき,以下のように書いています。

バンドル指定箇所

$ cat config/webpack.config.js

module.exports = {
  mode,
  entry: {
    application: [
      './app/javascript/application.js',
      './app/javascript/styles/global.scss',
    ],
  },

格納指定箇所

  output: {
    filename: "[name].js",
    path: path.resolve(__dirname, '..', 'app/assets/builds')
  },

また、今回Sprocketsについて触れていませんがSprockets + webpackでassetsファイルを作成します。

jsbundling-railsについて

jsbundling-rails + webpackに置き換えたものの、jsbundling-railsの役割がいまいち分からなかったので調べてみました。
jsbundling-railsの役割は初期設定の提供とassets作成時にyarnを実行してくれることだと分かりました。

初期設定の提供

bundle exec rails javascript:install:webpack

lib/tasks/jsbundling/build.rake

namespace :javascript do
  desc "Build your JavaScript bundle"
  task :build do
    unless system "yarn install && yarn build"
      raise "jsbundling-rails: Command build failed, ensure yarn is installed and `yarn build` runs without errors"
    end
  end
end

if Rake::Task.task_defined?("assets:precompile")
  Rake::Task["assets:precompile"].enhance(["javascript:build"])
end

if Rake::Task.task_defined?("test:prepare")
  Rake::Task["test:prepare"].enhance(["javascript:build"])
elsif Rake::Task.task_defined?("spec:prepare")
  Rake::Task["spec:prepare"].enhance(["javascript:build"])
elsif Rake::Task.task_defined?("db:test:prepare")
  Rake::Task["db:test:prepare"].enhance(["javascript:build"])
end

jsbundling-railsのREADMEを見ていると理解しづらいので、実際にソースコードを見て理解するのは大切だと思いました。

今後の展望

これでWebpackerを削除することができたので次回はRails7に上げたいと思っています。
実はRailsをAPI専用アプリケーションにできれば、もっと簡単にWebpackerを削除できたと思います。
ただRailsのViewに依存している箇所が多いので、今回は断念してWebpackerの削除に臨みました。
Rails7に上げたあとはRailsをAPI専用アプリケーションにして、フロントとサーバーを完全に分離できるようとしたいと思います。

感想

サーバーサイドをメインに開発していたので、フロント側のエラーにはとても苦戦しました。
知識が浅いので問題の切り分けが難しく、原因の調査にかなり時間を要したと思います。
でもWebpackerの削除を通してassetsやwebpackの事を少しでも理解できたのは良かったです。
このブログが誰かの役に立てると幸いです。