こんにちは、あんはるです。
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のデータ、完了か未完了かは保持されます。
Github:
https://github.com/anharu2394/flask-hyperapp-todo_app
TodoアプリAPIの実装(バックエンド)
SQLAlchemyというORMでモデルを作る
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で。
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()
@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の設定
{
"presets": ["es2015"],
"plugins": [
[
"transform-react-jsx",
{
"pragma": "h"
}
]
]
}
webpackの設定
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
コードがごちゃごちゃしててすみません、、、
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
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
<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一覧を表示する機能
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 />)
}
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)
}).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) : '' } />
const state = {
todoValue: ""
}
oninput={e => actions.onInput(e.target.value)}
で、入力するやいなや、actions.onInputを実行させ、state.todoValueを更新しています。
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の略で、その時の要素のオブジェクトを返します。
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は濃さを正常にして、消去ボタンを見えなくします。
<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()を実行します。
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を書くこと(Railsだと自動でできたりする)、フロントのフレームワークでAPIを叩くことなかったのでとても楽しかったです。
FlaskではRailsのActiveRecordがない(MVCではない)ので、RailsでWebアプリを作るのとは違った感覚でした。
もちろんRails APIで書いた方が早い
ただ楽しい
Todoアプリのdbはテーブルがひとつしかないので、もう少し複雑なアプリもflask + Hyperappで作ってみたい。
Rails API + Hyperappもやってみたい
今、作りたい機械学習のモデルがあって、それをWebAPI化するのに、この経験を活かせると思います。
Top comments (0)