SprocketsとWebpackerを使わずWebpackのみでassets周りを管理した話(動かすまで編)
Rails 6 がリリースされてWebpackerが標準搭載されるようになった。
個人的にSprocketsとWebpackerってどっちかでよくないかと思ったので、Sprocketsは使わずWebpackerでassets周りを全部管理しようかと思った
でもこのWebpackerはフロントエンドのエンジニアの方々からあまり好かれていない様子。
それならWebpackだけでassets周りを管理できたらいいのかなと思いチャレンジしてみる。
せっかくなのでwebpack-dev-serverも使いたいのでその設定も
今回は使わないがフロントエンドでVueを使う想定していて作ったものなのでちょくちょくVueの設定も含まれている
出来上がったもの
参考にした記事
Railsプロジェクトの作成
testはMinitestではなくRSpecを使いたいので--skip-testする。
rails new . -d mysql --skip-turbolinks --skip-test
Gemfileから不要なものを消す
とりあえずこんな感じのGemfile
source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '2.6.3' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '~> 6.0.0' # Use mysql as the database for Active Record gem 'mysql2', '>= 0.4.4' # Use Puma as the app server gem 'puma', '~> 3.11' # Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker gem "rack-proxy" # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder # gem 'jbuilder', '~> 2.7' # Use Redis adapter to run Action Cable in production # gem 'redis', '~> 4.0' # Use Active Model has_secure_password # gem 'bcrypt', '~> 3.1.7' # Use Active Storage variant # gem 'image_processing', '~> 1.2' # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', '>= 1.4.2', require: false # Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible # gem 'rack-cors' gem "slim-rails" group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] end group :development do gem "web-console", ">= 3.3.0" gem 'listen', '>= 3.0.5', '< 3.2' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' gem "rubocop", "~> 0.63.1", require: false end group :test do gem "capybara" gem "selenium-webdriver" gem "database_cleaner" gem "webmock" gem "simplecov" gem "factory_bot_rails" end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
package.jsonを設置してyarn installする
package.json はこんな感じ。
{ "name": "webpack_pure", "private": true, "dependencies": { "@rails/actioncable": "^6.0.0", "@rails/actiontext": "^6.0.0", "@rails/activestorage": "^6.0.0", "@rails/ujs": "^6.0.0", "axios": "^0.18.0", "import-glob-loader": "^1.1.0", "moment": "^2.24.0", "trix": "^1.0.0" , "vue": "^2.6.8", "vue-loader": "^15.7.0", "vue-template-compiler": "^2.6.8" }, "version": "0.1.0", "scripts": { "build:dev": "webpack --progress --mode=development --config ./webpack.config.js", "build:pro": "webpack --progress --mode=production --config ./webpack.config.js" }, "devDependencies": { "@babel/core": "^7.3.4", "@babel/plugin-proposal-class-properties": "^7.3.4", "@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-transform-runtime": "^7.3.4", "@babel/polyfill": "^7.2.5", "@babel/preset-env": "^7.3.4", "@fortawesome/fontawesome-free": "^5.8.1", "babel-loader": "^8.0.5", "babel-plugin-macros": "^2.5.0", "css-loader": "^2.1.1", "mini-css-extract-plugin": "^0.5.0", "node-sass": "^4.11.0", "pug": "^2.0.3", "pug-plain-loader": "^1.0.0", "sass-loader": "^7.1.0", "vue-style-loader": "^4.1.2", "webpack-cli": "^3.2.3", "webpack-dev-server": "^3.2.1", "webpack-manifest-plugin": "^2.0.4" } }
yarn install する
$ yarn install
webpack.config.js を設置する
webpack.config.jsはこんな感じ
gemのほうのwebpackerで出力されるconfigをカスタマイズしながら作った
RAILS_ROOT/frontend 以下にimage css javascript のファイルを置くのを想定
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/entry/**/*.js").map(function(file) { let name = file.replace("./frontend/entry/", "").split(".")[0]; entries[name] = file; }); const assets_path = path.join(__dirname, "public", "assets"); module.exports = (env, argv) => { return { entry: entries, output: { filename: "js/[name]-[hash].js", chunkFilename: "js/[name]-[hash].chunk.js", hotUpdateChunkFilename: "js/[id]-[hash].hot-update.js", path: assets_path, publicPath: "/assets/", pathinfo: true }, 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$/, use: [ { loader: "vue-loader", options: { extractCSS: true } } ] }, { 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 "/assets/images/" + path; } } }, } ] }, resolve: { alias: { vue: "vue/dist/vue.js" }, extensions: [ ".vue", ".mjs", ".js", ".sass", ".scss", ".css", ".module.sass", ".module.scss", ".module.css", ".png", ".svg", ".gif", ".jpeg", ".jpg" ], modules: ["node_modules"] }, devServer: { clientLogLevel: "info", compress: true, quiet: false, disableHostCheck: true, port: 3035, https: false, host: "0.0.0.0", hot: true, contentBase: assets_path, inline: false, useLocalIp: false, public: "0.0.0.0:3035", publicPath: "/assets/", historyApiFallback: { disableDotRule: true }, headers: { "Access-Control-Allow-Origin": "*" }, overlay: true, watchOptions: { ignored: "**/node_modules/**", poll: true } } }; };
Hot Module Replacement の設定は下記部分のinlineをtrueに設定すれば有効になるがローカルで開発する場合のみ有効
EC2のインスタンス上で開発していたりする場合はJSのエラーが出るのでfalseにしといたほうがよさそう
devServer: { clientLogLevel: "info", compress: true, quiet: false, disableHostCheck: true, port: 3035, https: false, host: "0.0.0.0", hot: true, contentBase: assets_path, inline: true, // ⬅︎これ useLocalIp: false, public: "0.0.0.0:3035", publicPath: "/assets/", historyApiFallback: { disableDotRule: true }, headers: { "Access-Control-Allow-Origin": "*" }, overlay: true, watchOptions: { ignored: "**/node_modules/**", poll: true } }
下記コマンドを叩くと RAILS_ROOT/public/assets/ 以下に manifest.json と各assetsが配布される
本番へデプロイするときに叩く用
$ yarn build:pro
RAILS_ROOT/app/helpers/webpack_bundle_helper.rbを設置する
RAILS_ROOT/frontend 以下にあるassetsファイル群を呼び出すためのhelper
module WebpackBundleHelper class BundleNotFound < StandardError; end def asset_bundle_path(entry, **options) valid_entry?(entry) asset_path(manifest.fetch(entry), **options) end def javascript_bundle_tag(entry, **options) path = asset_bundle_path("#{entry}.js") options = { src: path, defer: true }.merge(options) 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 def image_bundle_tag(entry, **options) raise ArgumentError, "Extname is missing with #{entry}" if File.extname(entry).blank? image_tag asset_bundle_path(entry), **options end private def manifest @manifest ||= JSON.parse(manifest_json_file) end def manifest_json_file return OpenURI.open_uri("http://railswebpack:3035/assets/manifest.json").read if Rails.env.development? File.read("public/assets/manifest.json") end def valid_entry?(entry) return true if manifest.key?(entry) raise BundleNotFound, "Could not find bundle with name #{entry}" end end
RAILS_ROOT/lib/dev_server_proxy.rbを設置してRAILS_ROOT/config/environments/development.rbを追記する
開発環境側のRAILS_ROOT/app/viewsからwebpack-dev-server上で作成されているassetsを参照するためにproxyを使う
RAILS_ROOT/lib/dev_server_proxy.rb を作成する。中身はこんな感じ
require "rack/proxy" class DevServerProxy < Rack::Proxy def perform_request(env) if env["PATH_INFO"].start_with?("/assets/") env["HTTP_HOST"] = dev_server_host env["HTTP_X_FORWARDED_HOST"] = dev_server_host env["HTTP_X_FORWARDED_SERVER"] = dev_server_host super else @app.call(env) end end private def dev_server_host "railswebpack:3035" end end
dev_sever_host はwebpack-dev-serverが動いている環境のhostを指定する
今回はdocker上でやっていたのでこんな感じ
RAILS_ROOT/config/environments/development.rbでdev_server_proxy.rbをrequireする
require Rails.root.join("lib", "dev_server_proxy.rb") Rails.application.configure do defultで設定されているもの ... config.middleware.use DevServerProxy, ssl_verify_none: true # ⬅︎これを追記 end
image css js の使い方
まずはエントリーポイントを設定する
今回はここに設置RAILS_ROOT/frontend/entry/application.js
actiontextも使えるようにしておく
require("@rails/ujs").start(); require("@rails/activestorage").start(); import "../../../node_modules/@fortawesome/fontawesome-free/js/all"; require("../javascripts/channels"); require("@rails/actiontext"); import "../stylesheets/application"; const images = require.context("../images/", true); // RAILS_ROOT/frontend/javascripts/ 以下のJSをインポートして使いたい場合はそれを記述しておく // import "../javascripts/common";
slim での利用を想定
レイアウトファイルで読み込むCSSとJSの設定
RAILS_ROOT/app/views/layouts/application.html.slim
doctype html html head title | Webpack Pure = csrf_meta_tags = csp_meta_tag / 使いたいエントリーポイント = javascript_bundle_tag "application" / 使いたいエントリーポイントの中でimportしているcss名 = stylesheet_bundle_tag "application", media: "all" body = yield
画像の使い方
RAILS_ROOT/app/views/tops/index.html.slim
.center = image_bundle_tag("images/ruby.jpg")
次はこのwebpackを使った状態でcapybaraのテストを書いていく