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の詳細な使い方は今後もっと勉強していきたいと思います。
dropbox-sdk-jsを使ってフォルダ内アイテムの共有リンクを取得
はじめに
最近フロントエンドの勉強をしています。
今作っているWebアプリで、Dropboxに保存してある写真を表示するという機能を作りたかったのですが、サーバサイドでAPIを叩き個別に取得して表示させるととても画面レスポンスが遅くなってしまいました。
そこでJavascriptで非同期に取得し、順次画面表示させてユーザーの体感速度を早くしようとしたのですが、そこで少しハマってしまったので解決方法を備忘として残しておきます。
dropbox-sdk-jsの導入
まずはdropbox-sdk-jsを導入します。
今回はnpmではなくCDN版のものを使用します。
Getting started | Dropbox JavaScript SDK
以下をhtmlに記載します。
<script src="https://unpkg.com/dropbox/dist/Dropbox-sdk.min.js"></script> <script src="https://unpkg.com/dropbox/dist/DropboxTeam-sdk.min.js"></script>
フォルダ内アイテム一覧を取得
まずは共有リンクを取得したい画像アイテムのリストを取得します。
filesListFolder
メソッドにフォルダのパスを渡せば取得出来るみたいです。
var dropbox_api_token = "((dropbox_api_token))"; var dbx = new Dropbox.Dropbox({ accessToken: dropbox_api_token }); var dir_path = "path/to/items" dbx.filesListFolder({path: dir_path}) .then(function(response) { console.log(response.entries) }) .catch(function(error) { console.error(error); });
これはチュートリアルに載っているコードそのままで取得できます。
これでresponse.entries
にアイテム一覧が配列で取得できました。
forループでフォルダ内のアイテムそれぞれの共有リンクを取得
一覧が取得できたので、それぞれのアイテムの共有リンクを取得しようと思います。
本来なら、共有リンクを作成dbx.sharingCreateSharedLinkWithSettings
の後に共有リンクを取得dbx.sharingListSharedLinks
する流れですが、自分の環境だとサーバサイドで共有リンクの作成は完了しているので、リンク取得のみを記述します。
こんな感じです。
var dropbox_api_token = "((dropbox_api_token))"; var dbx = new Dropbox.Dropbox({ accessToken: dropbox_api_token }); var entries; var urls = [] //共有リンクの配列 var dir_path = "path/to/items" dbx.filesListFolder({path: dir_path}) .then(function(response) { console.log(response.entries) entries = response.entries; for(var i = 0;i<entries.length;i++) { dbx.sharingListSharedLinks({path: entries[i].path_display}) .then(function(response) { urls.push(response.links[0].url); }) } }) .catch(function(error) { console.error(error); }); /////////////////////////////////////// //以下に共有リンクの配列(urls)を取得した後の処理を記載 ///////////////////////////////////////
JavaScript初心者だった自分は、なんとなくこれで動作しそうだなーと思って、こんなコードを書いたのですが、これが全然うまくいきませんでした。
共有リンクが取得できている前提で後の処理を書いても「urlsの中身が空だよー」とエラーになってしまいます。
再帰的に関数を呼び出す
非同期処理が入っているため順番がめちゃくちゃになってしまうようなので、きっちりと順番順番に処理をしてもらうために、以下のように関数を再帰的に呼び出して処理することにしました。
var dropbox_api_token = "((dropbox_api_token))"; var dbx = new Dropbox.Dropbox({ accessToken: dropbox_api_token }); var urls = [] //共有リンクの配列 var dir_path = "path/to/items" dbx.filesListFolder({path: path}) .then(function(response) { // 関数の呼び出し getSharingLinks(0, urls, response.entries); $("#loading-img").fadeOut('slow'); }) .catch(function(error) { console.error(error); }); // 関数を定義 function getSharingLinks(i, urls, entries) { dbx.sharingListSharedLinks({path: entries[i].path_display}) .then(function(response) { var url = response.links[0].url.replace("www.dropbox.com","dl.dropboxusercontent.com").replace("?dl=0",""); urls.push(url); i++; if ( i == entries.length) { return console.log(url); } else { // 再帰的に関数を呼び出し getSharingLinks(i, urls, entries); } }) .catch(function(error) { console.error(error); }); }
まとめ
Javascriptで非同期の処理を扱う際は、Promiseやasyncなどを使うのが基本みたいですが、ちょっと調べただけではよく理解できなかったので、今回はきれいではないですがとりあえずこんな感じで解決しました。 いつか必要になると思うのでasyncについてもいつか勉強しようと思います。
heroku container:push でエラー
先日からherokuCLIをアップデートしたあたりからherokuにデプロイしようとする度に以下のエラーが出るようになってしまいました。
(node:26614) Error Plugin: heroku-container-tools: files attribute must be specified in /Users/username/.local/share/heroku/node_modules/heroku-container-tools/package.json module: @oclif/config@1.6.27 plugin: heroku-container-tools root: /Users/username/.local/share/heroku/node_modules/heroku-container-tools See more details with DEBUG=* (node:26614) Error Plugin: heroku-container-tools: files attribute must be specified in /Users/username/.local/share/heroku/node_modules/heroku-container-tools/package.json module: @oclif/plugin-legacy@1.0.15 plugin: heroku-container-tools root: /Users/username/.local/share/heroku/node_modules/heroku-container-tools See more details with DEBUG=* Uninstalling heroku-container-tools... done
エラーメッセージを読んでもよくわからなかったのでいろいろ検索したところ以下の記事がヒットしました。
Latest Docker update broken Heroku cli?
症状は違いますがheroku-container-tools
が悪さをしているようだったので、以下コマンドでアンインストールし、heroku-container-registry
をインストールし直したところ、エラーは出なくなりました。
$ heroku plugins:uninstall heroku-container-tools $ heroku plugins:install heroku-container-registry