Rails環境でJS , CSSをwebpackで完全に管理する

Rails環境でJS , CSSをwebpackで完全に管理する:


追記

Viewファイル内でJSとCSSを読み込んでいたのをテンプレートファイル内で読み込むように変更。


なぜ人類は'webpacker'ではなく'webpack'でJS, CSSを管理すべきか?

Railsのバージョン5.1から登場したwebpackerでは、ReactやVueといったAltJSを簡単に導入することが可能になりました。

しかし、実際に導入・運用してみるとwebpackerの学習コストやwebpackのバージョンアップについていけないことが辛いといった経験が出てきます。

Railsにはサーバーサイドのロジックに集中してもらい、フロントエンドのことはwebpackに任せましょう。


前提

  • 脱Turbolinks(Ajaxを利用して画面遷移してくれるが、不必要なことが多い、Vueとの連携がうまくいかないので削除する。)
  • 脱Sprocket(webpackでアクション毎にバンドルしたファイルを各View内で読み込む。他のページのJSやCSSが邪魔をしない。)
  • 脱Webpacker
  • Vueを使う(単一ファイルコンポーネント)
  • webpack-dev-serverのHMRを利用してサクサク開発する


手順


脱Turbolinks

Vueと相性が悪く、turbolinks:loadedイベントを監視しないといけません。
Misocaさんのブログが参考になりました。

Vueと一緒に使うことも不可能では無いようですが、開発のしやすさを優先して削除します。
$ bundle exec gem uninstall turbolinks

Gemfileからgem 'turbolinks'の記述を削除
$ bundle install

テンプレートファイルの以下から、data-turbolinks-trackを削除する。

削除前

= stylesheet_link_tag "application", media: "all", "data-turbolinks-track": true 
= javascript_include_tag "application", "data-turbolinks-track": true 
削除後

= stylesheet_link_tag "application", media: "all" 
= javascript_include_tag "application" 
assets内のapplication.jsから以下の一行を削除する。

//= require jquery 
//= require jquery_ujs 
//= require turbolinks <- 削除 
//= require_tree . 


脱Sprockets

今回はSprocketsのアセットパイプラインによるファイルの配信を行わず、webpackでビルドしたjsとcssをViewファイル内で読み込ませます。

その為に思い切ってapp/asssets/ディレクトリを削除します。


脱webpacker

ここからはこちらのブログが大変参考になりました。

Gemfileからgem webpackerを削除してbundle install
yarn remove @rails/webpackerを実行

以下のファイルを削除

  • bin/webpack
  • bin/webpack-dev-server
  • config/webpack/development.js
  • config/webpack/environment.js
  • config/webpack/loaders/vue.js
  • config/webpack/production.js
  • config/webpack/test.js
  • config/webpacker.yml
config/environment/development.rbproduction.rbからconfig.webpacker.check_yarn_integrityを削除


ディレクトリ構成

ルートディレクトリ直下にfrontendディレクトリを作り、フロントエンドに関してのコードは基本的にこの中で管理します。

※ここで関係のないディレクトリについては省いて説明しています。

- frontend // フロントエンド開発用ディレクトリ 
  - pages // ページ全体のコンポーネント 
    - root // コントローラ名 
      - rootIndex.js // 名前を一意にする為に(コントローラ名)+(アクション名)にする。ページコンポーネントを登録する。 
      - index.vue // `root/index`ページのファイル 
      - rootShow.js 
      - show.vue // `root/show`ページのファイル 
  - components 
    - app.vue 
  - images 
    - image.jpg 
- public 
  - assets // 以下にバンドルファイルを出力する。実際にはファイル名にハッシュがついている。 
    - manifest.json 
    - javascripts 
      - rootIndex.js 
      - rootShow.js 
    - stylesheets 
      - rootIndex.css 
      - rootShow.css 
    - images 
      - image.jpg 
frontendディレクトリでフロントエンドに関する開発を行い、public/assets/以下にwebpackでバンドルしたファイルを吐き出します。

今回はページ全体をvueによってコンポーネントとし(便宜上ページコンポーネントと呼びます)、jsによってそれを登録します。


