12
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Web APIAdvent Calendar 2020

Day 11

vuetify+vue/cliによるフロントエンドと、pythonのresponder+sqlite3によるAPIで、SPA(single-page application)のウェブサイトを実装してみたり

Last updated at Posted at 2020-05-24

はじめに

この前responderSQLiteとの組み合わせで簡単なウェブサイトを作る例をしました。

今回はresponderとSQLに加えて、SQLデータベースと接続してデータの閲覧や追加や更新や削除したりできるSPA(single-page application)のウェブサイトにします。

フロントエンドは最近とても人気のjavascriptフレームワークであるvuetifyを使います。

vuetifyのコンポーネントは特に設定しなくてもレスポンシブデザインになっているというところは便利です。

pythonの方で使うモジュールは主にsqlite3とresponderとpandasで、サーバとAPIを提供します。

フロントエンドの方では、axiosでAPIからデータを取って、vuetifyでウェブサイトを構成するのです。

vuetifyの中で主役はデータを簡単に表示できるv-data-tableというコンポーネントです。

構造

機能

  • ウェブサイトの構造はvuetifyのコンポーネントからなる
  • たった1ページしかないSPA(single page application)
  • そのページで、データの閲覧と追加と編集と削除を全部やることができる
  • サーバはresponderモジュールを含むpythonコード
  • データベースはSQLiteで、pythonのsqlite3を使う
  • APIはresponderとsqlite3とpandasモジュールからなる
  • axiosでAPIからデータを取る
  • cssスタイルシートは直接触れる必要なく、全部vuetifyに任せる
  • vuetifyのv-data-tableでサイトにデータを表示する
  • データの編集と追加はそのページに出るv-dialogで行う

データベース

今回で作るデータベースはアニメキャラの名前と年齢と身長と色(イメージカラー)を収めるテーブルにします

  列名 データ型
名前 namae text
年齢 nenrei integer
身長 shinchou real
iro text

ここで色は#RGBのコード(#789ABCなど)にして、実際にサイトで本当の色に変換できるようにしています。

テーブルを作成するSQLコード

create table kyara (namae text,nenrei integer,shinchou real,iro text,primary key (namae))

ファイル

│- app.py    # サーバとAPIの実行
│- static    # vue/cliで完成したフロントエンドの置くフォルダ
│- vueax     # vue/cliプロジェクトのコードのフォルダ

サーバとAPIの方が一つのpythonファイルで実装しますが、フロントエンドの方がvue/cliで色々のファイルからプロジェクトを作ります。詳しくフロントエンドの説明パートで。

環境

今回の実装で使うパソコンの環境

  • Mac OS 10.15.4 Catalina
  • python 3.7.7
  • conda 4.8.3
  • responder 2.0.5
  • pandas 1.0.3
  • axios 0.19.2
  • vue/cli 4.3.1
  • yarn 1.22.4

使うモジュールについて

vuetifyとvue/cli

vuetifyを使う方法が色々ありますが、ここではvue/cliで構成します。

vuetifyとvue/cliの使い方については色んな記事で説明されているので、ここでは自分が参考にした記事を紹介します。

準備

vue/cliはyarnでもnpmでも簡単にインストールできますが、今回はyarnを使います。

yarn global add @vue/cli @vue/cli-service-global

pythonモジュール

pythonモジュールはresponderとsqlite3を使います。詳しい使い方は以下の記事でいっぱい書かれいます。

responder

sqlite3

準備

主要なモジュールであるresponderとpandasをインストールする。

pip install responder python-multipart pandas

サーバ(python)の実装

サーバを実行するapp.pyのコード

コード全体

app.py
import sqlite3,responder,os
import pandas as pd

cors_params={'allow_origins':'http://127.0.0.1:8080/',
             'allow_methods':['*'],
             'allow_headers': ['*']}

api = responder.API(cors=True,cors_params=cors_params)

dbfile = 'data.db'
api.add_route('/',static=True)



