はじめに
この前responderとSQLiteとの組み合わせで簡単なウェブサイトを作る例をしました。
今回は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の使い方については色んな記事で説明されているので、ここでは自分が参考にした記事を紹介します。
- Responder + Vue.jsプロジェクト作成手順
- vuetify環境構築とやたら出てくるv-slotについておさらい
- Vuetify2.x でよく使うUIComponents まとめ
- Vuetify 2.0 で datatable を使う
準備
vue/cliはyarnでもnpmでも簡単にインストールできますが、今回はyarnを使います。
yarn global add @vue/cli @vue/cli-service-global
pythonモジュール
pythonモジュールはresponderとsqlite3を使います。詳しい使い方は以下の記事でいっぱい書かれいます。
responder
- Responder + WebSocketで簡易チャットアプリ
- Python+ResponderでWEBアプリケーションを構築する。
- はじめての Responder(Python の次世代 Web フレームワーク)
- 人間のためのイケてるPython WebFramework「responder」、そして作者のKenneth Reitzについて
- responderでjsonを返す際に日本語をUnicodeエスケープさせない
sqlite3
準備
主要なモジュールであるresponderとpandasをインストールする。
pip install responder python-multipart pandas
サーバ(python)の実装
サーバを実行する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 にアクセスししみます。
最初はまだ何のデータも入っていないので空っぽ。
ここで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 にアクセスしたら、今回はデータがちゃんと入っていることは確認できます。
これでサーバと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コード
プロジェクトを作成したらプロジェクトのフォルダの中はこうなるでしょう。
大体のファイルはそのままで、変えるのはこれだけ
- ./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 )
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に書かれます。
<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のアプリは色んなコンポーネントから成すことができますが、今回は小さな簡単なウェブサイトで、細かく分けずにコンポーネントを一つのファイルにします。
<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の設定変更
module.exports = {
"transpileDependencies": [
"vuetify"
],
publicPath: "../static",
outputDir: "../static"
}
これによってbuildを実行する時に/staticフォルダにファイルが作成されます。パスも同じくあそこに設定されます。
defaultでは /distというフォルダに置かれますが、その後でstaticに移動しても動かないので、最初からstaticでbuiltするように設定する必要があるらしいです。
build
設定を変更したら、build実行。
yarn build
最後に全部のファイルはこうなります。
実行と結果
全て準備完成したら、サイトにアクセスしてみます。
サーバの方が先にさっきの段階で開始してテストしてみましたが、その後で追加したのはstaticの部分でpythonコードに変化はないのでサーバを閉じて再びの実行する必要がないでしょう。
でも一応サーバを再開した方がいいかも。
python app.py
ブラウザーで http://127.0.0.1:5042/ にアクセスして確認。
普段に600pxより広いウィンドウで見る場合はこのように見えます。
試して600pxより小さくしたらこうなります。
これはvuetifyのv-data-tableの最初から持っている機能です。特に何の設定する必要もなく、自動的にレスポンシブになっているので、便利です。
それと、各列による並び替えの機能もついているので、試して身長の列のヘッダをクリックして身長でソートすることができます。
そして下の方にはページあたりの行数を選択して変更することもできます。
これはv-data-tableのfooter-propsプロパティで設定されたitemsPerPageOptionsを[4, 8, -1]にしたから選べる行数は4と8とすべて(-1はすべてという意味)になります。
追加ポタンをクリックしたらフォームが出て、データを追加することができます。
同じように、一つの編集ポタンをクリックしてみたら、そのキャラのデータを編集するフォームが出て、保存ポタンを押したらデータは更新します。
削除のボタンをクリックしたら本当に消すかどうかを確認するウィンドウが出ます。
全部削除のボタンを押してOKしたらデータは全部消されます。
これで大体は上手く動けるようになっていることは確認できました。
終わりに
以上vuetifyとresponderで簡単なSPAのウェブサイトを作ってみました。
サーバとAPIはresponderで簡単に作れるし、フロントエンドの方はvuetifyのコンポーネントを使うと色々便利で簡単になります。