16
17

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 1 year has passed since last update.

【コピペでOK】Vue3×FastAPI×MySQL×DockerでWebアプリを作ってみた

Last updated at Posted at 2022-05-19

目次

1.はじめに
2.ゴール
3.DB構築
4.Backend構築
5.Frontend構築
6.動作確認

1. はじめに

よくあるWebアプリケーションの超簡略版を作ってみた。
「ちゃちゃっと作って遊びたい」「コピペで作ってみてから学びたい」人向けの簡単な内容を掲載する。

対象 使うもの
Front-end Vue3/Element Plus
Back-end FastAPI(python)
Database MySQL

frontendはVue3を使うことにした。Composition APIなどが優秀等々話は聞くが、もともとVuejsが好きという個人的な理由により採用。
わざわざVueを使うまでもない内容かもしれないが、とりあえず触れてみる事は非常に大事だと思うため使う。

CSSフレームワークをElement Plusを使用。Vue3に対応しているCSSフレームワークは少ない印象。洗礼されたデザインなのでおすすめ。

backendはFastAPIを、DBはMySQLを使用。 FastAPIとmysqlについて本記事より詳しい内容・手順をこちらで掲載しているため、是非一読して頂きたい。

2. ゴール

よくあるWebアプリケーションの超簡略版を作ってみた。」と冒頭に記載したが、具体的にはCRUD操作が可能な簡単なデータ管理アプリを作る。

データ管理アプリは下記を実現する

  • ユーザはデータを閲覧する
  • ユーザはデータを登録(追加)する
  • ユーザはデータを削除する
  • ユーザはデータを更新する

シンプルCRUD。

最終的な生成物は下記。

  • build済み静的コンテンツ
    • AWS S3などにポイっとおけば使える状態のfrontend
  • API Docker image
    • AWS ECSなどにポポイッとデプロイすれば良い状態のbackendのdocker image
  • DB Docker image
    • ローカルで検証するためのDB Conatiner用Docker image。front/bankをCloudにデプロイする場合は Managedサービスを使う方があるある(かもしれない)

3. DB構築

最初にDBを作る。Frontendから作りたい気持ちもわかるが、個人的にはデータ管理を行うためデータ層から順に着手した方が理解しやすかった。

まず全体の作業ディレクトリ(sample_app)とDB用のディレクトリ(db)を作成し、dbディレクトリ配下に下記2ファイルを新規作成する。

.
└── sample_app/
    └── db/
        ├── init.sql
        └── Dockerfile

3-1. initialze sql

DBコンテナを立ち上げた際に、テーブル作成や初期データ投入を行うためにinit.sqlを作成しておく。

init.sql
USE sample_db;