@api.route('/db/kyara/')
async def index(req,resp):
    with sqlite3.connect(dbfile) as conn:
        if(req.method=='get'):
            resp.headers = {"Content-Type": "application/json; charset=utf-8"}
            sql_select = '''
                select * from kyara
            ''' # 全てのキャラのデータを表示するSQLコード
            data = pd.read_sql(sql_select,conn) # データをpandasのデータフレームに読み込んでjsonに変換する
            resp.text = data.to_json(orient='records',force_ascii=0)

        elif(req.method=='post'):
            param = await req.media() # 追加するデータを取得
            sql_insert = '''
                insert into kyara (namae,nenrei,shinchou,iro) values (:namae,:nenrei,:shinchou,:iro)
            ''' # 新しいデータ追加
            conn.execute(sql_insert,param)
            


@api.route('/db/kyara/{namae}')
async def show(req,resp,*,namae):
    with sqlite3.connect(dbfile) as conn:
        if(req.method=='get'):
            resp.headers = {"Content-Type": "application/json; charset=utf-8"}
            sql_select = '''
                select * from kyara
                where namae==?
            ''' # その名前を持つキャラのデータを取る
            kyara = pd.read_sql(sql_select,conn,params=[namae])
            resp.text = kyara.iloc[0].to_json(force_ascii=0)
        elif(req.method in ['patch','post','patch']):

            param = await req.media() # 更新するデータを取得
            param['namae0'] = namae
            sql_update = '''
                update kyara
                set namae=:namae,nenrei=:nenrei,shinchou=:shinchou,iro=:iro
                where namae==:namae0
            ''' # データ更新
            conn.execute(sql_update,param)

        elif(req.method=='delete'):
            sql_delete = '''
                delete from kyara
                where namae==?
            ''' # データ削除
            conn.execute(sql_delete,[namae])
            


if(__name__=='__main__'):
    # 初めて実行した時、新たにテーブルを作っておく
    if(not os.path.exists(dbfile)):
        with sqlite3.connect(dbfile) as conn:
            sql_create = '''
                create table kyara (
                    namae text,
                    nenrei integer,
                    shinchou real,
                    iro text,
                    primary key (namae)
                )
            '''
            conn.execute(sql_create)

    # サーバ開始
    api.run()

次は各部分の詳しい説明

ルート

api.add_route('/',static=True)の部分はstatic/index.htmlをデフォルトのルートにするためです。

データベースとやり取りをするためのAPIのルートは2つですが、メソッドの違いによって5つになります

ルート メソッド 機能
/db/kyara/ get 全部データ表示
/db/kyara/ post 追加
/db/kyara/{名前} get 一つずつデータ表示
/db/kyara/{名前} post, patch, put 編集
/db/kyara/{名前} delete 削除

これは大体ruby on railsのresourcesと同じようにしています。

CORSについての話

cors_paramsの設定はCORS(Cross-Origin Resource Sharing)を使う必要があるためです。

vue/cliで開発する時にフロントエンドは http://127.0.0.1:8080/ をサーバにするので、responderの http://127.0.0.1:5042/ にデータを取るのはCORSの問題と会うので、APIオブジェクトに http://127.0.0.1:8080/ からアクセスできるように設定する必要があります。

実用する時にフロントエンドはAPIと同じくresponderのサーバに入るので、CORSは必要なくなって消してもいいです。

SQLとpandas

SQLデータベースに接続してjsonに変換する時にpandasのpd.read_sql()を使うと便利です

jsonに変換する時にto_json()を使ってorient='records'に指定しておくと「データのオブジェクトを列挙するアレイ」という形になります。

APIの試しながらデータを追加してみる

準備が完成したら、次はpythonコードを実行してサーバ開始。

python app.py

これでAPIの部分は使えるようになるはずです。

そしてブラウザーで http://127.0.0.1:5042/db/kyara にアクセスししみます。

最初はまだ何のデータも入っていないので空っぽ。

q01.png

ここでAPIの追加機能をテストするついでに、データを準備します。

requestsを使ってpostメソッドで一つずつデータを追加してみます。

import requests

