見出し画像

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 を使って開発していきたいエンジニアを募集しています!


この記事が気に入ったらサポートをしてみませんか?