Rails環境でJS , CSSをwebpackで完全に管理する
Rails環境でJS , CSSをwebpackで完全に管理する:
Viewファイル内でJSとCSSを読み込んでいたのをテンプレートファイル内で読み込むように変更。
Railsのバージョン5.1から登場したwebpackerでは、ReactやVueといったAltJSを簡単に導入することが可能になりました。
しかし、実際に導入・運用してみるとwebpackerの学習コストやwebpackのバージョンアップについていけないことが辛いといった経験が出てきます。
Railsにはサーバーサイドのロジックに集中してもらい、フロントエンドのことはwebpackに任せましょう。
Vueと相性が悪く、turbolinks:loadedイベントを監視しないといけません。
Misocaさんのブログが参考になりました。
Vueと一緒に使うことも不可能では無いようですが、開発のしやすさを優先して削除します。
Gemfileから
テンプレートファイルの以下から、
削除前
削除後
assets内の
今回はSprocketsのアセットパイプラインによるファイルの配信を行わず、webpackでビルドしたjsとcssをViewファイル内で読み込ませます。
その為に思い切って
ここからはこちらのブログが大変参考になりました。
Gemfileから
以下のファイルを削除
ルートディレクトリ直下に
※ここで関係のないディレクトリについては省いて説明しています。
今回はページ全体をvueによってコンポーネントとし(便宜上ページコンポーネントと呼びます)、jsによってそれを登録します。
単一ファイルコンポーネントについて
コンポーネントを1つのファイルで構成する単一ファイルコンポーネントを利用します。
ここではプリセットはPug, Sass(scss)を利用します。
他のプリセットを利用する場合は適宜、loader等を読み替えてください。
Vueを導入します。
webpack関連
JavaScriptをトランスパイルするBabel関連
Vueファイル内で使用する言語のloader等
webpack@4では
ここに気づかず、古い記事を参考にしたりしてかなりの時間ハマってしまいました。
vueのwebpackでの設定方法についてはvue-loaderの公式を読む必要があります。
package.jsonに追記
これで、
しかしこれでは画面は表示されません。
テンプレートファイル内でバンドルされたjsとcssを読み込む必要があります。
そしてテンプレートファイル内で今までJSとCSSを読み込んでいたメソッドを削除します。
これでバンドルファイルを読み込むことができました。
しかしこれではstyleを1行変更するだけでも、再ビルドしてブラウザをリロードしなければいけません。
非常に効率が悪いです。
そこでファイルの変更を検知して自動でビルドしてくれるようにwebpack-dev-serverを導入し、HMR(Hot Module Replacement)を有効にします。
HMRを有効にすることでファイルのセーブ時にファイルの変更を検知して自動でビルドを行ってくれます。
これでwebpack-dev-serverを利用することができます。
そして
その後、
を叩くことで通常と変わらずポート3000番でプロセスが立ち上がります。
これでwebpackによるビルド、webpack-dev-serverのHMR有効化が完了しました。
しかし、この状態で画像を表示しようとするとエラーになります。
なのでプロキシを設定します。
次に、config/environment/development.rbとproduction.rbのそれぞれに一行追記
これで画像が表示されるようになったはずです。
さて、ここで実際にどのようにVueを利用していくかお見せします。
ファイルの内容をお見せしながらどのような構成になっているのか詳しく説明します。
各ページコンポーネントをレンダリングする際にelementが必要になるので
追記しています。
Viewファイルに記述することはこの1行だけです。
コンポーネントを読み込むだけ。
ページコンポーネントでは使用する普通のコンポーネント(ここではApp)を読み込んで使用できます。
ここで
今回はRailsからwebpackerを完全に捨てて、webpackでアセットを管理する方法を紹介しました。
Vueでページコンポーネントを作成することによるファイル管理のしやすさもかなりいい感じにまとまりました。
参考になりましたら「いいね!」やTwitterフォローお願いします。
webpackで管理するような記事がそれほど見つからなかったので、開発の手助けになればと思います。
気が向いたらこの構成のテンプレートをGitHubで公開しようと思います。
https://inside.pixiv.blog/subal/4615
https://medium.com/studist-dev/goodbye-webpacker-183155a942f6
追記
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"
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.rb
とproduction.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
$ yarn add @babel/core @babel/polyfill @babel/preset-env babel-loader -D
$ 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
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を有効にすることでファイルのセーブ時にファイルの変更を検知して自動でビルドを行ってくれます。
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 } } }
$ yarn run webpack-dev-server --mode development
そして
rails s
すると確かに画面が表示され、ファイルを変更すると検知してくれます。
foreman
webpack-dev-server
とrails 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
画像を表示できるようにプロキシー設定
これで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.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
#app
を追記しています。
各Viewファイル
index.haml
%root-index // ViewにはVueのページ全体となるコンポーネントのみを書く
コンポーネントを読み込むだけ。
コンポーネント
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/4615https://medium.com/studist-dev/goodbye-webpacker-183155a942f6
コメント
コメントを投稿