1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

railsとviteとvue.jsによる簡単なウェブサイトを作ってみたり

Last updated at Posted at 2025-02-04

この記事はruby on railsとvue.js(特にvuetify)でウェブサイトを作る例を説明します。railsの中でvue.jsを使うにはvite_railsというgemを使います。

前置き

まずはこの記事を書くことになったきっかけについてです。(飛ばしても大丈夫です)

数年前からrailsによって書いたウェブサイトを更新しようとしています。そのウェブサイトはrailsの中でvue.jsを入れる為にwebpackerを使っていたのですが、webpackerはもうオワコンになっているので、更新するなら移行するべきだことになりました。webpackerの代わりの選択肢は色々ありますが、今回選択したのviteです。vite_railsというgemでrailsと一緒にvitを使うことができます。

viteはvue.jsの作者である尤雨溪によって作られた開発用サーバーなので、現在vue.jsを使う時に一番おすすめの手段でもあります。勿論react.jsなどを使う場合も同様にviteを使うことができます。

ということで私も使う為にvite_railsの使い方を勉強しました。そして折角だから、勉強したことを記事に書くことにしました。

私はrailsに関する記事を書くのは今回始めてですが、ずっと前から使っていました。今回記事を書くことで私もrails関連の理解の整理ができました。

ちょっと余談ですが、丁度最近deepseekが話題になっているので、今回のvite_railsの勉強は主にdeepseekで行いました。viteをおすすめしたのもdeepseekです。今まで本を読んだりgoogleで検索したりすることで勉強してきたのですが、このようにチャットボットで勉強するのは初めてです。

この記事で使う実装コードも主にdeepseekが生成してくれたものをベースにしています。ただし私なりにだいぶ整理しました。それにdeepseekの書いたことは完璧であるわけではなく、そのまま実行してエラーになることもあります。結局聞き直したり、googleで検索したりして完成させる必要があります。それでもgoogleばかり頼る時代より勉強が速いと思います。

これは意外といい勉強し方かもしれません。deepseekがもっと早く現れたらよかったのに、と思うくらい。

こんなものが人気になっていくと、qiitaなどの記事を読む人や書く人が少なくなるのではないか……とつい思ってしまいますが、とりあえず今はそこまでではないでしょう。

余談はここまで。次は本題に入ります。

概要

今回作ったウェブサイトはただ新幹線駅データを閲覧したり編集したりする簡単で小さなウェブサイトです。これを選んだ理由はただ何かデータベースを使う例をしたくて、そして最近新幹線のことを勉強しているところでもあって、先月丁度この記事を書きました。

今回使うデータはこの記事から取ったものです。ただし全部使うのは多すぎるので、東京駅から鹿児島中央駅までの東海道山陽新幹線と九州新幹線の全部47駅だけ使うことにしました。

こうやってできたウェブサイトはこれです。

截屏2025-02-04-23.02.02.png

これのテーブルはvuetifyコンポネントによるもので、ページングも自動で作られています。

「編集」ボタンを押したらその駅データの編集ページに入ります。

截屏2025-02-04-23.08.51.png

下にある「新しいデータを追加」ボタンを押したら追加ページに入ります。

截屏2025-02-04-23.02.39.png

全部3ページあります。これだけです。チュートリアルらしく簡単に作れるものですね。

実装

環境

railsもvue.jsもバージョンによって大きく違う場合が多いので、今回で使ったライブラリのバージョンも記述しておきましょう。

ruby 3.4.1
rails 8.0.1
vite_rails 3.0
vite 5.4.14
vite-plugin-ruby 5.1.1
vite-plugin-vuetify 2.1.0
vue 3.5.13
vuetify 3.7.11
axios 1.7.9

プロジェクト作成とパッケージのインストール

まずはプロジェクトの作成です。rails+vite+vue.jsなので、名前は適当に合わせてravivuにします。

