Vue.jsでRest APIサーバーと通信し、CRUDの各画面を作る
flaskを使ってRestAPIサーバを作ってみるで作成したAPIサーバーを呼び出すフロントエンドを作成します。APIサーバーはリクエストをJSON形式で送信することでアカウントを作成、検索、更新、削除ができます。フロントエンドも作成、検索、更新、削除が実現できるUIを実装してみましょう。
本ブログの内容を試すにあたって、python, pip, pipenv, mysql, git, dockerがインストールされている必要があります。Flask+MySQL on dockerを始める準備でインストール方法またはインストール方法が載っているサイトを紹介しています。
また、DBが必要です。Flask+MySQL on dockerで説明しているdockerを参考にMySQLのdockerコンテナを起動してください。
Webアプリケーションで躓くポイント
筆者の様に主にバックエンド開発をやってきた者にとってフロント開発で躓くポイントがいくつかあります。
- 許されていない人が使えない様にするためのセキュリティのルールがある
インフラ等の問題は解決しているのにエラーになる
- ブラウザのコードを動的に書き換えることによって動くプログラムであること
コードが書き変わる順番などがあり、最終的に何が何に変更したかがわからないと正しい表示がされない。
- 独自なデザインのルールがある
レスポンシブルデザインや、入出力の方法が複数ある。データの型があっているだけではアプリケーションとして成り立たない。
今回、セキュリティのルールの中でなかなか解消するのに手間がかかったオリジン間リソース共有を説明します。
CORS (オリジン間リソース共有)
Wiki Cross-origin resource sharing
の説明
Cross-origin resource sharing (CORS) is a mechanism that allows restricted resources on a web page to be requested from another domain outside the domain from which the first resource was served.
の通り、
オリジン間リソース共有は、最初に提供したリソースからドメインの外へ、制限されたWebページのリソースへ、他のドメインからリクエストを許す仕組みのことです。
つまり、ドメインが異なるアプリケーションからのリクエストは受け付けられない。これは同じホストでもポート番号が異なると異なるドメインということになり、vueが動くWebサーバーとflaskサーバーと異なるドメインとなるため、vueのアプリケーションからAPIのリクエストが失敗します。
これを回避するためにヘッダに許されるドメインを指定したり、特定のドメインからのアクセスを許すことができるCORS対応のパッケージをインストールするなどする必要があります。Flaskもflask-corsというパッケージでその制御を行なっている様です。
$ pipenv install flask_cors
githubのサンプルコードは既にflask_corsインストールした状態でPipfile
が作られているので上記コマンドは実行しなくても大丈夫です。
それではフロントの開発に入っていきます。
1. 画面の作成
今回、例としてアカウントの一覧、登録、編集、削除ができるWebアプリケーションを作ります。
フロントフレームワークであるVue.js+Vuetify導入
フロントフレームワークであるVue.js+Vuetify導入 2章
で作成したフロントアプリを拡張してアプリケーションを作っていきます。
1-1 フロント環境の準備
1. リポジトリをcloneします。
$ git clone git@github.com:kaorunix/flask_sv.git
本リポジトリは複数のqiita記事とかで題材に使っているので本記事では次のtagをチェックアウトしてください。
git checkout menu-template
2. flaskサーバーアプリ起動
Flask+MySQL on dockerを読み、mysqlの立ち上げ、flaskで作ったバックエンドを起動してください。
$ cd backend/src
$ pipenv run python main.py
* Serving Flask app 'main' (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 895-939-291
3. vue.jsフロントアプリ起動
プロジェクトのディレクトリから次の様に入力。
$ cd frontend
$ npm install
私が安定したモジュールを選んでいないためかワーニングが色々出てしまいます😅
もし、再実行して過去にインストールしたモジュールと競合してエラーになる時は次のディレクトリを削除してから再度実行してください。
flask_sv/frontend
配下で実行します。
$ rm -rf node_modules
次のコマンドでフロントアプリの実行です。
$ npm run serve
次のメッセージが出ると起動完了です。
You may use special comments to disable some warnings.
Use // eslint-disable-next-line to ignore the next line.
Use /* eslint-disable */ to ignore all warnings in a file.
App running at:
- Local: http://localhost:8080/
- Network: http://192.168.1.6:8080/
ブラウザで http://localhost:8080/にアクセスしてください。
次の画面が見れたらフロント部分の起動が確認できました。
サブメニューはまだ何もありません。ここにメニューを足していきましょう。
1-2 メニュー作成
ブラウザから見えるメニューは、frontend/src/App.vue
で定義しています。
<template>タグの中の<v-navigation-drawer> フロントフレームワークであるVue.js+Vuetify導入#Vuetifyの開発 のVuetifyでメニューを作るで解説したメニューを記述するv-list-item
タグに記載します。
navigation-drawer
は画面左側からスライドしてくることで表示できるメニューになります。
<template>
...
<v-navigation-drawer
v-model="drawer"
app cliped
>
<v-container>
<v-list-item>
メニュー
</v-list-item>
<v-divider/>
<v-list dense nav=false>
<v-list-group v-for="main_menu_item in main_menu"
:key="main_menu_item.name"
:prepend-icon="main_menu_item.icon"
no-action
:append-icon="main_menu_item.lists ? undefined : ''">
<template v-slot:activator>
<v-list-item-content>
<v-list-item-title>
{{ main_menu_item.name }}
</v-list-item-title>
</v-list-item-content>
</template>
<v-list-item v-for="sub_menu in main_menu_item.lists"
:key="sub_menu.name"
:to="sub_menu.link"
>
<v-list-item-content>
<v-list-item-title>{{ sub_menu.name }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list-group>
</v-list>
</v-container>
</v-navigation-drawer>
...
</template>
...
<script>
export default {
data () {
return {
drawer: null,
company_menu: [
{ name: '参照', link: '/' },
{ name: '編集', link: '/' },
{ name: '削除', link: '`mailto:s@a`' }
]
}
}
}
</script>
company_menuと同じ階層にアカウントのメニューを追加していきます。次のコードを追加してください。
<template>
...
</template>
<script>
...
company_menu: [
{ name: '参照', link: '/' },
{ name: '編集', link: '/' },
{ name: '削除', link: '`mailto:s@a`' }
],
// ここから追加
main_menu: [
{
name: 'アカウント',
icon: 'mdi-account',
lists: [
{ name: '一覧', link: '/account' },
{ name: '作成', link: '/account/create' }
]
},
]
// ここまで
}
}
}
</script>
<v-list-item-content>で埋め込まれている
main_menu_itemとsub_menuをjavascriptで定義します。
これを埋め込むとアカウントメニューが表示されます。
メニューを開くと、追加したメニューの通り一覧と作成のサブメニューも表示されました。
しかし、まだサブメニューをクリックしても画面は表示されません。
そのパスがどのコンポーネントに紐づいているかを記述します。
次のindex.js
にアカウントの一覧表示と作成画面のパスを追加しましょう。
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
上記のコードに以下のアカウント一覧とアカウント作成の設定を追加します。
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
...
{
path: '/account',
name: 'アカウント一覧',
component: () => import('../views/account/List.vue')
},
{
path: '/account/create',
name: 'アカウント作成',
component: () => import('../views/account/Create.vue')
},
...
}
]
同時に、次のvueも作りましょう。一旦空で作ります。
views/account/List.vue
views/account/Create.vue
コーディングを飛ばしたい方は次のtagをチェックアウトしてください。
$ git checkout account-menu
2. 検索画面
レコードを表示する必要があるため基本となる機能は検索です。
一覧ページに表示する内容は、検索の結果です。なので一覧画面を作りながら検索のAPIを実行することにしましょう。
2-1 テーブルの作成
Listはtableにしましょう。vuetifyに<v-simple-table>というタグがありました。
詳しくはv-simple-tableを参照してください。
<template>
<div class="about">
<p>{{ message }}</p>
<h3>アカウント一覧</h3>
<v-simple-table>
<template v-slot;default>
<thead>
<tr>
<th>アカウントID</th>
<th>アカウント名称</th>
<th>有効開始日</th>
<th>有効終了日</th>
<th>作成者</th>
<th>作成日時</th>
<th>更新者</th>
<th>更新日時</th>
<th>ステータス</th>
</tr>
</thead>
<tbody>
<tr>
<td>アカウントID(id)</td>
<td>アカウント名称(account_name)</td>
<td>有効開始日</td>
<td>有効終了日</td>
<td>作成者</td>
<td>作成日時</td>
<td>更新者</td>
<td>更新日時</td>
<td>ステータス</td>
</tr>
</tbody>
</template>
</v-simple-table>
</div>
</template>
<script>
export default {
name: 'List',
data () {
return {
message: '出力メッセージ'
}
}
}
</script>
http://localhost:8080/accountにアクセスしてみましょう。
2-2 vueでデータを表示
vueの機能を使ってtableにデータを埋め込みましょう。 List.vue
を次の様に変更してください。
<template>
...
<tbody>
<tr v-for="account in accounts" v-bind:key="account">
<td>{{ account.id }}</td>
<td>{{ account.account_name }}</td>
<td>{{ account.start_on }}</td>
<td>{{ account.end_on }}</td>
<td>{{ account.created_by }}</td>
<td>{{ account.created_at }}</td>
<td>{{ account.updated_by }}</td>
<td>{{ account.updated_at }}</td>
<td>{{ account.status }}</td>
</tr>
</tbody>
...
</template>
</v-simple-table>
</div>
</template>
<script>
var accounts = [
{
id: 123,
account_name: 'macbeth',
start_on: '2021/02/01 10:00:00',
end_in: '2021/11/30 18:00:00',
created_by: 10,
created_at: '2021/08/25 12:00:00',
updated_by: 10,
updated_at: '2021/08/25 12:00:00',
status: 0
},
{
id: 124,
account_name: 'duncan',
start_on: '2021/05/01 10:00:00',
end_in: '2021/10/30 18:00:00',
created_by: 12,
created_at: '2021/08/25 12:00:00',
updated_by: 10,
updated_at: '2021/08/25 12:00:00',
status: 1
}
]
export default {
name: 'List',
data () {
return {
accounts: accounts,
message: '出力メッセージ'
}
}
}
</script>
変更したらブラウザ再読み込みか、http://localhost:8080/accountにアクセスしてみましょう。
一覧画面が表示されます。
コーディングを飛ばしたい方は次のtagをチェックアウトしてください。
$ git checkout account-dummy-list
2-3 APIを呼び出して検索結果を表示する
検索画面を検索を行なって表示するため、flaskを使ってRestAPIサーバを作ってみる(検索編) で作成したhttp://localhost:5000/api/account/search
のAPIを呼び出します。
検索APIは次のフォーマットです。
request
{
"account_name":"account",
"start_on":"2021-05-05 00:00:00",
"end_on":"2030-12-31 00:00:00",
"created_by":100,
"updated_by":150
}
response
{
"body": [
{
"account_name":"account",
"start_on":"2021-05-05 00:00:00",
"end_on":"2030-12-31 00:00:00",
"created_by:100,
"created_at:"2021-05-05 00:00:00",
"updated_by:150,
"updated_at:"2021-09-01 00:00:00",
}
],
"status": {
"code" : "I0001",
"message" : "Found (4) records.",
"detail" : ""
}
}
それでは実際にAPIを呼び出すため、List.vue
を次の様に変更してください。
<template>
<div class="about">
<p>{{ message }}</p>
<h3>アカウント一覧</h3>
<v-simple-table>
<template v-slot;default>
<thead>
<tr>
<th>アカウントID</th>
<th>アカウント名称</th>
<th>有効開始日</th>
<th>有効終了日</th>
<th>作成者</th>
<th>作成日時</th>
<th>更新者</th>
<th>更新日時</th>
<th>ステータス</th>
</tr>
</thead>
...
<tbody>
<tr v-for="account in accounts" v-bind:key="account">
<td>{{ account.id }}</td>
<td>{{ account.account_name }}</td>
<td>{{ account.start_on }}</td>
<td>{{ account.end_on }}</td>
<td>{{ account.created_by }}</td>
<td>{{ account.created_at }}</td>
<td>{{ account.updated_by }}</td>
<td>{{ account.updated_at }}</td>
<td>{{ account.status }}</td>
</tr>
</tbody>
...
</template>
</v-simple-table>
</div>
</template>
<script>
var request = {
operation_account_id: 100
}
var url = 'http://localhost:5000/api/account/search'
const config = {
headers: {
'Content-Type': 'application/json'
}
}
export default {
name: 'List',
data () {
return {
accounts: [],
message: null
}
},
mounted () {
var self = this
this.axios
.post(url, request, config)
.then(function (response) {
console.log('List axios response %o', response.data.body)
self.accounts = response.data.body
self.message = response.data.status.message
})
.catch(err => {
console.log('List axios error')
console.log(err)
})
}
}
</script>
画面を確認しましょう。
DBのaccountテーブルに入っているレコードが表示されていれば成功です。
もし、本ブログから読み始めてレコードが無い人はインサートしておいてください。
$ mysql -u creist -p -h 127.0.0.1
Enter password:
Welcome to the MySQL monitor.
...
mysql> use flask_sv
mysql> insert into account (account_name, start_on, end_on, created_by, created_at, updated_by, updated_at, status) values ('accountA', '2021-09-01 00:00:00', '2021-12-31 00:00:00', 999, now(), 999, now(), 0);
mysql> select * from account;
...
話をfrontend/src/views/account/List.vue
戻し、
<script>の中を説明します。
APIのリクエストになるjsonとAPIの呼び出しに必要な情報です。
特にheaderにContent-Typeを正しく設定しないとCORSでエラーとなってしまいます。
<script>
var request = {
operation_account_id: 100
}
var url = 'http://localhost:5000/api/account/search'
const config = {
headers: {
'Content-Type': 'application/json'
}
}
ここからvueの実装です。
Vueオブジェクトのdata
プロパティのaccountsにresponseとして返却されたjson形式のaccount配列を代入します。
サーバーからのresponseは、jsonが入っているbody以外にもステータスコードなど通信内容が含まれたオブジェクトです。
<script>
...
export default {
name: 'List',
data () {
return {
accounts: [],
message: null
}
},
mounted () {
var self = this
this.axios
.post(url, request, config)
.then(function (response) {
console.log('List axios response %o', response.data.body)
self.accounts = response.data.body
self.message = response.data.status.message
})
.catch(err => {
console.log('List axios error')
console.log(err)
})
}
}
</script>
一覧画面はAPIを呼び出して検索ができるようになったので一度手を離しましょう。
$ git checkout account-search
3. 作成画面
3-1 フォームの作成
まずはテキスト入力できるフォームを作りましょう。
次の内容のCreate.vueを作成します。
<template>
<div class="about">
<p>{{ message }}</p>
<h1>アカウント</h1>
<v-form ref="form">
<v-simple-table>
<thead></thead>
<tbody>
<tr>
<th>アカウント名称</th>
<td>
<v-text-field
v-model="account_name"
:counter="64"
:rules="nameRules"
:value="account_name"
label="アカウント名称"
required
></v-text-field>
</td>
</tr>
<tr>
<th>有効開始日</th>
<td>
<v-text-field
slot="activator"
v-model="start_on"
:value="start_on"
label="有効開始日"
readonly
v-on="on"
></v-text-field>
</td>
</tr>
<tr>
<th>有効終了日</th>
<td>
<v-text-field
slot="activator"
v-model="end_on"
:value="end_on"
label="有効終了日"
readonly
v-on="on"
></v-text-field>
</td>
</tr>
<tr>
<th>作成者</th>
<td>
<v-text-field
v-model="created_by"
:value="created_by"
:counter="10"
:rules="nameRules"
label="作成者"
required
></v-text-field>
</td>
</tr>
</tbody>
</v-simple-table>
<v-btn class="mr-4" >保存</v-btn>
<v-btn >リセット</v-btn>
</v-form>
</div>
</template>
VuetifyのText fieldsに入力フォームのデザインや機能など載っています。
しかしこのままでは日付項目に日付のフォーマットで文字を入れなければなりません。
コードの次箇所を編集しましょう。
<template>
...
<tr>
<th>有効開始日</th>
<td>
<v-menu v-model="menu" max-width="290px" min-width="290px">
<!-- ポップアップを追加する要素にv-on="on" -->
<template v-slot:activator="{ on }">
<v-text-field
slot="activator"
v-model="start_on"
:value="start_on"
label="有効開始日"
readonly
v-on="on"
></v-text-field>
</template>
<v-date-picker v-model="start_on"></v-date-picker>
</v-menu>
</td>
</tr>
<tr>
<th>有効終了日</th>
<td>
<v-menu v-model="menu" max-width="290px" min-width="290px">
<!-- ポップアップを追加する要素にv-on="on" -->
<template v-slot:activator="{ on }">
<v-text-field
slot="activator"
v-model="end_on"
:value="end_on"
label="有効終了日"
readonly
v-on="on"
></v-text-field>
</template>
<v-date-picker v-model="end_on"></v-date-picker>
</v-menu>
</td>
</tr>
...
</template>
有効開始日、有効終了日にカーソルを持っていくとカレンダーから選択できる様になります。
VuetifyのDate pickersに日付選択するコンポーネントの説明が載っています。
3-2 vueのアプリケーションの実装
次にアプリケーションの実装をしましょう。
vuetifyのFormsを参考にリセットボタンを実装します。
<v-btn>で作成されたリセットボタンにclickされた時の関数を定義します。
<template>
...
</v-simple-table>
<v-btn class="mr-4" >保存</v-btn>
<v-btn @click="reset">リセット</v-btn>
</v-form>
</div>
</template>
<script>
export default {
name: 'account_create',
data: function () {
return {
account_name: '',
start_on: '',
end_on: '',
created_by: '',
message: '出力メッセージ'
}
},
methods: {
reset () {
this.$refs.form.reset()
},
clear () {
this.$v.$reset()
this.account_name = ''
this.start_on = ''
this.end_on = ''
this.created_by = ''
}
}
}
</script>
変更したら、作成画面を開いてフォームに入力し、リセットボタンを押下してみてください。
フォームに入力された文字が消されて空になればOKです。
3-3 フォームのデータをアカウント作成APIに送信する
flaskを使ってRestAPIサーバを作ってみる(作成編)で作ったアカウント作成APIのフォーマットは次の様になります。
request
{
"account_name": "account123",
"start_on": "2021-01-01 00:00:00",
"end_on": "2021-12-31 23:59:59",
"opration_account_id": 123
}
response
{
"body": "",
"status": {
"code" : "I0001",
"message" : "Created Account Succesfuly.",
"detail" : ""
}
}
Create.vueは次の様に修正します。
<template>
...
</v-simple-table>
<v-btn class="mr-4" @click="submit">保存</v-btn>
<v-btn @click="reset">リセット</v-btn>
</v-form>
</div>
</template>
<script>
var contentType = 'application/json'
var url = 'http://localhost:5000/api/account/create'
const config = {
headers: {
'Content-Type': contentType,
'Access-Control-Allow-Origin': 'http://localhost:5000'
}
}
export default {
name: 'account_create',
data: function () {
return {
account_name: '',
start_on: '',
end_on: '',
created_by: '',
message: '出力メッセージ'
}
},
computed: {
form () {
return {
account_name: this.account_name,
start_on: this.start_on.concat(' 00:00:00'),
end_on: this.end_on.concat(' 00:00:00'),
operation_account_id: parseInt(this.created_by)
}
}
},
methods: {
validate () {
this.$refs.form.validate()
},
reset () {
this.$refs.form.reset()
},
resetValidation () {
this.$refs.form.resetValidation()
},
submit () {
console.log(this.form)
this.axios
.post(url, this.form, config)
.then(function (response) {
console.log('Create axios response')
console.log(response)
document.location = 'http://localhost:8080/account'
})
.catch(err => {
console.log('Create axios error')
console.log(err)
})
},
clear () {
this.$v.$reset()
this.account_name = ''
this.start_on = ''
this.end_on = ''
this.created_by = ''
}
}
}
</script>
コードの説明をします。
searchと同じくAPIのリクエストになるjsonとAPIの呼び出しに必要な情報です。
<script>
var contentType = 'application/json'
var url = 'http://localhost:5000/api/account/create'
const config = {
headers: {
'Content-Type': contentType
}
}
...
</script>
dataはフォームに必要な項目を定義します。
<script>
...
export default {
name: 'account_create',
data: function () {
return {
account_name: '',
start_on: '',
end_on: '',
created_by: '',
message: '出力メッセージ'
}
},
...
</script>
form関数でAPIへ送信するJsonを作る。
<script>
...
computed: {
form () {
return {
account_name: this.account_name,
start_on: this.start_on.concat(' 00:00:00'),
end_on: this.end_on.concat(' 00:00:00'),
operation_account_id: parseInt(this.created_by)
}
}
},
...
</script>
フォームに入力された値が仕様に合っているかチェックするバリデーション、リセットなどの関数を定義している。
vuetifyのFormsを真似して作成します。
<script>
...
methods: {
validate () {
this.$refs.form.validate()
},
reset () {
this.$refs.form.reset()
},
resetValidation () {
this.$refs.form.resetValidation()
},
...
</script>
保存ボタンが押されるときに呼び出される関数です。HTTP通信を行うaxiosライブラリでAPIを呼び出します。正常に終わったらアカウント一覧画面にリダイレクトしています。
<script>
...
submit () {
console.log(this.form)
this.axios
.post(url, this.form, config)
.then(function (response) {
console.log('Create axios response')
console.log(response)
document.location = 'http://localhost:8080/account'
})
.catch(err => {
console.log('Create axios error')
console.log(err)
})
},
...
</script>
リセットボタンが押された時に呼ばれる関数です。
<script>
...
clear () {
this.$v.$reset()
this.account_name = ''
this.start_on = ''
this.end_on = ''
this.created_by = ''
}
}
}
</script>
コーディングを飛ばしたい方は次のtagをチェックアウトしてください。
$ git checkout account-create
4. 編集
アカウントレコードを編集するためには、アカウントIDを特定しなければならない。その変更対象のアカウントを選ぶのはそのアカウント情報を参照できるよう様な画面である必要がある。なので一覧画面などで該当のアカウントを選択して変種モードに移るのが良いでしょう。
4-1 一覧画面に編集ボタンを追加
アカウント一覧画面に戻って次の修正をしましょう。
<template>
...
<tbody>
<tr v-for="account in accounts" v-bind:key="account">
<td>
<v-btn
fab
x-small
class="mx-2"
slot="activator"
color="cyan"
dark
@click="
getAccount();
dialog = true;
"
>
<v-icon dark>
mdi-pencil
</v-icon>
</v-btn>
</td>
<td>{{ account.id }}</td>
...
</template>
<script>
...
</script>
編集ボタンが表示される様になりました。
4-2 一覧画面の編集ボタンからモーダル編集画面を起動
アイコンボタンからListの中にモーダルウィンドウを開くことにしましょう。モーダルはHTMLでは一つのコードとして作りますが、vueを使っているのでコンポーネントとして埋め込みましょう。
一覧画面は次の様に変更します。
コンポーネントのタグに置き換えます。埋め込むコンポーネントはvueファイルをimportして、componentsに追加します。
<template>
...
<tbody>
<tr v-for="account in accounts" v-bind:key="account">
<td><Update /></td>
<td>{{ account.id }}</td>
...
</template>
<script>
import Update from '@/views/account/Update.vue'
...
export default {
...
components: {
Update
}
}
</script>
次に、モーダルウィンドウで表示するフォームを Update.vue として作成します。
<template>
<v-dialog
v-model="dialog"
width="800"
>
<v-btn
fab
x-small
class="mx-2"
slot="activator"
color="cyan"
dark
@click="dialog = true; "
>
<v-icon dark>
mdi-pencil
</v-icon>
</v-btn>
<v-card>
<v-card-title
class="headline grey lighten-2"
primary-title
>
アカウント更新
</v-card-title>
<v-card-text>
<v-container grid-list-md>
<v-layout wrap>
<v-flex xs12>
<v-text-field label="アカウントid" disabled required v-model="account.id"></v-text-field>
</v-flex>
<v-flex xs12>
<v-text-field label="アカウント名称" required v-model="account.account_name"></v-text-field>
</v-flex>
<v-flex xs12>
<v-menu max-width="290px" min-width="290px">
<!-- ポップアップを追加する要素にv-on="on" -->
<template v-slot:activator="{ on }">
<v-text-field
slot="activator"
v-model="account.start_on"
:value="account.start_on"
label="有効開始日"
readonly
v-on="on"
></v-text-field>
</template>
<v-date-picker v-model="account.start_on"></v-date-picker>
</v-menu>
</v-flex>
<v-flex xs12>
<v-menu max-width="290px" min-width="290px">
<!-- ポップアップを追加する要素にv-on="on" -->
<template v-slot:activator="{ on }">
<v-text-field
slot="activator"
v-model="account.end_on"
:value="account.end_on"
label="有効終了日"
readonly
v-on="on"
></v-text-field>
</template>
<v-date-picker v-model="account.end_on"></v-date-picker>
</v-menu>
</v-flex>
<v-flex xs12>
<v-text-field label="作成者" required v-model="operation_account_id"></v-text-field>
</v-flex>
</v-layout>
</v-container>
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="primary"
text
@click="dialog = false"
>
更新
</v-btn>
<v-btn
color="primary"
text
@click="dialog = false"
>
キャンセル
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
name: 'Update',
data () {
return {
dialog: false,
account: {
id: 1234,
account_name: '',
start_on: '',
end_on: ''
},
operation_account_id: 4321
}
}
}
</script>
更新ボタンをクリックするとモーダル小画面が開きます。
抜粋で、コードの解説をします。
<v-dialog>でモーダルウィンドウやダイアログを作ることができます。
vuetifyのDialogsに載っているコードをいろいろ試して利用します。
<template>
<v-dialog
v-model="dialog"
width="800"
>
<v-btn
fab
x-small
class="mx-2"
slot="activator"
color="cyan"
dark
@click="dialog = true; "
>
<v-icon dark>
mdi-pencil
</v-icon>
</v-btn>
フォームはアカウント作成画面で使いました<v-text-field>で作っています。VuetifyのText fieldsを参照してください。
<v-layout>は、グリッドシステムを実現するタグです。グリッドシステムはvuetifyのことではないですが
絶対抑えておきたいWebデザインのグリッドシステムとフレームワーク #グリッドシステムが判りやすいかと思います。wrapで縦に分割する様です。
<v-flex>がグリッドの分割の単位になります。xsは、600px未満で12分割。ブラウザの幅によってグリッドの分割を変えることができるのでレスポンシブウェブデザインを実現できる様です。
<v-card>は1つにまとめるために使う様です。VuetifyのCardsの説明はあまり良くわからなく、v-cardを理解するためVuetify.js を使ってマテリアルデザインに挑戦しよう!を参考にしました。
<template>
...
<v-card>
<v-card-title
class="headline grey lighten-2"
primary-title
>
アカウント更新
</v-card-title>
<v-card-text>
<v-container grid-list-md>
<v-layout wrap>
<v-flex xs12>
<v-text-field label="アカウントid" disabled required v-model="account.id"></v-text-field>
</v-flex>
<v-flex xs12>
<v-text-field label="アカウント名称" required v-model="account.account_name"></v-text-field>
</v-flex>
<v-flex xs12>
<v-menu max-width="290px" min-width="290px">
<!-- ポップアップを追加する要素にv-on="on" -->
<template v-slot:activator="{ on }">
<v-text-field
slot="activator"
v-model="account.start_on"
:value="account.start_on"
label="有効開始日"
readonly
v-on="on"
></v-text-field>
</template>
<v-date-picker v-model="account.start_on"></v-date-picker>
</v-menu>
</v-flex>
<v-flex xs12>
<v-menu max-width="290px" min-width="290px">
<!-- ポップアップを追加する要素にv-on="on" -->
<template v-slot:activator="{ on }">
<v-text-field
slot="activator"
v-model="account.end_on"
:value="account.end_on"
label="有効終了日"
readonly
v-on="on"
></v-text-field>
</template>
<v-date-picker v-model="account.end_on"></v-date-picker>
</v-menu>
</v-flex>
<v-flex xs12>
<v-text-field label="作成者" required v-model="operation_account_id"></v-text-field>
</v-flex>
</v-layout>
</v-container>
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="primary"
text
@click="dialog = false"
>
更新
</v-btn>
<v-btn
color="primary"
text
@click="dialog = false"
>
キャンセル
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
dataにフォームに埋め込む初期値を設定します。編集画面なのでこれからAPIを呼び出しでDBから取得した値を埋め込むように作っていきましょう。
<script>
export default {
name: 'Update',
data () {
return {
dialog: false,
account: {
id: 1234,
account_name: '',
start_on: '',
end_on: ''
},
operation_account_id: 4321
}
}
}
</script>
4-3 編集画面に検索した結果を表示
一覧画面から各行のアカウントIDを連携する必要があります。一覧画面のUpdateタグを次の様に変更してください。
<template>
...
<td>
<Update v-bind:account-id="account.id"></Update>
</td>
...
</template>
<script>
...
</script>
一覧画面でv-bindで渡されたaccount-idという変数はvueの中では、propsプロパティでaccountIdと定義すると値を渡すことができます。
accountIdを使ってflaskを使ってRestAPIサーバを作ってみる(編集編)の課題 4.1で作成したロックを行うAPIを呼び出します。
このAPIは、アカウントIDで検索されたアカウント情報を返却するので返却されたアカウント情報をフォームに埋め込みます。
request
{
account_id : int,
operation_account_id : int
}
response
{
"body": {
"name": "account",
"id": <account_id>,
"account_name": <account_name>,
"start_on": "2021-01-01 10:00:00",
"end_on": "2025-12-31 21:00:00"
},
"status": {
"code" : "I0001",
"message" : "Account locked Succesfuly.",
"detail" : ""
}
}
Update.vue
のコードを次の様に修正しましょう。
<template>
...
<v-btn
fab
x-small
class="mx-2"
slot="activator"
color="cyan"
dark
@click="getAccount(); dialog = true; "
>
<v-icon dark>
mdi-pencil
</v-icon>
</v-btn>
...
</template>
<script>
var url = 'http://localhost:5000/api/account/'
const config = {
headers: {
'Content-Type': 'application/json'
}
}
function applyDateFormat (datestring) {
if (datestring.search(/^\d{4}[-/]\d{2}[-/]\d{2} \d{2}:\d{2}:\d{2}$/) === 0) {
return datestring
} else if (datestring.search(/^\d{4}[-/]\d{2}[-/]\d{2}$/) === 0) {
return datestring.concat(' 00:00:00')
}
}
function setAccount (account) {
var a = {
id: account.id,
account_name: account.account_name,
start_on: applyDateFormat(account.start_on),
end_on: applyDateFormat(account.end_on)
}
return a
}
export default {
name: 'Update',
props: {
accountId: {
type: Number,
required: true
}
},
data () {
return {
dialog: false,
account: {
id: '',
account_name: '',
start_on: '',
end_on: ''
},
operation_account_id: 50
}
},
methods: {
getAccount () {
var request = {
id: this.accountId,
operation_account_id: parseInt(this.operation_account_id)
}
console.log('getAccount was called')
const self = this
this.axios
.post(url + 'lock', request, config)
.then(function (response) {
console.log('Get axios response')
console.log(response)
self.account = setAccount(response.data.body)
self.meesage = response.status.message
})
.catch(err => {
console.log('Get axios error')
console.log(err)
})
},
}
}
</script>
コードの説明をします。
編集アイコンを押下すると、検索メソッド getAccount を呼び出しdialogがオンになりモーダルウィンドウを開きます。
<template>
...
<v-btn
fab
x-small
class="mx-2"
slot="activator"
color="cyan"
dark
@click="getAccount(); dialog = true; "
>
<v-icon dark>
mdi-pencil
</v-icon>
</v-btn>
...
</template>
アカウント一覧画面、アカウント作成画面同様、apiを呼ぶための情報を変数に設定しています。
<script>
var url = 'http://localhost:5000/api/account/'
const config = {
headers: {
'Content-Type': 'application/json'
}
}
フロントで扱う日付情報を変換する関数を作りました。
date-picker
で作った日付は時間が入っていにのですが、DB上の対応する項目はdatetimeなのでフォーマットを合わせます。もし、日付だけであった場合00:00:00を付加します。
<script>
...
function applyDateFormat (datestring) {
if (datestring.search(/^\d{4}[-/]\d{2}[-/]\d{2} \d{2}:\d{2}:\d{2}$/) === 0) {
return datestring
} else if (datestring.search(/^\d{4}[-/]\d{2}[-/]\d{2}$/) === 0) {
return datestring.concat(' 00:00:00')
}
}
...
</script>
setAccount は、日付情報を変換してaccountオブジェクトに変換する関数です。
<script>
...
function setAccount (account) {
var a = {
id: account.id,
account_name: account.account_name,
start_on: applyDateFormat(account.start_on),
end_on: applyDateFormat(account.end_on)
}
return a
}
...
</script>
props
を使ってアカウント一覧画面からアカウントIDを受け取れます。
<script>
...
export default {
name: 'Update',
props: {
accountId: {
type: Number,
required: true
}
},
data () {
return {
dialog: false,
account: {
id: '',
account_name: '',
start_on: '',
end_on: ''
},
operation_account_id: 50
}
},
...
</script>
methods
はVueインスタンスのメソッドとして機能します。データの変更や、サーバーにHTTPリクエストを送る時はここに記述する必要があります。
getAccount は、アカウントIDと操作アカウントIDを使ってAPIを呼び出しています。そして返ってきたアカウント情報をvueオブジェクトのaccountに入れ直すことでフォームを再描画します。
<script>
...
methods: {
getAccount () {
var request = {
id: this.accountId,
operation_account_id: parseInt(this.operation_account_id)
}
console.log('getAccount was called')
const self = this
this.axios
.post(url + 'lock', request, config)
.then(function (response) {
console.log('Get axios response')
console.log(response)
self.account = setAccount(response.data.body)
self.meesage = response.status.message
})
.catch(err => {
console.log('Get axios error')
console.log(err)
})
},
}
}
</script>
thisをselfに代入(bind)していますが、axios
のthenの中ではthisはaxios
オブジェクトのthisになってしまう様です。vueオブジェクトのaccountメンバにアクセスできませんでした。vueオブジェクトのaccountにアクセスするために、別変数selfに代入しておきます。
4-4 編集画面に入力した内容を更新
フォームで入力したアカウント情報でDBを更新しましょう。
flaskを使ってRestAPIサーバを作ってみる(編集編)の4.5. 排他編集で課題としていたロックを使った更新APIは次のフォーマットです。
request
{
"account_name":
"start_on":
"end_on":
"operation_account_id":
}
response
{
"body": "",
"status": {
"code" : "I0001",
"message" : "Updated Account Succesfuly.",
"detail" : ""
}
}
更新APIを呼び出すためUpdate.vue
を次の様に変更してください。
<template>
...
<v-btn
color="primary"
text
@click="
submit();
dialog = false;
"
>
更新
</v-btn>
...
</template>
<script>
export default {
name: "Update",
props: {
...
data() {
...
computed: {
form() {
return {
id: this.account.id,
account_name: this.account.account_name,
start_on: applyDateFormat(this.account.start_on),
end_on: applyDateFormat(this.account.end_on),
operation_account_id: parseInt(this.operation_account_id)
};
}
},
methods: {
getAccount() {
...
validate() {
this.$refs.form.validate();
},
submit () {
console.log(this.form)
this.axios
.post(url + 'update_for_lock', this.form, config)
.then(function (response) {
console.log('Update axios response')
console.log(response)
document.location = 'http://localhost:8080/account'
})
.catch(err => {
console.log('Update axios error')
console.log(err)
})
}
}
}
</script>
コードの説明をします。
アカウント変更画面の更新ボタンを押した時の実行されるVueのメソッド名を書きます。
<template>
...
<v-btn
color="primary"
text
@click="
submit();
dialog = false;
"
>
更新
</v-btn>
...
</template>
...
form関数は、入っている値をフォーマットしてからAPIに渡せるリクエストの形式にする関数です。
...
<script>
export default {
name: "Update",
props: {
...
data() {
...
computed: {
form() {
return {
id: this.account.id,
account_name: this.account.account_name,
start_on: applyDateFormat(this.account.start_on),
end_on: applyDateFormat(this.account.end_on),
operation_account_id: parseInt(this.operation_account_id)
};
}
},
...
</script>
submitメソッドはアカウント作成画面と同じ様にformの結果をaxios
でAPIに送り、一覧画面に遷移します。
...
<script>
...
methods: {
getAccount() {
...
validate() {
this.$refs.form.validate();
},
submit () {
console.log(this.form)
this.axios
.post(url + 'update_for_lock', this.form, config)
.then(function (response) {
console.log('Update axios response')
console.log(response)
document.location = 'http://localhost:8080/account'
})
.catch(err => {
console.log('Update axios error')
console.log(err)
})
}
}
}
</script>
コーディングを飛ばしたい方は次のtagをチェックアウトしてください。
$ git checkout account-update
5. 削除
5-1 削除アイコンの追加とダイアログ表示
まずは、一覧画面にアイコンを追加し、フォームでないダイアログを表示してみます。
次の修正をしてください。
<template>
<div class="account_list">
<p>{{ message }}</p>
<h3>アカウント一覧</h3>
<v-simple-table>
<template v-slot;default>
<thead>
<tr>
<th>編集</th>
<th>削除</th>
...
<tbody>
<tr v-for="account in accounts" v-bind:key="account.id">
<td>
<Update v-bind:account-id="account.id"></Update>
</td>
<td>
<Delete />
</td>
...
</template>
<script>
import Update from '@/views/account/Update.vue'
import Delete from '@/views/account/Delete.vue'
...
components: {
Update,
Delete
}
}
</script>
次の内容でDelete.vueを作りましょう。
<template>
<v-dialog v-model="dialog" width="500">
<v-btn
fab
x-small
class="mx-2"
slot="activator"
color="red lighten-2"
dark
@click="dialog = true"
>
<v-icon dark>
mdi-delete
</v-icon>
</v-btn>
<v-card>
<v-card-title class="headline grey lighten-2" primary-title>
アカウント削除
</v-card-title>
<v-card-text>
アカウント xxx を削除します。
</v-card-text>
<v-divider></v-divider>
<v-card-actions
>
<v-spacer></v-spacer>
<v-btn color="primary" flat @click="dialog = false">
キャンセル
</v-btn>
<v-btn color="primary" flat @click="dialog = false">
削除
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
name: 'Delete',
data () {
return {
dialog: false
}
}
}
</script>
コードの説明をします。
一覧画面に表示するアイコンと、アイコンを押下したときに開くダイアログを定義しています。
<template>
<v-dialog v-model="dialog" width="500">
<v-btn
fab
x-small
class="mx-2"
slot="activator"
color="red lighten-2"
dark
@click="dialog = true"
>
<v-icon dark>
mdi-delete
</v-icon>
</v-btn>
ダイアログに表示する内容です。メッセージと削除を決断したときに押されるボタンを作成します。
<template>
...
<v-card>
<v-card-title class="headline grey lighten-2" primary-title>
アカウント削除
</v-card-title>
<v-card-text>
アカウント xxx を削除します。
</v-card-text>
<v-divider></v-divider>
<v-card-actions
>Ï
<v-spacer></v-spacer>
<v-btn color="primary" flat @click="dialog = false">
キャンセル
</v-btn>
<v-btn color="primary" flat @click="dialog = false">
削除
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
Vueオブジェクトはダイアログを開くか閉じるかのdialogを定義しています。
<script>
export default {
name: 'Delete',
data () {
return {
dialog: false
}
}
}
</script>
実行し、削除アイコンをクリックすると次の画面の様になります。
5-2 ダイアログに削除対象のアカウント名称を表示
アカウント編集画面でやったように、アカウント一覧からアカウント情報を受け渡し、削除ダイアログに表示します。
変種画面と違うのは渡したい項目が複数あるのでオブジェクト形式で受け渡しています。
<template>
...
<tbody>
<tr v-for="account in accounts" v-bind:key="account.id">
<td>
<Update v-bind:account-id="account.id"></Update>
</td>
<td>
<Delete v-bind:account="account"></Delete>
</td>
...
</template>
...
Vueのprops
プロパティではtypeにObjectを指定します。
<script>
export default {
name: 'Delete',
props: {
account: {
type: Object,
required: true
}
},
...
data () {
...
</script>
5-3 削除APIの呼び出し
flaskを使ってRestAPIサーバを作ってみる(削除編)で作成した削除APIは次のフォーマットです。
request
http://localhost:5000/account/delete/<account_id>
へのGETリクエストです。
アカウントIDを指定するだけです。
response
{
"body": "",
"status": {
"code" : "I0001",
"message" : "deleted Account Succesfuly.",
"detail" : ""
}
}
アカウント編集画面とほぼ同じですが次の修正で削除できるようになります。
削除ボタンからsubmitメソッドを呼び出しアカウント削除APIを呼び出しています。削除APIはGETのAPIとして作成したのでURLにアカウントID足すだけです。
<template>
...
<v-btn color="primary" text @click="submit(); dialog = false">
削除
</v-btn>
...
</template>
<script>
var url = 'http://localhost:5000/api/account/'
const config = {
headers: {
'Content-Type': 'application/json'
}
}
...
methods: {
submit () {
console.log(this.account.id)
this.axios
.get(url + 'delete/' + this.account.id, config)
.then(function (response) {
console.log('Delete axios response')
console.log(response)
document.location = 'http://localhost:8080/account'
})
.catch(err => {
console.log('Update axios error')
console.log(err)
})
}
}
}
</script>
課題 5.1
削除の時に他のユーザーによって対象アカウント情報が編集・削除されるのを防ぐためユーザーを選ぶ時点でlockのAPIを呼び出しロックをかけるのが良いですが、
GETのAPIはoperation_account_idを取れないため実行者を特定できません。
flaskを使ってRestAPIサーバを作ってみる(削除編)の課題 5.1を行なった上で、アカウント一覧画面から削除アイコンをクリックしたら該当アカウントをロックし、削除ボタン押下時に、ロックした操作ユーザーであれば削除できるよう様変更してください。
6. ビルド
flask_svで作成したアプリサーバーのアドレスをmain.jsに反映します。
次の行、URLのホスト部を変更してください。
...
axios.defaults.baseURL = 'http://ec2-xxx-xxx-xxx-xxx.ap-northeast-1.compute.amazonaws.com:8080'
...
上記設定はAWS ec2としてアプリサーバーを構築した場合です。
Vueのアプリケーションを公開するの準備をします。VueのアプリケーションはJavaScriptで作られているためJavaScriptを圧縮したファイルをWebサーバに配置する静的コンテンツになります。
node.jsを動かした使い方は開発時のみです。
ビルドは frontend
ディレクトリの配下に vue.config.js
ファイルを作成し、次の設定を記述します。
module.exports = {
transpileDependencies: [
'vuetify'
],
publicPath: './'
}
設定は、vuetifyを使う指定と、ビルドしたコンテンツのパスをpublicPathに記載します。
パスを './' にするときは、デプロイするWebサーバーのトップディレクトリにデプロイする時に指定してください。
もし、デプロイするディレクトリがトップでない場合、次の様に'/'からのフルパスを指定します。
publicPath: '/account/'
トップディレクトリにデプロイする時の注意。
publicPath: './'
'./' と設定せず次の '/' と設定するとスタイルシートとなどのパスが正しく生成されないようで、真っ白な画面となってしまいました。
publicPath: '/'
vue.config.jsファイルを作成したら次のコマンドを実行します。
$ npm run build
> creist_vue_study@0.1.0 build
> vue-cli-service build
⠸ Building for production...Deprecation Warning: Using / for division is deprecated and will be removed in Dart Sass 2.0.0.
...
entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
Entrypoints:
app (686 KiB)
css/chunk-vendors.622851a2.css
js/chunk-vendors.28bb5db6.js
js/app.e5c05d7d.js
File Size Gzipped
dist/js/chunk-vendors.28bb5db6.js 339.11 KiB 111.90 KiB
dist/js/chunk-3f26258d.b69a3385.js 54.88 KiB 14.13 KiB
dist/js/chunk-3b23b504.f494e37c.js 14.36 KiB 4.67 KiB
dist/js/app.e5c05d7d.js 9.91 KiB 3.95 KiB
dist/js/chunk-2d0db120.9c50a2db.js 5.23 KiB 1.95 KiB
dist/js/about.8637adf0.js 0.44 KiB 0.31 KiB
dist/css/chunk-vendors.622851a2.css 337.29 KiB 40.70 KiB
dist/css/chunk-3f26258d.e4000e66.css 43.65 KiB 6.14 KiB
dist/css/chunk-3b23b504.c4030062.css 2.09 KiB 0.69 KiB
Images and other types of assets omitted.
DONE Build complete. The dist directory is ready to be deployed.
INFO Check out deployment instructions at https://cli.vuejs.org/guide/deployment.html
distディレクトリが作成され、その配下にWEBサーバーに配置すべきファイル群ができます。
7. デプロイ
デプロイはapacheやnginxのWEBサーバーのドキュメントディレクトリにdistディレクトリの配下をコピーします。
また、AWS s3に配置することでwebサーバーとしてデプロイすることもできます。
s3は所有するドメインのDNSで別名設定することでもWEBサーバーとして使用することができます。
WEBサーバーの設定がされているs3の spa-study.mydomain.com
バケットにdist配下のファイル群をコピー(syncは同期)するには次の様にコマンドを実行します。
$ aws s3 sync dist s3://spa-study.mydomain.com --delete --include "*"
- AWS s3を使うためにはAWSにアカウントが必要です。
- awsというコマンドは、AWS コマンドラインインターフェイスを参考にインストールしてください。
8. 最後に
CRUDができるAPIサーバーと通信してCRUDを行うフロントアプリを実装してみました。
まだこの状態では本番稼働できる状態とは程遠いですが、まだWebアプリを作ったことがない方の参考になればと思います。