
Rails に Webpack と Vue を導入しました!
こんにちは!最近は週3でリモートワークをしているはっさん(@hassasa3)です。
さて、今回は Rails に Webpack と Vue を導入した記事です!
背景
CAMPFIRE ではユーザーのアクションに対して HTML を変更したい際は jQuery を用いていました。これまでは jQuery で十分でしたが「インタラクティブな機能を作成するぞ」となった際に記述や管理が大変で、今後の新機能も jQuery で書いてメンテしていくのは辛いと感じていました。
昨今では Vue や React といった新しいフロントエンド技術が進んでいます。これらを使わない手はないため、技術選定を行い Webpack と Vue の導入に至りました。
前提
・Webpacker は使わない
Webpacker は使わない方針にしました。巷でも Webpacker を脱出し、素の Webpack に置き換えている記事が見受けられます。Webpacker を使わない理由は以下の記事と同じなため参考にしてください。
・Sprockets は使わない
Webpack とプラグインを使えば Sprockets がやっていることを代替できるため Sprockets は使いません。
・webpack-dev-server の HMR を利用して開発できるようにする
・Vue を導入する
・ ES6 / Sass を使えるようにする
必要なライブラリを yarn add
$ yarn add @babel/core @babel/preset-env babel-loader css-loader file-loader node-sass prettier prettier-webpack-plugin sass-loader url-loader vue vue-loader vue-template-compiler webpack webpack-cli webpack-dev-server webpack-merge webpack-manifest-plugin
Webpack が出力した manifest.json を読み込むビューヘルパーを定義する
webpack-manifest-plugin というプラグインを使うと、webpack でのビルド時に Fingerprint 付きのファイルと manifest.json を生成してくれます。
こちらを yarn install した後、ビルド後のファイルパスを返すビューヘルパーを定義し、テンプレートから呼び出せるようにします。
# app/helpers/application_helper.rb
def webpack_asset_path(path)
# webpack-dev-server を参照
return "http://localhost:8080/#{path}" if Rails.env.development?
host = Rails.application.config.action_controller.asset_host
manifest = Rails.application.config.assets.webpack_manifest
path = manifest[path] if manifest && manifest[path].present?
"#{host}/assets/#{path}"
end
# config/initializers/assets.rb
webpack_manifest_path = Rails.root.join('public', 'assets', 'manifest.json')
Rails.application.config.assets.webpack_manifest =
if File.exist?(webpack_manifest_path)
JSON.parse(File.read(webpack_manifest_path))
end
// app/views/hoges/index.html.erb
// テンプレートから呼び出せる
<%= javascript_include_tag webpack_asset_path('app.js') %>
これで Webpack でビルドしたスクリプトをテンプレートから読み出すことができます。以下は webpack-dev-server も利用した開発用の webpack.config.js 例です。
// webpack.config.js
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const ManifestPlugin = require('webpack-manifest-plugin');
const path = require('path');
module.exports = {
mode: 'development',
entry: {
app: path.resolve(__dirname, '../../frontend/javascripts/application.js')
},
output: {
path: path.resolve(__dirname, '../../public/assets'),
filename: '[name].js'
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
},
{
test: /\.(css|sass|scss)$/,
use: [
'vue-style-loader',
'css-loader',
'sass-loader'
]
},
{
test: /\.(jpg|png|gif)$/,
use: [{
loader: 'file-loader',
options: {
outputPath: 'images',
publicPath: 'assets/images',
name: '[name].[ext]'
}
}]
}
]
},
plugins: [
new VueLoaderPlugin(),
new ManifestPlugin()
],
resolve: {
alias: {
'vue': 'vue/dist/vue.js'
}
},
devServer: {
disableHostCheck: true,
hot: true,
public: 'localhost:8080',
headers: {
"Access-Control-Allow-Origin": "*"
},
contentBase: path.resolve(__dirname, '../../public/assets')
}
}
例ではエントリーポイントが一つだけですが、実際は粒度に応じて次々と追加されます。粒度に関しては、例えば「プロジェクトの詳細ページ、作成ページ..」など小さすぎず、大きすぎないようチームメンバーで話し合って決めました。そして、ビルドして吐き出されたスクリプトを必要なページで読み込むイメージです。
// app/views/projects/show.html.erb
// テンプレートから呼び出せる
<%= javascript_include_tag webpack_asset_path('project_show.js') %>
// entry
entry: {
..
project_show: path.resolve(__dirname, '../../frontend/javascripts/project_show.js')
},
それでは Vue コンポーネントのマウントはどうなるでしょうか。
Vue のコンポーネントをマウントする
Rails のアプリケーション直下に frontend/javascripts ディレクトリを配置し、そこにエントリーポイントを置いていきます。(例では application.js)
また、Vue コンポーネントを mount する際、要素の指定には id ではなく data-vue を使うようにしました。
// index.html.erb
<%= javascript_include_tag webpack_asset_path('app.js') %>
<main>
<div data-vue="Hoge"></div>
<div data-vue="Fuga"></div>
</main>
// frontend/javascripts/application.js
import Vue from "vue";
import App from './components/Hoge';
import Hoge from './components/Fuga';
export const components = {
Hoge,
Fuga
};
document.addEventListener("DOMContentLoaded", () => {
let templates = document.querySelectorAll("[data-vue]");
for (let el of templates) {
let app = new Vue(components[el.dataset.vue]);
app.$mount(el);
}
});
id を用いると複数の同じコンポーネントを同一ページにマウントできないかつ、 data-vue を用いることで「ここは Vue を使っているな」と直感的に分かるので開発しやすくなります。
CI の対応、デプロイ
assets のビルドからデプロイは全て Circle CI 上で完結します。
circleci/config.yml の assets compile しているタスクの中に yarn install と package.json に定義したビルドを実行するコマンドを書きます。
// package.json
"scripts": {
"dev": "webpack --config ./config/webpack/development.js",
"watch": "webpack-dev-server --config ./config/webpack/development.js",
..
"build": "webpack --config ./config/webpack/production.js"
},
# circleci/config.yml
- run: yarn install # 追加
...
- run:
command: |
if [ "${CIRCLE_BRANCH}" == "production" ]; then
bundle exec rails assets:precompile
yarn run build # 追加
bundle exec rails assets:sync
fi
...
ポイントは assets:precompile が吐くパスと webpack の output パスを同じ ( public/assets ) にしてあげる点です。
// config/webpack/production.js
..
module.exports = Merge(CommonConfig, {
mode: 'production',
output: {
path: path.resolve(__dirname, '../../public/assets'), // ポイント
filename: '[name]-[hash].js'
},
..
弊社では assets のアップロードに asset_sync を用いています。このようにするとコマンド一つで asset_sync がまとめて S3 にアップロードしてくれます。
webpack.config.js ファイルの構成
最終的に webpack.config.js のファイル構成は以下になりました。
共通部分は common.js に書き、環境独自の設定はそれぞれのファイルに書くようにしました。一つの config ファイル内で分岐が溢れることはなく、各環境で必要な修正がしやすい状態となっています。
config/webpack
├── common.js
├── development.js
├── production.js
├── qa.js
└── staging.js
最後に
以上を経て Rails に Webpack と Vue を導入することができました。
CAMPFIRE では事業・会社に興味があり、 Rails と Vue を使って開発していきたいエンジニアを募集しています!