DROP TABLE IF EXISTS item;
CREATE TABLE item
(
  item_id SERIAL PRIMARY KEY,
  name VARCHAR(50),
  price INT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

INSERT INTO item (name, price) VALUES ('apple', 100);
INSERT INTO item (name, price) VALUES ('orange', 200);

3-2. MySQL Container

続いてDockerfileについて。

Dockerfile
FROM mysql:latest

ENV MYSQL_DATABASE sample_db
ENV MYSQL_USER sample_user
ENV MYSQL_PASSWORD sample_password
ENV TZ "Asia/Tokyo"
ENV MYSQL_ROOT_PASSWORD p@assw0rd

COPY ./init.sql /docker-entrypoint-initdb.d/init.sql

では早速imageをbuild。

cd db # Dockerfileのあるディレクトリまで移動
docker build -t sample-mysql:1.0.0 .

次にコンテナも立ち上げてみる。

docker run -d --name sample-mysql-container -p 3306:3306 sample-mysql:1.0.0

確認

docker ps -f "name=sample-mysql-container" 
CONTAINER ID   IMAGE                COMMAND                  CREATED         STATUS         PORTS                                                  NAMES
9373afb82b08   sample-mysql:1.0.0   "docker-entrypoint.s…"   5 seconds ago   Up 4 seconds   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp   sample-mysql-container

いた。次にコンテナ内に入り、init.sql内で記載したinsert通りにデータが存在するか確認する。

  1. コンテナ内に入る。
    docker exec -it $(docker ps -q -f "name=sample-mysql-container") /bin/bash
    
  2. dbに接続。
    mysql sample_db -h localhost -u sample_user -psample_password
    
  3. レコードを確認
    mysql> select * from item;
    +---------+--------+-------+---------------------+---------------------+
    | item_id | name   | price | created_at          | updated_at          |
    +---------+--------+-------+---------------------+---------------------+
    |       1 | apple  |   100 | 2022-05-19 22:16:31 | 2022-05-19 22:16:31 |
    |       2 | orange |   200 | 2022-05-19 22:16:31 | 2022-05-19 22:16:31 |
    +---------+--------+-------+---------------------+---------------------+
    2 rows in set (0.00 sec)
    

無事構築が完了したので、exitでmysql/containerのコンソールを抜けておこう。

後続の構築が完了した後、sample-mysql-containerに接続するため立ち上げっぱなしでOK。

4. Backend構築

続いてBackendを構築する。

API用のディレクトリ(backend)を作成し、backendディレクトリ配下にAPIを構築する、

.
└── sample_app/
    ├── db/
    │   ├── init.sql
    │   └── Dockerfile
    └── backend/
        ├── Dockerfile
        ├── .env
        └── app/
            ├── requirements.txt
            └── main.py

4-1. requirements.txt

パッケージ一括管理用にrequirements.txtを(使用する内容は少数だが)作成しておく。

requirements.txt
sqlalchemy==1.4.31
mysqlclient==2.1.0

4-2. app

早速コーディングしてみる。DBの接続先情報は環境変数から取得できるようにしておく。

main.py
import os
from fastapi import FastAPI, Depends, Query, status
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
from sqlalchemy import Column, TIMESTAMP, Integer, String, create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.schema import FetchedValue


ROOT_PATH="/api"

app = FastAPI()

SQLALCHEMY_DATABASE_URI = 'mysql://{}:{}@{}:{}/{}'.format(
        os.environ.get("DB_USER"), 
        os.environ.get("DB_PASSWORD"),
        os.environ.get("DB_HOST"), 
        os.environ.get("DB_PORT"), 
        os.environ.get("DB_NAME")
    )

engine = create_engine(SQLALCHEMY_DATABASE_URI)
Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def session():
    db = Session()
    try:
        yield db
    finally:
        db.close()

Base = declarative_base(bind=engine)

# Entity Item
class Item(Base):
    __tablename__ = "item"
    __table_args__ = {"autoload": True}
    item_id = Column(Integer, primary_key = True, nullable=False)
    name = Column(String(50), nullable=False)
    price = Column(Integer, nullable=False)
    created_at = Column(TIMESTAMP, FetchedValue())
    updated_at = Column(TIMESTAMP, FetchedValue())

# Request Body
class ItemRequest(BaseModel):
    name: str = Query(..., max_length=50)
    price: float

# GetItemByName
@app.get('%s/item' % ROOT_PATH)
def get_items(db: Session = Depends(session)):
    result_set = db.query(Item).all()    
    response_body = jsonable_encoder({"list": result_set})
    return JSONResponse(status_code=status.HTTP_200_OK, content=response_body)

# CreateItem
@app.post('%s/item' % ROOT_PATH)
def create_item(request: ItemRequest, db: Session = Depends(session)):
    item = Item(
                name = request.name,
                price = request.price
            )
    db.add(item)
    db.commit()
    response_body = jsonable_encoder({"item_id" : item.item_id})
    return JSONResponse(status_code=status.HTTP_200_OK, content=response_body)

# UpdateItem
@app.put('%s/item/{id}' % ROOT_PATH)
def update_item(id: int, request: ItemRequest, db: Session = Depends(session)):
    item = db.query(Item).filter(Item.item_id == id).first()
    if item is None:
            return JSONResponse(status_code=status.HTTP_404_NOT_FOUND)
    item.name = request.name
    item.price = request.price
    db.commit()
    return JSONResponse(status_code=status.HTTP_200_OK)

# DeleteItem
@app.delete('%s/item/{id}' % ROOT_PATH)
def delete_item(id: int, db: Session = Depends(session)):
    db.query(Item).filter(Item.item_id == id).delete()
    db.commit()
    return JSONResponse(status_code=status.HTTP_200_OK)

4-3. 環境変数

次にローカルでの稼働確認時にコンテナに渡す環境変数を定義する。

.env
DB_HOST=host.docker.internal
DB_PORT=3306
DB_NAME=sample_db
DB_USER=sample_user
DB_PASSWORD=sample_password

注意 ※1
本記事ではdocker-composeを使用しないため無理にHOST指定をhost.docker.internalとしているが、いつまでサポートされているか不明なため可能であればcomposeした方が良い。

公式より引用

ホストの IP アドレスは変動します(あるいは、ネットワークへの接続がありません)。18.03 よりも前は、特定の DNS 名 host.docker.internal での接続を推奨していました。これはホスト上で内部の IP アドレスで名前解決します。これは開発用途であり、Docker Desktop forMac 外の本番環境では動作しません。

4-4. Api Container

次にDockerfile。

Dockerfile
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9-slim

COPY ./app /app

RUN apt-get update && \
    apt-get -y install gcc libmariadb-dev && \
    pip install --no-cache-dir --upgrade -r /app/requirements.txt

では早速imageをbuild。

cd ../backend # Dockerfileのあるディレクトリまで移動
docker build -t sample-backend:1.0.0 . 

次にコンテナを立ち上げるが、立ち上げの際に環境変数定義ファイルを渡す。

docker run -d --env-file=.env --name sample-backend-container -p 80:80 sample-backend:1.0.0

docker ps -f "name=sample-backend-container" 
CONTAINER ID   IMAGE                  COMMAND       CREATED         STATUS         PORTS                               NAMES
db4751373a62   sample-backend:1.0.0   "/start.sh"   3 seconds ago   Up 2 seconds   0.0.0.0:80->80/tcp, :::80->80/tcp   sample-backend-container

無事コンテナの立ち上げが完了したので、ログを確認して動いているか見てみる。

docker logs --tail=-1  $(docker ps -q -f "name=sample-backend-container") 
#省略
[2022-05-19 13:19:25 +0000] [1] [INFO] Listening at: http://0.0.0.0:80 (1)
#省略

動いてそう。念のためcurlで呼び出せるか確認してみる。

curl -s -X GET 'http://localhost:80/api/item' | jq

{
  "list": [
    {
      "created_at": "2022-05-19T22:16:31",
      "item_id": 1,
      "updated_at": "2022-05-19T22:16:31",
      "price": 100,
      "name": "apple"
    },
    {
      "created_at": "2022-05-19T22:16:31",
      "item_id": 2,
      "updated_at": "2022-05-19T22:16:31",
      "price": 200,
      "name": "orange"
    }
  ]
}

とれた。本来ならばfrontやdbと結合する前にUTを行うべきだが、本記事はちゃちゃっと作って遊びたい人向けなため行わない。

5. Frontend構築

続いてFrontendを構築する。

Frontendはコンテナとして立ち上げない想定なので、専用のディレクトリを手動では作成せず、npmに任せてみる。

本記事では下記実行環境で開発を進めている。

node -v
v17.6.0

npm -v
8.9.0

では早速構築してみる。

cd ../ # sample_appへ移動
npm init vue@latest # project作成

Vue.js - The Progressive JavaScript Framework

✔ Project name: … frontend
✔ Add TypeScript? … Yes # Yesを選択
# 以降は本記事では全てNoで問題なし。
✔ Add JSX Support? … No 
✔ Add Vue Router for Single Page Application development? … No
✔ Add Pinia for state management? … No
✔ Add Vitest for Unit Testing? … No
✔ Add Cypress for both Unit and End-to-End testing? … No
✔ Add ESLint for code quality? … No

projectが作成されたので構築を進める。

一旦作成されたprojectを立ち上げてみる。

cd frontend
npm install
npm run dev

下記のような表示がでればOK.(viteで立ち上がってて嬉しい。)

vite v2.9.8 dev server running at:

> Local: http://localhost:3000/
> Network: use `--host` to expose

ready in 287ms.

ではwebブラウザでhttp://localhost:3000/アクセスしてみる。

template.png

作成されたtemplate projectではダークモードに対応しており、OSの設定や作業時間によっては配色が異なる。
鬱陶しさを感じた場合は、OSのダークモードを切るか frontend/src/assets/base.css@media (prefers-color-scheme: dark)オブジェクトをごっそりコメントアウトしてしまっても良いと思う。

ctrl+cなどで停止できるが、停止せずともファイルに更新があれば自動で反映される。pcが重い場合や複数コンソールを開くのが嫌な場合はctrl+cなどで停止しておこう。

現在のファイル構成を書きに記す。frontendのモジュールはファイル数が多いため割愛。

.
└── sample_app/
    ├── db/
    │   ├── init.sql
    │   └── Dockerfile
    ├── backend/
    │   ├── Dockerfile
    │   ├── .env
    │   └── app/
    │       ├── requirements.txt
    │       └── main.py
    └── frontend/
        ├── 割愛
        └── src/
            ├── assets/
            │     ├── 割愛
            │     └── base.css
            ├── components/
            │     └── 割愛(vueが気になる方は是非中身も見て欲しい)
            └── main.ts

次にelement-plusの導入

npm install element-plus # element-plusとを導入
npm install axios # api call用にとaxiosを導入
npm install vue-router@latest # vue-routerの導入
jq .dependencies package.json # jqが無ければcatでもOK
{
  "axios": "^0.27.2",
  "element-plus": "^2.1.11",
  "vue": "^3.2.33",
  "vue-router": "^4.0.15"
}

次に、使用できるようにsrc/main.tsを編集する。

main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from '@/router';
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

const app = createApp(App)

app.use(ElementPlus)
app.use(router)
app.mount('#app')

他にも追加する場合は同様にimportとして.use(xxx)すれば良いが、広げすぎるとパパッとではなくなるため詳しくは割愛。

backendを呼び出すためのファイルを作成する。

src/
└── service/
    ├── httpclient.ts
    └── DataApiService.ts

api呼び出しの機能はaxiosを使い、共通化してhttpclient.tsとして作成しておく。

httpclient.ts
import type { AxiosInstance } from "axios";
import axios from "axios";

const apiClient: AxiosInstance = axios.create({});

export default apiClient;

わざわざ別定義するまでもないかもしれないが、拡張性を考慮して定義しておく。

次に、実際のcall部分を定義する。

DataApiService.ts
import http from "@/service/httpclient";

const APPLICATION_JSON : string = "application/json";

class DataApiService {
  getAll(): Promise<any> {
    return http.get("/api/item");
  }

  create(name: string, price: number): Promise<any> {
    return http.post(
        `/api/item`, 
        { 
            name: `${name}`,
            price: `${price}`
        }
    );
  }

  update(id: number, name: string, price: number): Promise<any> {
    return http.put(
        `/api/item/${id}`,
        { 
            name: `${name}`,
            price: `${price}`
        }
    );
  }

  delete(id: number): Promise<any> {
    return http.delete(`/api/item/${id}`);
  }
}

export default new DataApiService();

backendに用意した4つのAPIを呼び出す単純なクラスを作成した。

続いてroutingを行う定義体をsrc直下にrouter.tsとして作成する。

router.ts
import { createRouter,createWebHistory } from 'vue-router';
import List from './components/ListPage.vue';
import Add from './components/AddPage.vue';
import Edit from './components/EditPage.vue';
 
const routes = [
    { path: '/', name: 'list', component: List },
    { path: '/add', name: 'add', component: Add },
    { path: '/edit', name: 'edit', component: Edit, props: true },
]
 
const router = createRouter({
    history: createWebHistory(), // HTML5 History モード
    routes,
})
 
export default router;

さらにApp.vueを編集し、routingされたcomponentを表示するrouter-viewを追加する。

App.vue
<template>
  <el-main>
    <router-view></router-view>
  </el-main>
</template>

<style>
@import './assets/base.css';
#app {
  max-width: 1280px;
  margin: 0 auto;
  padding: 0rem;
  font-weight: normal;
}

