追記
Viewファイル内でJSとCSSを読み込んでいたのをテンプレートファイル内で読み込むように変更。
リポジトリを用意しました。開発にそのまま使用できます。
記事を読んでいちいち設定めんどいなと思ったらこのリポジトリをクローンしてアプリ開発に使ってください。
https://github.com/shan-shan95/rails-vue-template
1/27 追記
「webpackに乗り換えることのメリット」の項目を追加しました。
ファイル構成、webpackの設定を少し変更しました。
この構成にさらにbulma, fontawesome5, normalize.cssを入れる手順です。
めんどくさい人は上のリポジトリに入っているのでそのまま使ってください。
https://shanshan.hatenadiary.jp/entry/2019/01/26/152807
なぜ人類は'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を利用してサクサク開発する
完全にwebpackに乗り換えることのメリット
- webpackerの学習コストを0にする。
- webpackのバージョン移行時にgemの依存関係を考えなくて良い。
- 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/assets/
ディレクトリを削除します。
脱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
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に追記
"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を読み込む必要があります。
ヘルパータグの実装
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を読み込んでいたメソッドを削除します。
// この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
に追記します。
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
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', // HMRが重くなる原因なので外した方がいい。
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-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
を叩くことで通常と変わらずポート3000番でプロセスが立ち上がります。
画像を表示できるようにプロキシー設定
これでwebpackによるビルド、webpack-dev-serverのHMR有効化が完了しました。
しかし、この状態で画像を表示しようとするとエラーになります。
なのでプロキシを設定します。
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
ファイルの内容をお見せしながらどのような構成になっているのか詳しく説明します。
テンプレートファイル
!!!
%html
%head
%meta{ charset: "utf-8" }
%meta{ name: "viewport", content: "width=device-width" }
%title
MieMa
= csrf_meta_tags
= csp_meta_tag
= javascript_bundle_tag "#{params[:controller]}#{params[:action].capitalize}" // 追記
= stylesheet_bundle_tag "#{params[:controller]}#{params[:action].capitalize}" // 追記
%body
#app // <div id="app">を追加
= yield
各ページコンポーネントをレンダリングする際にelementが必要になるので#app
を
追記しています。
各アクションで使用するアセットを読み込んでいます。
各Viewファイル
%root-index // ViewにはVueのページ全体となるコンポーネントのみを書く
Viewファイルに記述することはこの1行だけです。
コンポーネントを読み込むだけ。
コンポーネント
components
ディレクトリにはボタンやモーダルといった普通のコンポーネントを書きます。
<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)を読み込んで使用できます。
<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ファイル内で呼び出すことができます。
import Vue from 'vue'
import RootIndex from './index'
new Vue({
el: '#app',
components: {
RootIndex // これでViewファイル内では<root-index />で呼べます。
}
})
まとめ
今回はRailsからwebpackerを完全に捨てて、webpackでアセットを管理する方法を紹介しました。
Vueでページコンポーネントを作成することによるファイル管理のしやすさもかなりいい感じにまとまりました。
参考になりましたら「いいね!」やTwitterフォローお願いします。
その他参考サイト
https://inside.pixiv.blog/subal/4615
https://medium.com/studist-dev/goodbye-webpacker-183155a942f6