LoginSignup
0
1

More than 1 year has passed since last update.

BottleとVueで作成したアプリ(ローカル開発編)

Last updated at Posted at 2022-05-31

きっかけ

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アプリを作成します。

result.gif

Vueで作成したformに情報を入れると、裏側でPythonが動いて値を返すようなものです。VueJSだけで作れる程度のものですが、単純な構造のアプリの方が、バックエンドとフロントエンドの挙動が理解しやすいと思います。

本日のお品書き

  • Webアプリの準備
  • Bottleとフロントエンドの連携
  • BottleとVueCLIの連携

本記事ではひたすらディレクトリ構成とパス(Path)を意識する話がメインです。
なお下記の知識に関しては、大小なりとも省略している部分があります。
本記事の内容において下記の解説は省略しております。あらかじめご了承ください。

前提知識

  • Gitに対するある程度の理解。
  • Pythonに対するある程度の理解。
  • Webフレームワークに対するある程度の理解。
  • クラウドに対するある程度の理解。
  • Vuejsの書き方など。

自己紹介

smile.jpg

自己紹介ページ

環境情報

環境情報
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のことを何も考えずに、カッコイイデザインにしたい」です。ものぐさなものでして。。

terminal
$ 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は本記事内で解説しないです。ディレクトリ構成などについて後から見返せるように記入しています。
解説はこの辺を参考にしてください。

https://wa3.i-3-i.info/word15053.html

備考:最終的なディレクトリ構成(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をインストールしています。

Terminal
$ npm install -g @vue/cli
$ vue --version
@vue/cli 5.0.4

vueCLIのバージョンは、インストールした時期によって変わると思います。(2022/5/24現在)

qiita_test/
$ vue create frontend
> Please pick a preset: Default ([Vue 2] babel, eslint)

vue create frontendを打つと、Vue2, Vue3, カスタマイズしてインストールするかの3択が出てきます。
自分は特定のUIフレームワークを利用したかったので、Vue2を選択しました。

frontend_vuecli.png

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だけ変更しました。

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を使わない場合は、下記のように書くことができます。

vue.config.js
module.exports = {
  publicPath: '/frontend/dist/',
  assetsDir: 'static'
}

追加した項目に関しては、後々必要となります。
バックエンドのBottleとの紐付けに使うPath周りで利用するため、追加しています。
npm run buildで出力されるディレクトリをfrontend/distに変更していて、写真やfaviconなどの静的ファイルのディレクトリを指定します。詳細はまた後ほど解説いたします。

他にもいくつか設定できる項目はあるようですが、今回はpublicpathassetsdir以外は使いませんでした。

なおnpm run serveを実行してみると、下記のURLにトップページが表示されます。

frontend_url.png

frontend_top.png

ここでフロントエンドの作業は一旦終了です。
バックエンドを作っていきます。

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ファイル自体のローカル配置です。便利なので、ぜひ。

https://www.jiriki.co.jp/blog/python/python-bottle-install

PipfilePipfile.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配下のディレクトリ構成は省略)

現在のバージョンに修正したため参考にした記事とは少し異なりますが、それぞれのファイルのソースコードは下記のようになっています。

main.py
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)

app.js
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);
               })
        }
    }
 
});
form.html
<!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>

こちらを実行してみると、下記のようにローカルサーバーが立ち上がります。

terminal
$ 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?)

bottle_sample_before.png

が表示されるはずです。
テキトーな値を入力して、submitすると、下記のようになると思います。

bottle_sample_after.png

これでBottleでの制御ができるようになりました。

Bottleのやっていることを紹介します。

main.py
from bottle import Bottle,route,template,static_file,request,HTTPResponse
import json

Bottleから、各種必要なものをimportしています。他にも使いたいものがある場合は、importすることができます。
後ほど紹介しますが、フロントエンドとの接続の際には、template_pathというものをimportします。

main.py

app = Bottle()
app.config['autojson'] = True

appをBottleのインスタンスにしています。ここは深く考えると、時間がかかるので、そういうものだと思ってください。Pythonがわかってくると、抽象的な概念もだんだんわかってきます。自分は3年くらいかかってインスタンスの意味が少しだけわかりました。まだまだわからないことだらけです。焦らずにいきましょう。

備考:インスタンスを作りたくない場合 色々Webをあさっていると、app = Bottle()を使っていないサイトを見かけるときがあります。 そこからコピペして使いたい場合は下記のmain_no_app.pyをご利用ください。
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しています。
まぁどちらでも良いと思います。きちんとわかってないので、詳しい方教えてください。

main.py

@app.route('/static/<filename>')
def static(filename):
    return static_file(filename, root='./static')

こちらはURLにstaticが含まれる場合は、現在のディレクトリ配下にあるstaticディレクトリ内のfilenameを返す。みたいな意味です。Vueとの接続で大事な箇所です。

main.py
# 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"へ
変更すると、下記のような変化をします。

bottle_sample_before.png

main.py
# 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)

bottle_semple_HW.png

となります。
Pythonで生成した値を簡単にフロントエンドへ受け渡しすることができます。

main.py

# 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
という流れでデータが動いています。

main.py

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.jsviews/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を変更します。

main.py
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)

上記のように修正しています。

terminal
$ 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ファイルの修正・反映が簡単になります。)

terminal
$ 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が出てくる場合。

こんなエラーが出ますね。

Terminal
~/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の事例ではありませんが、対応方法もあるようですが根本解決にはないっていないようですね。
よくわかりません。特に運用上問題なさそうなので、スルーです。

https://qiita.com/zaneli@github/items/db680489d1fbccac44f2

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
      └─その他
main.py
- 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
      └─その他

興味のある人向けにソースコードだけおいておきます。

App.vue
<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をおいておきます。
form.html
<!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>
app.js
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);
               })
        }
    }
 
});
FormInput.vue
<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する必要があります。下記のようにやります。

Terminal
$ npm install axios

npm run buildを行うと、frontend/publicfrontend/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を更新する。
  • 不要ファイルを全て除去する。
main.py
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開発を発信していきます。興味があったらご覧ください。 大抵はくだらないことです。

Sempleのツイッター

その他の細かな話

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は公式ドキュメント読んでもネットで調べても、エラー理由がわからなかったので、ソースコードを眺めていました。もちろん解決しませんでしたけどネ。

参考記事

  1. 実際に色々調べたのは結構前(2~3年前ですかね。)で、業務に使いやすそうなフロントエンドのフレームワークを探していた頃です。当時のスタンダードは、AngularとReactとVueの3つが有名で、その中から試しやすそうなVueを選びました。

  2. この記事を執筆し終わりくらいで、ふと自分の過去記事を見返したら、同じような記事を書いてますね。クラウド技術ばかり触っていて根本的な理解が全然できてない傍証ですね。精進します。

  3. 長すぎるので、要約しないとよくないですね。現状は、辞書的に使っていただけたらと思います。

0
1
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
0
1