Vue導入

単一ファイルコンポーネントについて

コンポーネントを1つのファイルで構成する単一ファイルコンポーネントを利用します。

ここではプリセットはPug, Sass(scss)を利用します。

他のプリセットを利用する場合は適宜、loader等を読み替えてください。

Vueを導入します。

$ yarn add vue --save 


webpack導入

webpack関連

$ yarn add webpack webpack-cli -D 
JavaScriptをトランスパイルするBabel関連

$ yarn add @babel/core @babel/polyfill @babel/preset-env babel-loader -D 
Vueファイル内で使用する言語のloader等

$ yarn add css-loader file-loader mini-css-extract-plugin pug pug-plain-loader sass-loader vue-loader vue-style-loader vue-template-compiler webpack-manifest-plugin -D 
mini-css-extract-pluginは単一ファイルコンポーネント内に記述したCSSを.jsファイルではなく.cssファイルに抽出するためのものです。

webpack@4ではextract-text-webpack-pluginではなくmini-css-extract-pluginを使わなければいけません。

ここに気づかず、古い記事を参考にしたりしてかなりの時間ハマってしまいました。

vueのwebpackでの設定方法についてはvue-loaderの公式を読む必要があります。

package.jsonに追記

package.json
"scripts": { 
  "build:dev": "webpack --progress --mode=development", 
  "build:pro": "webpack --progress --mode=production" 
}, 


webpackの設定

webpack.config.js
const path = require('path') 
const glob = require('glob') 
const webpack = require('webpack') 
const MiniCssExtractPlugin = require('mini-css-extract-plugin') 
const VueLoaderPlugin = require('vue-loader/lib/plugin') 
const ManifestPlugin = require('webpack-manifest-plugin') 
 
let entries = {} 
glob.sync('./frontend/pages/**/*.js').map(function(file) { 
  let name = file.split('/')[4].split('.')[0] 
  entries[name] = file 
}) 
 
module.exports = (env, argv) => { 
  const IS_DEV = argv.mode === 'development' 
 
  return { 
    entry: entries, 
    devtool: IS_DEV ? 'source-map' : 'none', 
    output: { 
      filename: 'javascripts/[name]-[hash].js', 
      path: path.resolve(__dirname, 'public/assets') 
    }, 
    plugins: [ 
      new VueLoaderPlugin(), 
      new MiniCssExtractPlugin({ 
        filename: 'stylesheets/[name]-[hash].css' 
      }), 
      new webpack.HotModuleReplacementPlugin(), 
      new ManifestPlugin({ 
        writeToFileEmit: true 
      }) 
    ], 
    module: { 
      rules: [ 
        { 
          test: /\.js$/, 
          exclude: /node_modules/, 
          loader: 'babel-loader', 
          options: { 
            presets: [ 
              [ 
                '@babel/preset-env', 
                { 
                  targets: { 
                    ie: 11 
                  }, 
                  useBuiltIns: 'usage' 
                } 
              ] 
            ] 
          } 
        }, 
        { 
          test: /\.vue$/, 
          loader: 'vue-loader' 
        }, 
        { 
          test: /\.pug/, 
          loader: 'pug-plain-loader' 
        }, 
        { 
          test: /\.(c|sc)ss$/, 
          use: [ 
            { 
              loader: MiniCssExtractPlugin.loader, 
              options: { 
                publicPath: path.resolve(__dirname, 'public/assets/stylesheets') 
              } 
            }, 
            'css-loader', 
            'sass-loader' 
          ] 
        }, 
        { 
          test: /\.(jpg|png|gif)$/, 
          loader: 'file-loader', 
          options: { 
            name: '[name]-[hash].[ext]', 
            outputPath: 'images', 
            publicPath: function(path) { 
              return 'images/' + path 
            } 
          } 
        } 
      ] 
    }, 
    resolve: { 
      alias: { 
        vue: 'vue/dist/vue.js' 
      }, 
      extensions: ['.js', '.scss', 'css', '.vue', '.jpg', '.png', '.gif', ' '] 
    }, 
    optimization: { 
      splitChunks: { 
        cacheGroups: { 
          vendor: { 
            test: /.(c|sa)ss/, 
            name: 'style', 
            chunks: 'all', 
            enforce: true 
          } 
        } 
      } 
    } 
  } 
} 
これで、$ yarn run build:devを実行することでpublic/assets/以下にそれぞれバンドルファイルとmanifest.jsonが生成されます。

