概要 †1つのAWS Lambda関数でSPAフレームワーク作成 の別バージョン。 [補足]
目次 †
フォルダ構成 †/aws_build.sh ・・・ ビルド用シェル ( CloudFormation実行用のシェル ) サーバ側 †Cloudformationテンプレート †AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: "1LambdaでSPAのサンプル"
Globals:
Api:
Cors:
AllowMethods: "'PUT,GET,POST,DELETE,OPTIONS'"
AllowHeaders: "'Content-Type'"
AllowOrigin: "'*'"
Resources:
ImageSupportSpaApi:
Type: AWS::Serverless::Api
Properties:
Name: ImageSupportSpaApi
StageName: Prod
BinaryMediaTypes:
- "image/*"
ImageSupportSpaFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: ImageSupportSpaFunction
CodeUri: ./src
Handler: index.handler
Runtime: python3.7
Timeout: 60
MemorySize: 128
Role: !GetAtt ImageSupportSpaFunctionRole.Arn
Events:
Root:
Type: Api
Properties:
RestApiId: !Ref ImageSupportSpaApi
Path: /
Method: get
Favicon:
Type: Api
Properties:
RestApiId: !Ref ImageSupportSpaApi
Path: /favicon.ico
Method: get
StaticCss:
Type: Api
Properties:
RestApiId: !Ref ImageSupportSpaApi
Path: /css/{filename}
Method: get
StaticJs:
Type: Api
Properties:
RestApiId: !Ref ImageSupportSpaApi
Path: /js/{filename}
Method: get
StaticImage:
Type: Api
Properties:
RestApiId: !Ref ImageSupportSpaApi
Path: /img/{filename}
Method: get
ListActions:
Type: Api
Properties:
RestApiId: !Ref ImageSupportSpaApi
Path: /api/{resource}
Method: get
GetActions:
Type: Api
Properties:
RestApiId: !Ref ImageSupportSpaApi
Path: /api/{resource}/{id}
Method: get
PostActions:
Type: Api
Properties:
RestApiId: !Ref ImageSupportSpaApi
Path: /api/{resource}
Method: post
PutActions:
Type: Api
Properties:
RestApiId: !Ref ImageSupportSpaApi
Path: /api/{resource}/{id}
Method: put
DeleteActions:
Type: Api
Properties:
RestApiId: !Ref ImageSupportSpaApi
Path: /api/{resource}/{id}
Method: delete
ImageSupportSpaFunctionRole:
Type: "AWS::IAM::Role"
Properties:
RoleName: ImageSupportSpaFunctionRole
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Service: "lambda.amazonaws.com"
Action: "sts:AssumeRole"
Policies:
- PolicyName: "ImageSupportSpaFunctionPolicy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action: # 必要に応じて権限を付与
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- s3:GetObject
Resource: "*"
Outputs:
ImageSupportSpaApiUrl:
Value: !Sub "https://${ImageSupportSpaApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
Export:
Name: ImageSupportSpaApiUrl
src/myenv.py †"""共通処理(PATHのセットなど)."""
import sys
import os
def add_path(path):
"""PATH追加."""
sys.path.append(os.path.abspath(os.path.dirname(__file__)) + path)
def is_local(event):
"""ローカルでの動作確認用かどうかの判断."""
return event["requestContext"]["identity"]["sourceIp"] == "127.0.0.1"
src/index.py †"""メイン処理."""
import base64
import importlib
import json
import os
import re
import myenv
# PATH追加
myenv.add_path("/libs")
def handler(event, context):
"""メインハンドラ."""
print(json.dumps(event, indent=4))
status_code = 200
response_body = ""
headers = {"Content-Type": "text/html"}
if myenv.is_local(event):
headers["Access-Control-Allow-Origin"] = "*"
headers["Access-Control-Allow-Methods"] = "PUT,POST,DELETE,OPTIONS"
is_base64_encoded = False
if event["path"] == "/":
with open("client/index.html") as f:
response_body = f.read()
elif event["path"] == "/favicon.ico":
file_path = "client/favicon.ico"
if os.path.exists(file_path):
headers["Content-Type"] = "image/x-icon"
with open(file_path, "rb") as f:
response_body = base64.b64encode(f.read()).decode("utf-8")
is_base64_encoded = True
elif re.match(r"^/(css|js)/.+", event["path"]):
file_path = "client/" + event["path"][1:]
if re.match(r"^/css/.+", event["path"]):
headers["Content-Type"] = "text/css"
else:
headers["Content-Type"] = "application/javascript"
if os.path.exists(file_path):
with open(file_path) as f:
response_body = f.read()
elif re.match(r"^/img/.+", event["path"]):
file_path = "client/" + event["path"][1:]
headers["Content-Type"] = "image/png"
if os.path.exists(file_path):
with open(file_path, "rb") as f:
response_body = base64.b64encode(f.read()).decode("utf-8")
is_base64_encoded = True
elif re.match(r"^/api/.+", event["path"]):
headers["Content-Type"] = "application/json"
resource = event["pathParameters"]["resource"]
# print("api call " + event["path"])
# print("./api/" + resource + ".py")
if os.path.exists("./api/" + resource + ".py"):
m = importlib.import_module("api." + resource)
result = m.handler(event, context)
if isinstance(result, dict):
if "statusCode" in result:
status_code = result["statusCode"]
if "headers" in result:
headers = {**headers, **result["headers"]}
if "body" in result:
response_body = result["body"]
if "isBase64Encoded" in result:
is_base64_encoded = result["isBase64Encoded"]
else:
status_code = 500
response_body = {"message": "Invalid API Response Format!"}
if response_body == "":
status_code = 501
response_body = {"message": "API Not Found!"}
else:
status_code = 501
response_body = {"message": "Not Implemented!"}
if isinstance(response_body, dict):
response_body = json.dumps(response_body)
return {
"statusCode": status_code,
"headers": headers,
"isBase64Encoded": is_base64_encoded,
"body": response_body
}
src/api/sample1.py †"""サンプル1."""
import json
def handler(event, context):
"""メインハンドラ."""
request_body = {}
if event["body"] and isinstance(event["body"], str):
request_body = json.loads(event["body"])
elif event["body"] and isinstance(event["body"], dict):
request_body = event["body"]
return {
"body": json.dumps(
{
"message": "This is sample1 api message!",
"httpMethod": event["httpMethod"],
"path": event["path"],
"resource": event["resource"],
"pathParameters": event["pathParameters"],
"queryStringParameters": event["queryStringParameters"],
"request_body": request_body,
}
)
}
src/api/sample2.py †"""サンプル2."""
import json
def handler(event, context):
"""メインハンドラ."""
response_body = {
"items": [{"name": "item1", "price": 1000},
{"name": "item2", "price": 2000},
{"name": "item3", "price": 3000},
{"name": "item4", "price": 4000}]
}
return {
"body": json.dumps(response_body)
}
src/client/* †vue.js のビルド済みファイルをこの配下にコピーする。(後述) クライアント周りの作成 †プロジェクトの作成 †vue create client-project 設定ファイル †client-project/.env.development †NODE_ENV="development" VUE_APP_API_ROOT="http://127.0.0.1:3000/api/" ※Vue.js をローカル起動している場合は、SAMローカルで動作中のLambdaにアクセスさせる。 client-project/.env.production †NODE_ENV='production' VUE_APP_API_ROOT='./api/' ※ API Gateway & Lambda のデプロイ後は、ルート直下の api でアクセス。 client-project/vue.config.js †module.exports = {
publicPath: './'
}
※ API Gateweay の Stage 名がURLの頭に付加されるケースを想定し、相対PATHで記述。 client-project/src/main.js †import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
client-project/src/main.js †import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
client-project/src/App.vue †<template>
<div id="app">
<Header />
<component v-bind:is="currentView"></component>
<Footer />
</div>
</template>
<script>
import EventBus from './EventBus.js'
import Header from './components/Header.vue'
import PageTop from './components/PageTop.vue'
import PageSample1 from './components/PageSample1.vue'
import PageSample2 from './components/PageSample2.vue'
import Footer from './components/Footer.vue'
export default {
name: 'app',
components: {
Header,
Footer,
PageTop,
PageSample1,
PageSample2,
},
data: function(){
return {
currentView: "PageTop"
}
},
methods: {
switchView(viewName) {
this.currentView = viewName;
}
},
created: function(){
EventBus.$on('switchView', this.switchView)
}
}
</script>
<style>
html, body {
margin: 0;
}
h1 {
margin: 0;
font-size: 120%;
}
#header {
padding: 2px 0;
}
#main {
padding: 5px 10px;
}
#footer {
position: fixed;
padding: 5px;
bottom: 0;
width: 100%;
background: #eee;
}
</style>
client-project/src/EventBus.js †import Vue from 'vue' const EventBus = new Vue() export default EventBus client-project/src/Header.vue †<template>
<div id="header">
<h1>Header Content</h1>
<ul id="header-menu">
<li v-on:click="menuClicked('PageTop')">TOP</li>
<li v-on:click="menuClicked('PageSample1')">サンプル1</li>
<li v-on:click="menuClicked('PageSample2')">サンプル2</li>
</ul>
</div>
</template>
<script>
import EventBus from '../EventBus.js'
export default {
name: 'Header',
methods: {
"menuClicked" : function(page){
EventBus.$emit('switchView', page)
},
}
}
</script>
<style scoped>
#header h1 {
padding: 0 5px;
}
#header-menu {
list-style-type: none;
margin: 0;
padding: 0;
background: #333;
color: #fff;
}
#header-menu.ul,#header-menu li {
display: inline-block;
}
#header-menu li {
padding: 10px;
border-right: 1px dotted #fff;
cursor: pointer;
}
#header-menu li:hover {
opacity: 0.8;
text-decoration: underline;
}
</style>
client-project/src/Footer.vue †<template>
<div id="footer">
<div>Footer Content</div>
</div>
</template>
<script>
export default {
name: 'Footer',
}
</script>
client-project/src/PageTop.vue †<template>
<div id="main">
<div>
Top Content<br />
<button v-on:click="listAction()">get</button>
<button v-on:click="postAction()">post</button>
<button v-on:click="putAction(1)">put</button>
<button v-on:click="deleteAction(1)">delete</button>
<hr/>
<img src="img/sample.png" width="500"/>
<hr/>
<pre>{{ result }}</pre>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'PageTop',
data: function(){
return {
msg: process.env.VUE_APP_API_ROOT,
result: ""
}
},
created: function(){
},
methods: {
listAction: function(){
this.result = "loading.."
axios
.get(process.env.VUE_APP_API_ROOT + "sample1" + "?param1=123")
.then(response => {
console.log(response)
this.result = response.data
})
},
postAction: function(){
this.result = "loading.."
axios
.post(process.env.VUE_APP_API_ROOT + "sample1", {"var1": "abc"})
.then(response => {
console.log(response)
this.result = response.data
})
},
putAction: function(){
this.result = "loading.."
axios
.put(process.env.VUE_APP_API_ROOT + "sample1/1", {"var2": "def"})
.then(response => {
console.log(response)
this.result = response.data
})
},
deleteAction: function(){
this.result = "loading.."
axios
.delete(process.env.VUE_APP_API_ROOT + "sample1/1")
.then(response => {
console.log(response)
this.result = response.data
})
},
}
}
</script>
client-project/src/PageSample1.vue †<template>
<div id="main">
<div>
サンプル1<br />
<hr/>
<button v-on:click="callApi()">API呼び出し</button>
<div style="margin: 20px;" v-if="status != ''">{{ status }}</div>
<div style="margin:20px;">{{ message }}</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Sample1',
data: function(){
return {
status: "",
message: "",
}
},
methods: {
callApi: function(){
this.status = "loading.."
this.message = ""
let url = process.env.VUE_APP_API_ROOT + "sample1"
let self = this
axios
.get(url)
.then(response => {
console.log(response)
self.status = ""
self.message = response.data["message"]
}).catch(error => {
console.log(error)
self.status = "api call error!"
})
},
}
}
</script>
client-project/src/PageSample2.vue †<template>
<div id="main">
<div>
サンプル2<br />
<hr/>
<button v-on:click="callApi()">API呼び出し</button>
<div style="margin: 20px;" v-if="status != ''">{{ status }}</div>
<table id="sample2_table" v-if="items.length > 0">
<thead>
<tr>
<th>Name</th><th>Price</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items">
<td>{{ item.name }}</td><td>{{ item.price }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Sample2',
data: function(){
return {
status: "",
items: [],
}
},
methods: {
callApi: function(){
this.status = "loading.."
this.items = []
let url = process.env.VUE_APP_API_ROOT + "sample2"
let self = this
axios
.get(url)
.then(response => {
console.log(response)
self.status = ""
self.items = response.data["items"]
}).catch(function (error) {
self.status = "api call error!"
})
},
}
}
</script>
<style>
#sample2_table {
margin-top: 20px;
border-collapse: collapse;
}
#sample2_table th {
background: #ccc;
color: #333;
}
#sample2_table th,
#sample2_table td{
padding: 5px;
border: 1px solid #333;
}
#sample2_table tbody tr:nth-child(even) td{
background: #efe;
}
</style>
動作確認 †SAMローカル起動 †sam local start-api --template template.yml Vue.js をローカル起動 †cd client-project npm run serve http://localhost:8080/ からアクセス確認 デプロイ †Vue.js ビルド # ビルド cd client-project npm run build # ビルド済みファイルを Lambda 用の src 配下にコピー rm -rf ../src/client && cp -R dist ../src/client Lambdaデプロイ ./awsbuild.sh deploy ※ awsbuild.sh の内容は CloudFormation実行用のシェル を参照。 |