@media (min-width: 1024px) {
  #app {
    padding: 0 2rem;
  }
}

.dialog-footer {
     text-align: center;
}
.dialog-footer button {
    width: 100px;
    margin: 0 0 10px 0;
}

@media (min-width: 1024px) {
  .dialog-footer button{
      width: 100px;
      margin: 0 10px 0 0;
      
  }
}
</style>

とてもシンプル。フロントが専門ではない私はこれぐらいシンプルな方が理解しやすくvueは好き。

router-viewに込められるComponentを3つ定義する。

src/
└── components/
    ├── ListPage.vue
    ├── AddPage.vue
    └── EditPage.vue

まずはListPage.vueについて。こちらは単にDBをselectして一覧表示するというもの。

ListPage.vue

<script lang='ts' setup>
import { 
  ref,
} from 'vue'

import { 
    useRouter,
} from 'vue-router'

import {
  Edit,
  Delete,
} from '@element-plus/icons-vue'

import { 
  ElMessage, 
  ElMessageBox,
} from 'element-plus'

import DataApiService from "@/service/DataApiService";

const router = useRouter()

const isLoading = ref(false)
const data = ref([])

const initilize = async () => {
    console.log('initilize')
    return new Promise(async (resolve, reject) => {  
      isLoading.value = true;
      await DataApiService.getAll()
        .then((response) => {
          data.value = response.data.list
          resolve(200);
        })
        .catch((error) => {
          console.log(error)
          data.value = [];
          reject(500);
        })
        .finally(() => {
          isLoading.value = false;
        })
  });
    
}

