Rails に Webpack と Vue を導入しました!

こんにちは!最近は週3ペースでリモートワークをしつつ、業後に TypeScript の勉強をしている はっさん です。

さて、今回は 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 を使えるようにする

Webpack が出力した manifest.json を読み込むビューヘルパーを定義する

webpack-manifest-plugin というプラグインを使うと、webpack でのビルド時に Fingerprint 付きのファイルと manifest.json を生成してくれます。

その後、ビルド後のファイルパスを返すビューヘルパーを定義し、テンプレートから呼び出せるようにします。

# 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')
  }
}

Vue のコンポーネントをマウントする

Vue コンポーネントを mount する際、要素の指定には id ではなく data-vue を使うようにしました。以下は例です。

// frontend/javascripts/components.js

import App from './components/App';
import Hoge from './components/Hoge';

export const components = {
  App, Hoge
};
// index.html.erb

<main>
  <div data-vue="App"></div>
  <div data-vue="Hoge"></div>
</main>
// frontend/javascripts/application.js

import Vue from "vue";
import { components } from "./components";

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


この記事が気に入ったら、サポートをしてみませんか?気軽にクリエイターを支援できます。

12

CAMPFIRE

#スタートアップ 記事まとめ

スタートアップが手がけたnoteが集まるマガジンです。スタートアップが読むべき、知るべきnoteも選んでいきます。
1つのマガジンに含まれています
コメントを投稿するには、 ログイン または 会員登録 をする必要があります。