LoginSignup
80
82

More than 3 years have passed since last update.

Flask + HyperappでTodoアプリを作ってみた

Last updated at Posted at 2018-05-05

こんにちは、あんはるです。

Flask + HyperappでTodoアプリを作りました

Flaskとは?

Python製の軽量Webアプリケーションフレームワーク。RubyでいうSinatraみたいなもの。

Hyperappとは?

1 KBという超軽量のフロントエンドのフレームワーク。
とてもシンプルなのですぐに理解することができ、使いやすい。
QiitaのフロントエンドにHyperappが採用されたことから話題になる。

なぜ、Flask + Hyperappか。

Flaskは機械学習モデルをWebAPIにするのによく使われています。
今、機械学習もやっていてプロトタイプとして機械学習モデルをWebAPIにしてみようと思っているので、
Flaskを使う練習としてFlaskを使おうと思いました。

Hyperappは、HyperappでWebAPIからデータを取得したりする処理をしてみたかったので、Hyperappにしました。(普通にHyperappが好き)

こんな感じのTodoアプリを作った

todo_gif.gif

データベースと繋がっているので、ローディングしても、Todoのデータ、完了か未完了かは保持されます。
todo_gif_lo.gif

Github:
https://github.com/anharu2394/flask-hyperapp-todo_app

TodoアプリAPIの実装(バックエンド)

SQLAlchemyというORMでモデルを作る

db.py
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy(api)
class Todo(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    value = db.Column(db.String(20), unique=True)
    completed = db.Column(db.Boolean)

    def __init__(self,value,completed):
        self.value = value
        self.completed = completed

    def __repr__(self):
        return '<Todo ' + str(self.id) + ':' + self.value + '>'

APIはFlaskで。

main.py
import json
from flask import Flask, jsonify, request, url_for, abort, Response,render_template
from db import db


api = Flask(__name__)
api.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
def createTodo(value):
    create_todo = Todo(value,False)
    db.session.add(create_todo) 
    try: 
        db.session.commit()
        return create_todo
    except:  
        print("this todo is already registered todo.")
        return {"error": "this todo is already registered todo."}

def deleteTodo(todo_id):
    try:
        todo = db.session.query(Todo).filter_by(id=todo_id).first()
        db.session.delete(todo)
        db.session.commit()
        return todo
    except:
        db.session.rollback()
        print("failed to delete this todo.")
        return {"error": "failed to delete this todo."}

def updateTodo(todo_id,completed):
    try:
        todo = db.session.query(Todo).filter_by(id=todo_id).first()
        todo.completed = completed
        db.session.add(todo)
        db.session.commit()
        return todo
    except:
        db.session.rollback()
        print("failed to update this todo.")
        return {"error": "failed to update this todo."}

def getTodo():
    return Todo.query.all()

# 一覧表示
try:
    print(Todo.query.all())
except:
    print("中身がない")

@api.route('/')
def index():
    return render_template("index.html")

@api.route('/api')
def api_index():
            return jsonify({'message': "This is the Todo api by Anharu."})

@api.route('/api/todos', methods=['GET'])
def todos():
    todos = []
    for todo in getTodo():
        todo = {"id": todo.id, "value": todo.value,"completed": todo.completed}
        todos.append(todo)

    return jsonify({"todos":todos})

@api.route('/api/todos', methods=['POST'])
def create():
    value = request.form["value"]
    create_todo = createTodo(value)
    if isinstance(create_todo,dict):
        return jsonify({"error": create_todo["error"]})
    else:
        return jsonify({"created_todo": create_todo.value})

@api.route('/api/todos/<int:todo_id>',methods=['PUT'])
def update_completed(todo_id):
    if request.form["completed"] == "true":
        completed = True
    else:
        completed = False
    print(completed)
    update_todo = updateTodo(todo_id,completed)
    if isinstance(update_todo,dict):
        return jsonify({"error": update_todo["error"]})
    else:
        return jsonify({"updated_todo": update_todo.value})

@api.route('/api/todos/<int:todo_id>', methods=['DELETE'])
def delete(todo_id):
    delete_todo = deleteTodo(todo_id)
    if isinstance(delete_todo,dict):
        return jsonify({"error": delete_todo["error"]})
    else:
        return jsonify({"deleted_todo": delete_todo.value})

@api.errorhandler(404)
def not_found(error):
    return jsonify({'error': 'Not found'})
if __name__ == '__main__':
    api.run(host='0.0.0.0', port=3333)

サーバー起動

python main.py

getTodo(全Todo取得)、createTodo(Todoを追加する)、updateTodo(Todoを編集する)、deleteTodo(Todoを消す)という4つの関数を作り、
ルーティングを指定して、各関数を実行し、それの結果をjsonで返すように実装します。
APIはこのような感じです。

path HTTP method 目的
/api GET なし
/api/todos GET 全Todoの一覧を返す
/api/todos POST Todoを追加する
/api/todos/:id PUT Todoを編集する
/api/todos/:id DELETE Todoを消す
/api/todosのレスポンス例
{
  "todos": [
    {
      "completed": false,
      "id": 1,
      "value": "todo1"
    },
    {
      "completed": false,
      "id": 2,
      "value": "todo2"
    },
    {
      "completed": false,
      "id": 3,
      "value": "todo3"
    },
    {
      "completed": false,
      "id": 4,
      "value": "todo4"
    },
    {
      "completed": false,
      "id": 5,
      "value": "todo5"
    }
  ]
}

フロントエンドの実装

ディレクトリ構成
todo_app
   ├-- main.py
   ├-- index.js
   ├-- index.css
   ├── node_modules
   ├── static
   ├── templates
   |      └── index.html
   ├── package.json
   ├── webpack.config.js
   └── yarn.lock

必要なパッケージの追加

yarn init -y
yarn add hyperapp
yarn add webpack webpack-cli css-loader style-loader babel-loader babel-core babel-preset-env babel-preset-react babel-preset-es2015 babel-plugin-transform-react-jsx -D

babelの設定

.babelrc
{
  "presets": ["es2015"],
  "plugins": [
    [
      "transform-react-jsx",
      {
        "pragma": "h"
      }
    ]
  ]
}

webpackの設定

webpack.config.js
module.exports = {
  mode: 'development',
  entry: "./index.js",
  output: {
    filename: "bundle.js",
    path: __dirname + "/static"     
  },
  module: {
      rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                ['env', {'modules': false}]
              ]
            }
          }
        ]
      },
      {
        test: /\.css$/,
        loaders: ['style-loader', 'css-loader?modules'],
      }
    ]
  }

}