const deleteData = (index: number) => {
 console.log( data.value[index])
  ElMessageBox.confirm(
    '本当に削除しますか?',
    '確認',
    {
      confirmButtonText: '削除する',
      cancelButtonText: 'キャンセル',
      type: 'warning',
      center: true,
    }
  )
    .then(() => {
      console.log('start api call');
      DataApiService.delete(data.value[index]["item_id"])
        .then(() => {
          initilize();
          ElMessage({
            type: 'success',
            message: '削除が完了しました。',
          })
        })
        .catch((error) => {
          console.log('error_%s',error);
           ElMessage({
            type: 'error',
            message: '削除に失敗しました。',
          })
        });  
    })
    .catch(() => {
      ElMessage({
        type: 'info',
        message: '中止しました。',
      })
    })
}

const editData = (index: number) => {
    router.push({ name:'edit',params: { 
            id: data.value[index]["item_id"],
            name: data.value[index]["name"],
            price: data.value[index]["price"]
        }
    })
}

const addData = () => {
    router.push({name:'add'})
}


initilize()
</script>

<template>
 <el-table :data='data' style='width: 100%' max-height='500' v-loading.fullscreen.lock="isLoading">
    <el-table-column fixed prop='item_id' label='ID' width='50' />
    <el-table-column prop='name' label='名称' width='100' />
    <el-table-column prop='price' label='価格' width='100' />
    <el-table-column prop='created_at' label='作成日時' width='200' />
    <el-table-column prop='updated_at' label='更新日時' min-width='200' />
    <el-table-column fixed='right' label='操作' width='120'>
      <template #default='scope'>
        <el-button type='primary' :icon='Edit' circle @click.prevent='editData(scope.$index)'/>
        <el-button type='danger' :icon='Delete' circle @click.prevent='deleteData(scope.$index)'/>
      </template>
    </el-table-column>
  </el-table>
  <el-button class='mt-4' style='width: 100%' @click='addData'>Add Item</el-button>