rails new ravivu -J -S -T -M -G -C --skip-docker --skip-kamal --skip-solid
cd ravivu

ここで-Jというオプションはturbo-railsなど基本のjavascript関連の機能を入れないようにする為です。代わりにviteを使うから。

他のオプションはただ必要ないものを作らないようにする為だけです。入れなくても普通に使えます。作りたいプロジェクトや好みにもよります。詳しくは本題と関係なくてそこまで気にしなくてもいいので割愛します。railsはデフォルトで色んないいものを準備してくれて便利ですが、そこまで要らない場合も多くて逆に取り除く手間がかかりますね。

次はviteを追加します。

bundle add vite_rails
bundle exec vite install

そしてyarnでvue.js関連のパッケージを追加します。

yarn add vue @vitejs/plugin-vue

今回vuetifyも使うのでその為に必要なパッケージも追加します。

yarn add vuetify vite-plugin-vuetify @mdi/font sass-embedded

ウェブサイトの中でAPIとウェブページの間のデータやり取りを行う為にaxiosを使うのでこれも入れておきます。

yarn add axios

基本的な設定の調整

次は色んな設定ファイルを修正します。まずはviteの設定です。

vite.config.ts
import { defineConfig } from 'vite'
import RubyPlugin from 'vite-plugin-ruby'
import vue from '@vitejs/plugin-vue'
import vuetify from 'vite-plugin-vuetify'

export default defineConfig({
  plugins: [
    RubyPlugin(),
    vue(),
    vuetify({ autoImport: true }),
  ],
})

次はウェブサイトのルーティングです。今回はメインページ、データ追加ページ、個別データ編集ページ、全部3ページを作ります。又、データベースと接続為のAPIも必要です。

config/routes.rb
Rails.application.routes.draw {
  root "home#index"
  get "add" => "home#add"
  get "edit/:id" => "home#edit"
  resources "api", only: [:create, :update, :destroy]
}

これで6つのルーティングができます。

  パス メソッド コントローラー
メインページ / get home#index
データ追加ページ /add get home#add
データ編集ページ /edit/ {id} get home#edit
データ追加API /api post api#create
データ編集API /api/ {id} patch api#update
データ削除API /api/ {id} delete api#destroy

尚、CORS問題を防ぐ為にconfigフォルダの中のapplication.rbにこれを入れる必要があります。

config/application.rb
config.action_controller.forgery_protection_origin_check = false

これを設定しないとproductionの時にaxiosを使うところにこのようなエラーが出ます。