これで環境は整った。

メインのフロントを書いているindex.js

コードがごちゃごちゃしててすみません、、、

index.js
import { h, app } from "hyperapp"
import axios from "axios"
import styles from "./index.css"

const state = {
    todoValue: "",
    todos: [],
    is_got: false
}

const actions = {
    getTodo:  () => (state,actions) => {
        axios.get("/api/todos").then(res => {
            console.log(res.data)
            actions.setTodo(res.data.todos)
        })
    },
    setTodo: data => state => ({todos: data}),
    addTodo: todoValue => (state,actions) => {
        console.log(todoValue)
        var params = new URLSearchParams()
        params.append("value",todoValue)
        axios.post("/api/todos",params).then(resp => {
            console.log(resp.data)
         }).catch(error=>{
            console.log(error)
        }
        )
        actions.todoEnd()
        actions.getTodo()
    },
    onInput: value => state => {
        state.todoValue = value
    },
    deleteTodo: id => (state,actions) => {
        console.log(id)
        axios.delete("/api/todos/" + id).then(resp => {
            console.log(resp.data)
        }).catch(error => {
            console.log(error)
        })
        actions.getTodo()
    },
    checkTodo: e => {
        console.log(e)
        console.log(e.path[1].id)
        const id = e.path[1].id
        console.log("/api/todos/" + id)
        var params = new URLSearchParams()
        params.append("completed",e.target.checked)
        axios.put("/api/todos/" + id,params).then(resp => {
            console.log(resp.data)
        }).catch(error => {
            console.log(error)
        })

        if (e.target.checked == true){
            document.getElementById(id).style.opacity ="0.5"
            document.getElementById("button_" + id).style.display = "inline"
        }
        else{
            document.getElementById(id).style.opacity ="1"
            document.getElementById("button_" + id).style.display = "none" 
        }
    },
    todoEnd: () => state => ({todoValue:""})
}

const Todos = () => (state, actions) => (
    <div class={styles.todos}>
        <h1>Todoリスト</h1>
        <h2>Todoを追加</h2>
        <input type="text" value={state.todoValue} oninput={e => actions.onInput(e.target.value)} onkeydown={e => e.keyCode === 13 ? actions.addTodo(e.target.value) : '' }  />
        <p>{state.todos.length}個のTodo</p>
        <ul>
        {
         state.todos.map((todo) => {
            if (todo.completed){
                return (
                        <li class={styles.checked} id={ todo.id}><input type="checkbox" checked={todo.completed} onclick={e => actions.checkTodo(e)} />{todo.value}<button class={styles.checked}id={"button_" + todo.id} onclick={() => actions.deleteTodo(todo.id)}>消去</button></li>
                )
            }
            else{
                return (
                        <li id={todo.id}><input type="checkbox" checked={todo.completed} onclick={e => actions.checkTodo(e)}/>{todo.value}<button id={"button_" + todo.id} onclick={() => actions.deleteTodo(todo.id)}>消去</button></li>
                )
            }
        })
        }
        </ul>
    </div>
)
const view = (state, actions) => {
    if (state.is_got == false){
        actions.getTodo()
        actions.todoGot()
    }
    return (<Todos />) 
}

