Dockerのalpine上 Flask-Askをpipでインストールしようとするとエラー
はじめに
年末にEcho dotを購入したので、Alexaのカスタムスキルを作ろうと思いました。
Alexaのカスタムスキルは以前作成したことがあったのでその際に使用したFlask_Askを使って作成しようと思い取り掛かりました。
ところがFlask_Askはしばらくメンテナンスされていないようで、pipでインストールする際にエラーが出てしまったのでその対処を備忘としてまとめます。
pip installでエラー
今回はDocker上に環境を作っており、Python3.7.2-alpine3.8のDockerimageを使用しました。
早速開発しようとFlask_Askをpipでインストールしようとしたらエラーになりました。
$ pip install flask_ask Collecting flask_ask Using cached https://files.pythonhosted.org/packages/6a/f5/d4709ae94584a0b1541e9b52b2d25a8a1bdb6e2da9d6870f23fdd0523a30/Flask-Ask-0.9.8.tar.gz Complete output from command python setup.py egg_info: Traceback (most recent call last): File "<string>", line 1, in <module> File "/tmp/pip-install-la1z83pq/flask-ask/setup.py", line 8, in <module> from pip.req import parse_requirements ModuleNotFoundError: No module named 'pip.req' ---------------------------------------- Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-install-la1z83pq/flask-ask/
'pip.req'が無いと言われています。
いろいろ調べてみると既知のエラーらしく、コミュニティでも話題になっていました。
どうやらsetup.pyを編集すれば解消するようです。
pip downloadでダウンロードしてからオフラインでインストール
setup.pyを編集するためには、まずはソースをダウンロードしなくてはなりません。
こちらを参考に以下の手順で実施しました。
まずはsrc
ディレクトリを作成し、そこにパッケージをダウンロードします。
$ mkdir src $ pip download -d src --no-binary :all: flask_ask Collecting flask_ask Downloading https://files.pythonhosted.org/packages/6a/f5/d4709ae94584a0b1541e9b52b2d25a8a1bdb6e2da9d6870f23fdd0523a30/Flask-Ask-0.9.8.tar.gz (40kB) 100% |████████████████████████████████| 40kB 2.8MB/s Saved ./src/Flask-Ask-0.9.8.tar.gz Complete output from command python setup.py egg_info: Traceback (most recent call last): File "<string>", line 1, in <module> File "/tmp/pip-download-on3tlt2h/flask-ask/setup.py", line 8, in <module> from pip.req import parse_requirements ModuleNotFoundError: No module named 'pip.req' ---------------------------------------- Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-download-on3tlt2h/flask-ask/ $ ls -l src/ total 40 -rw-r--r-- 1 root root 40142 Jan 8 07:03 Flask-Ask-0.9.8.tar.gz
何やらまたsetup.pyでエラーが出ていますが、ちゃんとダウンロード出来ているようです。
setup.pyの編集
ダウンロードしたtar.gzファイルを解凍し、setup.pyを編集します。
$ cd src/ $ tar -zxvf Flask-Ask-0.9.8.tar.gz $ ls -l total 40 drwxr-xr-x 15 501 wheel 480 Jan 8 07:05 Flask-Ask-0.9.8 -rw-r--r-- 1 root root 40142 Jan 8 07:03 Flask-Ask-0.9.8.tar.gz $ cd Flask-Ask-0.9.8 $ vim setup.py
setup.pyを以下のように編集します。
""" Flask-Ask ------------- Easy Alexa Skills Kit integration for Flask """ from setuptools import setup # from pip.req import parse_requirements try: # for pip >= 10 from pip._internal.req import parse_requirements except ImportError: # for pip <= 9.0.3 from pip.req import parse_requirements setup( name='Flask-Ask', version='0.9.8', url='https://github.com/johnwheeler/flask-ask', license='Apache 2.0', author='John Wheeler', author_email='john@johnwheeler.org', description='Rapid Alexa Skills Kit Development for Amazon Echo Devices in Python', long_description=__doc__, packages=['flask_ask'], zip_safe=False, include_package_data=True, platforms='any', install_requires=[ str(item.req) for item in parse_requirements('requirements.txt', session=False) ], test_requires=[ 'mock', 'requests' ], test_suite='tests', classifiers=[ 'License :: OSI Approved :: Apache Software License', 'Framework :: Flask', 'Programming Language :: Python', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'Operating System :: OS Independent', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules' ] )
setup.pyを編集したパッケージフォルダを再度tarで固めます。
$ mv Flask-Ask-0.9.8.tar.gz Flask-Ask-0.9.8_bk.tar.gz $ tar -zcvf Flask-Ask-0.9.8.tar.gz Flask-Ask-0.9.8 $ ls -l total 80 drwxr-xr-x 15 501 wheel 480 Jan 8 07:06 Flask-Ask-0.9.8 -rw-r--r-- 1 root root 40284 Jan 8 07:08 Flask-Ask-0.9.8.tar.gz -rw-r--r-- 1 root root 40142 Jan 8 07:03 Flask-Ask-0.9.8_bk.tar.gz
alpineにgccのインストール
pipでインストールを実行したところ、何やら別のエラーが出ました。
$ pip install Flask-Ask-0.9.8.tar.gz Processing ./Flask-Ask-0.9.8.tar.gz Collecting aniso8601==1.2.0 (from Flask-Ask==0.9.8) Downloading https://files.pythonhosted.org/packages/5b/fb/251a0dd2f4710e60664ddd8bd3485bd8362530f47af9e88f4061fe589ebf/aniso8601-1.2.0.tar.gz (59kB) 100% |████████████████████████████████| 61kB 2.2MB/s Collecting Flask==0.12.1 (from Flask-Ask==0.9.8) Downloading https://files.pythonhosted.org/packages/f4/43/fb2d5fb1d10e1d0402dd57836cf9a78b7f69c8b5f76a04b6e6113d0d7c5a/Flask-0.12.1-py2.py3-none-any.whl (82kB) 100% |████████████████████████████████| 92kB 5.1MB/s Collecting pyOpenSSL==17.0.0 (from Flask-Ask==0.9.8) Downloading https://files.pythonhosted.org/packages/8c/b7/0048adbf09ad93f571e70fb3100e696d4711cdcbfe38161690d716a5ed35/pyOpenSSL-17.0.0-py2.py3-none-any.whl (51kB) 100% |████████████████████████████████| 61kB 3.1MB/s Collecting PyYAML==3.12 (from Flask-Ask==0.9.8) Downloading https://files.pythonhosted.org/packages/4a/85/db5a2df477072b2902b0eb892feb37d88ac635d36245a72a6a69b23b383a/PyYAML-3.12.tar.gz (253kB) 100% |████████████████████████████████| 256kB 7.0MB/s Collecting six==1.11.0 (from Flask-Ask==0.9.8) Downloading https://files.pythonhosted.org/packages/67/4b/141a581104b1f6397bfa78ac9d43d8ad29a7ca43ea90a2d863fe3056e86a/six-1.11.0-py2.py3-none-any.whl Collecting python-dateutil (from aniso8601==1.2.0->Flask-Ask==0.9.8) Downloading https://files.pythonhosted.org/packages/74/68/d87d9b36af36f44254a8d512cbfc48369103a3b9e474be9bdfe536abfc45/python_dateutil-2.7.5-py2.py3-none-any.whl (225kB) 100% |████████████████████████████████| 235kB 5.4MB/s Requirement already satisfied: click>=2.0 in /usr/local/lib/python3.7/site-packages (from Flask==0.12.1->Flask-Ask==0.9.8) (7.0) Requirement already satisfied: Jinja2>=2.4 in /usr/local/lib/python3.7/site-packages (from Flask==0.12.1->Flask-Ask==0.9.8) (2.10) Requirement already satisfied: itsdangerous>=0.21 in /usr/local/lib/python3.7/site-packages (from Flask==0.12.1->Flask-Ask==0.9.8) (1.1.0) Requirement already satisfied: Werkzeug>=0.7 in /usr/local/lib/python3.7/site-packages (from Flask==0.12.1->Flask-Ask==0.9.8) (0.14.1) Collecting cryptography>=1.7 (from pyOpenSSL==17.0.0->Flask-Ask==0.9.8) Downloading https://files.pythonhosted.org/packages/f3/39/d3904df7c56f8654691c4ae1bdb270c1c9220d6da79bd3b1fbad91afd0e1/cryptography-2.4.2.tar.gz (468kB) 100% |████████████████████████████████| 471kB 6.3MB/s Installing build dependencies ... error Complete output from command /usr/local/bin/python -m pip install --ignore-installed --no-user --prefix /tmp/pip-build-env-x0_m8e40 --no-warn-script-location --no-binary :none: --only-binary :none: -i https://pypi.org/simple -- setuptools>=18.5 wheel "cffi>=1.7,!=1.11.3; python_implementation != 'PyPy'": Collecting setuptools>=18.5 Downloading https://files.pythonhosted.org/packages/37/06/754589caf971b0d2d48f151c2586f62902d93dc908e2fd9b9b9f6aa3c9dd/setuptools-40.6.3-py2.py3-none-any.whl (573kB) Collecting wheel Downloading https://files.pythonhosted.org/packages/ff/47/1dfa4795e24fd6f93d5d58602dd716c3f101cfd5a77cd9acbe519b44a0a9/wheel-0.32.3-py2.py3-none-any.whl Collecting cffi!=1.11.3,>=1.7 Downloading https://files.pythonhosted.org/packages/e7/a7/4cd50e57cc6f436f1cc3a7e8fa700ff9b8b4d471620629074913e3735fb2/cffi-1.11.5.tar.gz (438kB) Complete output from command python setup.py egg_info: No working compiler found, or bogus compiler options passed to the compiler from Python's standard "distutils" module. See the error messages above. Likely, the problem is not related to CFFI but generic to the setup.py of any Python package that tries to compile C code. (Hints: on OS/X 10.8, for errors about -mno-fused-madd see http://stackoverflow.com/questions/22313407/ Otherwise, see https://wiki.python.org/moin/CompLangPython or the IRC channel #python on irc.freenode.net.) ---------------------------------------- Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-install-zydf2ff8/cffi/ ---------------------------------------- Command "/usr/local/bin/python -m pip install --ignore-installed --no-user --prefix /tmp/pip-build-env-x0_m8e40 --no-warn-script-location --no-binary :none: --only-binary :none: -i https://pypi.org/simple -- setuptools>=18.5 wheel "cffi>=1.7,!=1.11.3; python_implementation != 'PyPy'"" failed with error code 1 in None
コンパイラが見つからないよーというエラーが出ています。
調べると対処方法が見つかりました。
alpineにgccが含まれていない事が原因のようです。
早速alpine上でパッケージをインストールしてから再試行しますと、また別のエラーが、、、
$ apk add build-base libffi-dev $ pip install Flask-Ask-0.9.8.tar.gz 〜〜〜〜略〜〜〜〜 build/temp.linux-x86_64-3.7/_openssl.c:498:30: fatal error: openssl/opensslv.h: No such file or directory #include <openssl/opensslv.h> ^ compilation terminated. error: command 'gcc' failed with exit status 1 ---------------------------------------- Failed building wheel for cryptography 〜〜〜〜略〜〜〜〜 build/temp.linux-x86_64-3.7/_openssl.c:498:30: fatal error: openssl/opensslv.h: No such file or directory #include <openssl/opensslv.h> ^ compilation terminated. error: command 'gcc' failed with exit status 1 ---------------------------------------- Command "/usr/local/bin/python -u -c "import setuptools, tokenize;__file__='/tmp/pip-install-z1_hjgoi/cryptography/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" install --record /tmp/pip-record-rfu9q4ne/install-record.txt --single-version-externally-managed --compile" failed with error code 1 in /tmp/pip-install-z1_hjgoi/cryptography/
今度はopensslが無い!と怒られてしまっています。インストールしてから再度pipでインストール実行します。
$ apk add openssl-dev $ pip install Flask-Ask-0.9.8.tar.gz 〜〜〜〜略〜〜〜〜 Successfully built Flask-Ask cryptography Installing collected packages: cryptography, pyOpenSSL, PyYAML, Flask-Ask Successfully installed Flask-Ask-0.9.8 PyYAML-3.12 cryptography-2.4.2 pyOpenSSL-17.0.0
無事インストール出来ました。
$ python Python 3.7.1 (default, Dec 21 2018, 03:21:42) [GCC 6.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from flask_ask import Ask, statement, question, session >>> >>>
ちゃんとimportも出来るようになりました。
これでFlask_Askを使用できるようになったので、これからカスタムスキルを開発していきたいと思います。
locustでLINEメッセージ受信サーバに負荷試験
はじめに
業務でLINE Messeging APIを使用しているのですが、先日突然メッセージ受信サーバ(LINEからのWebhookを受け付けるサーバ)に対して、負荷試験を実施してほしいと先輩に依頼されました。
負荷試験ツールではjMeterが有名ですがノウハウがなく、もっと簡単に実施できるものが無いか調べたところ、 locustというPythonで記述できるツールがあることを知りました。
その内容を、自分用メモレベルですが備忘としてまとめます。
locustのインストール
公式サイト
Locust - A modern load testing framework
LocustはPython3.6までにしか対応していないので、Python3.6系の環境をあらかじめpyenvなどで準備しておきます。
Locust supports Python 2.7, 3.4, 3.5, and 3.6.
Macにてインストールする場合、libevが必要なので先にインストールしておきます。
$ brew install libev $ pip install locustio
基本的な使い方は上記公式サイトや以下サイトを参照しました。
シナリオ作成
LINEから送られるWebhookを模してJSONのhttpヘッダーにx-line-signature
パラメータを追加して送っています。
リクエスト先はLine developerで設定しているcallbackURLのパスを指定しています。(今回は/callback
)
メッセージIDは、受け付けるサーバ側(今回負荷をかけるサーバ)でユニークなものでないと処理できない仕様になっていたようなので、タイムスタンプを設定して送っています。
Messaging API - LINE Developers
from locust import HttpLocust, TaskSet, task import base64 import hashlib import hmac import datetime import json channel_secret = ' channnel secret ' class UserBehavior(TaskSet): @task(1) def index(self): payload = { "events": [ { "replyToken": "0f377", "type": "message", "timestamp": 1462629479859, "source": { "type": "user", "userId": "Ue636d0*****************" }, "message": { "id": str(int(datetime.datetime.now().timestamp()*1000000)), "type": "text", "text": "性能試験実施中" } } ], "destination":"U6415f13f***********" } hash = hmac.new(channel_secret.encode('utf-8'),json.dumps(payload).encode('utf-8'), hashlib.sha256).digest() signature = base64.b64encode(hash) headers = {'content-type': 'application/json', 'x-line-signature': signature} r = self.client.post("/callback", data=json.dumps(payload), headers=headers, catch_response=True) print(r) class WebsiteUser(HttpLocust): task_set = UserBehavior min_wait = 1000 max_wait = 1000
以下コマンドで起動します。
$ locust -H http://"負荷をかけるサーバURL"
画面が起動して
- Number of users to simulate:
何クライアント作成するか - Hatch rate:
クライアントの作成スピード(毎秒)
Vue.jsとFlaskでフルスタックなWebアプリの開発環境を構築 その3〜〜ログイン認証追加〜〜
はじめに
今回は、前回作成したフロントのUIがVue.js
、バックエンドのAPIがFlask
のWebアプリケーションに対してFlask-Login
を使用してログイン機能を作成します。
Flask-Login
↓前々回 kittagon.hateblo.jp
まずはFlask-Login
とログインフォームの作成に使用するFlask-WTF
を以下コマンドでインストールします。
$ pip install flask-login flask_wtf
ユーザーモデルの追加
まずはログインユーザーとなるユーザーモデルをmodels.py
に記述します。
公式サイトにも以下のように記載されており、UserクラスはUserMixin
を継承する形で記述すると簡単なようです。
To make implementing a user class easier, you can inherit from UserMixin, which provides default implementations for all of these properties and methods. (It’s not required, though.)
また、ログインフォームはとあえず簡単にflask_wtfを使いLoginForm
クラスを作成しておきます。
# backend/models.py from flask_login import UserMixin from flask_wtf import FlaskForm from wtforms import TextField, PasswordField, validators from sqlalchemy.orm import synonym from werkzeug import check_password_hash, generate_password_hash from backend import db class LoginForm(FlaskForm): email = TextField('email', validators=[validators.Required()]) password = PasswordField('Password', validators=[validators.Required()]) def __str__(self): return ''' <form action="/-/login" method="POST"> <p>%s: %s</p> <p>%s: %s</p> %s <p><input type="submit" name="submit" /></p> </form> ''' % (self.email.label, self.email, self.password.label, self.password, self.csrf_token) class User(UserMixin, db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(255), unique=True, nullable=False) email = db.Column(db.String(255), unique=True, nullable=False) _password = db.Column('password', db.String(255), nullable=False) active = db.Column(db.Boolean(), default=True) def _get_password(self): return self._password def _set_password(self, password): if password: password = password.strip() self._password = generate_password_hash(password) password_descriptor = property(_get_password, _set_password) password = synonym('_password', descriptor=password_descriptor) # フォームで送信されたパスワードの検証 def check_password(self, password): password = password.strip() if not password: return False return check_password_hash(self.password, password) # 認証処理 @classmethod def auth(cls, query, email, password): user = query(cls).filter(cls.email==email).first() if user is None: return None, False return user, user.check_password(password) class Task(db.Model): __tablename__ = 'tasks' id = db.Column(db.Integer, primary_key=True) title = db.Column(db.Text) text = db.Column(db.Text) def to_dict(self): return dict( id=self.id, title=self.title, text=self.text ) def __repr__(self): return '<Entry id={id} title={title!r}>'.format( id=self.id, title=self.title) def init(): db.create_all()
ログイン処理の追加
次にFlask側の処理の中に実際のログイン処理を記述していきます。
__init__.py
でFlask-Login
の初期化
view.py
でログイン画面のルーティングとログイン処理を実装します。
このあたりを参考。
Flask-Loginの使い方 - Qiita
Python Flaskでつくる LDAPログインページ - AAA Blog
Flaskで認証 - Kensuke Kousaka's Blog
config.py
# backend/config.py import os class BaseConfig(object): DEBUG = True SQLALCHEMY_DATABASE_URI = 'sqlite:///backend.db' # cookieを暗号化する秘密鍵 SECRET_KEY = os.urandom(24) # csrf token用秘密鍵 WTF_CSRF_SECRET_KEY = os.urandom(24)
init.py
Flask-login
を使うとcurrent_user
、login_user
、logout_user
という変数が払い出されて払い出されていい感じにログイン状態の確認ができます。
また、後述するview.py
に記載するルーティングを-
というプレフィックスをつけて取り込んでいます。
# backend/__init__.py from flask import Flask, request, redirect, url_for from flask_cors import CORS import requests from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager, current_user, login_user, logout_user, login_required # ここを追記 app = Flask('FLASK-VUE', static_folder = "./dist/static", template_folder = "./dist") app.config.from_object('backend.config.BaseConfig') db = SQLAlchemy(app) ######ここから追記###### login_manager = LoginManager() login_manager.init_app(app) from backend.models import User @login_manager.user_loader def load_user(user_id): return User.query.get(user_id) # 未認証の際のリダイレクト先を設定 @login_manager.unauthorized_handler def unauthorized_callback(): return redirect(url_for('app.login')) ############# route ############## from backend.view import view app.register_blueprint(view, url_prefix="/-") ######ここまで追記###### from backend.api import api app.register_blueprint(api, url_prefix="/api") cors = CORS(app, resources={r"/api/*": {"origins": "*"}}) @app.route('/', defaults={'path': ''}) @app.route('/<path:path>') @login_required # ここを追記 def catch_all(path): if app.debug: return requests.get('http://localhost:8080/{}'.format(path)).text return render_template("index.html")
view.py
from flask import Blueprint, redirect, request from flask_login import login_user, logout_user from backend import db from backend.models import User, LoginForm view = Blueprint('app', __name__, template_folder='templates', static_folder='templates/static') @view.route('/login', methods=['GET', 'POST']) def login(): form = LoginForm(request.form) if form.validate_on_submit(): user, authenticated = User.auth(db.session.query, form.email.data, form.password.data) if authenticated: login_user(user, remember=True) print('Login successfully.') return redirect('/tasks') else: print('Login failed.') return str(form) @view.route('/logout', methods=['POST']) def logout(): logout_user() return redirect('/login')
ログアウトボタンの追加
ログインした後のタスク画面にログアウトボタンを追加します。
以下のボタンをTasks.vue
の中に記述します。
場所はどこでも結構です。
// frontend/src/component/Tasks.vue <b-form v-if="show" action="/-/logout" method="post" > <b-button type="submit">ログアウト</b-button> </b-form>
ログインユーザーの作成
まだログインユーザーを作成する画面を作っていないので、
Pythonのインタラクティブモードからユーザーを作成し、動作確認をします。
$ python >>> from backend.models import User, db >>> User.query.all() [] >>> user = User(name='test_user', email='test@example.com') >>> user._set_password('password') >>> user <User (transient 4334868296)> >>> db.session.add(entry) >>> db.session.commit() >>> User.query.all() [<User 1>]
http://localhost:5000/-/login
を開いてログイン画面が表示され、
'test@example.com'と'password'でログインできればOKです。
また、ログインしていない状態でhttp://localhost:5000/tasks
を表示しようとすると
ログイン画面にリダイレクトされます。
ログアウトボタンでログアウトできます。
ログイン画面修正
最初に作成したのは最低限の要素のみ記述したので、もう少しきれいなレイアウトに変更します。
現状は↓のようにフォームを文字列で定義しておき、それを表示しているだけだったので、
画面をテンプレートから呼び出せるよう変更します。
def __str__(self): return ''' <form action="/-/login" method="POST"> <p>%s: %s</p> <p>%s: %s</p> %s <p><input type="submit" name="submit" /></p> </form> ''' % (self.email.label, self.email, self.password.label, self.password, self.csrf_token)
テンプレートの作成には、Vue.js側で作成している画面と揃えるために、Flask-Bootstrap
を使用します。
まずは以下コマンドでインストールします。
$ pip install flask-bootstrap
__init__.py
でflask-bootstrapの初期化をします。
# backend/__init__.py from flask import Flask, request, redirect, url_for from flask_cors import CORS import requests from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager, current_user, login_user, logout_user from flask_bootstrap import Bootstrap # ここを追記 app = Flask('FLASK-VUE', static_folder = "./dist/static", template_folder = "./dist") app.config.from_object('backend.config.BaseConfig') db = SQLAlchemy(app) bootstrap = Bootstrap(app) # ここを追記 #############以下略#############
view.py
のログイン画面表示を修正します。
テンプレートの呼び出しに加えて、action
をFlaskテンプレートの変数としてurl_for
を渡すように修正しています。
# backend/view.py from flask import Blueprint, redirect, request, render_template, url_for # render_template, url_forを追加 #############略############# @view.route('/login', methods=['GET', 'POST']) def login(): form = LoginForm(request.form) if form.validate_on_submit(): user, authenticated = User.auth(db.session.query, form.email.data, form.password.data) if authenticated: login_user(user, remember=True) print('Login successfully.') return redirect('/tasks') else: print('Login failed.') # return str(form) ## ここを修正 return render_template('login.html', form=form, action=url_for('app.login')) #############略#############
models.py
からstr要素を削除します。
また、Loginform
内に新たにsubmit要素を定義します。
# backend/models.py from flask_login import UserMixin from flask_wtf import FlaskForm from wtforms import TextField, PasswordField, validators, SubmitField # SubmitFieldの追加 from sqlalchemy.orm import synonym from werkzeug import check_password_hash, generate_password_hash from backend import db class LoginForm(FlaskForm): email = TextField('email', validators=[validators.Required()]) password = PasswordField('Password', validators=[validators.Required()]) submit = SubmitField('ログイン') # submit追加 # def __str__(self):の削除
次にログインテンプレートを作成します。
テンプレートフォルダはview.py
にて以下のように定義されています。
view = Blueprint('app', __name__, template_folder='templates', static_folder='templates/static')
view.py
と同階層にtemplates
フォルダを作成します。
app_dir ┣manage.py ┣frontend/ ┗backend/ ┣__init__.py ┣api.py ┣view.py ┣models.py ┣config.py ┗templates/
templates
フォルダ内にログイン画面用テンプレートを作成します。
<!-- backend/templates/login.html--> {% import "bootstrap/wtf.html" as wtf %} {% extends "bootstrap/base.html" %} {% block title %}This is a login page{% endblock %} {% block content %} <div class="container"> {{ wtf.quick_form(form, action=action, method="post") }} </div> {% endblock %}
またまた画面デザインは適当ですが、Bootstrap
により良い感じのフォームができました。
まとめ
これで認証までも含めたWebアプリができました。
基本的な機能はほぼ実装することができたので、今後はこれをベースにいろんな機能を実装していきたいと思います。
参考サイト
vue-cliのnpm run devでwebpack-dev-serverのエラー
はじめに
vue-cli
を使ってVue.jsの開発をしていたら、あるときからnpm run dev
実行時に↓のようなエラーが出てしまい開発用サーバが立ち上がらなくなりました。
(なんの操作をしていたときか忘れてしまいました。。)
$ npm run dev > frontend@1.0.0 dev /Users/hogehoge/Develop/practice-flask-vue/frontend > webpack-dev-server --inline --progress --config build/webpack.dev.conf.js The CLI moved into a separate package: webpack-cli Please install 'webpack-cli' in addition to webpack itself to use the CLI -> When using npm: npm i -D webpack-cli -> When using yarn: yarn add -D webpack-cli internal/modules/cjs/loader.js:583 throw err; ^ Error: Cannot find module 'webpack-cli/bin/config-yargs' at Function.Module._resolveFilename (internal/modules/cjs/loader.js:581:15) at Function.Module._load (internal/modules/cjs/loader.js:507:25) at Module.require (internal/modules/cjs/loader.js:637:17) at require (internal/modules/cjs/helpers.js:20:18) at Object.<anonymous> (/Users/hogehoge/Develop/practice-flask-vue/frontend/node_modules/webpack-dev-server/bin/webpack-dev-server.js:84:1) at Module._compile (internal/modules/cjs/loader.js:689:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10) at Module.load (internal/modules/cjs/loader.js:599:32) at tryModuleLoad (internal/modules/cjs/loader.js:538:12) at Function.Module._load (internal/modules/cjs/loader.js:530:3) npm ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR! frontend@1.0.0 dev: `webpack-dev-server --inline --progress --config build/webpack.dev.conf.js` npm ERR! Exit status 1 npm ERR! npm ERR! Failed at the frontend@1.0.0 dev script. npm ERR! This is probably not a problem with npm. There is likely additional logging output above. npm ERR! A complete log of this run can be found in: npm ERR! /Users/hogehoge/.npm/_logs/2018-11-19T11_12_55_011Z-debug.log
webpack-cliのインストール
しばらくパッケージをアップデートしていないので、なぜ今のタイミングで
The CLI moved into a separate package: webpack-cli Please install 'webpack-cli' in addition to webpack itself to use the CLI -> When using npm: npm i -D webpack-cli -> When using yarn: yarn add -D webpack-cli
が出るのか不明ですが、ひとまず指示どおりにインストールします。
$ npm i -D webpack-cli npm WARN url-loader@1.1.0 requires a peer of webpack@^4.0.0 but none is installed. You must install peer dependencies yourself. npm WARN webpack-dev-middleware@3.4.0 requires a peer of webpack@^4.0.0 but none is installed. You must install peer dependencies yourself. npm WARN webpack-dev-server@3.1.10 requires a peer of webpack@^4.0.0 but none is installed. You must install peer dependencies yourself. npm WARN webpack-cli@3.1.2 requires a peer of webpack@^4.x.x but none is installed. You must install peer dependencies yourself. + webpack-cli@3.1.2 added 25 packages from 7 contributors and audited 34200 packages in 37.642s found 0 vulnerabilities
インストールが完了しました。
webpack@^4.0.0
をインストールせよと言われてしまいます。
$ npm run dev > frontend@1.0.0 dev /Users/hogehoge/Develop/practice-flask-vue/frontend > webpack-dev-server --inline --progress --config build/webpack.dev.conf.js TypeError: Cannot destructure property `compile` of 'undefined' or 'null'. at addHooks (/Users/hogehoge/Develop/practice-flask-vue/frontend/node_modules/webpack-dev-server/lib/Server.js:114:49) at new Server (/Users/hogehoge/Develop/practice-flask-vue/frontend/node_modules/webpack-dev-server/lib/Server.js:127:5) at startDevServer (/Users/hogehoge/Develop/practice-flask-vue/frontend/node_modules/webpack-dev-server/bin/webpack-dev-server.js:355:14) at processOptions (/Users/hogehoge/Develop/practice-flask-vue/frontend/node_modules/webpack-dev-server/bin/webpack-dev-server.js:309:5) at process._tickCallback (internal/process/next_tick.js:68:7) at Function.Module.runMain (internal/modules/cjs/loader.js:745:11) at startup (internal/bootstrap/node.js:266:19) at bootstrapNodeJSCore (internal/bootstrap/node.js:596:3)
やっぱりまだだめみたいです。
webpack-dev-serverのダウングレード
TypeError: Cannot destructure property `compile` of 'undefined' or 'null'.
そこでこちらのエラーについて更にいろいろと調べてみると、同じようなissueがありました。
1 - Upgrade webpack to v4, you must change the webpack.config.js file for compatibility. or
2 - Downgrade webpack-dev-server to exactly v3.0.0 as @Nufeen said.
やはり上で突っ込まれているように、v4以上にアップグレードするか、v3にダウングレードすれば直るようです。
webpack
の知識はほぼ無く、webpack.config.js
を編集するのもハードルが高そうだったので今回はダウングレードして対応することとしました。
$ npm install -D webpack-dev-server@3.0.0 npm WARN webpack-cli@3.1.2 requires a peer of webpack@^4.x.x but none is installed. You must install peer dependencies yourself. npm WARN webpack-dev-server@3.0.0 requires a peer of webpack@^4.0.0-beta.1 but none is installed. You must install peer dependencies yourself. + webpack-dev-server@3.0.0 added 37 packages from 16 contributors, removed 28 packages, updated 10 packages and audited 35284 packages in 30.206s found 1 high severity vulnerability run `npm audit fix` to fix them, or `npm audit` for details
インストール出来たので再度試してみます。
$ npm un dev
DONE Compiled successfully in 26990ms 8:59:34 PM
I Your application is running here: http://localhost:8080
無事開発用サーバが起動しました。
スティード400を復活!!〜キャブ洗浄編〜
はじめに
最近またバイクが乗りたくなってきたので、
高校時代に乗っていたスティード400を修理することにしました。
大学3年ぐらいの時に軽く乗って以来実家の倉庫に眠りっぱなしなので、かれこれ8年ぶり?ぐらいになります。
今年のお盆に作業して、車のバッテリーと接続してエンジンをかけることは出来たのでまとめます。
洗浄方法
自分ではバイクの整備はよく分からないので詳しい友人に聞きながら作業を進めました。
まずはキャブをオーバーホールしなきゃならないだろうなーと思っていたのですが、
中のパーツ交換するではなく洗浄するだけならもっと簡単な方法があるとの事で、その方法を実践しました。
なにやらスティードはタンクからのガソリンを一旦シート下のポンプに流し、そのポンプからキャブへ燃料を送っている構造になっており、
ポンプのチューブからクリーナーを噴射すれば簡単にキャブ内の洗浄が出来るらしいです。
今回は友人お勧めのワコーズのクリーナーを使いました。
いざ実践!
こちらが僕のスティード400です。
高校の頃に乗っていたままで実家の車庫に眠っていました。
バッテリーも上がっておりエンジンもかかりません。
まずはシートを外します。ここはシート下のボルトを外すだけなので簡単ですね。
次はタンクを外しにかかりますが、スティードの構造上燃料コックを外さないとタンクが外せません。
燃料コックの奥にあるネジを回し、外していきます。必ずOFFにしてガソリンの流れを止めておくように。
(かなり奥にあるので鈍らないように注意!!)
こんな感じで外せます。
燃料コックが外せると、タンクを少し上に浮かせることができるので、チューブに繋がってるボルトを外します。
また、タンク端のネジを外します。
外せました!
次に、まずキャブのドレンボルトを外しておきます。
ここを外しておくことでポンプ側からクリーナーを入れた時に汚れが外に流れ出てきます。
キャブの中にガソリンを残したまま長期間放置してしまうとキャブの中に汚れが溜まり、
クリーナーを入れる前からドロっとした汚れが出てくるので、
あらかじめ紙などを敷いておきましょう。
右側と左側のキャブそれぞれにあるのでどちらも忘れずに開けましょう。
洗浄途中の写真ですが、こんな感じに外れます。
次にようやくポンプ側のチューブを取り外しにかかります。
ガソリンを送るポンプはシート下にあります。
こちらのチューブがキャブに繋がっているのでこれを外します。
ここが外せればあとはこちらのクリーナーを入れるだけです。
- 注入口をしっかりと塞いで5秒ほど噴射して注入
- 汚れがだらだらとドレンボルトから流れ出てくるのでしばらく待つ
というのを繰り返します。
するとこんな感じに徐々に出てくる汚れが薄くなっていきます。
最終的にはこれぐらいに薄まるまで上記の手順を繰り返します。
これで概ねキャブの洗浄は完了です。
後はこのままポンプやチューブをつなぎ直してエンジンをかけてみます。
ドレンボルトから流れ出た分以外のクリーナーが少しキャブの中に残ってしまいますが、ガソリンで流されるので問題ないそうです。
とりあえず車のバッテリーと接続してエンジンをかけてみます。
車側のプラス端子とバイク側のバッテリープラス端子を接続し、車側のマイナス端子はどこか導電体の金属の箇所に接続し、アースします。
これでセルを回すと無事エンジンがかかりました!!
かなり久しぶりにエンジンの音を聞いたので感動です!!
終わりに
今回はとりあえず簡易的な方法でキャブを洗浄し、エンジンをかけるところまで実践しました。
が、実際に乗るためには整備しなくてはならない箇所がまだまだ残っています。
ぱっと思いつく限りでこんなに。。
- エアクリーナーのゴムパッキン交換
- エンジンオイル交換
- チェーン交換
- サイドカバー交換
- シフトレバー交換
- フロントフォークのオイル漏れ修理
- シート交換
- ウィンカー交換
- マフラー交換
- ディスクブレーキ専用液補充
- タイヤ、ホイール交換
- ラジエーター修理
- 全体、サビ落とし、塗装
来年、遅くとも再来年にはもう一度乗れるよう細々と修理していきます!
Vue.jsとFlaskでフルスタックなWebアプリの開発環境を構築 その2〜〜投稿画面作成〜〜
はじめに
前回作ったこちらの環境を用いて、タスク管理アプリ的なものを作成していきたいと思います。
タスクのモデルを作成(Flask)
まずはこちらのサイトを参考に、タスクのクラスを作成し一通りFlask側のみで動作するところまでを作成いたします。
2. Flaskチュートリアル
前回からのファイル構成変更点
その前に、前回作成した構成を若干修正します。
前回は下記の構成だったかと思いますが、今後の扱いやすさを考慮しファイル名とその中の記載内容を変更します。
app_dir ┣appserver.py → manage.pyにファイル名変更 ┣frontend/ ┗backend/ ┣api.py ┣application.py → __init__.pyにファイル名変更 ┗config.py
それぞれファイル名を変更したものの中身は以下です。
# __init__.py(←application.py) from flask import Flask from flask_cors import CORS app = Flask('FLASK-VUE', static_folder = "./dist/static", template_folder = "./dist") app.config.from_object('backend.config.BaseConfig') from backend.api import api app.register_blueprint(api, url_prefix="/api") cors = CORS(app, resources={r"/api/*": {"origins": "*"}}) @app.route('/', defaults={'path': ''}) @app.route('/<path:path>') def catch_all(path): if app.debug: return requests.get('http://localhost:8080/{}'.format(path)).text return render_template("index.html")
# maange.py (←appserver.py) from backend import app if __name__ == '__main__': app.run()
sqliteを使用する設定
チュートリアルに倣い、sqliteはSQLAlchemyで操作します。
まずは以下コマンドでインストールします。
Flask-SQLAlchemyのインストール
$ pip install Flask-SQLAlchemy
次にconfig.py
を編集します.
# config.py import os class BaseConfig(object): DEBUG = True ## 以下を追記 SQLALCHEMY_DATABASE_URI = 'sqlite:///backend.db' # cookieを暗号化する秘密鍵 SECRET_KEY = os.urandom(24)
# __init__py from flask import Flask from flask_cors import CORS import requests from flask_sqlalchemy import SQLAlchemy # ここを追記 app = Flask('FLASK-VUE', static_folder = "./dist/static", template_folder = "./dist") app.config.from_object('backend.config.BaseConfig') db = SQLAlchemy(app) # ここを追記 from backend.api import api app.register_blueprint(api, url_prefix="/api") cors = CORS(app, resources={r"/api/*": {"origins": "*"}}) @app.route('/', defaults={'path': ''}) @app.route('/<path:path>') def catch_all(path): if app.debug: return requests.get('http://localhost:8080/{}'.format(path)).text return render_template("index.html")
models.pyの作成
チュートリアルに倣い、以下のように記述します。
# backend/models.py from backend import db class Task(db.Model): __tablename__ = 'tasks' id = db.Column(db.Integer, primary_key=True) title = db.Column(db.Text) text = db.Column(db.Text) def to_dict(self): return dict( id=self.id, title=self.title, text=self.text ) def __repr__(self): return '<Task id={id} title={title!r}>'.format( id=self.id, title=self.title) def init(): db.create_all()
コマンドラインからデータベースを初期化します。
$ python >>> from backend.models import init >>> init()
テータを登録してみます。
$ python >>> from backend.models import Task, db >>> Task.query.all() [] >>> task = Task(title='title', text='text') >>> db.session.add(task) >>> db.session.commit() >>> Task.query.all() [<Task id=1 title=u'title'>] >>> task = Task.query.get(1) >>> task <Task id=1 title=u'title'> >>> task.title = 'hello' >>> task.text = 'Hello world' >>> db.session.add(task) >>> db.session.commit() >>> Task.query.all() [<Task id=1 title=u'hello'>]
APIの作成(Flask)
まずはFlask側で登録のAPIを作成します。
基本は先程のチュートリアルを参考にしつつ、以下のように記述します。
# backend/api.py from flask import Blueprint, jsonify, request, url_for, make_response from random import * from flask_cors import CORS from backend import app, db from backend.models import Task api = Blueprint('api', __name__) @api.route('/hello/<string:name>/') def say_hello(name): response = { 'msg': "Hello {}".format(name) } return jsonify(response) @api.route('/random') def random_number(): response = { 'randomNumber': randint(1, 100) } return jsonify(response) @api.route('/get', methods=['GET']) def get_taks(): taks = Task.query.order_by(Task.id.desc()).all() taks_dict = [task.to_dict() for task in taks] return jsonify(taks_dict) @api.route('/add', methods=['POST']) def add_task(): task = Task( title=request.form['title'], text=request.form['text'] ) db.session.add(task) db.session.commit() task = Task.query.order_by(Task.id.desc()).first() id = str(task.id) r = make_response(id) return r @api.route('/delete', methods=['POST']) def delete_task(): id=request.form['id'] task = Task.query.get(id) db.session.delete(task) db.session.commit() r = make_response(id) return r
http://127.0.0.1:5000/api/get
にアクセスして、以下のようにレスポンスが返却されればOKです。
[ { "id": 1, "text": "Hello world", "title": "hello" } ]
登録画面の作成(Vue.js)
ルーティングの追加
続いて、Vue.jsのSPA側で、タスクの登録と閲覧をする画面を作成します。
$ cd frontend/src/component $ cp Home.vue Tasks.vue
また、SPA側のルーティングにもタスク画面を表示するように記述します。
// frontend/src/router/index.js /////// 省略 /////// const routerOptions = [ { path: '/', component: 'Home' }, { path: '/about', component: 'About' }, { path: '/tasks', component: 'Tasks' },//ここを追記 { path: '*', component: 'NotFound' } ] /////// 省略 ///////
bootstrap-vueの導入
登録画面を作成するにあたってはbootstrap-vueを使用したいと思います。
公式サイトに記載されている通り、以下コマンドでインストールします。
$ npm install jquery $ npm install bootstrap-vue
また、こちらも記載されている通り、frontend/src/main.js
とfrontend/src/App.vue
に以下の記述を記載します。
// frontend/src/main.js import Vue from 'vue' import BootstrapVue from 'bootstrap-vue' //ここを追記 import App from './App' import router from './router' Vue.config.productionTip = false /* eslint-disable no-new */ new Vue({ el: '#app', router, render: h => h(App) }) Vue.use(BootstrapVue) //ここを追記
// frontend/src/App.vue /////// 省略 /////// <script> import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap-vue/dist/bootstrap-vue.css' export default { name: 'App' } </script> /////// 省略 ///////
画面の作成
そして、Tasks.vue
にて先程作成したバックエンド側のAPIを叩き、UIに表示するviewを作成します。
デザインは適当です笑。
// frontend/src/component/Task.vue <template> <div class="container"> <div align="center"> <div class="col-sm-8 col-md-6 col-lg-6"> <p>タスクの登録</p> <b-form v-if="show"> <b-form-group label="Title:" label-for="title"> <b-form-input id="title" type="text" required placeholder="Enter title"> </b-form-input> </b-form-group> <b-form-group label="Text:" label-for="title"> <b-form-textarea id="text" placeholder="Enter something" :rows="3" :max-rows="6"> </b-form-textarea> </b-form-group> <div align="center"> <div class="col-sm-4 col-md-2 col-lg-2"> <b-button block @click="addTask" variant="success">Add</b-button> </div> </div> </b-form> </div> </div> <b-list-group v-for="(task, index) in tasks" :key='index'> <b-list-group-item> {{ task.title }}<br> {{ task.text }} <b-button v-bind:src="task.id" block @click="deleteTasks(index, task.id)">削除</b-button> </b-list-group-item> </b-list-group> </div> </template> <script> import axios from 'axios' export default { data () { return { randomNumber: 0, tasks: [], show: true } }, methods: { getTasks () { const path = 'http://localhost:5000/api/get' axios.get(path) .then(response => { this.tasks = response.data }) .catch(error => { console.log(error) }) }, addTask () { const path = 'http://localhost:5000/api/add' var title = document.getElementById('title') var text = document.getElementById('text') let params = new URLSearchParams() params.append('title', title.value) params.append('text', text.value) var titleValue = title.value var textValue = text.value title.value = '' text.value = '' axios.post(path, params) .then(response => { var id = response.data var task = {'id': id, 'text': textValue, 'title': titleValue} this.tasks.unshift(task) console.log(response) }) .catch(error => { console.log(error) }) }, deleteTasks (taskIndex, taskId) { console.log(taskIndex) console.log(taskId) const path = 'http://localhost:5000/api/delete' let params = new URLSearchParams() params.append('id', taskId) this.tasks.splice(taskIndex, 1) axios.post(path, params) .then(response => { console.log(response) }) .catch(error => { console.log(error) }) } }, created () { this.getTasks() } } </script>
http://127.0.0.1:5000/tasks
を開けば以下のように登録と削除ができる画面が動作すると思います。
あまりタスク管理っぽくないですが。笑
終わりに
これで、バックエンドとフロントエンドの役割が明確なWebアプリを作成出来ました。
あとはバックエンドのFlaskでAPIを作成し、フロントエンドのVue.jsでUIを描画すればいろんなWebアプリが作れるようになります。
次回はこのWebアプリにログイン機能をつけたいと思います。
参考サイト
Vue.jsとFlaskでフルスタックなWebアプリの開発環境を構築 その1〜〜環境構築〜〜
- はじめに
- vue-cliで雛形のインストール(フロントエンド)
- サンプルページの追加
- ビルドディレクトリの変更
- Flaskのインストール(バックエンド)
- Flaskのサンプル作成
- 404ページの追加
- APIの追加
- 終わりに
はじめに
最近のWebアプリはReact.jsやVue.jsを使用したSPAが主流かと思いますが、データ取得のAPIを実装したりなど、サーバサイドの実装も必要になります。
自分はJavascriptに不慣れなのでサーバサイドはJavascriptではなくPythonを使いたかったので、Flaskと共存できる構成にしたかったのがきっかけです。
いろいろと探してみたらすでに先人がやっていました。
Full-stack single page application with Vue.js and Flask
Single Page Apps with Vue.js and Flask
以下のような構成で開発できるようにします。
app_dir ┗frontend // Vue.jsのプログラム ┗backend // Flaskのプログラム
英語記事なのと、それぞれの記事で若干やっていることが異なるため
それぞれを混ぜる形で環境を構築したので、備忘として自分の言葉でまとめておこうと思います。
vue-cliで雛形のインストール(フロントエンド)
以下コマンドでvue-cliをインストールできます。
$ npm install -g vue-cli
アプリの雛形をインストールします。
$ vue init webpack frontend ? Project name (frontend):[Enter] ? Project description (A Vue.js project):[Enter] ? Author (hogehoge <hogehogehogehoge@gmail.com>):[Enter] ? Vue build (Use arrow keys):[Enter] ❯ Runtime + Compiler: recommended for most users Runtime-only: about 6KB lighter min+gzip, but templates (or any Vue-specific HTML) are ONLY allowed in .vue files - render functions are required elsewhere ? Install vue-router? (Y/n):[Enter] ? Use ESLint to lint your code? (Y/n):[Enter] ? Pick an ESLint preset (Use arrow keys) ❯ Standard (https://github.com/standard/standard) Airbnb (https://github.com/airbnb/javascript) none (configure it yourself) ? Set up unit tests (Y/n):[Enter] ? Pick a test runner (Use arrow keys):[Enter] ❯ Jest Karma and Mocha none (configure it yourself) ? Setup e2e tests with Nightwatch? (Y/n):[Enter] ? Should we run `npm install` for you after the project has been created? (recommended) (Use arrow keys) ❯ Yes, use NPM Yes, use Yarn No, I will handle that myself vue-cli · Generated "frontend". # Installing project dependencies ... # ======================== (略) # Project initialization finished! # ======================== To get started: cd frontend npm run dev Documentation can be found at https://vuejs-templates.github.io/webpack $ cd frontend $ npm install # after installation $ npm run dev DONE Compiled successfully in 21497ms 12:42:34 I Your application is running here: http://localhost:8080
ブラウザでhttp://localhost:8080
を開いて以下の画面が表示されれば、アプリの雛形インストール完了です。
サンプルページの追加
Vue.jsの構成や説明はここでは記載しませんが、練習用にページを追加してみましょう。
frontend/src/components
にHome.vue
とAbout.vue
を追加します。
// Home.vue <template> <div> <p>Home page</p> </div> </template>
// About.vue <template> <div> <p>About</p> </div> </template>
また、frontend/src/router/index.js
を以下のように編集します。
import Vue from 'vue' import Router from 'vue-router' // もともと記載されていたものは削除orコメントアウト // import HelloWorld from '@/components/HelloWorld' // Vue.use(Router) // export default new Router({ // routes: [ // { // path: '/', // name: 'HelloWorld', // component: HelloWorld // } // ] // }) const routerOptions = [ { path: '/', component: 'Home' }, { path: '/about', component: 'About' } ] const routes = routerOptions.map(route => { return { ...route, component: () => import(`@/components/${route.component}.vue`) } }) Vue.use(Router) export default new Router({ routes, mode: 'history' })
このように編集したら再度ブラウザでlocalhost:8080
とlocalhost:8080/about
を開いてページが表示されたらOKです。
ビルドディレクトリの変更
npm run build
コマンドで立ち上がるのは開発用のサーバなので、実際にサービス提供する際はブラウザが解釈できる形にビルドする必要があります。
そのビルドした結果がどこに出力されるかはfrontend/config/index.js
に記載があります。
もともとの記載は、
build: { // Template for index.html index: path.resolve(__dirname, '../dist/index.html'), // Paths assetsRoot: path.resolve(__dirname, '../dist'), //////以下略////// }
となっており、frontend/dist
配下にビルドされたソースコードが出力される設定となっております。
このままだとFlaskでindex.htmlにアクセスするときにfrontend
配下を参照しなくてはならなくなってしまうので、管理上明確に分離するためにfrontend
の上の階層に出力されるようにします。
build: { // Template for index.html index: path.resolve(__dirname, '../../dist/index.html'), // Paths assetsRoot: path.resolve(__dirname, '../../dist'), //////以下略////// }
以下コマンドでビルドします。
$ npm run build
以下のような構成になります。
app_dir ┗frontend // Vue.jsのプログラム ┗dist // コンパイルされたVue.jsのプログラム
$ ls ../dist/
index.html static
Flaskのインストール(バックエンド)
バックエンドのプログラムには今回はFlaskを使用します。
元記事にはPythonの環境を整えるのにvirtualenvを使用していますが、僕はpyenv(pyenv-virtualenv)のほうが使い慣れているのでこちらを使います。
pyenvは以下コマンドでインストールできます。
$ brew install pyenv $ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile $ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile $ echo 'eval "$(pyenv init -)"' >> ~/.bash_profile $ source ~/.bash_profile
以下コマンドで本アプリ用のPython3.6.1の環境を作ります。
$ pyenv install --list $ pyenv install 3.6.1 $ pyenv virtualenv 3.6.1 vue-flask $ pyenv versionos system 3.6.1 3.6.1/envs/vue-flask vue-flask
app_dir
に行き、以下のコマンドを実行します。
$ pyenv local vue-flask (vue-flask) $ # このようにプロンプトが変わったらOK
これで、app_dir
内に閉じたPython3.6.1の環境が作れました。
(この中でパッケージのインストールを行っても他の環境には影響ありません。)
以下コマンドでFlaskをインストールします。
$ pip install Flask
$ mkdir backend
Flaskのサンプル作成
いよいよFlaskでバックエンドのプログラムを作成していきます。
今回は以下のようなファイル構成にしようと思います。
app_dir ┗appserver.py ┗frontend/ ┗backend/ ┗api.py ┗application.py ┗config.py ┗router.py
それぞれのファイルの中身は以下です。
# appserver.py from backend.application import create_app from flask import render_template app = create_app() @app.route('/') def index(): return render_template("index.html") if __name__ == '__main__': app.run()
# application.py from flask import Flask def create_app(app_name='FLASK-VUE'): app = Flask(app_name, static_folder = "./dist/static", template_folder = "./dist") app.config.from_object('backend.config.BaseConfig') return app
# config.py class BaseConfig(object): DEBUG = True
ポイントは、application.py
において、
app = Flask(app_name, static_folder = "./dist/static", template_folder = "./dist")
を指定することです。
これを指定することで、Flaskのテンプレート参照先が./dist
配下になり、Vue.jsでビルドしたindex.html
が参照されることになります。
以下コマンドで、Flaskの開発用サーバが立ち上がります。
$ python appserver.py * Serving Flask app "FLASK-VUE" (lazy loading) * Environment: production WARNING: Do not use the development server in a production environment. Use a production WSGI server instead. * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) * Restarting with stat * Debugger is active! * Debugger PIN: 977-628-540
http://localhost:5000/
にアクセスすると、先程vue.jsで作成したホーム画面が表示されます。
しかし、vue.js側でルーティングを実装したはずの、http://localhost:5000/about
にアクセスしても、Not Foundとなってしまいます。
これは、Flask側ではルーティングをしていないにもかかわらず、Flask側で処理しようとしてしまっているためです。
そこで以下をappserver.py
に追記して、/
以下のパスをvue.jsでビルドしたindex.html
にリダイレクトするようにします。
# appserver.py from backend.application import create_app from flask import render_template app = create_app() #### 以下を追記 ### @app.route('/', defaults={'path': ''}) @app.route('/<path:path>') def catch_all(path): return render_template("index.html") ################# if __name__ == '__main__': app.run()
再度、http://localhost:5000/about
にアクセスするとAboutページが表示されます。
404ページの追加
これで、ブラウザ上のルーティングがvue.jsでビルドしたindex.html
にリダイレクトされるので、vue.js側で404ページも追加しておかなくてはなりません。
frontend/src/components
配下に以下NotFound.vue
を作成します。
// NotFound.vue <template> <div> <p>404 - Not Found</p> </div> </template>
また、frontend/src/router/index.js
に以下の通り、ページがない場合のパスを追記します。
const routerOptions = [ { path: '/', component: 'Home' }, { path: '/about', component: 'About' }, { path: '*', component: 'NotFound' } ]
http://localhost:8080/
配下の適当なパスにアクセスしてNotFoundのページが表示されればOKです。
npm run build
でビルドすればFlask側のサーバhttp://localhost:5000/
でも同様にNotFoundのページが表示されます。
APIの追加
さて、基本的な環境は整ってきたので、バックエンド側にAPIエンドポイントを追加していきたいと思います。
バックエンド(Flask)→APIエンドポイントの追加
フロントエンド(Vue.js)→APIアクセスし結果を描画
まずはFlask側にAPIに対するルーティングを追加します。
index.html
に対するルーティングと同様に、そのままappserver.py
に記載しても良いのですが、APIのルーティングは別ファイルで管理したいので、FlaskのBluprintを使用します。
backend
配下にapi.py
というファイルを作成します。
from flask import Blueprint, jsonify, request from random import * api = Blueprint('api', __name__) @api.route('/hello/<string:name>/') def say_hello(name): response = { 'msg': "Hello {}".format(name) } return jsonify(response) @api.route('/random') def random_number(): response = { 'randomNumber': randint(1, 100) } return jsonify(response)
今回は試しに名前に対してHelloのメッセージを返すAPIと乱数を返すAPIを作成しました。
backend/application.py
にてこのAPIのルーティングを記載したBlueprintファイルをimportします。
# application.py from flask import Flask def create_app(app_name='FLASK-VUE'): app = Flask(app_name, static_folder = "./dist/static", template_folder = "./dist") app.config.from_object('backend.config.BaseConfig') ##### ここを追記 ##### from backend.api import api app.register_blueprint(api, url_prefix="/api") #################### return app
url_prefix="/api"
を指定しているので、ここで記載するルーティングはすべて/api/
配下のルーティングとなります。
http://localhost:5000/api/random
にアクセスすると以下の結果
{ "randomNumber": 38 }
http://localhost:5000/api/hello/hoge
にアクセスすると以下の結果
{ "msg": "Hello hoge" }
上記のような結果になればOKです。
次にfrontend
配下のVue.jsのプログラム側でこのAPIにリクエストをし、画面表示させたいと思います。
まずは、Vue.jsでajaxを利用するために、axiosをインストールします。
$ cd frontend $ npm install --save axios
frontend/src/components/Home.vue
を以下のように書き換えます。
// Home.vue <template> <div> <p>Home page</p> <p>Random number from backend: {{ randomNumber }}</p> <button @click="getRandom">New random number</button> </div> </template> <script> import axios from 'axios' export default { data () { return { randomNumber: 0 } }, methods: { getRandom () { const path = 'http://localhost:5000/api/random' axios.get(path) .then(response => { this.randomNumber = response.data.randomNumber }) .catch(error => { console.log(error) }) } }, created () { this.getRandom() } } </script>
バックエンドのFlask側では、デフォルトでは他のサーバからのリクエストを受け付ける設定になっていないため、フロントエンドのVue.jsからのAjaxのリクエストを受け入れるようにします。
まずは以下コマンドでCORSをインストールします。
$ pip install -U flask-cors
backend/application.py
を以下のように書き換えます。
# application.py from flask import Flask from flask_cors import CORS # ここを追記 def create_app(app_name='FLASK-VUE'): app = Flask(app_name, static_folder = "./dist/static", template_folder = "./dist") app.config.from_object('backend.config.BaseConfig') from backend.api import api app.register_blueprint(api, url_prefix="/api") # ここを追記 cors = CORS(app, resources={r"/api/*": {"origins": "*"}}) return app
これでhttp://localhost:8080/
アクセスしたときにAPIから取得した乱数を表示することができるようになったのですが、
もし静的なファイルをFlask経由で取得する必要が無いのであれば、COREの機能を使う必要はありません。
その場合、backend/appserver/py
を次のように書き換えればOKです。
# appserver.py from backend.application import create_app from flask import render_template import requests # ここを追記 app = create_app() @app.route('/', defaults={'path': ''}) @app.route('/<path:path>') def catch_all(path): #### 以下を追記 ### if app.debug: return requests.get('http://localhost:8080/{}'.format(path)).text return render_template("index.html") ################# if __name__ == '__main__': app.run()
必要に応じて以下コマンドを実行。
$ pip install requests
これで、開発モード(DEBUG=1
)の時は、http://localhost:5000
にアクセスした時はhttp://localhost:8080
にリダイレクトされるようになり、Vue.js側の画面でちゃんと乱数が取得できるようになります。
Flask側のサーバhttp://localhost:5000
とVue.js側のサーバhttp://localhost:8080
どちらにアクセスしても同様の結果が得られます。
終わりに
これで、バックエンドのAPIエンドポイントはFlaskで実装し、フロントエンドのUIはVue.jsで実装するフルスタックなアプリケーションが構築できました。
Flask側の開発用サーバとVue.jsの開発用サーバどちらも起動しておけば、それぞれ配下のプログラムを編集した際にホットリロードでリアルタイムに反映されるので、とても開発がしやすいです。
FlaskとVue.jsの詳細な使い方は今後もっと勉強していきたいと思います。