Rails + Vue3 + TypeScriptで新しいプロジェクトを作ってみた


Vue3がリリースされたみたいなのでRailsとvue、tsで新しくプロジェクトを作てみました。
後から気づいたけど、vue/cliを使えばもvue3とTypeScriptを同時インストールできたかも?
コード: https://github.com/tOba1357/vue3_rails/tree/73e679541da8308010fde8eab0da3298b77a6f48

setup rails

rbenv local 2.7.2

Gemfile作成

source 'https://rubygems.org'
gem 'rails', '6.0.3.4'

railsプロジェクト作成

bundle install
bundle exec rails new -d postgresql --api --skip-action-mailer --skip-active-storage --skip-action-cable .
# database.ymlを編集
./bin/rails s

install vue3

参考: https://v3.vuejs.org/guide/installation.html

npm init vite-app frontend
cd frontend
nodenv local 14.15.0
npm install

defaultのportが3000でrailsと被っているのでviteのportを8080に変更とaliasの設定をします。
webpackと違って@...ができないみたいなので代わりに'/@/...'を使います。https://github.com/vitejs/vite/issues/88
frontend/vite.config.js

const path = require('path')

export default {
    port: 8080,
    alias: {
        '/@/': path.resolve(__dirname, 'src')
    }
}
npm run dev

あとは、http://localhost:8080/ にアクセスしてみて表示されればおk

setup TypeScript

参考: https://v3.vuejs.org/guide/typescript-support.html
tsconfig.json作成

// tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    // this enables stricter inference for data properties on `this`
    "strict": true,
    "jsx": "preserve",
    "moduleResolution": "node",
    "baseUrl": ".",
    "paths": {
      "/@/*": [
        "src/*"
      ]
    }
  }
}

install TypeScript

npm install --global @vue/cli
vue add typescript

frontend/src/App.vueを編集

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <HelloWorld msg="Hello Vue 3.0 + Vite" />
</template>

<script lang="ts">
import HelloWorld from './components/HelloWorld.vue'
import { defineComponent } from 'vue'

export default defineComponent({
  components: {
    HelloWorld
  }
})
</script>

フロントのサーバをリスタートして http://localhost:8080/ にアクセスして、同じように表示されればおk

vueからRailsApiを叩く

ここでは、ユーザの作成して一覧取得する機能を作成します。

userモデルとcontroller作成

nameだけ持ったuserを作成します。

bin/rails g model user

migration

class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :name, null: false
      t.timestamps
    end
    add_index :users, :name, unique: true
  end
end
bin/rails g db:migrate

app/controllers/users_controller.rb

class UsersController < ApplicationController
  def index
    render json: User.all
  end

  def create
    user = User.new(user_params)
    if user.save
      render json: user, status: 201
    else
      render json: user.errors.full_messages, status: 400
    end
  end

  private
    def user_params
      params.require(:user).permit(:name)
    end
end

config/routes.rb

Rails.application.routes.draw do
  resources :users
end

作ったAPIをAdvanced REST clientを使って叩いてみる。
user作成

user取得

vueからuser作成、一覧取得

CORSの対応

rack-cors(https://github.com/cyu/rack-cors)というgemを使ってCORSを許可。
Gemfileに

gem 'rack-cors'

を追加、bundle installして config/initializers/cors.rbを下記のよう編集

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'localhost:8080'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

axiosの設定

api通信するようにaxiosをinstall。

npm install axios

hostが異なるので、hostを設定したaxiosを使うようします。
frontend/src/lib/axios.ts

import axios from 'axios'
export default axios.create({
    baseURL: 'http://localhost:3000/',
})

user一覧取得と、作成

ここからは私の理想のtsの構成になっていますので、参考程度にしてください。

Userモデルを作成。
フロントで扱うuserモデルを定義します。
dayjsを使っているのでdayjsをインストールしてください。
frontend/src/models/user.ts

import dayjs, {Dayjs} from 'dayjs';


export default class User {
    id: number
    name: string
    createdAt: Dayjs
    updatedAt: Dayjs


    constructor(id: number, name: string, createdAt: string, updatedAt: string) {
        this.id = id
        this.name = name
        this.createdAt = dayjs(createdAt)
        this.updatedAt = dayjs(updatedAt)
    }
}

フロントでrailsのapiを叩く機能を抽出します。
userを作成する時のパラメータの定義と、responseからUserモデルを作成する機能をここで持ちます
frontend/src/apis/users_api.ts

import axios from "/@/lib/axios"
import User from "/@/models/user"

function createUserFromResponse(res: any): User {
    return new User(res.id, res.name, res.created_at, res.updated_at)
}

export const getUsers: () => Promise<User[]> = async () => {
    const res = await axios.get('/users')
    return res.data.map((res: any) => createUserFromResponse(res))
}

export interface UserCreateParams {
    name: String
}

export const createUser: (params: UserCreateParams) => Promise<User> = async (params: UserCreateParams) => {
    const res = await axios.post('/users', {user: params})
    return createUserFromResponse(res.data)
}

最後にコンポーネントを作って終わりです。
frontend/src/App.vue

<template>
  <div>
    <form>
      <label>
        name
        <input v-model="form.name" type="text"/>
      </label>
      <button @click.prevent="createUser">保存</button>
    </form>

    <table>
      <thead>
      <tr>
        <th>id</th>
        <td>name</td>
        <td>createdAt</td>
        <td>updatedAt</td>
      </tr>
      </thead>
      <tbody>
      <tr v-for="user in users" :key="user.id">
        <th>{{ user.id }}</th>
        <td>{{ user.name }}</td>
        <td>{{ user.createdAt.format() }}</td>
        <td>{{ user.updatedAt.format() }}</td>
      </tr>
      </tbody>
    </table>
  </div>
</template>

<script lang="ts">
import {createUser, getUsers} from '/@/apis/users_api'
import {defineComponent} from 'vue'
import User from '/@/models/user'

export default defineComponent({
  data() {
    return {
      form: {
        name: '' as string
      },
      users: [] as User[]
    }
  },
  methods: {
    async createUser() {
      const user = await createUser(this.form)
      this.users.push(user)
      this.form.name = ''
    }
  },
  async created() {
    this.users = await getUsers()
  }
})
</script>

defineComponentにして型定義するだけで、あとはVue2と変わらないですね。

最後に

意外と簡単に型の指定できました。Vue2でもCompositionAPIを入れればできたのかな?
今度はVuexを使った実装もやっていきます!

Next: Vue3のComposition APIを試してみる