app(state, actions, view, document.body)

CSS

index.css
body {
}
.todos {
    margin:auto;
}
ul{
  padding: 0;
  position: relative;
  width: 50%;
}

ul li {
  color: black;
  border-left: solid 8px orange;
  background: whitesmoke;
  margin-bottom: 5px;
  line-height: 1.5;
  border-radius: 0 15px 15px 0;
  padding: 0.5em;
  list-style-type: none!important;
}
li.checked {
    opacity: 0.5;
}
button {
    display: none;
}
button.checked {
    display: inline;
}

HTML

templates/index.html
<html>
  <head>
    <meta charset="utf-8">
    <title>The Todo App with Flask and Hyperapp</title>
  </head>
  <body>
    <script src="/static/bundle.js"></script>
  </body>
</html>

webpackでビルドして、サーバー起動

yarn run webpack; python main.py

 機能の仕組みの解説

Todo一覧を表示する機能

Todosコンポーネント
const Todos = () => (state, actions) => (
    <div class={styles.todos}>
        <h1>Todoリスト</h1>
        <h2>Todoを追加</h2>
        <input type="text" value={state.todoValue} oninput={e => actions.onInput(e.target.value)} onkeydown={e => e.keyCode === 13 ? actions.addTodo(e.target.value) : '' }  />
        <p>{state.todos.length}個のTodo</p>
        <ul>
        {
         state.todos.map((todo) => {
            if (todo.completed){
                return (
                        <li class={styles.checked} id={ todo.id}><input type="checkbox" checked={todo.completed} onclick={e => actions.checkTodo(e)} />{todo.value}<button class={styles.checked}id={"button_" + todo.id} onclick={() => actions.deleteTodo(todo.id)}>消去</button></li>
                )
            }
            else{
                return (
                        <li id={todo.id}><input type="checkbox" checked={todo.completed} onclick={e => actions.checkTodo(e)}/>{todo.value}<button id={"button_" + todo.id} onclick={() => actions.deleteTodo(todo.id)}>消去</button></li>
                )
            }
        })
        }
        </ul>
    </div>
)

const view = (state, actions) => {
    if (state.is_got == false){
        actions.getTodo()
        actions.todoGot()
    }
    return (<Todos />) 
}
stateたち
const state = {
    todoValue: "",
    todos: [],
    is_got: false
}
action.getTodo()とaction.todoGot()
const actions = {
    getTodo:  () => (state,actions) => {
        axios.get("/api/todos").then(res => {
            console.log(res.data)
            actions.setTodo(res.data.todos)
        }).catch(error => {
            console.log(error)
        })
    },
    setTodo: data => state => ({todos: data}),
    todoGot: () => state => ({is_got:true})
}

actions.getTodo()を実行して、state.todosをセットし、その後Todosコンポーネントで表示します。
actions.getTodo()はaxiosでAPIにGETしていますが、fetchでもできます。


view の部分を

if (state.is_got == false){
    actions.getTodo()
    actions.todoGot()
}

こうしてるのは、そのまま、

actions.getTodo()

とすると、Stateが変更されるアクションなので、再レンダリングされ、また、actions.getTodo()が実行され、っと、無限に再レンダリングされてしまうので、is_gotというstateを作って、一回しか実行されないようにします。

Todoを追加する機能

<input type="text" value={state.todoValue} oninput={e => actions.onInput(e.target.value)} onkeydown={e => e.keyCode === 13 ? actions.addTodo(e.target.value) : '' }  />
stete
const state = {
    todoValue: ""
}

oninput={e => actions.onInput(e.target.value)}で、入力するやいなや、actions.onInputを実行させ、state.todoValueを更新しています。

actions.onInput
const actions = {
    onInput: value => state => {
        state.todoValue = value
    }
}

onkeydown={e => e.keyCode === 13 ? actions.addTodo(e.target.value) : '' }
Enterキーを押した時(Keyコードが13)に、actions.addTodo()を実行します。