しかしこれでは画面は表示されません。

テンプレートファイル内でバンドルされたjsとcssを読み込む必要があります。


ヘルパータグの実装

app/helpers/webpack_bundle_helper.rb
require "open-uri" 
 
module WebpackBundleHelper 
  class BundleNotFound < StandardError; end 
 
  def javascript_bundle_tag(entry, **options) 
    path = asset_bundle_path("#{entry}.js") 
 
    options = { 
      src: path, 
      defer: true, 
    }.merge(options) 
 
    # async と defer を両方指定した場合、ふつうは async が優先されるが、 
    # defer しか対応してない古いブラウザの挙動を考えるのが面倒なので、両方指定は防いでおく 
    options.delete(:defer) if options[:async] 
 
    javascript_include_tag "", **options 
  end 
 
  def stylesheet_bundle_tag(entry, **options) 
    path = asset_bundle_path("#{entry}.css") 
 
    options = { 
      href: path, 
    }.merge(options) 
 
    stylesheet_link_tag "", **options 
  end 
 
  private 
 
  def asset_server 
    port = Rails.env === "development" ? "3035" : "3000" 
    "http://#{request.host}:#{port}" 
  end 
 
  def pro_manifest 
    File.read("public/assets/manifest.json") 
  end 
 
  def dev_manifest 
    OpenURI.open_uri("#{asset_server}/public/assets/manifest.json").read 
  end 
 
  def test_manifest 
    File.read("public/assets-test/manifest.json") 
  end 
 
  def manifest 
    return @manifest ||= JSON.parse(pro_manifest) if Rails.env.production? 
    return @manifest ||= JSON.parse(dev_manifest) if Rails.env.development? 
    return @manifest ||= JSON.parse(test_manifest) 
  end 
 
  def valid_entry?(entry) 
    return true if manifest.key?(entry) 
    raise BundleNotFound, "Could not find bundle with name #{entry}" 
  end 
 
  def asset_bundle_path(entry, **options) 
    valid_entry?(entry) 
    asset_path("#{asset_server}/public/assets/" + manifest.fetch(entry), **options) 
  end 
end 
そしてテンプレートファイル内で今までJSとCSSを読み込んでいたメソッドを削除します。

layout/application.haml
// この2行を削除 
= stylesheet_link_tag 'application', media: 'all' 
= javascript_include_tag 'application' 
 
// この2行を追加 
= javascript_bundle_tag "#{params[:controller]}#{params[:action].capitalize}" 
= stylesheet_bundle_tag "#{params[:controller]}#{params[:action].capitalize}" 
これでバンドルファイルを読み込むことができました。

しかしこれではstyleを1行変更するだけでも、再ビルドしてブラウザをリロードしなければいけません。

非常に効率が悪いです。

そこでファイルの変更を検知して自動でビルドしてくれるようにwebpack-dev-serverを導入し、HMR(Hot Module Replacement)を有効にします。

HMRを有効にすることでファイルのセーブ時にファイルの変更を検知して自動でビルドを行ってくれます。


1_KdajGOiUJTeIiUmFId5ivw.gif



web-pack-dev-serverの導入

$ yarn add webpack-dev-server -D 
webpack.config.jsに追記します。

webpack.config.js
devServer: { 
      host: 'localhost', 
      port: 3035, 
      publicPath: 'http://localhost:3035/public/assets/', 
      contentBase: path.resolve(__dirname, 'public/assets'), 
      hot: true, 
      disableHostCheck: true, 
      historyApiFallback: true 
    } 


最終的なwebpack.config.js

webpack.config.js
const path = require('path') 
const webpack = require('webpack') 
const MiniCssExtractPlugin = require('mini-css-extract-plugin') 
const VueLoaderPlugin = require('vue-loader/lib/plugin') 
const ManifestPlugin = require('webpack-manifest-plugin') 
 