nyuuryoku = '''
高坂穂乃果,16,1.57,#F39800
南ことり,16,1.59,#808080
星空凛,15,1.55,#00B7CE
小泉花陽,15,1.56,#008000
園田海未,16,1.59,#0067C0
東條希,17,1.59,#A757A8
矢澤にこ,17,1.54,#F8ABA6
西木野真姫,15,1.61,#ED1A3D
絢瀬絵里,17,1.62,#AFDFE4
'''.strip().split('\n')

retsumei = 'namae,nenrei,shinchou,iro'.split(',')

for gyou in nyuuryoku:
    data = {k:v for k,v in zip(retsumei,gyou.split(','))}
    url = 'http://127.0.0.1:5042/db/kyara/'
    r = requests.post(url,data)
    print(r) # <Response [200]>が出たら問題ない

コードを実行した後、もう一度 http://127.0.0.1:5042/db/kyara にアクセスしたら、今回はデータがちゃんと入っていることは確認できます。

q02.png

これでサーバとAPIの準備が完成です。

フロントエンド(javascript)の実装

vueプロジェクト作成

app.pyとstaticフォルダと同じ場所でvueaxというフォルダを作ってその中でvuetifyのプロジェクトを作成します。

mkdir vueax
cd vueax # プロジェクトの置くフォルダへ
vue create . # そこでプロジェクト作成

作成する時の選択はdefaultを選んでいいです。

Vue CLI v4.3.1
? Generate project in current directory? (Y/n) y
? Please pick a preset: 
❯ default (babel, eslint) 
  Manually select features 
✨  Creating project in /Users/phyblas/vueres/vueax.
🗃  Initializing git repository...
⚙️  Installing CLI plugins. This might take a while...

yarn install v1.22.4
info No lockfile found.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Saved lockfile.
✨  Done in 54.19s.
🚀  Invoking generators...
📦  Installing additional dependencies...

yarn install v1.22.4
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...

success Saved lockfile.
✨  Done in 12.83s.
⚓  Running completion hooks...

📄  Generating README.md...

🎉  Successfully created project vueax.
👉  Get started with the following commands:

そして使うモジュールであるvuetifyとaxiosを追加します。

vuetify追加

vue add vuetify # vuetifyをインストール

ここも選択が出ますが、defaultのままで。

📦  Installing vue-cli-plugin-vuetify...

yarn add v1.22.4
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Saved lockfile.
success Saved 4 new dependencies.
info Direct dependencies
└─ vue-cli-plugin-vuetify@2.0.5
info All dependencies
├─ interpret@1.2.0
├─ rechoir@0.6.2
├─ shelljs@0.8.4
└─ vue-cli-plugin-vuetify@2.0.5
✨  Done in 9.86s.
✔  Successfully installed plugin: vue-cli-plugin-vuetify

? Choose a preset: (Use arrow keys)
❯ Default (recommended) 
  Prototype (rapid development) 
  Configure (advanced) 

🚀  Invoking generator for vue-cli-plugin-vuetify...
📦  Installing additional dependencies...

yarn install v1.22.4
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Saved lockfile.
✨  Done in 13.37s.
⚓  Running completion hooks...

✔  Successfully invoked generator for plugin: vue-cli-plugin-vuetify
 vuetify  Discord community: https://community.vuetifyjs.com
 vuetify  Github: https://github.com/vuetifyjs/vuetify
 vuetify  Support Vuetify: https://github.com/sponsors/johnleider

axios追加

yarn add axios vue-axios # axiosをインストール
yarn add v1.22.4
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
warning " > sass-loader@8.0.2" has unmet peer dependency "webpack@^4.36.0 || ^5.0.0".
warning " > vuetify-loader@1.4.3" has unmet peer dependency "webpack@^4.0.0".
[4/4] 🔨  Building fresh packages...
success Saved lockfile.
success Saved 3 new dependencies.
info Direct dependencies
├─ axios@0.19.2
└─ vue-axios@2.1.5
info All dependencies
├─ axios@0.19.2
├─ follow-redirects@1.5.10
└─ vue-axios@2.1.5
✨  Done in 5.13s.

開発段階のvueとjsコード

