5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Vue.js+vuetifyとflaskで作ったRest APIサーバー間で通信し、CRUDを行うアプリケーションを作る​

Last updated at Posted at 2021-10-24

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/にアクセスしてください。
次の画面が見れたらフロント部分の起動が確認できました。

image.png

サブメニューはまだ何もありません。ここにメニューを足していきましょう。

image.png

1-2 メニュー作成

ブラウザから見えるメニューは、frontend/src/App.vue で定義しています。

<template>タグの中の<v-navigation-drawer> フロントフレームワークであるVue.js+Vuetify導入#Vuetifyの開発Vuetifyでメニューを作るで解説したメニューを記述するv-list-itemタグに記載します。

navigation-drawerは画面左側からスライドしてくることで表示できるメニューになります。

frontend/src/App.vue
<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と同じ階層にアカウントのメニューを追加していきます。次のコードを追加してください。

frontend/src/App.vue
<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_itemsub_menuをjavascriptで定義します。

これを埋め込むとアカウントメニューが表示されます。

image.png

メニューを開くと、追加したメニューの通り一覧と作成のサブメニューも表示されました。

image.png

しかし、まだサブメニューをクリックしても画面は表示されません。
そのパスがどのコンポーネントに紐づいているかを記述します。

次のindex.jsにアカウントの一覧表示と作成画面のパスを追加しましょう。

frontend/src/router/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

上記のコードに以下のアカウント一覧とアカウント作成の設定を追加します。

frontend/src/router/index.js
    // 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を参照してください。

frontend/src/views/account/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>
           <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にアクセスしてみましょう。

image.png

2-2 vueでデータを表示

vueの機能を使ってtableにデータを埋め込みましょう。 List.vue を次の様に変更してください。

frontend/src/views/account/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にアクセスしてみましょう。

image.png

一覧画面が表示されます。

コーディングを飛ばしたい方は次の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 を次の様に変更してください。

frontend/src/views/account/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テーブルに入っているレコードが表示されていれば成功です。

image.png

もし、本ブログから読み始めてレコードが無い人はインサートしておいてください。

$ 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でエラーとなってしまいます。

frontend/src/views/account/List.vue
<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以外にもステータスコードなど通信内容が含まれたオブジェクトです。

frontend/src/views/account/List.vue
<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を作成します。

frontend/src/views/account/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>

image.png

VuetifyのText fieldsに入力フォームのデザインや機能など載っています。
しかしこのままでは日付項目に日付のフォーマットで文字を入れなければなりません。

コードの次箇所を編集しましょう。

frontend/src/views/account/Create.vue
<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>

image.png

有効開始日、有効終了日にカーソルを持っていくとカレンダーから選択できる様になります。
VuetifyのDate pickersに日付選択するコンポーネントの説明が載っています。

3-2 vueのアプリケーションの実装

次にアプリケーションの実装をしましょう。
vuetifyのFormsを参考にリセットボタンを実装します。
<v-btn>で作成されたリセットボタンにclickされた時の関数を定義します。

frontend/src/views/account/Create.vue
<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は次の様に修正します。

frontend/src/views/account/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の呼び出しに必要な情報です。

frontend/src/views/account/Create.vue
<script>

var contentType = 'application/json'
var url = 'http://localhost:5000/api/account/create'
const config = {
  headers: {
    'Content-Type': contentType
  }
}
...
</script>

dataはフォームに必要な項目を定義します。