module.exports = (env, argv) => { 
  const IS_DEV = argv.mode === 'development' 
 
  return { 
    entry: { 
      main: './frontend/application.js' 
    }, 
    devtool: IS_DEV ? 'source-map' : 'none', 
    output: { 
      filename: 'javascripts/bundle/[name]-[hash].js', 
      path: path.resolve(__dirname, 'app/assets') 
    }, 
    plugins: [ 
      new VueLoaderPlugin(), 
      new MiniCssExtractPlugin({ 
        filename: 'stylesheets/bundle/[name]-[hash].css' 
      }), 
      new webpack.HotModuleReplacementPlugin(), 
      new ManifestPlugin({ 
        writeToFileEmit: true 
      }) 
    ], 
    module: { 
      rules: [ 
        { 
          test: /\.js$/, 
          exclude: /node_modules/, 
          loader: 'babel-loader', 
          options: { 
            presets: ['@babel/preset-env'] 
          } 
        }, 
        { 
          test: /\.vue$/, 
          loader: 'vue-loader' 
        }, 
        { 
          test: /\.pug/, 
          loader: 'pug-plain-loader' 
        }, 
        { 
          test: /\.(c|sc)ss$/, 
          use: [ 
            { 
              loader: MiniCssExtractPlugin.loader, 
              options: { 
                publicPath: path.resolve( 
                  __dirname, 
                  'app/assets/stylesheets/bundle' 
                ) 
              } 
            }, 
            'css-loader', 
            'sass-loader' 
          ] 
        }, 
        { 
          test: /\.(jpg|png|gif)$/, 
          loader: 'file-loader', 
          options: { 
            name: '[name]-[hash].[ext]', 
            outputPath: 'images/bundle', 
            publicPath: function(path) { 
              return 'images/bundle/' + path 
            } 
          } 
        } 
      ] 
    }, 
    resolve: { 
      alias: { 
        vue: 'vue/dist/vue.js' 
      }, 
      extensions: ['.js', '.scss', 'css', '.vue', '.jpg', '.png', '.gif', ' '] 
    }, 
    optimization: { 
      splitChunks: { 
        cacheGroups: { 
          vendor: { 
            test: /.(c|sa)ss/, 
            name: 'style', 
            chunks: 'all', 
            enforce: true 
          } 
        } 
      } 
    }, 
    devServer: { 
      host: 'localhost', 
      port: 3035, 
      publicPath: 'http://localhost:3035/app/assets/', 
      contentBase: path.resolve(__dirname, 'app/assets'), 
      hot: true, 
      disableHostCheck: true, 
      historyApiFallback: true 
    } 
  } 
} 
これでwebpack-dev-serverを利用することができます。


$ yarn run webpack-dev-server --mode development


そしてrails sすると確かに画面が表示され、ファイルを変更すると検知してくれます。


foreman

webpack-dev-serverrails serverをいちいち立ち上げるのはめんどくさいので、foremanというgemを利用して、二つのプロセスを一度に立ち上げられるようにします。
gem "foreman"をGemfileに追記して$ bundle installします。
Procfileという名前のファイルをルートディレクトリ直下に作成し、下記のように記載します。

rails: bundle exec rails server 
webpack-dev-server: yarn run webpack-dev-server --color --mode development 
その後、

$ bundle exec foreman start -p 3000 
を叩くことで通常と変わらずポート3000番でプロセスが立ち上がります。


画像を表示できるようにプロキシー設定

これでwebpackによるビルド、webpack-dev-serverのHMR有効化が完了しました。

しかし、この状態で画像を表示しようとするとエラーになります。

なのでプロキシを設定します。

lib/tasks/assets_path_proxy.rb
require "rack/proxy" 
 
class AssetsPathProxy < Rack::Proxy 
  def perform_request(env) 
    if env["PATH_INFO"].include?("/images/") 
      if Rails.env != "production" 
        dev_server = env["HTTP_HOST"].gsub(":3000", ":3035") 
        env["HTTP_HOST"] = dev_server 
        env["HTTP_X_FORWARDED_HOST"] = dev_server 
        env["HTTP_X_FORWARDED_SERVER"] = dev_server 
      end 
      env["PATH_INFO"] = "/public/assets/images/" + env["PATH_INFO"].split("/").last 
      super 
    else 
      @app.call(env) 
    end 
  end 
