以前、Vue.js と Rails の組み合わせをする記事を複数書いていました。
今までの組み合わせ方が、正直なところあまりよく用いられる方法でないようでした。
CORS を解決して、組み合わせる方法を確認できたので、メモがてらまとめておきます。
目次
参考
作るもの
フロントを vue.js
バックエンドの API サーバーを rails で作成し、ログイン機能を用意する。
概要
今回用意する構成の概要は以下の通りです。

(drow.io で描いてみました。Rails のアイコンと Vue.js のアイコン?もあったので便利でした。)
準備 1 API - Rails 側
これまでは認証の機能を自前で作っていましたが、今回は devise を使用して用意します。
- ログインユーザーを管理する
Account
を devise を使用して作る
- 認証の API と、ユーザー情報問い合わせの API を用意する
- CORS 対応する
以下、Rails の環境設定が済んでいてYay! You’re on Rails!
の画面は確認できているものとします。
API の作成
devise 設定 環境用意
devise を使用してAccount
モデルを作成します。
Gemfile に以下を記述し、bundle install
を実行します。
1 2 3 4 5 6 7 8
| bundle exec rails generate devise:install
bundle exec rails generate devise Account
bundle exec rails db:migrate
|
ここまでで、devise の設定が行われますが、ルーティングは不要なのでskip
で除外します。
config/routes.rb1 2 3 4 5
| Rails.application.routes.draw do devise_for :accounts, skip: [:sessions, :passwords, :registrations] end
|
コントローラの用意
app/controllers/api
以下のコントローラのベースになるapp/controllers/api/application_controller.rb
。
app/controllers/api/application_controller.rb1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Api::ApplicationController < ActionController skip_before_action :verify_authenticity_token
before_action :check_xhr_header
private def check_xhr_header return if request.xhr?
render json: { error: 'forbidden' }, status: :forbidden end end
|
ログイン・ログアウト処理を行うapp/controllers/api/session_controllser.rb
。
app/controllers/api/session_controllser.rb1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Api::SessionController < Api::ApplicationController def log_in account = Account.find_for_database_authentication(email: account_param[:email])
return render json: {result: false} if account.nil?
if account.valid_password?(account_param[:password]) sign_in :account, account render json: {state: true} else render json: {state: false} end end
def log_out sign_out current_account render json: {state: true} end
private def account_param params.require(:account).permit(:email, :password) end end
|
ログインしたユーザーがアカウント情報を問合せできるapp/controllers/api/accounts_controller.rb
。
app/controllers/api/accounts_controller.rb1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Api::AccountsController < Api::ApplicationController
before_action :account_login
def show render json: {email: current_account[:email]} end
private def account_login return render json: {} unless account_signed_in? end end
|
ルーティングの用意
config/routes.rb1 2 3 4 5 6 7 8 9
| Rails.application.routes.draw do devise_for :accounts, skip: [:sessions, :passwords, :registrations]
namespace :api do post '/login', to: 'session#log_in' post '/logout', to: 'session#log_out' get '/account', to: 'accounts#show' end end
|
CORS 設定
rack-cors
を使用して CORS の設定を追加します。
Gemfile に以下を記述し、bundle install
を実行します。
rack-cors
の設定を記述します。
config/initializers/cors.rb1 2 3 4 5 6 7 8 9 10 11 12
| Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins "localhost:3001" resource "^/api", headers: :any, methods: [:get, :post, :patch, :delete, :head, :options], credentials: true end end
|
ここまでの設定で、localhost:3001
にアクセスしたクライアントからの xhr(XMLHttpRequest) のみに応答するようになりました。
準備 2 フロント - Vue.js 側
先に作成した API へアクセスするためのフロント側のアプリケーションを作成します。
Vue.js 環境準備
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| npm install @vue/cli --save-dev
npx vue create test-app
> . ([Vue 2] babel, router) Default ([Vue 2] babel, eslint) Default (Vue 3 Preview) ([Vue 3] babel, eslint) Manually select features
cd test-app
npm install axios --save
|
vue.config.js
を作成して、開発サーバーのポートと、proxy の設定を追加します。
vue.config.js1 2 3 4 5 6 7 8 9 10 11 12
| module.exports = { devServer: { port: 3001, proxy: { "^/api": { target: "http://localhost:3000", }, }, }, };
|
ここまで出来たら、npm run serve
で開発サーバーを起動します。
API を叩くためのページの用意
vue-router のルーティング記述
src/router/index.js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import Vue from "vue"; import VueRouter from "vue-router"; import index from "../views/index.vue"; import login from "../views/Login.vue"; import login from "../views/Home.vue";
Vue.use(VueRouter);
const routes = [ { path: "/", name: "Index", component: index, }, { path: "/login", name: "Login", component: login, }, { path: "/home", name: "Home", component: home, }, ];
const router = new VueRouter({ mode: "history", base: process.env.BASE_URL, routes, });
export default router;
|
ページ作成
/
の時に表示するIndex.vue
。
src/views/Index.vue1 2 3 4 5
| <template> <div class="Index"> <h1>This is index page</h1> </div> </template>
|
/login
の時に表示するsrc/views/Login.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| <template> <div class="login"> log in <div> <div>{{ this.message }}</div> <input type="text" v-model="email" placeholder="NAME" /><br /> <input type="password" v-model="password" placeholder="PASSWORD" /><br /> <button v-on:click="login()">LOGIN</button> </div> </div> </template>
<script> import axios from "../../util/axios"; const qs = require("qs");
export default { name: "logIn", data: function () { return { email: "", password: "", message: "", }; }, methods: { async login() { const self = this; const result = await axios .post("/api/login", { account: { email: this.email, password: this.password, }, paramsSerializer: function (params) { return qs.stringify(params, { arrayFormat: "brackets" }); }, }) .catch((e) => { console.error(e); });
if (!result) { this.message = "エラー"; return; } if (!result.data) { this.message = "エラー"; return; }
if (result.data.state) { this.$router.push("/home"); } }, }, }; </script>
|
/home
の時に表示するHome.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| <template> <div class="home"> <span>{{ email }}</span> </div> </template>
<script> import axios from "../../util/axios";
export default { name: "Home", data() { return { email: "", }; }, methods: { async getAccountData() { const result = await axios.get("/api/account").catch((e) => { console.error(e); });
if (!result) { this.redirectLogin(); return; } if (!result.data.email) { this.redirectLogin(); return; }
this.email = result.data.email; }, redirectLogin() { this.$router.push("/login"); }, }, async mounted() { this.getAccountData(); }, }; </script>
|
ログアウトのリンクのコンポーネント。
src/components/Logout.vue1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| <template> <a v-on:click="login()">Logout</a> </template>
<script> import axios from "../../util/axios";
export default { name: "logout", methods: { async login() { const self = this; const result = await axios.post("/api/logout").catch((e) => { console.error(e); });
if (!result) { this.message = "エラー"; return; } if (!result.data) { this.message = "エラー"; return; }
if (result.data.state) { this.$router.push("/"); } }, }, }; </script>
|
axios のカスタマイズ
バックエンドの rails への post をするために、http クライアントとして使用する axios のカスタマイズを行います。
util/axios.js1 2 3 4 5 6 7 8 9 10
| import axios from "axios";
export default axios.create({ headers: { "X-Requested-With": "XMLHttpRequest", }, withCredentials: true, });
|
先に示した.vue
ファイルでは、こちらのデフォルト設定を施したaxios
を使わせます。
リクエストヘッダーに"X-Requested-With": "XMLHttpRequest"
を付与し、Rails 側ではrequest.xhr?
にて xhr のリクエストであるかをチェックしています。
拡張ヘッダーは、いわゆるフォームでリクエストする際には使用ができません。
そのうえで、origin のチェックを行うことで、CSRF を防ぎます。
動作確認
2 つのコンソールでnpm run serve
とbundle exec rails s
の 2 つを実行します。
localhost:3001
にアクセスして、ログインを試します。
/
では、機能がないページを表示しているので、Login
を押して遷移します。

ログインのフォームが表示されるので、入力します。

ログインすると、/home
に遷移して、メールアドレス(マスクしています)が表示されます。

ログアウトすると/login
に遷移されます。
ログインしていない状態で、/home
に遷移しても、/login
に戻されます。
ドメインが異なるバックエンドのサーバーへのリクエストを送り、ログインの機能を作ることができました。
今回は、これまでやってきたログイン機能の実装を別ドメインのサーバーで立ててもできるように試みました。
これまでの rails の public にビルドしたファイルを展開させる方法よりもよほどこちらの方がいいと感じています。
ホットリロードも効きますし。
フロントエンドは変わってもバックエンドを Rails で立てるなら毎回この構成でいいと感じました。
気が付いたらログインログアウトの機能だけで、4 回ほど記事を書いてしまったので、そろそろ別なことがしたいですがややネタ切れです。
ではでは。