プロジェクトを作成したらプロジェクトのフォルダの中はこうなるでしょう。

q03.png

大体のファイルはそのままで、変えるのはこれだけ

  • ./src/App.vue
  • ./src/components/HelloWorld.vue >> ./src/components/index.vue
  • ./src/plugins/vuetify.js
  • ./vue.config.js

vue.config.jsは開発の時はそのままで良いですが、実用する時にちょっと設定の変更が必要となります。

src/plugins/vuetify.js

機能の追加を設定するのはsrc/plugins/vuetify.jsファイルです。ここで日本語化の設定をして、色のテーマを3つ(qiitaの緑, twitterの青, gmailの赤)追加します。(これについてこの記事でも参考 https://qiita.com/azukiazusa/items/c364f81d90c7734ba2e6

src/plugins/vuetify.js
import Vue from 'vue';
import Vuetify from 'vuetify/lib';
import ja from "vuetify/src/locale/ja.ts";

Vue.use(Vuetify);

export default new Vuetify({
  lang: {
    locales: { ja },
    current: "ja"
  },
  theme: {
    dark: true,
    themes: {
      dark: {
        qiita: "#55c500",
        twitter: "#1da1f2",
        gmail: "#B23121"
      },
    },
  },
});

src/App.vue

ウェブサイトの構造はApp.vueに書かれます。

src/App.vue
<template>
  <v-app>
    <v-app-bar app color="qiita" class="title">
      ṿữểţĩƒỹ<v-spacer/>~~<v-spacer/>
      řệşṕồñđễř<v-spacer/>μ's<v-spacer/>
      <v-btn href="https://qiita.com/phyblas" target="_blank" text >
        <v-icon>mdi-open-in-new</v-icon>
      </v-btn>
    </v-app-bar>

    <v-content>
      <v-card>
        <index/>
      </v-card>
    </v-content>
  </v-app>
</template>

<script>
import index from './components/index';

export default {
  name: 'App',
  components: {
    index,
  }
};
</script>

全部はv-appに囲まれるのが決まりで、中にはv-app-barとv-contentに分けられます。

v-app-barはページの一番上の部分で、ここでは簡単にあまり何も置かないことにします。

v-contentの部分はコンポーネントを使っています。

<index/>の部分はsrc/components/index.vueに書いたコンポーネントに変わります。

そのためにscript部分の中で'./components/index'をimportしてコンポーネントに指定する必要があります。

src/components/index.vue

もともとsrc/componentsのフォルダの中にはHelloWorld.vueというファイルがあったが、index.vueにします。

普通はvueのアプリは色んなコンポーネントから成すことができますが、今回は小さな簡単なウェブサイトで、細かく分けずにコンポーネントを一つのファイルにします。

src/components/index.vue
<template>
  <v-container>
    <v-row class="text-center">
      <v-data-table
        :headers="header"
        :items="kyaradata"
        :items-per-page="4"
        :footer-props="{itemsPerPageOptions: [4, 8, -1]}"
      >
        <template #item.namae="{ item: { namae,iro } }">
          <v-card class="title d-flex justify-space-between" :style="{background: iro}">
            <v-card v-for="(s,i) in namae" :key="i" style="margin: 3px">{{s}}</v-card>
          </v-card>
        </template>
        <template #item.nenrei="{ item: { nenrei } }">
          {{nenrei}} 歳
        </template>
        <template #item.shinchou="{ item: { shinchou } }">
          {{shinchou*100}} cm
        </template>
        <template #item.iro="{ item: { iro } }">
          <v-card class="headline d-flex justify-center" :style="{'color': iro}"></v-card>
        </template>
        <template #item.hoka="{ item: k }">
          <v-btn color="twitter" @click="henshuu(k)">編集</v-btn>
          <v-btn color="gmail" @click="sakujo(k)">削除</v-btn>
        </template>
      </v-data-table>
    </v-row>

    <v-container>
      <v-btn color="twitter" dark fab class="title" width="120" @click="tsuika">追加 +</v-btn>
      <v-btn color="gmail" dark fab class="title" width="120" @click="zenmetsu">全部削除</v-btn>
    </v-container>

    <v-dialog v-model="henshuuchuu" max-width="50%">
      <v-card color="twitter">
        <v-card-title>
          データ
          <span class="headline" v-if="i_henshuu==-1">追加 +</span>
          <span class="headline" v-else>編集</span>
        </v-card-title>
        <v-card-text>
          <v-container>
            <v-card v-for="k in header.slice(0,4)" :key="k.value">
              <v-text-field v-model="sonokyara[k.value]" :label="k.text" style="margin: 5px"></v-text-field>
            </v-card>
          </v-container>
        </v-card-text>
        <v-card-actions>
          <v-spacer></v-spacer>
          <v-btn class="title" color="qiita" @click="hozon">保存</v-btn>
          <v-btn class="title" color="gmail" @click="tojiru">取消</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>
  </v-container>
</template>

<script>
import axios from 'axios'; // APIからのデータを取るためにaxiosが必要
export default {
  name: 'index',
  data: () => ({
    kyaradata: [], // APIから取るキャラのデータはここに置く
    sonokyara: {}, // 編集するキャラのデータ
    header: [ // テーブルの列の構造
      {text: "名前", value: "namae"},
      {text: "年齢", value: "nenrei"},
      {text: "身長", value: "shinchou"},
      {text: "", value: "iro", sortable: false},
      {value: "hoka", sortable: false}, // 編集と削除のボタンのある列
    ],
    henshuuchuu: false, // 編集開始したらtrueになる
    i_henshuu: -1, // 編集しているデータのインデックス
  }),
  methods: {
    henshuu(item) { // 編集ボタンを押すと編集フォームのv-dialogが現れる
      this.i_henshuu = this.kyaradata.indexOf(item);
      this.sonokyara = Object.assign({},item);
      this.henshuuchuu = true;
    },
    tsuika(){ // 追加ボタンを押すと同じく編集フォームも現れるが「追加」モードになる
      this.i_henshuu = -1;
      this.sonokyara = {namae: "", nenrei: 0, shinchou: 0, iro: "#FFFFFF"}; // 最初の値
      this.henshuuchuu = true;
    },
    tojiru(){ // 取消ボタンを押すと編集フォームは消える
      this.henshuuchuu = false;
      this.sonokyara = {};
      this.i_henshuu = -1;
    },
    async hozon(){ // 保存ボタンを押すとデータが更新してデータベースに保存される
      if(this.i_henshuu==-1){ // 追加の場合
        let url = "http://127.0.0.1:5042/db/kyara/";
        let r = await axios.post(url,this.sonokyara); // APIを通じてデータを追加
        console.log(r);
        this.kyaradata.push(this.sonokyara); // テーブルにもデータ追加
        this.tojiru(); // フォームを閉じる
      }
      else{ // 編集の場合
        let url = "http://127.0.0.1:5042/db/kyara/" + encodeURIComponent(this.kyaradata[this.i_henshuu].namae);
        let r = await axios.patch(url, this.sonokyara); // APIを通じてデータを更新
        console.log(r);
        Object.assign(this.kyaradata[this.i_henshuu],this.sonokyara); // テーブルの中のデータも更新
        this.tojiru(); // フォームを閉じる
        }
    },
    async sakujo(item) { // 取消ボタンを押すと
      if(confirm('消していいのか?')){ // まずは本当に消すかどうか確認してから
        let url = "http://127.0.0.1:5042/db/kyara/" + encodeURIComponent(item.namae);
        let r = await axios.delete(url); // APIを通じてデータを消す
        console.log(r);
        let i = this.kyaradata.indexOf(item);
        this.kyaradata.splice(i,1); // テーブルからもデータを消す
      }
    },
    async zenmetsu() { // 全部削除ボタンを押すと、データを全て消される
      if(confirm('本当に全部消していいわけ?')){
        for (let k of this.kyaradata){
          let url = "http://127.0.0.1:5042/db/kyara/" + encodeURIComponent(k.namae);
          let r = await axios.delete(url); // APIを通じて一つずつデータを消す
          console.log(r);
        }
        this.kyaradata = []; // 空っぽのテーブル
      }
    }
  },
  async created(){ // 最初の時にAPIを通じてキャラのデータを取得する
    this.kyaradata = (await axios.get("http://127.0.0.1:5042/db/kyara/")).data;
    console.log(this.kyaradata);
  },
}
</script>

index.vueの中で前半はhtmlのtemplateで、後半はjavascriptです。

templateの部分は大体3つに分けられます。

  • 全部のデータを列挙するv-data-table
  • 追加と全部削除のポタン
  • 編集や追加をするフォームを持つv-dialog(最初は非表示にしていて、使う時に表示される)

テーブルの列は5つ

  • 名前
  • 年齢
  • 身長
  • 編集と削除のボタン

名前はclassをd-flexとjustify-space-betweenに設定して文字の間隔は広げられる。

身長はデータの中ではメートルだけど、ここではcmにする。

色はコードを色にして四角を描く。

最後の列はデータを表示するのではなくデータの編集と削除をするためのボタンの置かれるところ。

開発段階のサーバ

開発している時vue/cliは http://127.0.0.1:8080/ でサーバを作ります。

vue/cliプロジェクトのフォルダでserveコマンドでサーバ開始

yarn serve

http://127.0.0.1:8080/ へアクセスして上手く動けることを確認できます。

実用段階

コードが完成したら次は実用のための準備。

vue.config.jsを変更してbuildコマンドで実用のサイトを作成します。

buildする直前にちょっとvue.config.jsの中のpublicPathとoutputDirの設定を変える必要があります。

vue.config.jsの設定変更

vue.config.js
module.exports = {
  "transpileDependencies": [
    "vuetify"
  ],
  publicPath: "../static",
  outputDir: "../static"
}

これによってbuildを実行する時に/staticフォルダにファイルが作成されます。パスも同じくあそこに設定されます。

defaultでは /distというフォルダに置かれますが、その後でstaticに移動しても動かないので、最初からstaticでbuiltするように設定する必要があるらしいです。

build

設定を変更したら、build実行。

yarn build

最後に全部のファイルはこうなります。

q04.png

実行と結果

全て準備完成したら、サイトにアクセスしてみます。

サーバの方が先にさっきの段階で開始してテストしてみましたが、その後で追加したのはstaticの部分でpythonコードに変化はないのでサーバを閉じて再びの実行する必要がないでしょう。

でも一応サーバを再開した方がいいかも。

python app.py

ブラウザーで http://127.0.0.1:5042/ にアクセスして確認。

普段に600pxより広いウィンドウで見る場合はこのように見えます。

q05.png

試して600pxより小さくしたらこうなります。

q06.png

これはvuetifyのv-data-tableの最初から持っている機能です。特に何の設定する必要もなく、自動的にレスポンシブになっているので、便利です。

それと、各列による並び替えの機能もついているので、試して身長の列のヘッダをクリックして身長でソートすることができます。

q07.png

そして下の方にはページあたりの行数を選択して変更することもできます。

q08.png

これはv-data-tableのfooter-propsプロパティで設定されたitemsPerPageOptionsを[4, 8, -1]にしたから選べる行数は4と8とすべて(-1はすべてという意味)になります。

追加ポタンをクリックしたらフォームが出て、データを追加することができます。

q09.png

同じように、一つの編集ポタンをクリックしてみたら、そのキャラのデータを編集するフォームが出て、保存ポタンを押したらデータは更新します。

q10.png

削除のボタンをクリックしたら本当に消すかどうかを確認するウィンドウが出ます。

q11.png

全部削除のボタンを押してOKしたらデータは全部消されます。

q12.png

これで大体は上手く動けるようになっていることは確認できました。

終わりに

以上vuetifyとresponderで簡単なSPAのウェブサイトを作ってみました。

サーバとAPIはresponderで簡単に作れるし、フロントエンドの方はvuetifyのコンポーネントを使うと色々便利で簡単になります。

12
14
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
12
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?