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

参考サイト