frontend/src/views/account/Create.vue
<script>
...
export default {
  name: 'account_create',
  data: function () {
    return {
      account_name: '',
      start_on: '',
      end_on: '',
      created_by: '',
      message: '出力メッセージ'
    }
  },
...
</script>

form関数でAPIへ送信するJsonを作る。

frontend/src/views/account/Create.vue
<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を真似して作成します。

frontend/src/views/account/Create.vue
<script>
...
  methods: {
    validate () {
      this.$refs.form.validate()
    },
    reset () {
      this.$refs.form.reset()
    },
    resetValidation () {
      this.$refs.form.resetValidation()
    },
...
</script>

保存ボタンが押されるときに呼び出される関数です。HTTP通信を行うaxiosライブラリでAPIを呼び出します。正常に終わったらアカウント一覧画面にリダイレクトしています。

frontend/src/views/account/Create.vue
<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>

リセットボタンが押された時に呼ばれる関数です。

frontend/src/views/account/Create.vue
<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 一覧画面に編集ボタンを追加

アカウント一覧画面に戻って次の修正をしましょう。

frontend/src/views/account/List.vue
<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>

編集ボタンが表示される様になりました。

image.png

4-2 一覧画面の編集ボタンからモーダル編集画面を起動

アイコンボタンからListの中にモーダルウィンドウを開くことにしましょう。モーダルはHTMLでは一つのコードとして作りますが、vueを使っているのでコンポーネントとして埋め込みましょう。

一覧画面は次の様に変更します。
コンポーネントのタグに置き換えます。埋め込むコンポーネントはvueファイルをimportして、componentsに追加します。

frontend/src/views/account/List.vue
<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 として作成します。

frontend/src/views/account/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>

更新ボタンをクリックするとモーダル小画面が開きます。

image.png

抜粋で、コードの解説をします。
<v-dialog>でモーダルウィンドウやダイアログを作ることができます。
vuetifyのDialogsに載っているコードをいろいろ試して利用します。

frontend/src/views/account/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-text-field>で作っています。VuetifyのText fieldsを参照してください。
<v-layout>は、グリッドシステムを実現するタグです。グリッドシステムはvuetifyのことではないですが
絶対抑えておきたいWebデザインのグリッドシステムとフレームワーク #グリッドシステムが判りやすいかと思います。wrapで縦に分割する様です。
<v-flex>がグリッドの分割の単位になります。xsは、600px未満で12分割。ブラウザの幅によってグリッドの分割を変えることができるのでレスポンシブウェブデザインを実現できる様です。
<v-card>は1つにまとめるために使う様です。VuetifyのCardsの説明はあまり良くわからなく、v-cardを理解するためVuetify.js を使ってマテリアルデザインに挑戦しよう!を参考にしました。

frontend/src/views/account/Update.vue
<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から取得した値を埋め込むように作っていきましょう。

frontend/src/views/account/Update.vue
<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タグを次の様に変更してください。

frontend/src/views/account/List.vue
<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のコードを次の様に修正しましょう。

frontend/src/views/account/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がオンになりモーダルウィンドウを開きます。

frontend/src/views/account/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>

アカウント一覧画面、アカウント作成画面同様、apiを呼ぶための情報を変数に設定しています。

frontend/src/views/account/Update.vue
<script>
var url = 'http://localhost:5000/api/account/'
const config = {
  headers: {
    'Content-Type': 'application/json'
  }
}

フロントで扱う日付情報を変換する関数を作りました。
date-picker で作った日付は時間が入っていにのですが、DB上の対応する項目はdatetimeなのでフォーマットを合わせます。もし、日付だけであった場合00:00:00を付加します。

frontend/src/views/account/Update.vue
<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オブジェクトに変換する関数です。

frontend/src/views/account/Update.vue
<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を受け取れます。

frontend/src/views/account/Update.vue
<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に入れ直すことでフォームを再描画します。

frontend/src/views/account/Update.vue
<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を次の様に変更してください。

frontend/src/views/account/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のメソッド名を書きます。

frontend/src/views/account/Update.vue
<template>
...
        <v-btn
          color="primary"
          text
          @click="
            submit();
            dialog = false;
          "
        >
          更新
        </v-btn>
...
</template>
...

form関数は、入っている値をフォーマットしてからAPIに渡せるリクエストの形式にする関数です。

frontend/src/views/account/Update.vue
...
<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に送り、一覧画面に遷移します。

frontend/src/views/account/Update.vue
...
<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 削除アイコンの追加とダイアログ表示

まずは、一覧画面にアイコンを追加し、フォームでないダイアログを表示してみます。

次の修正をしてください。

frontend/src/views/account/List.vue
<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を作りましょう。

frontend/src/views/account/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>

コードの説明をします。

一覧画面に表示するアイコンと、アイコンを押下したときに開くダイアログを定義しています。

frontend/src/views/account/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>

ダイアログに表示する内容です。メッセージと削除を決断したときに押されるボタンを作成します。

frontend/src/views/account/Delete.vue
<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を定義しています。

frontend/src/views/account/Delete.vue
<script>
export default {
  name: 'Delete',
  data () {
    return {
      dialog: false
    }
  }
}
</script>

実行し、削除アイコンをクリックすると次の画面の様になります。

image.png

5-2 ダイアログに削除対象のアカウント名称を表示

アカウント編集画面でやったように、アカウント一覧からアカウント情報を受け渡し、削除ダイアログに表示します。
変種画面と違うのは渡したい項目が複数あるのでオブジェクト形式で受け渡しています。

frontend/src/views/account/List.vue
<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を指定します。

frontend/src/views/account/Delete.vue
<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/&lt;account_id>
へのGETリクエストです。
アカウントIDを指定するだけです。

response
{
   "body": "",
   "status": {
       "code" : "I0001",
       "message" : "deleted Account Succesfuly.",
       "detail" : ""
   }
}

アカウント編集画面とほぼ同じですが次の修正で削除できるようになります。

削除ボタンからsubmitメソッドを呼び出しアカウント削除APIを呼び出しています。削除APIはGETのAPIとして作成したのでURLにアカウントID足すだけです。

frontend/src/views/account/Delete.vue
<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のホスト部を変更してください。

frontend/src/main.js
...
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 ファイルを作成し、次の設定を記述します。

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 "*"

8. 最後に

CRUDができるAPIサーバーと通信してCRUDを行うフロントアプリを実装してみました。
まだこの状態では本番稼働できる状態とは程遠いですが、まだWebアプリを作ったことがない方の参考になればと思います。

5
4
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
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?