Svelte から AWS Lambda を API Gateway をトリガーに呼び出す
Svelte で書かれた、氏名とメールアドレスを入力する欄があるだけのシンプルなフォームの Submit ボタンが押されたとき、AWS Lambda 関数を呼び出す場合を考えます。
Svelte プロジェクト作成
以下のコマンドを実行して、プロジェクトを作成し、バリデーションのfelteをインストールします。<project-name>
は、任意の名前です。
$ npx degit sveltejs/template <project-name>
$ cd <project-name>
$ npm install
$ npm install --save felte yup @felte/validator-yup
ちなみにnpm install
はnpm i
に、npm --save
はnpm -S
に置き換えられます。
src
直下にあるApp.svelte
を以下に書き換えます。felte を読み込んでいます。
<script>
import { createForm } from 'felte'
import { validator } from '@felte/validator-yup'
import * as yup from 'yup'
</script>
<main>
</main>
バリデーションの効いたフォームの作成
yup.object
でバリデーションをする要素を設定することができます。
設定するkey
は<input>
要素に追加するname
属性と同じ名前を設定する必要があり、これをschema
という定数で定義します。
次のcreateForm
では、これらのバリデーションを管理していきます。書き換える部分はonSubmit
内です。この中にはSubmit
ボタンが押されたときに実行したい処理を書くことができます。ここでは、フォームで入力された内容を AWS Lambda 関数に送信するための、API コールをする関数を呼び出しています(後ほど実装)。引数には、フォームの内容をJSON.stringify
メソッドでstring
にしたデータを渡しています。
<script>
const schema = yup.object({
email: yup.string().email().required(),
name: yup.string().required(),
})
const { form, errors } = createForm({
extend: validator,
validateSchema: schema,
onSubmit: async (values) => {
sendContact(JSON.stringify(values, null, 2))
},
})
</script>
<main>
<form use:form on:submit|preventDefault>
<input type="text" name="name" />
{#if $errors.name}
<span>Error Message</span>
{/if}
<input type="email" name="email" />
{#if $errors.email}
<span>Error Message</span>
{/if}
<button type="submit">Submit</button>
</form>
</main>
フォームは<form use:form on:submit|preventDefault>
とします。
この中に書かれた、対応するname
属性の要素についてバリデーションを行い、独自のエラーメッセージを表示させることができます。エラーメッセージはカスタマイズすることができ、例えば<input>
要素のタイプがemail
の場合であれば、「入力されていない場合」と「正しいメールの形式ではない場合」の 2 つのエラーメッセージを発生させたい場合が考えられます。それぞれを別のエラーとして分けるには、felte
が出力するエラーメッセージで分岐させる必要があり「入力されていない場合」はemail is a required field
、「正しいメールの形式ではない場合」はemail must be a valid email
というエラーメッセージが出力されます。このエラーメッセージ内のemail
というのは、<input>
要素内に設定されたname
属性の値です。なので、このメッセージによって表示させたいエラーメッセージを分けることができ、以下のよう書くことができます。
{#if $errors.email == 'email must be a valid email'}
Please enter a valid email address
{:else if $errors.email == 'email is a required field'}
Please enter your email address
{/if}
これを適応したときのApp.svelte
↓
<script>
const schema = yup.object({
email: yup.string().email().required(),
name: yup.string().required(),
})
const { form, errors } = createForm({
extend: validator,
validateSchema: schema,
onSubmit: async (values) => {
sendContact(JSON.stringify(values, null, 2))
},
})
</script>
<main>
<form use:form on:submit|preventDefault>
<input type="text" name="name" />
{#if $errors.name}
<span>Please enter your name</span>
{/if}
<input type="email" name="email" />
{#if $errors.email == 'email must be a valid email'}
<span>Please enter a valid email address</span>
{:else if $errors.email == 'email is a required field'}
<span>Please enter your email address</span>
{/if}
<button type="submit">Submit</button>
</form>
</main>
これでバリデーションが適応されたフォームを作成することができました。
バリデーションから外れている場合にはonSubmit
内の処理が実行されることはありません。
Lambda 関数を呼び出す
今回の API の呼び出しはcurl
コマンドだと、氏名とメールアドレスを送るため、-d
オプションを使い、以下のように書くことができます。
curl <api endpoint> -d "{\"name\": \"xxx\", \"email\": \"xxx@yyy.zzz\"}"
<api endpoint>
は AWS API Gateway で作成した URL です(後ほど作成)。次に-H
オプションでヘッダー情報にContent-Type: application/json
を乗せると、コマンド全体は以下のようになります ↓
$ curl <api endpoint> -d "{\"name\": \"xxx\", \"email\": \"xxx@yyy.zzz\"}" -H "Content-Type: application/json"
このコマンドをcurlconverterでNode.js(fetch)
を指定して変換すると以下のコードが得られます。
var fetch = require("node-fetch");
fetch("<api endpoint>", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "xxx", email: "xxx@yyy.zzz" }),
});
これを関数として実行できるようにし、返値を取得できるようにすると、以下のコードになります。mode: "cors"
は CORS を許可するために追加しています。-d
オプションはbody
で、フォームの内容を関数の引数として受け取り、そのまま流すようにしています。
let promise;
const sendContact = (bodyContent) => {
promise = fetch('<api-endpoint>', {
mode: 'cors",
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: bodyContent,
}).then((x) => x.json());
};
処理が実行されてから、終了するまでに実行したい処理は以下のように書くことができます。
ここでは、レスポンスが帰ってくるまではローディングのアニメーションを実行し、
レスポンスが正常に帰ってきた場合は、そのデータを表示させ、エラーが発生した場合は、
独自のエラーメッセージを表示させています。
{#await promise}
<span><i class="fas fa-circle-notch fa-spin"></i></span>
{:then data}
{#if data}
<pre><code>{data}</code></pre>
{:else}
{/if}
{:catch error}
<span>An error occurred</span>
{/await}
これで Svelte 側の処理を実装することができました。
これまでのコード ↓
<script>
import { createForm } from 'felte'
import { validator } from '@felte/validator-yup'
import * as yup from 'yup'
let promise;
const sendContact = (bodyContent) => {
promise = fetch("<api-endpoint>", {
mode: "cors",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: bodyContent,
}).then((x) => x.json());
};
const schema = yup.object({
email: yup.string().email().required(),
name: yup.string().required(),
})
const { form, errors } = createForm({
extend: validator,
validateSchema: schema,
onSubmit: async (values) => {
sendContact(JSON.stringify(values, null, 2))
},
})
</script>
<main>
<form use:form on:submit|preventDefault>
<input type="text" name="name" />
{#if $errors.name}
<span>Please enter your name</span>
{/if}
<input type="email" name="email" />
{#if $errors.email == 'email must be a valid email'}
<span>Please enter a valid email address</span>
{:else if $errors.email == 'email is a required field'}
<span>Please enter your email address</span>
{/if}
<button type="submit">Submit</button>
</form>
{#await promise}
<span><i class="fas fa-circle-notch fa-spin"></i></span>
{:then data}
{#if data}
<pre><code>{data}</code></pre>
{:else}
{/if}
{:catch error}
<span>An error occurred</span>
{/await}
</main>
Lambda 関数の実装
まず Lambda 関数を作ります。今回は test という名前で作成しました。
作成した Lambda 関数を選択すると以下ような画面に移動します。
次に「トリガーを追加」を選択します。API Gateway を選択して、以下の設定にします。Svelte の<api endpoint>
は、作成した API Gateway のエンドポイント URL に書き換えてください。
次に作成した API Gateway の CORS を有効にします。「CORS の有効化」を選択します。
次に「CORS を有効にして既存の CORS ヘッダーを置換」を選択します。
手順:「アクション」→「CORS の有効化」→「CORS を有効にして既存の CORS ヘッダーを置換」
これで API Gateway の CORS を有効にできました。
次に Lambda 関数の CORS を有効にします。
Lambda 関数は、レスポンスヘッダーに、以下のようにAccess-Control-Allow-Headers
、Access-Control-Allow-Origin
、Access-Control-Allow-Methods
を追加することで CORS を有効にすることができます。
import ast
import json
def lambda_handler(event, content):
data = ast.literal_eval(event['body'])
name = data['name']
email = data['email']
body_content = f'Name: {name}, Email: {email}'
return {
'statusCode': 200,
'headers': {
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'OPTIONS,POST'
},
'body': json.dumps(body_content)
}
関数内の初めの 4 行では、送られてきたデータを Python で扱える形にしています。
def lambda_handler(event, content):
data = ast.literal_eval(event['body'])
name = data['name']
email = data['email']
body_content = f'Name: {name}, Email: {email}'
...
Python の標準ライブラリにあるast
のliteral_eval
メソッドは、文字列の辞書や配列を、オブジェクトとしての辞書や配列に変換してくれます。
ast --- 抽象構文木 — Python 3.10.0b2 ドキュメント
送られてきたデータが Python で変換できない時があります(JavaScript ではtrue
だが、Python ではTrue
のような時)。例えば、チェックボックスのチェックの有無を Lambda 関数に送信したとき、Python では、これを変換することができずにエラーになってしまいます。なので、JavaScript 側であらかじめtrue
を文字列に変換するか、(0, 1)
で表現する必要があります。もしtrue
を Python のTrue
に変換する場合は、標準ライブラリdistutils
にあるutil.strtobool
を使うことができます。これは(0, 1)
を返します。
>>> from distutils.util import strtobool
>>> strtobool("true")
1
9. API リファレンス — Python 3.10.0b2 ドキュメント
おまけ
SendGridを Lambda 関数で使う場合と、SlackでIncoming Webhooksを使う場合を紹介します。
SendGrid
sendgrid
をインストールします。
$ pip3 install sendgrid
SENDGRID_APIKEY
は作成した API のキー、FROM
は SendGrid で送信用に登録したメールアドレスです。to_emails
にはメールを送信するリストを指定します。メールの内容は、plain_text_content
ではなくhtml_content
を指定することも可能です。
import os
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
SENDGRID_APIKEY = os.environ['SENDGRID_APIKEY']
FROM = os.environ['FROM']
mail_data = Mail(
from_email=(FROM, 'me'),
subject='Hello, World!',
to_emails=[
('bob@example.com', 'Bob'),
('alice@example.com', 'Alice')
],
plain_text_content='Hello, World!'
)
sendgrid = SendGridAPIClient(SENDGRID_APIKEY)
sendgrid.send(mail_data)
Incoming Webhooks
requests
をインストールします。
$ pip3 install requests
コード ↓
import os
import requests
WEBHOOK_URL = os.environ['WEBHOOK_URL']
response = requests.post(
WEBHOOK_URL,
data=json.dumps({
'text' : 'Hello, World!',
'username' : 'Bot'
}
)
)
どちらの場合も、Lambda で実行する場合には外部パッケージを含めたフォルダをアップロードする必要があります。
ここからは SendGrid と Incoming Webhooks の両方を Lambda 関数で使う場合を扱います。以下のコマンドを実行します。
$ mkdir <package name>
$ pip3 install sendgrid requests -t ./<package name>
$ touch ./<package name>/lambda_function.py
$ zip -r ./<package name>.zip ./<package name>
lambda_function.py
には先ほどのメール送信のプログラムを書きます。
「アップロード元」から「.zip ファイル」をクリックして、先ほど作成した<package name>.zip
を選択します。ここで左のフォルダツリーはtest/<package name>/lambda_function.py
となるので、test/lambda_function.py
となるように移動します。この時、外部パッケージのフォルダ(sendgrid, requests)も全てtest
直下に移動します。
これで Lambda 関数が正常に動作します。
Discussion