SvelteのフロントエンドをAWSのバックエンドと繋げてみる
はじめに
以前にSvelteで遊んでいる時に①TODOアプリ(フロントエンドだけ)を作ってて、そのあとでAWS Amplifyで遊び始めてから②AmplifyでTODOのバックエンドをつくったんですが、ここまできたら流れ的に①と②を繋いでみようかな、となります。そういう回です。
①と②の記事はこちら:
できあがり
お恥ずかしながら、できあがりのデモ画面を。
構成はこんな感じ
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Frontend │ │ APIGW │ │ Lambda │ │ │
│ (Svelte) ├─────┤► (REST) ├──────► with ├─────► DynamoDB │
│ │ │ │ │ RubySDK │ │ │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
コード
Svelte
前回のフロントエンドだけで動いていたやつをAPIコールするように変更したバージョンです
<script>
const baseURL = 'https://<api-id>.execute-api.<region>.amazonaws.com/dev/tasks'
let taskName = ""
let checked = false
let modalOn = false
// sort
function compare( a, b ) {
console.log('compare called')
if ( a['updated-at'] > b['updated-at'] ){ return -1; }
if ( a['updated-at'] < b['updated-at'] ){ return 1; }
return 0;
}
function sortByCreatedAt(todos){
console.log('sortBy called')
todos.then(data => {
data = data.sort(compare);
});
return todos
}
function statusText(taskStatus){ return taskStatus ? "Active" : "Done" }
const listTasks = async () => {
console.log('listTasks called')
const res = await fetch(baseURL);
return await sortByCreatedAt(res.json())
}
const getTask = async (taskId) => {
console.log('getTask called')
const task = await fetch(baseURL + '/' +taskId);
return await task.json()
}
const addTask = async (taskName) => {
console.log('addTask called!')
let taskData = {
"task-id": Date.now().toString(36),
"is-active": true,
"task-name": "",
"updated-at": Date.now().toString(),
"created-at": Date.now().toString(),
"user-id": "100"
}
taskData['task-name'] = taskName;
const res = await fetch(baseURL, {
method: 'POST',
body: JSON.stringify(taskData)
});
todos = listTasks()
}
const deleteTask = async (taskId) => {
console.log('deleteTask called!')
const res = await fetch(baseURL + '/' + taskId, {
method: 'DELETE',
});
todos = listTasks() // assignment to ignite the reactivity
}
const completeTask = async (taskId, status) => {
const res = await fetch(baseURL + '/' + taskId, {
method: 'PUT',
body: JSON.stringify({ 'task-id': taskId, 'is-active': !status })
});
todos = listTasks()
}
let taskDetail = ''
function showDetail(taskId) {
console.log('showDetail called')
modalOn = true
//return getTask(taskId)
taskDetail = getTask(taskId)
}
// format date
function fd(date) {
console.log('fd called')
let d = new Date(parseInt(date))
return d.getFullYear() + '/' + d.getMonth() + '/' + d.getDate() + ' ' + d.getHours() + ':' + d.getMinutes()
}
// todos
let todos = listTasks(); // Initialized as Promise
$: activeTodos = checked ? todos : todos.then(data => data.filter(todo => todo['is-active']))
</script>
<main>
<h1>HELLO TODO!</h1>
<div class="container is-fluid">
<div class="columns is-centered">
<div class="field has-addons">
<div class="control" style="margin-bottom: 5em;">
<input class="input" type="text" placeholder="Add a task" bind:value={taskName}>
</div>
<div class="control">
<a class="button is-danger" on:click={() => addTask(taskName)}><i class="fas fa-plus"></i></a>
</div>
</div>
</div>
<div class="columns" style="width: 200px; margin-left: 60%; margin-bottom: 3em">
<div class="field">
<input id="switchRoundedDanger" type="checkbox" name="switchRoundedDanger" class="switch is-rounded is-danger is-rtl" bind:checked={checked}>
<label for="switchRoundedDanger">Show completed</label>
</div>
</div>
<div class="columns is-centered">
{#await activeTodos}
<span class="spinner-loader" style="margin-top: 10em">Loading…</span>
{:then tasks}
<table class="table is-hoverable">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Created at</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{#each tasks as task}
<tr>
<td>{task['task-id']}</td>
<td>{task['task-name']}</td>
<td>{fd(task['created-at'])}</td>
<td>{statusText(task['is-active'])}</td>
<td>
<i class="fas fa-check" on:click={() => completeTask(task['task-id'], task['is-active'])}></i>
<i class="fas fa-info" on:click={() => showDetail(task['task-id'])}></i>
<i class="far fa-trash-alt" on:click={() => deleteTask(task['task-id'])}></i>
</td>
</tr>
{/each}
</tbody>
</table>
{/await}
</div>
<div class="modal { modalOn ? 'is-active' : ''}">
<div class="modal-background"></div>
<div class="modal-content">
<dev class="box">
{#await taskDetail}
<span class="spinner-loader" style="margin-top: 10em">Loading…</span>
{:then td}
<table class="table is-fullwidth">
<thead>
<tr><td>key</td><td>value</td></tr>
</thead>
<tbody>
<tr><td>task-id</td><td><span class="tag">{td['task-id']}</span></td></tr>
<tr><td>task-name</td><td><span class="tag">{td['task-name']}</span></td></tr>
<tr><td>created-at</td><td><span class="tag">{fd(td['created-at'])}</span></td></tr>
<tr><td>updated-at</td><td><span class="tag">{fd(td['updated-at'])}</span></td></tr>
<tr><td>is-active</td><td><span class="tag">{td['is-active']}</span></td></tr>
<tr><td>user-id</td><td><span class="tag">{td['user-id']}</span></td></tr>
</tbody>
</table>
{/await}
</dev>
</div>
<button class="modal-close is-large" aria-label="close" on:click="{() => modalOn = false}"></button>
</div>
</div>
</main>
<style>
main {
text-align: center;
padding: 1em;
max-width: 240px;
}
h1 {
color: #ff3e00;
text-transform: uppercase;
font-size: 4em;
font-weight: 100;
margin-bottom: 1.5em;
}
td i {
padding: 5px;
cursor: pointer;
color: gray;
}
label { font-family: monospace; }
table { font-family: monospace; }
@media (min-width: 640px) {
main {
max-width: none;
}
}
</style>
Lambda
以前の②のポストの内容とほぼ同じですが...
require 'json'
require 'json/add/exception'
require 'aws-sdk-dynamodb'
CORS_HEADER = {
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Origin": '*',
"Access-Control-Allow-Methods": "OPTIONS,POST,PUT,GET"
}
def add_task(table, body)
begin
table.put_item({ item: body })
{ statusCode: 200, headers: CORS_HEADER, body: JSON.generate(body) }
rescue => e
{ statusCode: 500, body: e.to_json }
end
end
def delete_task(table, task_id)
begin
params = { table_name: table, key: { 'task-id': task_id } }
table.delete_item(params)
list_task(table)
rescue => e
{ statusCode: 500, body: e.to_json }
end
end
def update_task(table, body)
begin
params = {
table_name: table,
key: { 'task-id': body['task-id'] },
attribute_updates: {
'is-active': { value: body['is-active'], action: "PUT" },
}
}
table.update_item(params)
{ statusCode: 200, headers: CORS_HEADER, body: JSON.generate(body) }
rescue => e
{ statusCode: 500, headers: CORS_HEADER, body: e.to_json }
end
end
def list_task(table)
begin
scan_output = table.scan({ limit: 50, select: "ALL_ATTRIBUTES" })
{ statusCode: 200, headers: CORS_HEADER, body: JSON.generate(scan_output['items']) }
rescue => e
{ statusCode: 500, body: e.to_json }
end
end
def get_task(table, task_id)
begin
params = { key: { 'task-id': task_id } }
task = table.get_item(params)
{ statusCode: 200, headers: CORS_HEADER, body: JSON.generate(task['item']) }
rescue => e
{ statusCode: 500, body: e.to_json }
end
end
def lambda_handler(event:, context:)
begin
http_method = event['httpMethod']
dynamodb = Aws::DynamoDB::Resource.new(region: 'us-east-2')
table = dynamodb.table('todoTable-dev')
case http_method
when 'GET'
path_param = event.dig('pathParameters', 'proxy')
if path_param.nil?
list_task(table)
else
get_task(table, path_param)
end
when 'PUT' then update_task(table, JSON.parse(event['body']))
when 'POST' then result = add_task(table, JSON.parse(event['body']))
when 'DELETE' then delete_task(table, event['pathParameters']['proxy'])
else 0
end
rescue => e
{ statusCode: 500, body: e.to_json }
end
end
学び的なもの、もしくはただの感想
単なるTODOアプリでもバカにしてはいけない。新しいフレームワーク勉強するなら特に
Svelte編
1. Await block が変態的だがいい感じ
初見では、おっ、テンプレでawait
するのか!って思う書き方ですが、いいですね。
<h1>Task list</h1>
{#await todos}
<p>Loading...</p>
{:then tasks}
{#each tasks as task}
<p>{task['task-id']}, {task['task-name']}</p>
{/each}
{/await}
on:click
で発火する関数に引数を渡す方法
2. 引数がない関数のコールはこれでいいけど
<button on:click="{listTask}"></button>
引数がある場合はこう書いてしまうとクリック時じゃなくて即時実行になってしまう
<button on:click="{getTask(taskId)}"></button>
代わりにこう書く
<button on:click="{() => getTask(taskId)}"></button>
3. 一つのアクションで二つの関数はコールできない
Vueみたいにこんな風にかけるかと思ったがだめだった。まあ、前の関数の中で後の関数を呼べばいいんだよな。
<button on:click="{listTask; getTask}"></button>
selected
などのショートハンドがいけてる
4. チエックボックスやモーダル開く時にselected
などのclass
を操作することがあります。
<style>
/* ...other CSS... */
span.cell.selected {
outline-color: lightblue;
outline-style: dotted;
}
</style>
<span class="cell {selected === true ? 'selected' : ''}">
{value}
</span>
というのは、以下のように書くことができますが
<span class="cell" class:selected="{selected}">
{value}
</span>
さらにショートハンドでこうなる
<span class="cell" class:selected>
{value}
</span>
いいですね。ただし、class名にハイフンを使ったりすると動かなくて困ります。
出典はここ
CORS編
fetch
のno-cors
は万能ではない。PUT
だと動かない
1. CORS周りはハマるとかなしい。そんなときno-cors
で逃げられるとは限らない
2. エラーメッセージにもCORSヘッダつけておいた方がデバッグしやすい
これはAPIGW側で500
の場合は、とかで一括でつけれると思う
JavaScript編
1. JSのDateは相変わらずつらい
書きたくもない
2. Promiseからは逃げられない
中身だけが欲しいと思って、どんなに中身だけを切り出そうと頑張っても、返ってくるのはいつもPromise
。仲良くしないといけない
LoadingのCSSにはこれを使え編
こういう細かいところで雰囲気が変わって、ちょっとだけ作るモチベーションが長持ちするもんだ。
DynamoDB編
1. ソートはクエリでやるのではなく、テーブルにソートキーというのをつけるみたいだ...
Firebaseにすればよかった...
2. ソートキーをつけ忘れたら、テーブル作り直しな
DynamoDB doesn't allow to add a sort key for an already existing table. Your only option is to create a new table with redefined key attributes and then copy the data from the existing table to the newly created table.
まじか.. フロントでソートしたわ。
:return_values
はいまいち
3. 更新系の処理をした時に、値を返してくれるが更新前の値(ALL_OLD
とか)だったりしていまいち使いずらい。どういうアプリを想定しているの?
Lambda編
最初はクライアントが楽かなと思って、Lambda側でPOSTやPUT受け付けたら、更新後のタスクリストをGETして返すようなことをやっていたが、やめた方がいい。各リクエストはAtomicにして、フロント側から操作する方がややこしくならない
おつかれさまです
Discussion