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、バックエンドのAPIFlaskのWebアプリケーションに対してFlask-Loginを使用してログイン機能を作成します。
Flask-Login

↓前回
kittagon.hateblo.jp

↓前々回 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__.pyFlask-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_userlogin_userlogout_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を使用します。

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がありました。

TypeError: Cannot read property 'compile' of undefined · Issue #1334 · webpack/webpack-dev-server · GitHub

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です。
高校の頃に乗っていたままで実家の車庫に眠っていました。
バッテリーも上がっておりエンジンもかかりません。
f:id:ti_taka:20181117212708j:plain

まずはシートを外します。ここはシート下のボルトを外すだけなので簡単ですね。
f:id:ti_taka:20181117212730j:plain

次はタンクを外しにかかりますが、スティードの構造上燃料コックを外さないとタンクが外せません。
燃料コックの奥にあるネジを回し、外していきます。必ずOFFにしてガソリンの流れを止めておくように。
(かなり奥にあるので鈍らないように注意!!)
f:id:ti_taka:20181117212758j:plain

こんな感じで外せます。
f:id:ti_taka:20181117212820j:plain

燃料コックが外せると、タンクを少し上に浮かせることができるので、チューブに繋がってるボルトを外します。
f:id:ti_taka:20181117212845j:plainf:id:ti_taka:20181117212856j:plainf:id:ti_taka:20181117212906j:plain

また、タンク端のネジを外します。
f:id:ti_taka:20181117212934j:plain

外せました!
f:id:ti_taka:20181117213023j:plainf:id:ti_taka:20181117213035j:plain

次に、まずキャブのドレンボルトを外しておきます。
f:id:ti_taka:20181117213101j:plain

ここを外しておくことでポンプ側からクリーナーを入れた時に汚れが外に流れ出てきます。
キャブの中にガソリンを残したまま長期間放置してしまうとキャブの中に汚れが溜まり、
クリーナーを入れる前からドロっとした汚れが出てくるので、
あらかじめ紙などを敷いておきましょう。
右側と左側のキャブそれぞれにあるのでどちらも忘れずに開けましょう。
洗浄途中の写真ですが、こんな感じに外れます。
f:id:ti_taka:20181117213131j:plain

次にようやくポンプ側のチューブを取り外しにかかります。
ガソリンを送るポンプはシート下にあります。
f:id:ti_taka:20181117213153j:plain

こちらのチューブがキャブに繋がっているのでこれを外します。
f:id:ti_taka:20181117213236j:plainf:id:ti_taka:20181117213241j:plain

ここが外せればあとはこちらのクリーナーを入れるだけです。

  • 注入口をしっかりと塞いで5秒ほど噴射して注入
  • 汚れがだらだらとドレンボルトから流れ出てくるのでしばらく待つ

というのを繰り返します。
f:id:ti_taka:20181117213301j:plain

するとこんな感じに徐々に出てくる汚れが薄くなっていきます。
f:id:ti_taka:20181117213323j:plain

最終的にはこれぐらいに薄まるまで上記の手順を繰り返します。
f:id:ti_taka:20181117213346j:plainf:id:ti_taka:20181117213400j:plain

これで概ねキャブの洗浄は完了です。
後はこのままポンプやチューブをつなぎ直してエンジンをかけてみます。
ドレンボルトから流れ出た分以外のクリーナーが少しキャブの中に残ってしまいますが、ガソリンで流されるので問題ないそうです。

とりあえず車のバッテリーと接続してエンジンをかけてみます。
車側のプラス端子とバイク側のバッテリープラス端子を接続し、車側のマイナス端子はどこか導電体の金属の箇所に接続し、アースします。
f:id:ti_taka:20181117213437j:plainf:id:ti_taka:20181117213450j:plainf:id:ti_taka:20181117213501j:plain

これでセルを回すと無事エンジンがかかりました!!
かなり久しぶりにエンジンの音を聞いたので感動です!!

終わりに

今回はとりあえず簡易的な方法でキャブを洗浄し、エンジンをかけるところまで実践しました。
が、実際に乗るためには整備しなくてはならない箇所がまだまだ残っています。 ぱっと思いつく限りでこんなに。。

  • エアクリーナーのゴムパッキン交換
  • エンジンオイル交換
  • チェーン交換
  • サイドカバー交換
  • シフトレバー交換
  • フロントフォークのオイル漏れ修理
  • シート交換
  • ウィンカー交換
  • マフラー交換
  • ディスクブレーキ専用液補充
  • タイヤ、ホイール交換
  • ラジエーター修理
  • 全体、サビ落とし、塗装

来年、遅くとも再来年にはもう一度乗れるよう細々と修理していきます!

Vue.jsとFlaskでフルスタックなWebアプリの開発環境を構築 その2〜〜投稿画面作成〜〜

はじめに

前回作ったこちらの環境を用いて、タスク管理アプリ的なものを作成していきたいと思います。

kittagon.hateblo.jp

タスクのモデルを作成(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.jsfrontend/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を開けば以下のように登録と削除ができる画面が動作すると思います。
f:id:ti_taka:20181113213852g:plain

あまりタスク管理っぽくないですが。笑

終わりに

これで、バックエンドとフロントエンドの役割が明確なWebアプリを作成出来ました。
あとはバックエンドのFlaskでAPIを作成し、フロントエンドのVue.jsでUIを描画すればいろんなWebアプリが作れるようになります。
次回はこのWebアプリにログイン機能をつけたいと思います。

↓続き kittagon.hateblo.jp

参考サイト

Vue.jsとFlaskでフルスタックなWebアプリの開発環境を構築 その1〜〜環境構築〜〜

はじめに

最近の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/componentsHome.vueAbout.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:8080localhost: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の詳細な使い方は今後もっと勉強していきたいと思います。

↓続編 kittagon.hateblo.jp

kittagon.hateblo.jp

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についてもいつか勉強しようと思います。