</template>

長くなってしまった。関数群は別ファイルに出したいところだが、componentごとに紐づいたものは1ファイルに纏めておきたかったので上記のようなボリュームになってしまった。

初期表示時に、initilizeが呼び出され、DBから情報を取得するAPIをcall(DataApiService.getAll())をしている。

<template>については、element plusの要素を置いているだけ。

Add Itemボタンを押下すると発火される関数は単純でrouter.pushを呼び、nameがaddとして定義された画面に遷移する。

router.push({name:'add'})

Editボタンを押下した場合は同じくrouter.pushしてname:editに遷移しているが、選択した項目の情報を渡している。

router.push({ name:'edit',params: { 
        id: data.value[index]["item_id"],
        name: data.value[index]["name"],
        price: data.value[index]["price"]
    }
})

Deleteボタンを押下すると、確認popupを挟んでからDeleteのAPI Call(DataApiService.delete())をし、正常終了後に再びinitilizeを呼ぶ。

続いてListPageにてAdd Itemを押下し遷移する先(AddPage.vue)を定義する。

AddPage.vue
<script lang="ts" setup>
import { 
    reactive,
} from 'vue';

import { 
  ElMessage, 
  ElMessageBox,
} from 'element-plus'

import { 
    useRouter,
} from 'vue-router'

import DataApiService from "@/service/DataApiService";

const router = useRouter()

const formLabelWidth = '120px'
const form = reactive({
  name: '',
  price: 0,
})

const cancel = () => {
 router.push({name:'list'})
}

const add = (index: number) => {
  ElMessageBox.confirm(
    '本当に追加しますか?',
    '確認',
    {
      confirmButtonText: '追加する',
      cancelButtonText: 'キャンセル',
      type: 'warning',
      center: true,
    }
  )
    .then(() => {
      console.log('start api call');
      DataApiService.create(form.name,form.price)
        .then(() => {
          router.push({name:'list'})
          ElMessage({
            type: 'success',
            message: '追加が完了しました。',
          })
        })
        .catch((error) => {
          console.log('error_%s',error);
           ElMessage({
            type: 'error',
            message: '追加に失敗しました。',
          })
        });  
    })
    .catch(() => {
      ElMessage({
        type: 'info',
        message: '中止しました。',
      })
    })
}
</script>