const actions = {
    getTodo:  () => (state,actions) => {
        axios.get("/api/todos").then(res => {
            console.log(res.data)
            actions.setTodo(res.data.todos)
        })
    },
    addTodo: todoValue => (state,actions) => {
        console.log(todoValue)
        var params = new URLSearchParams()
        params.append("value",todoValue)
        axios.post("/api/todos",params).then(resp => {
            console.log(resp.data)
         }).catch(error=>{
            console.log(error)
        }
        )
        actions.todoEnd()
        actions.getTodo()
    },
    todoEnd: () => state => ({todoValue:""})
}

actions.addTodo()では、/api/todosにPOSTし、新しいTodoを作ります。
actions.todoEnd()でstate.todoValueを空白にさせ次のTodoを入力しやすいようにします。
actions.getTodo()を実行させ、追加されたTodoも取得し表示させます。

Todoの完了未完了を設定する機能

<input type="checkbox" checked={todo.completed} onclick={e => actions.checkTodo(e)} />

チェックボックスをチェックした時(clickした時、)にactions.checkTodo()を実行します。
eは、elementの略で、その時の要素のオブジェクトを返します。

actions.checkTodo()
const actions = {
    checkTodo: e => {
        console.log(e)
        console.log(e.path[1].id)
        const id = e.path[1].id
        console.log("/api/todos/" + id)
        var params = new URLSearchParams()
        params.append("completed",e.target.checked)
        axios.put("/api/todos/" + id,params).then(resp => {
            console.log(resp.data)
        }).catch(error => {
            console.log(error)
        })

        if (e.target.checked == true){
            document.getElementById(id).style.opacity ="0.5"
            document.getElementById("button_" + id).style.display = "inline"
        }
        else{
            document.getElementById(id).style.opacity ="1"
            document.getElementById("button_" + id).style.display = "none" 
        }
    }
}

e.path[1].idから、チェックされたTodoを見つけ、e.target.checkedで、完了か未完了かを、取得し、/api/todos/1(id) へPUTします。

その後、完了のtodoは濃さを薄くし消去ボタンを表示させ、未完了のtodoは濃さを正常にして、消去ボタンを見えなくします。

Todoコンポーネントの一部分
        <ul>
        {
         state.todos.map((todo) => {
            if (todo.completed){
                return (
                        <li class={styles.checked} id={ todo.id}><input type="checkbox" checked={todo.completed} onclick={e => actions.checkTodo(e)} />{todo.value}<button class={styles.checked}id={"button_" + todo.id} onclick={() => actions.deleteTodo(todo.id)}>消去</button></li>
                )
            }
            else{
                return (
                        <li id={todo.id}><input type="checkbox" checked={todo.completed} onclick={e => actions.checkTodo(e)}/>{todo.value}<button id={"button_" + todo.id} onclick={() => actions.deleteTodo(todo.id)}>消去</button></li>
                )
            }
        })
        }
        </ul>

ローディングしてもそのままの状態を保持するため、完了か未完了かで条件分岐しています。

Todoを消す機能

<button id={"button_" + todo.id} onclick={() => actions.deleteTodo(todo.id)}>消去</button>

clickした時、 actions.deleteTodo()を実行します。

actions.deleteTodo()とactions.getTodo()
const actions = {
    getTodo:  () => (state,actions) => {
        axios.get("/api/todos").then(res => {
            console.log(res.data)
            actions.setTodo(res.data.todos)
        })
    },
    deleteTodo: id => (state,actions) => {
        console.log(id)
        axios.delete("/api/todos/" + id).then(resp => {
            console.log(resp.data)
        }).catch(error => {
            console.log(error)
        })
        actions.getTodo()
    }
}

actions.deleteTodo()では、引数のidのTodoを消去するため、/api/todosへDELETEします。
そして、actions.getTodo()実行し、Todoの一覧を再取得しています。

ソースコード

Github:
https://github.com/anharu2394/flask-hyperapp-todo_app

感想

自分でAPIを書くこと(、フロントのフレームワークでAPIを叩くことなかったのでとても楽しかったです。

FlaskではRailsのActiveRecordのようなものが最初っからない(MVCではない)ので、RailsでWebアプリを作るのとは違った感覚でした。

もちろんRails APIで書いた方が早い
しかし、ただ楽しい

Todoアプリのdbはテーブルがひとつしかないので、もう少し複雑なアプリもflask + Hyperappで作ってみたい。(めんどくさいだろう)

Rails API + Hyperappもやってみたい

今、作りたい機械学習のモデルがあって、それをWebAPI化するのに、この経験を活かせると思います。

 ぜひ、Flask + Hyperappで簡単なWebアプリを作ってみてください!

参考文献

80
82
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
80
82