今回初めて Nuxt.js を触りました。
Todoアプリを作ろうかなと思ったのですが, せっかくならAPIを叩こうじゃないかということでサーバーサイドも用意してみました。
サーバーサイドはRuby on Rails(API), クライアントサイドはNuxt.ts(Nuxt.js + TypeScript), DBはpostgresという構成で実装していきます。
環境構築に関しては, サーバーサイド/クライアンドサイド共にDocker上で動かしており, ディレクトリ構成はモノシリックにまとめました。
動作環境
macOS Catalina : version 10.15.4
Docker for macはインストール済みとする。
ディレクトリ構成
.
├── client-side
├── server-side
└── docker-compose.yml
1. サーバーサイド(Ruby on Rails)
Dockerfile作成
server-side/
配下にdockerfileを作成。
FROM ruby:2.7.0
RUN apt-get update -qq && \
apt-get install -y \
build-essential \
libpq-dev \
nodejs \
postgresql-client
WORKDIR /app
COPY Gemfil Gemfile.lock /app/
RUN bundle install
Gemfile, Gemfile.lock作成
同じく server-side/
配下にGemfileとGemfile.lockを作成。
Gemfile内に以下を記述。
source 'https://rubygems.org'
gem 'rails', '6.0.3'
Gemfile.lockは空のままで大丈夫。
docker-compose.yml作成
railsとpostgresの設定をdocker-compose.ymlに書いていきます。
version: '3.8'
volumes:
db_data:
services:
db:
image: postgres
volumes:
- db_data/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: password
server-side:
build: ./server-side/
command: bundle exec rails server -b 0.0.0.0
image: server-side
ports:
- 3000:3000
volumes:
- ./server-side:/server-app
tty: true
stdin_open: true
depends_on:
- db
links:
- db
APIモードで rails new
以下のコマンドを叩けば, server-side/
配下にrails関連のファイル群が作成されます。
$ docker-compose run server-side rails new . --api --force --database=postgresql --skip-bundle
database.yml
の内容を修正
このままだとserver-sideのコンテナからDBのコンテナにアクセスできないので database.yml
の内容を修正します。
以下のようになっていると思うので
default: &default
adapter: postgresql
encoding: unicode
# For details on connection pooling, see Rails configuration guide
# https://guides.rubyonrails.org/configuring.html#database-pooling
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
以下のように編集。
default: &default
adapter: postgresql
encoding: unicode
host: db
user: postgres
password: password
# For details on connection pooling, see Rails configuration guide
# https://guides.rubyonrails.org/configuring.html#database-pooling
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
server-side
ホストを受け入れるように修正
この設定をすることで, Nuxtからserver-sideにアクセスできます。
config.hosts << "server-side"
DBを作成
以下のコマンドを叩いてdbを作成。
$ docker-compose run server-side rails db:create
動作させてみる
以下のコマンドを打って, localhost:3000
にアクセス。
railsのデフォ画面が表示されればOK!
$ docker-compose up -d
サーバーサイドのAPIを実装
以下のコマンドを叩き, コンテナの中に入った上で作業を進めていきます。
$ docker exec -it server-side bash
ルーティングを設定。
Rails.application.routes.draw do
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
namespace :api do
namespace :v1 do
resources :todos do
collection do
get :complete
end
end
end
end
end
Todoモデル, todosコントローラーを作成。
$ rails g model Todo title:string isDone:boolean
$ rails db:migrate
$ rails g controller api::v1::todos
controllerの中身は以下のように書きました。
class Api::V1::TodosController < ApplicationController
before_action :set_todo, only: [:update, :destroy]
def index
todos = Todo.where(isDone: false)
render json: { status: 'SUCCESS', message: 'Loaded todos', data: todos }
end
def complete
todos = Todo.where(isDone: true)
render json: { status: 'SUCCESS', message: 'Loaded todos', data: todos }
end
def create
todo = Todo.new(todo_params)
if todo.save
render json: { status: 'SUCCESS', data: todo }
else
render json: { status: 'ERROR', data: todo.errors }
end
end
def destroy
@todo.destroy
render json: { status: 'SUCCESS', message: 'Deleted the todo', data: @todo }
end
def update
if @todo.update(todo_params)
render json: { status: 'SUCCESS', message: 'Updated the todo', data: @todo }
else
render json: { status: 'ERROR', message: 'Not updated', data: @todo.errors }
end
end
private
def set_todo
@todo = Todo.find(params[:id])
end
def todo_params
params.require(:todo).permit(:title, :isDone)
end
end
動作確認
この記事を参考に, Postmanを利用してCRUD操作ができるかどうか確認します。
curlコマンドでも確認できますが, たぶんPostmanの方が楽。
2. クライアントサイド(Nuxt.js)
環境構築
基本的には 公式のInstallation に沿って進めるだけ。
nodeはインストール済みとします。(今回の環境では12/15現時点でのLTS ver. 14.15.1を使用しています。)
プロジェクトの作成
まずは create-nuxt-app
で雛形作りましょう。
$ npx create-nuxt-app client-side
色々質問されると思うのですが, 今回は以下のように設定しました。(その他はデフォルト)
? Project name: client-side
? Programming language: TypeScript
? Package manager: Yarn
? UI framework: None
? Nuxt.js modules: Axios
? Linting tools: None
? Testing framework: None
? Rendering mode: Single Page App
? Deployment target: Server (Node.js hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Continuous integration: None
? Version control system: None
この辺りの設定は各自の好みで設定してください。
全てのオプションは ここから 確認できます。
Dockerfile作成
client-side/
配下にDockerfileを作成。
FROM node:14.15.1
WORKDIR /client-app
COPY package.json yarn.lock ./
RUN yarn install
CMD ["yarn", "dev"]
docker-compose.ymlに client-side
の設定を追加
server-side
の設定を記述したdocker-compose.yml に client-side
の設定を追加します。
version: '3.8'
volumes:
db_data:
services:
db:
image: postgres
volumes:
- db_data/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: password
server-side:
build: ./server-side/
image: server-side
ports:
- 3000:3000
volumes:
- ./server-side:/server-app
command: bundle exec rails server -b 0.0.0.0
tty: true
stdin_open: true
depends_on:
- db
links:
- db
# ここから下を追加
client-side:
build: ./client-side/
image: client-side
ports:
- 8000:8000
volumes:
- ./client-side:/client-app
- /client-app/node_modules
command: sh -c "yarn && yarn dev"
portの設定
このままだとエラーが出るので, portとhostを以下のように設定します。
export default {
// Disable server-side rendering (https://go.nuxtjs.dev/ssr-mode)
ssr: false,
// ここを追記
server: {
port: 8000,
host: '0.0.0.0',
},
// 以下省略
}
動作させてみる
以下のコマンドを打って, localhost:8000
にアクセスするとNuxt.jsのデフォ画面が表示されます。
$ docker-compose up -d
これで環境構築は完了!
3. サーバーサイドとクライアントサイドの連携
いよいよクライアント側からサーバーサイドのAPIを叩きにいきます。
感動の瞬間。。。
CORS (オリジン間リソース共有) 問題を解消
CORSについては こちらの記事 が参考になると思います。
公式とGitHubのREADMEに解決方法がありました。
READMEの記述を参考に @nuxtjs/proxy
をインストールし, app/nuxt.config.js
を以下のように編集します。
サーバーサイドのポート番号をは3000で指定していたので, ここは server-side:3000
で。(コンテナ間の通信はコンテナ名で解決するため, localhost
ではなくserver-side
にしている。)
$ yarn add @nuxtjs/proxy
modules: [
'@nuxtjs/axios',
'@nuxtjs/proxy'
],
// 以下を追加
proxy: {
'/api': {
target: 'http://server-side:3000',
pathRewrite: {
'^/api': '/api/v1/',
},
},
},
Composition APIとaxiosを設定
この辺り使いたいので設定しましたが, なくてもCRUD操作はできます。
$ yarn add @nuxtjs/composition-api
modules: [
'@nuxtjs/proxy',
//追加
'@nuxtjs/axios',
'@nuxtjs/composition-api',
],
"types": [
"@types/node",
"@nuxt/types",
#追加
"@nuxtjs/axios"
]
型定義
client-side
に新たに models/todo.ts
ディレクトリを作り, 以下を記述。
export interface ITodo {
id: number;
title: string;
isDone: boolean;
}
viewを記述
本当はコンポーネントに分割して書くべきですが, 今回は1ファイルにまとめた方が見やすいかなと思ったのでまとめます。
client-side/pages/index.vue
に以下の内容を記述。
<script lang="ts">
import {
defineComponent,
reactive,
ref,
onMounted,
} from "@nuxtjs/composition-api";
import { ITodo } from "../models/todo";
import $axios from "@nuxtjs/axios";
export default defineComponent({
setup(_, { root }) {
onMounted(() => {
getTodo();
});
const todoItem = reactive({
title: "",
isDone: false,
});
const todoList = ref<ITodo[]>([]);
const completeTodoList = ref<ITodo[]>([]);
// todoをpost
const addTodo = async () => {
try {
await root.$axios.post("/api/todos/", {
title: todoItem.title,
isDone: todoItem.isDone,
});
getTodo();
todoItem.title = "";
} catch (e) {
console.log(e);
}
};
// todoをget
const getTodo = async () => {
try {
const response = await root.$axios.get("/api/todos");
todoList.value = { ...response.data.data };
getCompleteTodo();
} catch (e) {
console.log(e);
}
};
// todoをupdate
const updateTodo = async (i: number, todo: ITodo) => {
try {
const newTodo = todoList.value[i].title;
await root.$axios.patch(`/api/todos/${todo.id}`, { title: newTodo });
} catch (e) {
console.log(e);
}
};
// todoをdelete
const deleteTodo = async (id: number) => {
try {
await root.$axios.delete(`/api/todos/${id}`);
getTodo();
} catch (e) {
console.log(e);
}
};
// todoをdone
const completeTodo = async (todo: ITodo) => {
try {
todo.isDone = !todo.isDone;
await root.$axios.patch(`/api/todos/${todo.id}`, {
isDone: todo.isDone,
});
getTodo();
} catch (e) {
console.log(e);
}
};
// complete_todoをget
const getCompleteTodo = async () => {
try {
const response = await root.$axios.get("/api/todos/complete");
completeTodoList.value = { ...response.data.data };
} catch (e) {
console.log(e);
}
};
return {
todoItem,
todoList,
completeTodoList,
addTodo,
deleteTodo,
updateTodo,
completeTodo,
};
},
});
</script>
<template>
<div class="container">
<section class="todo-new">
<h1>Add todos</h1>
<input v-model="todoItem.title" type="text" placeholder="todoを記入" />
<button @click="addTodo()">Todoを追加</button>
</section>
<section class="todo-index">
<h1>Incomplete todos</h1>
<ul>
<li v-for="(todo, i) in todoList" :key="i">
<input
class="item"
type="checkbox"
:checked="todo.isDone"
@change="completeTodo(todo)"
/>
<input
class="item"
type="text"
v-model="todo.title"
@change="updateTodo(i, todo)"
/>
<button @click="deleteTodo(todo.id)">削除する</button>
</li>
</ul>
</section>
<section class="todo-complete">
<h1>Complete todos</h1>
<ul>
<li v-for="(todo, i) in completeTodoList" :key="i">
<input
class="item"
type="checkbox"
:checked="todo.isDone"
@change="completeTodo(todo)"
/>
{{ todo.title }}
<button @click="deleteTodo(todo.id)">削除する</button>
</li>
</ul>
</section>
</div>
</template>
<style>
.container {
margin: 80px auto;
min-height: 100vh;
text-align: center;
}
section {
margin-bottom: 30px;
}
.item {
font-size: 1rem;
margin: 0 10x;
}
li {
list-style: none;
margin-bottom: 0.5em;
}
</style>
実際に動作させてみる
docker-compose upさせて, localhost:8000
にアクセスすると以下のような画面になると思います。
実際にtodoを追加/編集/削除してみてください。
まとめ
Dockerfileを1から書いたのも初めてだったので良い勉強になりました。
Nuxt.jsに関しては知らないことしかないので勉強していきます。
「ここのコードもっとこうした方がいいよ!」というのがあればぜひアドバイスお願いします。