[9b52a8e7-e50c-4c18-843e-f462af1086f7] HTTP Origin header (http://127.0.0.1:3000) didn't match request.base_url (https://127.0.0.1:3000)
[9b52a8e7-e50c-4c18-843e-f462af1086f7] Completed 422 Unprocessable Content in 1ms (ActiveRecord: 0.0ms (0 query, 0 cached) | GC: 0.0ms)

実は私は最初はこの問題にぶつかって解決する為に数時間もかけたので、特に注意して欲しいです。

これはrack-corsを使っても解決できません。尚、一般公開するAPIを作る場合rack-corsを使う必要があるはずですが、今回はウェブサイトの中でデータのやり取りをするだけなのでrack-corsとは関係ありません。

その他にaxios.defaults.headersの設定も必要です。これについては後述のapplication.tsに書きます。

又、これは必須ではないが、個人的おすすめ。ファイル名や変数名にsが勝手に追加されることを防ぐ為のおまじないです。

config/initializers/inflections.rb
ActiveSupport::Inflector.inflections { |inflect|
  inflect.plural /^.*\S$/, '\0'
  inflect.singular /^.*\S$/, '\0'
}

データベース

駅のテーブルのデータを保存するデータベースの設定です。実際にproductionの時はpostgresやmysqlを使う場合が多いが、今回は簡単な為にdevelopmentもproductionもデフォルトのsqliteを使うことにします。

まずはyamlでのの設定です。

config/database.yml
default: &default
  adapter: sqlite3
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  <<: *default
  database: storage/development.sqlite3

production:
  <<: *default
  database: storage/production.sqlite3

次はこのようにデータテーブルを作りたいです。

  列名 データ型
駅名 namae text
読み方 yomi text
都道府県 ken text
開業日時 kaigyou date

これを実行することでその通りのテーブルを定義します。ただしidというプライマリキーと、追加と更新の日付の列も、自動的に追加されます。

rails g model eki namae yomi ken kaigyou:date

そうしたらdb/migrate/フォルダの中にデータベースのテーブルを作成するrubyコードが生成されます。特に変更する必要なくこのままmigrateを実行します。

rails db:migrate

今回デフォルトであるsqliteを使うので、storageフォルダの中にdevelopment.sqlite3ファイルができます。

次はデータの準備です。データを入れる為のseeds.rbを使います。

db/seeds.rb
ekidata = %w[
  東京,とうきょう,東京都,1964-10-01
  品川,しながわ,東京都,1964-10-01
  新横浜,しんよこはま,神奈川県,1964-10-01
  小田原,おだわら,神奈川県,1964-10-01
  熱海,あたみ,静岡県,1964-10-01
  三島,みしま,静岡県,1964-10-01
  新富士,しんふじ,静岡県,1988-03-13
  静岡,しずおか,静岡県,1964-10-01
  掛川,かけがわ,静岡県,1988-03-13
  浜松,はままつ,静岡県,1964-10-01
  豊橋,とよはし,愛知県,1964-10-01
  三河安城,みかわあんじょう,愛知県,1988-03-13
  名古屋,なごや,愛知県,1964-10-01
  岐阜羽島,ぎふはしま,岐阜県,1964-10-01
  米原,まいばら,滋賀県,1964-10-01
  京都,きょうと,京都府,1964-10-01
  新大阪,しんおおさか,大阪府,1964-10-01
  新神戸,しんこうべ,兵庫県,1972-03-15
  西明石,にしあかし,兵庫県,1972-03-15
  姫路,ひめじ,兵庫県,1972-03-15
  相生,あいおい,兵庫県,1972-03-15
  岡山,おかやま,岡山県,1972-03-15
  新倉敷,しんくらしき,岡山県,1975-03-10
  福山,ふくやま,広島県,1975-03-10
  新尾道,しんおのみち,広島県,1988-03-13
  三原,みはら,広島県,1975-03-10
  東広島,ひがしひろしま,広島県,1988-03-13
  広島,ひろしま,広島県,1975-03-10
  新岩国,しんいわくに,山口県,1975-03-10
  徳山,とくやま,山口県,1975-03-10
  新山口,しんやまぐち,山口県,1975-03-10
  厚狭,あさ,山口県,1999-03-13
  新下関,しんしものせき,山口県,1975-03-10
  小倉,こくら,福岡県,1975-03-10
  博多,はかた,福岡県,1975-03-10
  博多南,はかたみなみ,福岡県,1990-04-01
  新鳥栖,しんとす,佐賀県,2011-03-12
  久留米,くるめ,福岡県,2011-03-12
  筑後船小屋,ちくごふなごや,福岡県,2011-03-12
  新大牟田,しんおおむた,福岡県,2011-03-12
  新玉名,しんたまな,熊本県,2011-03-12
  熊本,くまもと,熊本県,2011-03-12
  新八代,しんやつしろ,熊本県,2004-03-13
  新水俣,しんみなまた,熊本県,2004-03-13
  出水,いずみ,鹿児島県,2004-03-13
  川内,せんだい,鹿児島県,2004-03-13
  鹿児島中央,かごしまちゅうおう,鹿児島県,2004-03-13
]
ekidata = ekidata.map{|eki|
  eki = eki.split(",")
  {
    namae: eki[0],
    yomi: eki[1],
    ken: eki[2],
    kaigyou: eki[3]
  }
}
Eki.create!(ekidata)

そしてseedの実行。

rails db:seed

これでデータが整いました。

コントローラー

次はルーターで定義した通りコントローラーを作ります。

app/controllers/api_controller.rb
class ApiController < ApplicationController
  def create
    konoeki = Eki.create(params[:ekidata].permit(:id, :namae, :yomi, :ken, :kaigyou))
    render(json: konoeki)
  end

  def update
    konoeki = Eki.find(params[:id])
    ekidata = params[:ekidata].permit(:id, :namae, :yomi, :ken, :kaigyou)
    konoeki.update(ekidata)
    render(json: konoeki)
  end

  def destroy
    konoeki = Eki.find(params[:id])
    konoeki.destroy
    render(json: konoeki[:namae]+"駅を削除完了")
  end
end
app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
    @taitoru = "新幹線駅一覧"
    @jsondata = Eki.all.to_json
  end

  def add
    @taitoru = "駅追加"
    @jsondata = ""
  end

  def edit
    @jsondata = Eki.find(params[:id])
    @taitoru = @jsondata.namae+"駅編集"
    @jsondata = @jsondata.to_json
  end
end

apiの方は応答のデータをそのままjsonとしてrenderしますが、homeはjsonを@jsondataという変数に入れておいてその後vueから使います。

@taitoruはページのtitleで、erbレイアウトの中で使われます。

erb

ウェブサイトの各ページはvueで定義するので、erbはただレイアウトページ1つだけでいいです。

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title><%= @taitoru %></title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= vite_client_tag %>
    <%= vite_typescript_tag "application" %>
    <%= vite_stylesheet_tag "application.scss" %>
  </head>
  <body>
    <div id="jsondata" hidden><%= @jsondata.html_safe %></div>
    <div id="app"></div>
  </body>
</html>

ここで<div id="app"></div>はvueで形を作る部分です。

vite_typescript_tagはTypeScriptを使う場合であり、JavaScriptを使う場合はvite_javascript_tagとなります。

そして各ページのerbです。ただしvueでページを作るつもりだから実は使わないが、railsのルールでは各ページに合わせるerbがないとエラーになるから、わざわざ空っぽなerbを作る必要があります。今回3あるのでこの通りに作ります。

app/views/home/index.html.erb
app/views/home/add.html.erb
app/views/home/edit.html.erb

entrypoint

railsプロジェクトにviteを追加した時にapp/frontend/entrypointsの中にapplication.jsは自動的に作成されているはずです(ただしもしプロジェクト作成の時に-Jオプションを付けていなかった場合、場所はapp/javascript/entrypointsになる)。今回TypeScriptを使うので、application.tsに書き換えます。

app/frontend/entrypoints/application.ts
import { createApp } from "vue";
import { createVuetify } from "vuetify";
import { ja } from "vuetify/locale";

const vuetify = createVuetify({
  locale: {
    locale: "ja",
    messages: { ja },
  },
  theme: {
    defaultTheme: 'darkTheme',
    themes: {
      darkTheme: {
        dark: true,
        colors: {
          qiita: "#55c500",
          pixiv: "##0097f7",
          yahoo: "#f70131",
          deepseek: "#4d6bfe",
        }
      },
    },
  },
});

import indexPage from "../index-page.vue";
import addPage from "../add-page.vue";
import editPage from "../edit-page.vue";

document.addEventListener("DOMContentLoaded", () => {
  if(document.URL.includes("edit")){
    var app = createApp(editPage);
  }
  else if(document.URL.includes("add")) {
    var app = createApp(addPage);
  }
  else {
    var app = createApp(indexPage);
  }
  app.use(vuetify);
  app.mount("#app");
});

import axios from "axios";
axios.defaults.headers["X-CSRF-TOKEN"] = document.getElementsByName("csrf-token")[0].getAttribute("content");

ここでURLによって違うvueページが使われます。ページを分ける手段としてはvue-routeなどがありますが、今回は単純にifで分岐することにします。あまり簡潔でないかもしれないけど、わかりやすくて直感的だから。

themeは色のテーマの設定するのです。特徴を持つウェブサイトの名前を色の名前に使います。

localeは表示する言語の設定です。これはvuetifyのv-data-tableなどの表示に反映します。

尚、vuetifyのv-data-tableとテーマを使う例は以前の記事に書いたことがあります。今回は実はその記事を参考にした部分が多いです。

しかし4年も経ってvueもvuetifyも使い方が大分変わったので、その記事の使い方はほぼもう使い物にならなく、ただ参考程度にしかない状態になっていますね。

その他にスタイルシートファイルもこのように追加します。

app/frontend/entrypoints/application.scss
@use 'vuetify/styles';
@use '@mdi/font/css/materialdesignicons.css';

vue

最後にページの形を作る.vueファイルです。
メインページ。

app/frontend/index-page.vue
<template>
  <v-app>
    <v-main>
      <v-container>
        <div class="text-h4 px-4 py-3">新幹線駅一覧</div>
        <v-data-table :items="eki_list" :headers="header_list" :items-per-page="8"
          :items-per-page-options="[8, 20, -1]">
          <template #item.namae="{ item: { namae } }">
            <a :href="'https://ja.wikipedia.org/wiki/' + namae + '駅'" target="_blank" style="font-size: 13pt;">{{ namae
              }}</a>
          </template>
          <template #item.kaigyou="{ item: { kaigyou } }">
            <v-sheet v-if="kaigyou">{{ (kaigyou as string).replace("-", "").replace("-", "") + "" }}</v-sheet>
          </template>
          <template #item.henshuu="{ item: { id } }">
            <v-btn @click="henshuu(id)" color="pixiv">編集</v-btn>
          </template>
          <template #item.sakujo="{ item: { id } }">
            <v-btn @click="sakujo(id)" color="yahoo">削除</v-btn>
          </template>
        </v-data-table>
        <v-btn @click="tsuika" color="deepseek">新しいデータを追加</v-btn>
      </v-container>
    </v-main>
  </v-app>
</template>

<script lang="ts">
import axios from "axios";

export default {
  name: "indexpage",
  data: () => ({
    eki_list: [],
    header_list: [
      { value: "id", },
      { title: "駅名", value: "namae", },
      { title: "読み方", value: "yomi", sortable: true },
      { title: "都道府県", value: "ken", sortable: true },
      { title: "開業", value: "kaigyou", sortable: true },
      { value: "henshuu" },
      { value: "sakujo" },
    ],
  }),
  methods: {
    tsuika() {
      location.href = "/add";
    },
    henshuu(id: number) {
      location.href = "/edit/" + id;
    },
    sakujo(id: number) {
      if (confirm("消して宜しいの?")) {
        let url = "/api/" + id;
        axios.delete(url)
          .then((res) => {
            if (res.status == 200) {
              location.reload();
            }
          })
          .catch((err) => {
            alert(err);
          });
      }
    },
  },
  mounted() {
    this.eki_list = JSON.parse(document.getElementById("jsondata")!.textContent as string);
  },
};
</script>

データを追加するページ。

app/frontend/add-page.vue
<template>
  <v-app>
    <v-main>
      <v-container>
        <v-card color="deepseek" style="width: 300px;">
          <v-card-title>
            駅データ追加
          </v-card-title>
          <v-card-text>
            <v-sheet v-for="col in Object.keys(ekidata)">
              <v-text-field v-model="ekidata[col]" :label="col" style="margin: 5px" hide-details></v-text-field>
            </v-sheet>
          </v-card-text>
        </v-card>
        <v-btn @click="tsuika" class="mx-1" color="qiita" width="160">追加</v-btn>
        <v-btn @click="modoru" class="my-1" color="yahoo" width="100">取消</v-btn>
      </v-container>
    </v-main>
  </v-app>
</template>

<script lang="ts">
import axios from "axios";

export default {
  name: "addpage",
  data: () => ({
    ekidata: { namae: "", yomi: "", ken: "", kaigyou: "" }
  }),
  methods: {
    tsuika() {
      axios.post("/api/", { ekidata: this.ekidata })
        .then((res) => {
          if (res.status == 200) {
            this.modoru();
          }
        })
        .catch((err) => {
          alert(err);
        });
    },
    modoru() {
      location.href = "/";
    },
  },
};
</script>

データを更新するページ。

app/frontend/edit-page.vue
<template>
  <v-app>
    <v-main>
      <v-container>
        <v-card color="pixiv" style="width: 300px;">
          <v-card-title>
            駅データ編集
          </v-card-title>
          <v-card-text>
            <v-sheet v-for="col in Object.keys(ekidata).slice(0, 5)">
              <v-text-field v-model="ekidata[col]" :label="col" style="margin: 5px" hide-details></v-text-field>
            </v-sheet>
          </v-card-text>
        </v-card>
        <v-btn @click="hozon" class="mx-1" color="qiita" width="160">保存</v-btn>
        <v-btn @click="modoru" class="my-1" color="yahoo" width="100">取消</v-btn>
      </v-container>
    </v-main>
  </v-app>
</template>

<script lang="ts">
import axios from "axios";

export default {
  name: "editpage",
  data: () => ({
    ekidata: {},
    id: null,
  }),
  methods: {
    hozon() {
      axios.patch("/api/" + this.id, { ekidata: this.ekidata })
        .then((res) => {
          if (res.status == 200) {
            this.modoru();
          }
        })
        .catch((err) => {
          alert(err);
        });

    },
    modoru() {
      location.href = "/";
    },
  },
  mounted() {
    this.ekidata = JSON.parse(document.getElementById("jsondata")!.textContent as string);
    this.id = this.ekidata.id;
  },
};
</script>

尚、ページに使うデータはaxiosを使うことでAPIから読み込むという方法もできますが、今回はコントローラーから準備しておいたjsondataを使うことにしています。axiosを使うのはデータの削除/編集/追加の時だけとなっています。

これでコードの準備は整いました。

developmentで起動

開発の時はdevelopmentモードで、2つのサーバーを同時に起動する必要があります。まずviteサーバーを起動します。

vite dev

そしてrailsサーバーの起動。

rails s

そうしたらhttp://127.0.0.1:3000/からウェブサイトにアクセスできるようになります。

productionに向かう

開発が終わったら次は本番に向かいます。まずデータベースの準備ですが、普段developmentとproductionでデータベースが違うので、個別で準備する必要があります。migrateとseedもproductionの環境でもう一度やる必要があります。

rails db:migrate RAILS_ENV=production
rails db:seed RAILS_ENV=production

そしてJavaScriptをプリコンパイルします。これでもう個別で本番ではサーバーを立てる必要がなくなります。

vite build

最後にサーバーを起動します。

rails s -e production

以上これで完成です。

纏め

色んなファイルを編集したり作成したりしたので、見落とさないようにここで纏めておきます。

vite.config.ts
config/routes.rb
config/application.rb
config/database.yml
config/initializers/inflections.rb
db/seeds.rb
app/controllers/api_controller.rb
app/controllers/home_controller.rb
app/views/layouts/application.html.erb
app/views/home/index.html.erb
app/views/home/add.html.erb
app/views/home/edit.html.erb
app/frontend/index-page.vue
app/frontend/add-page.vue
app/frontend/edit-page.vue
app/frontend/entrypoints/application.ts
app/frontend/entrypoints/application.scss

参考

vite_rails

その他

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?