<template>
    <el-form :model="form">
        <el-form-item label="ID" :label-width="formLabelWidth">
            <el-input :disabled="true" placeholder="自動採番"/>
        </el-form-item>
        <el-form-item label="名称" :label-width="formLabelWidth">
            <el-input v-model="form.name" autocomplete="off" />
        </el-form-item>
        <el-form-item label="価格" :label-width="formLabelWidth">
            <el-input v-model="form.price" autocomplete="off" />
        </el-form-item>
    </el-form>
    <div class="dialog-footer">
        <el-button @click="cancel">Cancel</el-button>
        <el-button type="primary" @click="add">Confirm</el-button>
    </div>
</template>

templateについては特筆すべきことはなく、element plusの要素をペタペタ置いただけ。

Confirmボタンを押下すると関数addが呼び出され、deleteと同様に確認popupを挟みAPIcall(DataApiService.create())を呼び出す。
完了後は、name:listつまり一覧表示画面(ListPage.vue)に遷移するようにした。

続いてListPageにてEditを押下し遷移する先(EditPage.vue)を定義する。

EditPage.vue
<script lang="ts" setup>
import { 
    reactive,
} from 'vue';

import { 
  ElMessage, 
  ElMessageBox,
} from 'element-plus'

import { 
    useRouter,
} from 'vue-router'

import DataApiService from "@/service/DataApiService";

const router = useRouter()

const formLabelWidth = '120px'
const form = reactive({
  id: 0,
  name: '',
  price: 0,
})

const cancel = () => {
 router.push({name:'list'})
}

const edit = () => {
  ElMessageBox.confirm(
    '本当に更新しますか?',
    '確認',
    {
      confirmButtonText: '更新する',
      cancelButtonText: 'キャンセル',
      type: 'warning',
      center: true,
    }
  )
    .then(() => {
      console.log('start api call');
      DataApiService.update(form.id,form.name,form.price)
        .then(() => {
          router.push({name:'list'})
          ElMessage({
            type: 'success',
            message: '更新が完了しました。',
          })
        })
        .catch((error) => {
          console.log('error_%s',error);
           ElMessage({
            type: 'error',
            message: '更新に失敗しました。',
          })
        });  
    })
    .catch(() => {
      ElMessage({
        type: 'info',
        message: '中止しました。',
      })
    })
}




const props = defineProps({
  id: Number,
  name: String,
  price: Number
})

const initilize = () => {
    if(props.id === undefined) {
        router.push({name:'list'})
        ElMessage({
            type: 'error',
            message: '情報の取得に失敗しました。',
        })
    } else {
        form.id = props.id;
        form.name = props.name == undefined ? '' : props.name;
        form.price = props.price == undefined ? 0 : props.price;
    }
}

initilize();
</script>


<template>
    <el-form :model="form">
        <el-form-item label="ID" :label-width="formLabelWidth">
            <el-input v-model="form.id" :disabled="true"/>
        </el-form-item>
        <el-form-item label="名称" :label-width="formLabelWidth">
            <el-input v-model="form.name" autocomplete="off" />
        </el-form-item>
        <el-form-item label="価格" :label-width="formLabelWidth">
            <el-input v-model="form.price" autocomplete="off" />
        </el-form-item>
    </el-form>
    <div class="dialog-footer">
        <el-button @click="cancel">Cancel</el-button>
        <el-button type="primary" @click="edit">Confirm</el-button>
    </div>
</template>

基本的にはAddPage.vueと同じである。違いは遷移時に値を受け渡している箇所だ。

前述のrouter.tsで定義したがpropsプロパティをtrueと設定する必要がある。

router.ts
{ path: '/edit', name: 'edit', component: Edit, props: true }

さらに、値を受け取る側であるEditPage.vueでは definePropsを定義する。

const props = defineProps({
  id: Number,
  name: String,
  price: Number
})

以上で完成。

6. 動作確認

では早速動かしてみる。

npm run dev  

最終的な完成物・動作画面はこちら。
qiita_webapp_sample.gif

説明は掻い摘んだがコピペで作成可能な量で、とりあえず動かすものを手に入れてから遊びたい方は是非参考にしていただければと思う。

以上。

16
17
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
16
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?