end 
次に、config/environment/development.rbとproduction.rbのそれぞれに一行追記

config.middleware.use AssetsPathProxy, ssl_verify_none: true 
これで画像が表示されるようになったはずです。


Vueの利用の仕方

さて、ここで実際にどのようにVueを利用していくかお見せします。
frontendディレクトリの構成をもう一度お見せします。

- frontend // フロントエンド開発用ディレクトリ 
  - pages // ページ全体のコンポーネント 
    - root // コントローラ名 
      - rootIndex.js // 名前を一意にする為に(コントローラ名)+(アクション名)にする。ページコンポーネントを登録する。 
      - index.vue // `root/index`ページのファイル 
      - rootShow.js 
      - show.vue // `root/show`ページのファイル 
  - components 
    - app.vue 
  - images 
    - image.jpg 
ファイルの内容をお見せしながらどのような構成になっているのか詳しく説明します。


テンプレートファイル

layout/application.haml
!!! 
%html 
    %head 
        %meta{ charset: "utf-8" } 
        %meta{ name: "viewport", content: "width=device-width" } 
        %title 
            MieMa 
        = csrf_meta_tags 
        = csp_meta_tag 
    %body 
        #app <= <div id="app">を追加 
            = yield 
各ページコンポーネントをレンダリングする際にelementが必要になるので#app

追記しています。


各Viewファイル

index.haml
%root-index // ViewにはVueのページ全体となるコンポーネントのみを書く 
Viewファイルに記述することはこの1行だけです。

コンポーネントを読み込むだけ。


コンポーネント

componentsディレクトリにはボタンやモーダルといった普通のコンポーネントを書きます。

components/app.vue
<template lang="pug"> 
  div 
    p {{ message }} 
</template> 
 
<script> 
export default { 
  data() { 
    return { 
      message: 'Hello Vue!' 
    } 
  } 
} 
</script> 
 
<style lang="scss" scoped> 
p { 
  font-size: 2em; 
  text-align: center; 
  background-color: greenyellow; 
} 
</style> 


ページコンポーネント

ページコンポーネントでは使用する普通のコンポーネント(ここではApp)を読み込んで使用できます。

pages/root/index.vue
<template lang="pug"> 
#root-index 
  App 
  p Index page 
  img(src='../../images/image.jpg') 
</template> 
 
<style lang="scss"></style> 
 
<script> 
import App from '../../components/app' 
 
export default { 
  components: { 
    App 
  } 
} 
</script> 


ページコンポーネント登録

ここでel: '#app'を指定して登録することでViewファイル内で呼び出すことができます。

pages/root/rootIndex.js
import Vue from 'vue' 
import RootIndex from './index' 
 
new Vue({ 
  el: '#app', 
  components: { 
    RootIndex // これでViewファイル内では<root-index />で呼べます。 
  } 
}) 


まとめ

今回はRailsからwebpackerを完全に捨てて、webpackでアセットを管理する方法を紹介しました。

Vueでページコンポーネントを作成することによるファイル管理のしやすさもかなりいい感じにまとまりました。

参考になりましたら「いいね!」やTwitterフォローお願いします。

webpackで管理するような記事がそれほど見つからなかったので、開発の手助けになればと思います。

気が向いたらこの構成のテンプレートをGitHubで公開しようと思います。


その他参考サイト

https://inside.pixiv.blog/subal/4615
https://medium.com/studist-dev/goodbye-webpacker-183155a942f6

コメント

このブログの人気の投稿

投稿時間:2021-06-17 05:05:34 RSSフィード2021-06-17 05:00 分まとめ(1274件)

投稿時間:2021-06-20 02:06:12 RSSフィード2021-06-20 02:00 分まとめ(3871件)

投稿時間:2020-12-01 09:41:49 RSSフィード2020-12-01 09:00 分まとめ(69件)