Rails で Vue.js を配信する手法についてキャッチアップする
目的
- Vue で書かれた SPA のエントリポイントを Rails から配信するパターンについて、どういった設計になっているのかを理解する
- ついでにRailsのアセットパイプライン周りの仕組みやツールなどを理解したい
こちらを見てキャッチアップする
記載された技術スタックを見た感じ、自分が学びたいことにマッチしていそう。
今回はRubyは3.3.6、Nodeはv.20.14.0を使おうと思う。
- 技術スタック
Ruby 2.7.1
Rails 7.0.4
rspec
Node.js 16.18.1
Vue.js 3.2.45
webpack 5.75.0
TypeScript
headless chrome
まずは rails new する必要があるので、bundle init
してGemfile生成。
railsのバージョンを合わせて記述した。
# frozen_string_literal: true
source "https://rubygems.org"
gem "rails", '7.0.4'
その後 bundle install
bundler 経由で rails が使えるようになったので、bundle exec rails new
を実行。
参考サイトに従ってオプションを指定。
途中、Gemfileをオーバーライドしてよいか聞かれるので Y
$ rails new \
--skip-action-mailer \ # サンプルなのでいらないもの
--skip-action-mailbox \ # サンプルなのでいらないもの
--skip-action-text \ # サンプルなのでいらないもの
--skip-active-job \ # サンプルなのでいらないもの
--skip-active-storage \ # サンプルなのでいらないもの
--skip-action-cable \ # sprockets/webpackerなしなのでいらないもの
--skip-asset-pipeline \ # sprockets/webpackerなしなのでいらないもの
--skip-javascript \ # sprockets/webpackerなしなのでいらないもの
--skip-hotwire \ # sprockets/webpackerなしなのでいらないもの
--skip-test \ # rspecを使うため
--skip-bundle \
.
以下のディレクトリもいらないらしいので消しておく。
rm -rf app/assets app/helpers
フロントエンドで必要として npm install したもの一覧
{
"dependencies": {
"axios": "^1.7.8",
"destyle.css": "^4.0.1",
"vue": "^3.5.13",
"vue-axios": "^3.5.2"
},
"devDependencies": {
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
"@babel/preset-typescript": "^7.26.0",
"@types/webpack-env": "^1.18.5",
"assets-webpack-plugin": "^7.1.1",
"babel-loader": "^9.2.1",
"babel-preset-typescript-vue3": "^2.0.17",
"clean-webpack-plugin": "^4.0.0",
"css-loader": "^7.1.2",
"sass": "^1.81.1",
"sass-loader": "^16.0.3",
"style-loader": "^4.0.0",
"ts-loader": "^9.5.1",
"typescript": "^5.7.2",
"vue-loader": "^17.4.2",
"vue-style-loader": "^4.1.3",
"vue-template-compiler": "^2.7.16",
"webpack": "^5.97.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.1.0",
"webpack-merge": "^6.0.1"
}
}
フロントに必要なディレクトリ
mkdir -p app/frontend/{assets,components,styles,plugins}
- assets: 画像等を入れるところかな
- components: Vue コンポーネント配置場所
- styles: styleファイル置き場
- plugins: ここなんだろ
rails で空ページが表示できる状態にしておくみたい
routes.rb
Rails.application.routes.draw do
root 'pages#home'
get 'home', to: 'pages#home'
get 'user', to: 'pages#user'
end
pages_controller.rb
class PagesController < ApplicationController
def home
render 'empty'
end
def user
render 'empty'
end
end
render で指定した empty ファイルの作成
$ mkdir app/views/pages && touch app/views/pages/empty.html
app/views/layouts/application.html.erb も少し書き換え
<!DOCTYPE html>
<html>
<head>
<title>RailsVueSample</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
</head>
<body>
</body>
</html>
$ bundle exec rails s
で空のページが表示されることを確認
ここから frontend のファイルを作っていく
まずエントリポイントとなる app/frontend/main.ts
//
// Styles
//
import 'destyle.css'
//
// Scripts
//
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
reset cssの読み込み、vue と App.vue をimportして初期化、#appにマウントする。
どうでもいいけどマウントの指定でよくあるのって #id だよね
ここでちょっと詳しく調べたいファイルが登場した。
まあでも説明しれくれている通りで、TypeScript を vue ファイルで扱えるように、というイメージで良さそう。
ここでの目的「Vue で書かれた SPA のエントリポイントを Rails から配信するパターンについて知る」とはそれるので、その程度の理解で進める。
TypeScriptでVue.jsが使えるように以下のファイルも用意しておきます。
/* eslint-disable */
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
webpack.config.js も追加
ざっくり
- main.ts をエントリーファイルに指定
- バンドル後の出力先は public/dist
- 拡張子 ".js", ".ts", ".scss", ".vue" の省略
- 各種ローダーの設定
- 開発用・本番用ビルド毎の設定
const path = require('path')
const AssetsPlugin = require("assets-webpack-plugin")
const { CleanWebpackPlugin } = require("clean-webpack-plugin")
const { merge } = require("webpack-merge");
const { VueLoaderPlugin } = require("vue-loader")
let config = {
entry: "./app/frontend/main.ts",
output: {
path: path.resolve(__dirname, "public", "dist"),
},
resolve: {
extensions: [".js", ".ts", ".scss", ".vue"],
alias: {
vue: "@vue/runtime-dom",
vue$: "vue/dist/vue.esm.js",
},
},
module: {
rules: [
{
test: /\.vue$/,
loader: "vue-loader",
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: [
"@babel/preset-env"
]
}
},
},
{
test: /\.ts$/,
use: [
{
loader: "babel-loader",
options: {
presets: [
"@babel/preset-env",
"babel-preset-typescript-vue3",
"@babel/preset-typescript",
],
},
},
],
},
{
test: /\.css$/,
use: ["vue-style-loader", "css-loader"],
},
{
test: /\.scss$/,
use: ["vue-style-loader", "css-loader", "sass-loader"],
},
],
},
plugins: [
new AssetsPlugin({ removeFullPathAutoPrefix: true }),
new VueLoaderPlugin(),
],
};
module.exports = (env, argv) => {
if (argv.mode === "development") {
config = merge(config, {
output: {
filename: "build.js",
publicPath: "http://localhost:3001/",
},
devtool: "eval",
devServer: {
port: "3001",
hot: true,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
"Access-Control-Allow-Headers": "X-Requested-With, content-type, Authorization",
},
},
});
} else if (argv.mode === "production") {
config = merge(config, {
output: {
filename: "build-[fullhash].js",
publicPath: "dist/",
},
plugins: [
new CleanWebpackPlugin()
],
})
}
return config
}
package.json に以下追加
- dev: 開発用に webpack-dev-server からフロントエンドの成果物を配信して変更をWatchしてくれる
- build: フロントエンドのビルド
+ "scripts": {
+ "dev": "webpack-dev-server --mode=development",
+ "build": "webpack --mode=production"
+ },
ビルド成果物の出力先ディレクトリを作成
$ mkdir public/dist
ページコンポーネントの作成
Home.vue
と User.vue
Home.vue
<script setup lang="ts">
const msg = "Hello Home"
</script>
<template>
<div class="home">
<h1 class="title">{{ msg }}</h1>
</div>
</template>
<style lang="scss" scoped>
.home {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100vh;
background: #c1c1ff;
> .title {
font-size: 2rem;
font-weight: bold;
}
}
</style>
User.vue
<script setup lang="ts">
</script>
<template>
<div class="user">
<h1 class="title">Users</h1>
</div>
</template>
<style lang="scss" scoped>
.user {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100vh;
background: #ffd0d0;
> .title {
font-size: 2rem;
font-weight: bold;
}
}
</style>
- buildしたファイルを読み込めるようにする
さて、buildしたファイルを読み込めないといけません。
実はwebpackを設定したときにassets-webpack-pluginというものを設定していたのですが、気がついたでしょうか?
これはbuildやwatchをしたときにファイルの出力先をjsonで出力してくれるプラグインです。
デフォルトではファイル名はwebpack-assets.jsonになっています。
こっから大事なところだ
webpack-assets.jsonの中身は
- watchしたとき
{"main":{"js":"http://localhost:3001/build.js"}}
- buildしたとき
{"main":{"js":"dist/build-fb8c6bd492d5c8b2a4db.js"}}
このファイルをrailsで読み込んでファイルの出力先を取得すれば良さそうです
なるほど、確かにそうすれば、Railsから配信するHTMLにVueのエントリーファイルを埋め込むことが可能だね
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
+ def script_for(bundle)
+ JSON.load(File.open(Rails.root.join('webpack-assets.json')))[bundle]['js']
+ end
end
受け取った bundle をキーとして、webpack-assets.json から対応するファイル名を取得するメソッド
app/controllers/pages_controller.rb
class PagesController < ApplicationController
+ before_action :set_script_path
def home
render 'empty'
end
def user
render 'empty'
end
+ private
+
+ def set_script_path
+ @script_path = script_for('main')
+ end
end
キーが main になってるファイル名を @script_path に代入しておく
それを使って、配信するHTMLに script タグを埋め込む
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>RailsVueSample</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
</head>
<body>
+ <script src="<%= @script_path %>"></script>
</body>
</html>