きっかけ
Pythonでプログラムを書いていると、フロントエンドからの入力部分が割と簡素なものになりがちです。
普段自分で使っているJupyter labなどは最低限の入力画面を作成できますので、他人に見せるときもそれなりの見栄えで紹介できます。しかし自分の作成したPythonプログラムはTerminalからだったり、Pythonコードを修正していじることが多かったため、他人に入力してもらいづらいです。
操作性や見栄えが悪いからといって、HTMLやCSSを書くのは億劫です。
個人的には、フロント周りをなんかこういい感じに作成して、最小限の努力でそれなりの見栄えを手に入れたいと思います。色々調べた結果、とっつきやすそうなVueで簡単に作成します。1
しかしバックエンドとフロントエンドの接続は、今までAWSを前提としたAmplify等でしか実装したことないため、個人で試すには十分そうなPythonのフレームワークであるBottleを用いて簡単に開発してみようと思いました。
なお今回の記事は三部作となっております。
WebサイトとWebアプリの違い
Webアプリの開発(ローカル) <-イマココ
Webアプリの公開(Heroku)
です。
WebアプリとWebサイトに関する未熟さ、 当時の知識レベルはこちらの記事にまとめました。
なお、要約すると下記のように認識しております。
Webサイト: ブログのようなもの。事前に作成した文章をWeb上でまとめたもの。QiitaもWebサイトですね。
Webアプリ: ユーザーの入力した内容をサーバー側で処理して値を返すもの。例えば電卓のようなものですね。
完全な定義は自分もわからないです。詳しい方教えてください。
さらに今回のWebアプリをローカル環境だけでなく、インターネットで公開するまでの軌跡に関しては以下にまとめました。。
Herokuに載せてみた。Pathまわりが結構大変でした。特にURL公開とかはしていません。
結論
最終的にはこんなWebアプリを作成します。
Vueで作成したformに情報を入れると、裏側でPythonが動いて値を返すようなものです。VueJSだけで作れる程度のものですが、単純な構造のアプリの方が、バックエンドとフロントエンドの挙動が理解しやすいと思います。
本日のお品書き
- Webアプリの準備
- Bottleとフロントエンドの連携
- BottleとVueCLIの連携
本記事ではひたすらディレクトリ構成とパス(Path)を意識する話がメインです。
なお下記の知識に関しては、大小なりとも省略している部分があります。
本記事の内容において下記の解説は省略しております。あらかじめご了承ください。
前提知識
- Gitに対するある程度の理解。
- Pythonに対するある程度の理解。
- Webフレームワークに対するある程度の理解。
- クラウドに対するある程度の理解。
- Vuejsの書き方など。
自己紹介
環境情報
macOS Monterey ver:12.2
vue 2.6.14
@vue/cli 5.0.4
node v16.8.0
npm 8.6.0
Python 3.9
最近はPipenvを使っています。
Pipenvに関する詳細は、こちらの記事がわかりやすかったので、リンクを貼っておきます。(古い記事ですのでご注意ください)
要約すると仮想環境でいい感じにライブラリを管理してくれるバージョン管理システムです。
Cf.) Python2系とかPython3系の意味が分からない人へ!
Pythonには2系と3系があって、最近始めた人ならほとんどPython3系だと思います。一応バージョンを確認する方法を記載しておきます。
MacOSに入っているターミナル(Terminal)でPythonのインタラクティブ(対話)モードを起動すればバージョン情報が表示されます。
$ Python
> Python 3.9.0 (default, May 4 2022, 01:10:11)
[Clang 12.0.0 (clang-1200.0.32.27)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
インタラクティブモードを辞めるには*exit()*を入力しましょう!
Let's Start
今回の記事は、JupyterLabではなく、VSCode上での実装です。
Webアプリの準備
今回のWebアプリはこちらのサイトを参考にして作成しました。
大筋やディレクトリ構成などはこちらの方を参考にして実装しております。
異なる点
- PythonのフレームワークをFlaskではなく、Bottleを採用。
- gunicornを未使用。
- Vuetifyの解説を除去。
初めに参考にした記事がBottle + Vueで作成していたので、そのままBottleで作成しました。
個人的にはBottleもFlaskもさわった事があったので、特に何も考えずまぁBottleでいいか、程度の判断です。2
gunicornは、記事執筆後に触ってみようと思います。多分。
Herokuへのデプロイまでは、なるべく最小構成で試したかったため、未使用です。これから勉強します。多分。
実際の開発ではもう少しリッチにVuetifyを使用したりしましたが、解説する際にはノイズとなるため本記事では省略しました。VuetifyはVueJSのUIフレームワークです。
Vueは他にもUIフレームワークが多数ありますが、比較的経験のあるVuetifyを使うことが多いです。明確に優れている点を列挙したりはできません。詳しい方、教えてください。自分の要望は、「CSSのことを何も考えずに、カッコイイデザインにしたい」です。ものぐさなものでして。。
$ mkdir qiita_test
$ cd qiita_test
$ git init
qiita_testというディレクトリ内で、git init
してください。
gitによる管理を開始します。
備考:最終的なディレクトリ構成
qiita_testというディレクトリからPythonファイルを実行するため、Pipenvのバージョン管理ツールを移行しています。qiita_test
├─backend(Bottleで作成する内容)
├─frontend(Vueで作成する内容)
├─Pipfile(Pipenvのバージョン管理)
├─Pipfile.lock(Pipenvのバージョン管理)
└─README.md(Githubでの管理と備忘録用)
README.mdは本記事内で解説しないです。ディレクトリ構成などについて後から見返せるように記入しています。
解説はこの辺を参考にしてください。
備考:最終的なディレクトリ構成(Heroku)
Procfileを追加しています。qiita_test
├─backend(Bottleで作成する内容)
├─frontend(Vueで作成する内容)
├─Pipfile(Pipenvのバージョン管理)
├─Pipfile.lock(Pipenvのバージョン管理)
├─Procfile(Herokuデプロイ用)
└─README.md(Githubでの管理用)
README.mdとProcfileは本記事内で解説しないです。
README.mdの解説はこの辺を参考にしてください。
https://wa3.i-3-i.info/word15053.html
Procfileは、次回記事で使用します。
端的にまとめると、Herokuというクラウドサービスで利用するファイルです。
VueCLI(フロントエンド)の準備
npmでVue CLIをインストールしています。
$ npm install -g @vue/cli
$ vue --version
@vue/cli 5.0.4
vueCLIのバージョンは、インストールした時期によって変わると思います。(2022/5/24現在)
$ vue create frontend
> Please pick a preset: Default ([Vue 2] babel, eslint)
vue create frontend
を打つと、Vue2, Vue3, カスタマイズしてインストールするかの3択が出てきます。
自分は特定のUIフレームワークを利用したかったので、Vue2を選択しました。
Vue2とVue3で何が変わっているのか知りたい方は下記をご覧ください。 自分は読んでません。
カスタマイズできる項目に関しては、公式ドキュメントをご参照ください。
> Please pick a preset: Default ([Vue 2] babel, eslint)
を選択するとfrontend
というディレクトリが作成され、下記のようなディレクトリになっていると思います。(VueCLIのバージョンによって多少の違いはあるかもしれません。)
frontend
├─README.md
├─node_modules
├─package.json
├─public
├─src
├─vue.config.js
└─その他
参考にさせていただいた記事では、いくつか変更しているようですが、自分は下記のvue.config.js
だけ変更しました。
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
- transpileDependencies: true
+ transpileDependencies: true,
+ publicPath: '/frontend/dist/',
+ assetsDir: 'static'
})
備考:defineConfig
defineConfigに関しては、なくても問題ないです。より精密な書き方らしいです。
Or, you can use the defineConfig helper from @vue/cli-service, which could provide better intellisense support
https://cli.vuejs.org/config/#vue-config-js
VueCLIをインストールした際に、自動でついてきたのでそのまま使っています。仮にdefineConfigを使わない場合は、下記のように書くことができます。
module.exports = {
publicPath: '/frontend/dist/',
assetsDir: 'static'
}
追加した項目に関しては、後々必要となります。
バックエンドのBottleとの紐付けに使うPath周りで利用するため、追加しています。
npm run buildで出力されるディレクトリをfrontend/distに変更していて、写真やfaviconなどの静的ファイルのディレクトリを指定します。詳細はまた後ほど解説いたします。
他にもいくつか設定できる項目はあるようですが、今回はpublicpath
とassetsdir
以外は使いませんでした。
なおnpm run serve
を実行してみると、下記のURLにトップページが表示されます。
ここでフロントエンドの作業は一旦終了です。
バックエンドを作っていきます。
Bottle(バックエンド)の準備
バックエンドは最終的にこのようなディレクトリ構成となります。
qiita_test
├─backend
│ ├── main.py
│ ├── static
│ │ └── app.js
│ └── views
│ └── form.html
├─Pipfile
└─Pipfile.lock
PipfileとPipfile.lockはPipenvを使うためのバージョン管理ツールです。普段から使っているため、今回も癖で使っています。なくても問題ないです。Pipenv
は仮想環境のアレです。なんか便利ですよね。
上でも紹介しましたが、詳細は下記のリンクをご覧ください。
pip install bottle
で仮想環境内にBottleを追加します。
備考:pipを使わないBottle
https://github.com/bottlepy/bottle/blob/master/bottle.py
pip install bottle
すら必要ないのが、Bottleの良いところです。
こちらのBottleファイルをそのままローカルに配置すればそれだけで使えます。
pip installよりも手厚いのがBottleファイル自体のローカル配置です。便利なので、ぜひ。
Pipfile
とPipfile.lock
をbackend内に置かない理由としては、実用性によるものです。あとはPathが複雑になってしまうからです。Bottleを用いてfrontendのファイルも扱う必要があるので、backendとfrontendを内包するディレクトリに仮想環境を作成しています。この辺は経験知です。詳しい方教えてください。
それではやっていきましょう。参考にしたのはこちらの記事です。
まずはbackendのディレクトリを紹介します。
backend
├── main.py
├── static
│ └── app.js
└── views
└── form.html
備考:qiita_testからのディレクトリ構成
なおqiita_testから見た全体像はこのようになっています。
qiita_test
├─backend
│ ├── main.py
│ ├── static
│ │ └── app.js
│ └── views
│ └── form.html
└─frontend
│ (frontend配下のディレクトリ構成は省略)
現在のバージョンに修正したため参考にした記事とは少し異なりますが、それぞれのファイルのソースコードは下記のようになっています。
from bottle import Bottle,route,template,static_file,request,HTTPResponse
import json
app = Bottle()
app.config['autojson'] = True
@app.route('/static/<filename>')
def static(filename):
return static_file(filename, root='./static')
# JSONで送出する入力フォーム
@app.route('/data/json-form', method='GET')
def index():
username = "Test Form Monitor with Vue.JS"
return template('form', username=username)
# JSONで受け取りJSONでレスポンスを返す。JSONエコー出力
@app.route('/data/json-get', method='POST')
def somethingjson():
data = request.json
# CORS対策
header = {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, Authorization'
}
ret = HTTPResponse(status=200, body=json.dumps(data, ensure_ascii=False, indent=2), headers=header)
return ret
if __name__ == '__main__':
app.run(host="0.0.0.0", port="3000", debug=True)
var app = new Vue({
el: '#app',
delimiters: ['[[', ']]'],
data: {
visual: false,
results: [],
info: {
name: '',
nickname: '',
gender: ''
}
},
methods: {
submit: function() {
axios.post('/data/json-get', this.info)
.then(response => {
this.results = response.data;
this.visual = true;
})
.catch(error => {
console.log(error);
})
}
}
});
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Vue throw Json with POST to Python Server when Form Data submitting</title>
</head>
<body>
<h1>{{ username }}</h1>
<div id="app">
<h1>入力フォーム</h1>
<hr>
<form v-on:submit.prevent="submit">
名前:<input v-model="info.name"><br>
ニックネーム: <input v-model="info.nickname"><br>
性別: <input v-model="info.gender"><br>
<input type="submit" value="submit">
</form>
<hr>
<div v-if="visual">
<h2>Json POST Response Results</h2>
<ul>
<li v-for="(item,index) in results"> [[index]] : [[ item ]] </li>
</ul>
</div>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src='https://unpkg.com/vue@2.6.14/dist/vue.js'></script>
<script src="https://unpkg.com/jquery"></script>
<script src="/static/app.js"> </script>
</body>
</html>
こちらを実行してみると、下記のようにローカルサーバーが立ち上がります。
$ cd backend
$ python main.py
Bottle v0.12.19 server starting up (using WSGIRefServer())...
Listening on http://0.0.0.0:3000/
Hit Ctrl-C to quit.
Bottleが生成したURLの配下にある/data/json-formにアクセスしてみます。
今回のコードをコピペした場合はこちら -> (http://0.0.0.0:3000/data/json-form?)
が表示されるはずです。
テキトーな値を入力して、submitすると、下記のようになると思います。
これでBottleでの制御ができるようになりました。
Bottleのやっていることを紹介します。
from bottle import Bottle,route,template,static_file,request,HTTPResponse
import json
Bottleから、各種必要なものをimportしています。他にも使いたいものがある場合は、importすることができます。
後ほど紹介しますが、フロントエンドとの接続の際には、template_pathというものをimportします。
app = Bottle()
app.config['autojson'] = True
appをBottleのインスタンスにしています。ここは深く考えると、時間がかかるので、そういうものだと思ってください。Pythonがわかってくると、抽象的な概念もだんだんわかってきます。自分は3年くらいかかってインスタンスの意味が少しだけわかりました。まだまだわからないことだらけです。焦らずにいきましょう。
備考:インスタンスを作りたくない場合
色々Webをあさっていると、app = Bottle()を使っていないサイトを見かけるときがあります。 そこからコピペして使いたい場合は下記のmain_no_app.pyをご利用ください。from bottle import route, run, template, static_file, request, HTTPResponse
import json
@route('/static/<filename>')
def static(filename):
return static_file(filename, root='./static')
# JSONで送出する入力フォーム
@route('/data/json-form', method='GET')
def index():
username = "Test Form Monitor with Vue.JS"
return template('form', username=username)
# JSONで受け取りJSONでレスポンスを返す。JSONエコー出力
@route('/data/json-get', method='POST')
def somethingjson():
data = request.json
# CORS対策
header = {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, Authorization'
}
ret = HTTPResponse(status=200, body=json.dumps(data, ensure_ascii=False, indent=2), headers=header)
return ret
if __name__ == '__main__':
run(host="0.0.0.0", port="3000", debug=True)
Bottleインスタンスを全て消去して、必要なrun
をbottleからimportしています。
まぁどちらでも良いと思います。きちんとわかってないので、詳しい方教えてください。
@app.route('/static/<filename>')
def static(filename):
return static_file(filename, root='./static')
こちらはURLにstaticが含まれる場合は、現在のディレクトリ配下にあるstaticディレクトリ内のfilenameを返す。みたいな意味です。Vueとの接続で大事な箇所です。
# JSONで送出する入力フォーム
@app.route('/data/json-form', method='GET')
def index():
username = "Test Form Monitor with Vue.JS"
return template('form', username=username)
最初のURL操作でアクセスした部分です。
username = "Test Form Monitor with Vue.JS" を
username = "Hello World!! from Semple"へ
変更すると、下記のような変化をします。
# JSONで送出する入力フォーム
@app.route('/data/json-form', method='GET')
def index():
- username = "Test Form Monitor with Vue.JS"
+ username = "Hello World!! from Semple"
return template('form', username=username)
となります。
Pythonで生成した値を簡単にフロントエンドへ受け渡しすることができます。
# JSONで受け取りJSONでレスポンスを返す。JSONエコー出力
@app.route('/data/json-get', method='POST')
def somethingjson():
data = request.json
# CORS対策
header = {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, Authorization'
}
ret = HTTPResponse(status=200, body=json.dumps(data, ensure_ascii=False, indent=2), headers=header)
return ret
ここはsubmitを押した後の処理に関する部分ですね。
form.html -> app.js -> main.py -> form.html
という流れでデータが動いています。
if __name__ == '__main__':
app.run(host="0.0.0.0", port="3000", debug=True)
hostやportはネットワークに関することです。
説明すると長く、本筋とも逸れるので今回はガッツリと省略します。気になる方はコメントください。追記します。 やるとは言ってない。
Botlleの処理は大体ご理解いただけましたでしょうか?
Bottleとフロントエンドの連携
いよいよフロントエンドとバックエンドを接続させます。まずは簡単なHTMLファイルとjsファイルをフロントエンドに配置して、バックエンドから呼び出せるようにします。(VueCLIとの連携はこの後やります。)
最終的な形は、backendにはPythonファイルのみ、frontendにhtmlやjsファイルを置くことになります。
qiita_test
├─Pipfile
├─Pipfile.lock
├─backend
│ └──main.py
└─frontend
├─README.md
├─node_modules
├─package.json
├─public
│ ├── static
│ │ └── app.js
│ └── views
│ └── form.html
├─src
├─vue.config.js
└─その他
backend配下にあったstatic/app.js
とviews/form.html
をfrontend/public配下に移動させています。ディレクトリ間の移動はMacのPCでコピペを用いて移動すれば良いですが、BottleのPathも同じように移動後のPathに更新する必要があります。
staticファイルの移動
まずは、static/app.js
をbackendからfrontendへ移動します。
qiita_test
├─Pipfile
├─Pipfile.lock
├─backend
│ └── views
│ │ └── form.html
│ └──main.py
└─frontend
├─README.md
├─node_modules
├─package.json
├─public
│ └─ static
│ └── app.js
├─src
├─vue.config.js
└─その他
ディレクトリの移動をしたら、main.pyを開いて、pathを変更します。
from bottle import Bottle,route,template,static_file,request,HTTPResponse
import json
+ from pathlib import Path
+
app = Bottle()
app.config['autojson'] = True
+ path_here = Path(__file__)
+ # print(f'現在のパス: {path_here}')
+
+ BASE_PATH = path_here.parents[1]
+ # print(f'BASE_PATH: {BASE_PATH}')
+ FRONTEND_PUBLIC = f'{BASE_PATH}/frontend/public'
@app.route('/static/<filename>')
def static(filename):
- return static_file(filename, root='./static')
+ return static_file(filename, root=f'{FRONTEND_PUBLIC}/static/')
# JSONで送出する入力フォーム
@app.route('/data/json-form', method='GET')
def index():
username = "Hello World!! from Semple"
return template('form', username=username)
# JSONで受け取りJSONでレスポンスを返す。JSONエコー出力
@app.route('/data/json-get', method='POST')
def somethingjson():
data = request.json
# CORS対策
header = {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, Authorization'
}
ret = HTTPResponse(status=200, body=json.dumps(data, ensure_ascii=False, indent=2), headers=header)
return ret
if __name__ == '__main__':
- app.run(host="0.0.0.0", port="3000", debug=True)
+ app.run(host="0.0.0.0", port="3000", debug=True, reloader=True)
上記のように修正しています。
$ cd backend
$ python main.py
Bottle v0.12.19 server starting up (using WSGIRefServer())...
Listening on http://0.0.0.0:3000/
Hit Ctrl-C to quit.
- pathlibの導入
pathlibやBASE_PATH関係の話は、どこからアクセスされてもきちんとstatic/配下のapp.jsへアクセスするために工夫しています。詳細は下部にまとめます。
- reloderの導入(Pythonファイルの修正・反映が簡単になります。)
$ python main.py
Bottle v0.12.19 server starting up (using WSGIRefServer())...
Listening on http://0.0.0.0:3000/
Hit Ctrl-C to quit.
毎回Ctrl-C
してPython main.pyを打つ必要がなくなります。
未解決:ResourceWarningが出てくる場合。
こんなエラーが出ますね。
~/bottle.py:3133: ResourceWarning: unclosed <socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('0.0.0.0', 3000)>
server.run(app)
ResourceWarning: Enable tracemalloc to get the object allocation traceback
Bottleの事例ではありませんが、対応方法もあるようですが根本解決にはないっていないようですね。
よくわかりません。特に運用上問題なさそうなので、スルーです。
templateファイルの移動
残りのviews/form.htmlをfrontendに移行します。
qiita_test
├─Pipfile
├─Pipfile.lock
├─backend
│ └──main.py
└─frontend
├─README.md
├─node_modules
├─package.json
├─public
│ ├── static
│ │ └── app.js
│ └── views
│ └── form.html
├─src
├─vue.config.js
└─その他
- from bottle import Bottle,route,template,static_file,request,HTTPResponse
+ from bottle import Bottle, template, TEMPLATE_PATH, static_file, request, HTTPResponse
import json
from pathlib import Path
app = Bottle()
app.config['autojson'] = True
path_here = Path(__file__)
# print(f'現在のパス: {path_here}')
BASE_PATH = path_here.parents[1]
# print(f'BASE_PATH: {BASE_PATH}')
FRONTEND_PUBLIC = f'{BASE_PATH}/frontend/public'
+ FRONTEND_PATH = f'{FRONTEND_PUBLIC}/'
+ TEMPLATE_PATH.append(FRONTEND_PATH)
+ # print(TEMPLATE_PATH)
@app.route('/static/<filename>')
def static(filename):
return static_file(filename, root=f'{FRONTEND_PUBLIC}/static/')
# JSONで送出する入力フォーム
@app.route('/data/json-form', method='GET')
def index():
username = "Hello World!! from Semple"
- return template('form', username=username)
+ return template('views/form', username=username)
# JSONで受け取りJSONでレスポンスを返す。JSONエコー出力
@app.route('/data/json-get', method='POST')
def somethingjson():
data = request.json
# CORS対策
header = {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, Authorization'
}
ret = HTTPResponse(status=200, body=json.dumps(data, ensure_ascii=False, indent=2), headers=header)
return ret
if __name__ == '__main__':
app.run(host="0.0.0.0", port="3000", debug=True, reloader=True)
Template_pathに関する記述。
Template_path(frontendのdistを探索しにいく。)
BottleのTemplateとstatic_file
デフォルトでは、['./', './views/']
のみを探索しに行くようになっているので、リストにviews/form.html
を移動させたfrontend/dist
を追加します。
BottleとVueCLIの連携
ここまでで、Bottleを用いて、フロントエンドのディレクトリのファイルを呼び出す方法がわかりました。いよいよVueCLIを使って、フロントエンドで作成したファイルをBottleで呼び出す方法を解説します。
最終的な形は、frontendでの開発はVueCLIで完結させて、出来上がったものをBottleから呼び出せるようにします。
今回は最終的に下記のようなディレクトリ構成となります。
qiita_test
├─Pipfile
├─Pipfile.lock
├─backend
│ └──main.py
└─frontend
├─README.md
├─dist
│ ├── static/
│ ├── favicon.ico
│ └── index.html
├─node_modules
├─package.json
├─public
│ ├── favicon.ico
│ └── index.html
├─src
│ ├── assets
│ │ └── logo.png
│ ├── components
│ │ └── formInput.vue
│ └── App.vue
├─vue.config.js
└─その他
フロントエンド部分を統合する。
バックエンド部分で作成したform.htmlを、frontendの作成時に自動生成されたデフォルトのVueファイルに統合します。
本記事ではここに関して、VueJSの開発は一切掘り下げません。
あくまでBottleとの接続という観点で解説いたします。VueJSの記法は検索してみてください。
form.htmlをVueファイルに統合する。
- HTMLとJSを.vueファイルに変更する。
現状下記のような構造となっていると思います。
qiita_test
├─Pipfile
├─Pipfile.lock
├─backend
│ └──main.py
└─frontend
├─README.md
├─node_modules
├─package.json
├─public
│ ├── static
│ │ └── app.js
│ └── views
│ └── form.html
├─src/
├─vue.config.js
└─その他
フロントエンドのsrc/の中身をいじっていない限り、現状はassetsとComponentとApp.vue含まれる下記の構成になっているはずです。
qiita_test
├─Pipfile
├─Pipfile.lock
├─backend
│ └──main.py
└─frontend
├─README.md
├─node_modules
├─package.json
├─public
│ ├── static
│ │ └── app.js
│ └── views
│ └── form.html
├─src
│ ├── assets
│ │ └── logo.png
│ ├── components
│ │ └── HelloWorld.vue
│ └── App.vue
├─vue.config.js
└─その他
Publicに含まれるstatic/app.jsとviews/formを全て.vueファイルに書き換えます。
この辺の解説はHTMLとJSをVueファイルに書き換える内容なので、割愛します。VueJS系の記事を読んでください。
qiita_test
├─Pipfile
├─Pipfile.lock
├─backend
│ └──main.py
└─frontend
├─README.md
├─node_modules
├─package.json
├─public
│ ├── favicon.ico
│ └── index.html
├─src
│ ├── assets
│ │ └── logo.png
│ ├── components
│ │ └── FormInput.vue
│ └── App.vue
├─vue.config.js
└─その他
興味のある人向けにソースコードだけおいておきます。
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
- <HelloWorld msg="Welcome to Your Vue.js App"/>
+ <FormInput />
</div>
</template>
<script>
- import HelloWorld from './components/HelloWorld.vue'
+ import FormInput from './components/FormInput.vue'
export default {
name: 'App',
components: {
- HelloWorld
+ FormInput
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
備考:FormInput.vueの元にしたform.htmlとapp.jsをおいておきます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Vue throw Json with POST to Python Server when Form Data submitting</title>
</head>
<body>
<h1>{{ username }}</h1>
<div id="app">
<h1>入力フォーム</h1>
<hr>
<form v-on:submit.prevent="submit">
名前:<input v-model="info.name"><br>
ニックネーム: <input v-model="info.nickname"><br>
性別: <input v-model="info.gender"><br>
<input type="submit" value="submit">
</form>
<hr>
<div v-if="visual">
<h2>Json POST Response Results</h2>
<ul>
<li v-for="(item,index) in results"> [[index]] : [[ item ]] </li>
</ul>
</div>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src='https://unpkg.com/vue@2.6.14/dist/vue.js'></script>
<script src="https://unpkg.com/jquery"></script>
<script src="/static/app.js"> </script>
</body>
</html>
var app = new Vue({
el: '#app',
delimiters: ['[[', ']]'],
data: {
visual: false,
results: [],
info: {
name: '',
nickname: '',
gender: ''
}
},
methods: {
submit: function() {
axios.post('/data/json-get', this.info)
.then(response => {
this.results = response.data;
this.visual = true;
})
.catch(error => {
console.log(error);
})
}
}
});
<template>
<v-container>
<h1>{{ username }}</h1>
<h2>入力フォーム</h2>
<form v-on:submit.prevent="submit">
名前:<input v-model="info.name"><br>
ニックネーム: <input v-model="info.nickname"><br>
性別: <input v-model="info.gender"><br>
<input type="submit" value="submit">
</form>
<hr>
<div v-if="visual">
<h2>Json POST Response Results</h2>
<ul>
<li v-for="(item, index) in results" :key='item'> {{index}} : {{ item }} </li>
</ul>
</div>
</v-container>
</template>
<script>
import axios from 'axios'
export default {
name: 'FormInput',
data: () => ({
username: "Hello!! VueJS + Bottle",
visual: false,
results: [],
info: {
name: '',
nickname: '',
gender: ''
}
}),
methods: {
submit: function() {
axios.post('/data/json-get', this.info)
.then(response => {
this.results = response.data;
this.visual = true;
})
.catch(error => {
console.log(error);
})
}
}
}
</script>
form.htmlでは、下部の方でaxiosをインストールしていましたが、vueファイルにaxiosをimportするには、qiita_test/frontend
においてnpmでinstallする必要があります。下記のようにやります。
$ npm install axios
npm run build
を行うと、frontend/public
とfrontend/src
の中身をいい感じにfrontend/dist内にビルドしてくれます。
この辺はわりと曖昧です。感覚で実装していてきちんと検証していません。詳しい方教えてください。
VueJSをかける方は、フロントエンドでVueJSを書いてnpm run serve
を使ってリアルタイムで編集しながら、フロントエンドをいい感じに作成することができます。
作成できたら、npm run build
でBottleからそのVueファイルを扱うこともできます。
この辺にVuetifyを絡めることでHTMLやCSSをいじることなく、それなりの見た目のWebアプリを作れるようになります。
需要があれば、その辺も加筆します。
qiita_test
├─Pipfile
├─Pipfile.lock
├─backend
│ └──main.py
└─frontend
├─README.md
├─dist
│ ├── static/
│ ├── favicon.ico
│ └── index.html
├─node_modules
├─package.json
├─public
│ ├── favicon.ico
│ └── index.html
├─src
│ ├── assets
│ │ └── logo.png
│ ├── components
│ │ └── formInput.vue
│ └── App.vue
├─vue.config.js
└─その他
static/app.jsとviews/formを統合してApp.vueから読み込むことになったので、
操作を担うmain.pyも変更します。
- public -> distに修正する。
- URLのRoutingを更新する。
- 不要ファイルを全て除去する。
from bottle import Bottle, template, TEMPLATE_PATH, static_file, request, HTTPResponse
import json
from pathlib import Path
app = Bottle()
app.config['autojson'] = True
path_here = Path(__file__)
print(f'現在のパス: {path_here}')
BASE_PATH = path_here.parents[1]
print(f'BASE_PATH: {BASE_PATH}')
print(type(BASE_PATH))
- FRONTEND_DIST = f'{BASE_PATH}/frontend/public'
+ FRONTEND_DIST = f'{BASE_PATH}/frontend/dist'
FRONTEND_PATH = f'{FRONTEND_DIST}/'
TEMPLATE_PATH.append(FRONTEND_PATH)
print(TEMPLATE_PATH)
- @app.route('/static/<filename>')
+ @app.route('/frontend/dist/static/<filename:path>')
def static(filename):
return static_file(filename, root=f'{FRONTEND_DIST}/static/')
# JSONで送出する入力フォーム
- @app.route('/data/json-form', method='GET')
- def index():
- username = "Hello World!! from Semple"
- return template('views/form', username=username)
+ @app.route('/index', method='GET')
+ def index():
+ return template('index.html')
# JSONで受け取りJSONでレスポンスを返す。JSONエコー出力
@app.route('/data/json-get', method='POST')
def somethingjson():
data = request.json
# CORS対策
header = {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, Authorization'
}
ret = HTTPResponse(status=200, body=json.dumps(data, ensure_ascii=False, indent=2), headers=header)
return ret
if __name__ == '__main__':
app.run(host="0.0.0.0", port="3000", debug=True, reloader=True)
以上です。
膨大な長さになりましたが、VueCLI + BottleファイルでWebアプリを作成する方法を紹介しました。
Pathに関する部分は、Pythonだけを触ってるときやVueJSだけを触ってる時はよしなにやってくれるだけあって、いざ自分で実装すると頭を使いますね。3
間違いあったら、コメントください。
To be Continued
終わりに
今後もTwitterでこのようなWeb開発を発信していきます。興味があったらご覧ください。 大抵はくだらないことです。
その他の細かな話
Pathの調整
Herokuにデプロイするときや今後の開発を考えてpython hoge/main.py
に依存しない形で、Pathを設定します。ディレクトリ構成とstaticファイルへのアクセスを担保するため、現在のmain.pyからの相対パスで全て動作するようにしています。
なお、staticやtemplateのそれぞれの振る舞いが異なるため、冗長な書き方をしています。
自分はここで二日位詰まりました。今回の記事の肝要はこの部分です。Bottleとパス周りの挙動。このあたりがローカル開発とWebアプリ開発の違いですね。
Herokuだと現在のパスがapp/qiita_test/backend/main.pyとなり、ローカル開発だとpythonを実行したディレクトリとなるためtemplate_pathとstaticの挙動を制御するために、pythonを実行したディレクトリではなく、main.pyからの相対パスで制御する必要がありました。
もっと良い方法がある場合は教えてください。
この辺りで詰まった際には、AWSのAmplifyに乗り換えるなら今では?と18回くらい頭をよぎりました。
まぁAmplifyでもローカルとAmplifyのapp(AWS Cloud上)にdeployした時で挙動が変わっていたので、同じ苦しみはあるんですよね多分。
Bottleの解釈
こちらのファイルの中を覗くと結構面白いもので、CGIサーバーやWSGIRefサーバーやgunicornサーバーなどの処理が書いてあります。
それぞれが具体的にどういった機能なのかはわかっていませんが(詳しい方教えてください)、Bottleの良いところとして、ファイル上で検索(Cmd+F)するときちんとPythonで書かれていることです。
上記のTemplate_pathは公式ドキュメント読んでもネットで調べても、エラー理由がわからなかったので、ソースコードを眺めていました。もちろん解決しませんでしたけどネ。
参考記事