コラム

[Lambda]Vue.jsとLambdaを使ってWebアプリケーション作成

構造

  1. Vue.jsはAmplifyにデプロイします。
  2. Vue.jsはrest形式でApi Gatewayに問い合わせをします。
  3. Api Gatewayは指定のLambdaを呼び出します。
  4. LambdaはDBデータ取得、登録を行います。(今回はDynamoDBを使用します)

画面側はS3を使用しても良いですが、Amplifyの方がzipファイルでのデプロイや、Gitからのデプロイに対応していて便利そうなので、そちらを使用しました。

設計

1.画面一覧

  • 商品一覧画面
  • 商品登録画面

2.画面イメージ

3.Lambda

  • 商品一覧取得用:GetGoodsList
  • ID指定で商品取得用:GetGoodsById
  • 登録、更新用:RegistGoods

以上の3つのLambda関数を用意します。

4.DB

商品情報テーブルとID作成用のシーケンステーブルを用意し、項目は以下を用意します。

商品テーブル

  • 商品テーブル
    • ID:ID
    • 商品コード:CODE
    • 商品名:NAME
    • カテゴリ:CATEGORY
    • 価格:PRICE
  • シーケンステーブル
    • テーブル名:TABLE
    • シーケンス:SEQ

5.Api Gateway

Api GatewayはVue.jsへのRestApiを提供するのに使用し、URLとメソッドに応じて、対応するLambda関数を呼び出します。URLとメソッドに応じた呼び出すLambda関数を表にまとめます。

URLメソッド呼び出すLambda
/goodslistGETGetGoodsList
/goodsbyidPOSTGetGoodsById
/registgoodsPOSTRegistGoods

実装・設定

1.DynamoDB設定

※今回はDynamoDBを使用していますが、JOINなど複雑な検索が必要なものはRDBを使用した方が良いと思います。

・テーブル作成画面で必要な情報を入力し、テーブルを作成する。
商品テーブル、シーケンステーブルの2つを作成します。

・テーブルを選択して、項目を追加していきます。

・シーケンステーブルも同様に項目を追加します。
シーケンステーブルは項目追加の際に、TABLE_NAMEにgoodsという文字列を入れておきます。

2.Lambda実装

・実行ロールの作成
LambdaからDynamoDBへのアクセスを許可するポリシーを持ったロールを作成します。
後にLambda作成の際にこのロールを使用します。

  • AmazonDynamoDBFullAccess
  • AWSLambdaDynamoDBExecutionRole

上記のポリシーを持ったロールを作成します。
ロール名は「LambdaAccessDynamoDB」にしました。

・関数作成

関数作成画面で必要な情報を入力し、関数を作成します。

  • 今回は「一から作成」を選択します。
  • 関数名に適当な名称を入力します。
  • ランタイムは今回は「Python3.8」を選択します。PythonでLambdaの実装をしていきます。
  • アーキテクチャは今回は「x86_64」を選択します。
  • アクセス権限は、先ほど作成したロールを設定するので「既存のロールを使用する」を選択します。
  • プルダウンに先ほど作成したロール名があるので、選択します。
  • 関数作成ボタンを押下すると関数が作成されます。

・関数の実装

Pythonであれば、AWSにエディタが用意されているので、そこから実装を行う事が出来るので、この画面で実装、
デプロイをして行きます。
デプロイ後、テストも出来るので便利です。

・商品一覧取得関数(GetGoodsList)の実装

import json
import boto3

# boto3を使用してDynamoDBにアクセス
dynamodb = boto3.resource('dynamodb')

def lambda_handler(event, context):
    # テーブル名を指定し、テーブル操作オブジェクト取得
    goodsTable = dynamodb.Table('Goods')
    
    # テーブルの全情報を取得する
    response = goodsTable.scan()

    
    if response['ResponseMetadata']['HTTPStatusCode'] != 200:
        print(response)	# エラーレスポンスを表示
    else:
        print('Successed.')
    return response

・ID指定で商品取得関数(GetGoodsById)の実装

import json
import boto3

# boto3を使用してDynamoDBにアクセス
dynamodb = boto3.resource('dynamodb')

def lambda_handler(event, context):
    
    # テーブル名を指定し、テーブル操作オブジェクト取得
    goodsTable = dynamodb.Table('Goods')

    # Lamba呼び出し元から渡されたIDを取得
    id = event['ID']

    # IDを指定して、データを取得
    response = goodsTable.get_item(
        Key={'ID': id }
    )
    
    if response['ResponseMetadata']['HTTPStatusCode'] != 200:
        print(response)	# エラーレスポンスを表示
    else:
        print('Successed.')
    return response

渡されるデータの形式の例は以下です。4の部分は適当な数値に代わります。
数値はダブルクォーテーションなどで囲まないようにして下さい。

{
  "ID": 4
}

・登録、更新関数の実装

import json
import boto3
import decimal


# boto3を使用してDynamoDBにアクセス
dynamodb = boto3.resource('dynamodb')

# シーケンステーブルからIDを取得しカウントアップし更新
def get_next_seq(table, tablename):
    response = table.update_item(
        Key = {
            'TABLE_NAME' : tablename
        },
        UpdateExpression='set SEQ = SEQ + :val',
        ExpressionAttributeValues = {
            ':val' : 1
		},
		ReturnValues='UPDATED_NEW'
    )
    return response['Attributes']['SEQ']


def lambda_handler(event, context):
    # テーブル名を指定し、テーブル操作オブジェクト取得
    goodsTable = dynamodb.Table('Goods')

    # IDを取得する
    id = event['ID']
    
    # IDが0の場合は、IDをシーケンスから取得する
    if id == 0:
        seqtable = dynamodb.Table('Sequence')
        id = get_next_seq(seqtable, 'goods')

    # テーブル更新メソッドを呼ぶ。
    # keyで設定されている値がなかった場合は新規登録、存在する場合は更新になる
    response = goodsTable.update_item(
        # キー項目の値設定
        Key={
            'ID': id
        },
        
        # 更新式を記述、項目名は別名を使用する
        UpdateExpression='SET #CODE = :code, #NAME = :name, #CATEGORY = :category, #PRICE = :price',
        
        # ここで項目の別名を指定しないとエラーになる
        ExpressionAttributeNames={
            "#CODE": "CODE",
            "#NAME": "NAME",
            '#CATEGORY': 'CATEGORY',
            '#PRICE': 'PRICE',
        },

        # 更新式で指定した変数の値を設定
        ExpressionAttributeValues={
            ':code': event['CODE'],
            ':name': event['NAME'],
            ':category': event['CATEGORY'],
            ':price': event['PRICE']
        }
    )
    
    if response['ResponseMetadata']['HTTPStatusCode'] != 200:
        print(response)	//エラーレスポンスを表示
    else:
        print('Successed.')
    return response

渡されるデータの形式の例は以下です。
上が新規登録例、下が更新例です。IDが0かどうかで判断しています。

{
  "ID": 0,
  "CODE": "GC9989",
  "NAME": "商品G",
  "CATEGORY": "002",
  "PRICE": 1000
}
{
  "ID": 5,
  "CODE": "GC9989",
  "NAME": "商品G",
  "CATEGORY": "002",
  "PRICE": 1000
}

3.ApiGateway設定

Vue.jsからアクセスするのはRestAPIでのアクセスが必要なので、RestAPIを提供しLambdaを呼び出してくれる、ApiGatewayという機能を使用します。

ApiGatewayサービスのページ行って、APIを作成ボタンを押下するとのタイプを選択できます。
「REST API」で構築します。

・API作成

  • プロトコルは「REST」を選択します。
  • 新しいAPIの作成は「新しいAPI」を選択します。
  • API名は今回は「GoodsApi」を入力します。
  • 説明は適当な値を入力して下さい。
  • エンドポイントタイプは今回は「リージョン」を選択します。リージョン内に配置します。

APIの作成を押すとAPIが作成されます。

・ステージの作成

まずステージを作成します。
ステージは開発版、本番用またはヴァージョン事に分けたい場合などに有効みたいです。
ACLの設定やログの設定などが可能です。
今回は、1つしか使用しないので、「prod」という名前で作成します。
指定した名称はアクセスする際のURLに含まれます。

・リソースの作成

次はリソースを作成します。
リソースを作成すると、その中にメソッドを作成しLambda関数と関連付けます。
リソースは、「5.Api Gateway」で定義したURL分の3つを作成します。

リソース名に「goodslist」、「goodsbyid」、「registgoods」をそれぞれ指定します。

・メソッドの作成

リソースを作成したら次はメソッドを作成していきます。
メソッドも「5.Api Gateway」で定義したメソッドを作成します。

リソースを選択してメソッド作成を押下します。

メソッドを選択します。

横のチェックボタンを押下するとメソッドが作成されます。
作成後、メソッドとLambda関数を関連付けます。

上記のようにLambda関数を指定して保存すれば完了です。

・CORS有効化

次に、他のオリジン(ドメインにプロトコル、ポート番号が追加されたもの)からアクセス可能にする為の設定を
行います。
これは各リソース毎に行う必要があります。

CORSの有効化を押下します。

右下のボタンを押下すると確認画面が表示されるので、それをはいを押下すると設定完了です。

Api GatewayへのアクセスURL

ステージ画面から見る事が出来ます。

このURLにリソース名を追加しアクセスします。

4.Vue.js実装

次はVue.jsの実装を見ていきます。
Vue.jsプロジェクトを作成して実装を行っています。
簡単な画面なので特に特別な事はしていませんので、実装だけ張っていきます。

・GoodsList.vue(商品一覧画面)

<template>
  <div style="text-align:left;">
		<div class="table" style="width:600px;">
			<table>
				<thead>
					<tr>
						<th>コード</th>
						<th>名前</th>
						<th>カテゴリ</th>
						<th>価格</th>
						<th></th>
					</tr>
				</thead>
				<tbody>
					<tr  v-for="(data) in goodsList" :key="data.ID">
						<td>{{ data.CODE }}</td>
						<td class="tdname">{{ data.NAME }}</td>
						<td>{{ categoryDatas.find( c => c.id === data.CATEGORY ).name }} </td>
						<td>{{ data.PRICE }}</td>
						<td><router-link :to="{ name: 'editGoods', params:{id:data.ID}}">編集</router-link></td>
					</tr>
				</tbody>
			</table>
		</div>
		<input type="button" @click="addGoods" value="新規登録">
  </div>
</template>

<script>
import goodsRepository from '../composables/GoodsRepository'
import { useRouter  } from 'vue-router'
import { ref } from 'vue'

export default ({
  name: 'GoodsList',
  setup(){
    //GoodsRepositoryモジュールからの変数、メソッドを宣言
    //カテゴリ用データ、商品リスト、商品取得メソッド
    const { categoryDatas, getGoods } = goodsRepository()

    const goodsList = ref([])
    
    //ルータ使用変数宣言
    const router = useRouter()

    //新規作成ボタン押下時の処理
    const addGoods = () => {
      router.push("/registGoods")
    }

    //データ取得時のコールバック( GoodsRepositoryのgetCodesメソッドに渡す)
    const callback = ( result, data )=>{
      if( result != "success"){
        alert( result );
      }else{
        goodsList.value = data
      }
    }

    //商品情報取得
    getGoods( callback )
    
    //template上で使用する変数を宣言
    return{
      goodsList,
      addGoods,
      getGoods,
      categoryDatas
    }
  }
})

</script>


<style>
	.table{
		height: 380px;
		width:580px;
		overflow: auto; 
		border:solid 1px;
	}

	table thead th {
		border: 1px solid #CCC;
		text-align: center;
		background-color: #EFEFEF;
		border-top : 1px solid #CCC;
		border-left : 1px solid #CCC;
		border-right : 1px solid #CCC;
		border-bottom : 0px solid #CCC;
		cursor:pointer;
	}

	table tbody td{
		min-width:100px;
		max-width:auto;
		height:25px;
	}

	
	.tdname{
		min-width:200px;
	}

</style>

・EditGoods.vue(商品登録・更新画面)

<template>
	<div>
		<div class="inputItemDiv"><div class="inputTitleDiv">コード</div><input class="input" type="text" v-model="goods.CODE"></div>

		<div class="inputItemDiv"><div class="inputTitleDiv">商品名</div><input class="input" type="text" v-model="goods.NAME"></div>
		
		<div class="inputItemDiv"><div class="inputTitleDiv">カテゴリ</div>
			<select class="select" name="category" v-model="goods.CATEGORY">
				<option v-for="categoryData in categoryDatas" :value="categoryData.id" :key="categoryData.id">
					{{ categoryData.name }}
				</option>
			</select>
		</div>
		<div class="inputItemDiv"><div class="inputTitleDiv">価格</div><input class="input" type="text" v-model="goods.PRICE"></div>		
		
		<div class="inputItemDiv" style="text-align:right">
			<button  type="button" class="button" @click='cancel'>キャンセル</button>
			<button :disabled="resAllVal"  type="button" class="button" @click='regist'>登録</button>
		</div>
		

	</div>

</template>

<script>
import goodsRepository from '../composables/GoodsRepository'
import { useRoute, useRouter  } from 'vue-router'
import { ref } from 'vue'

export default ({
  name: 'EditGoods',
  setup(  ){
    
    const goods = ref({ID:0, CODE:'',NAME:'',CATEGORY:'001',PRICE:0})
    
    //GoodsRepositoryモジュールからの変数、メソッドを宣言
    const { categoryDatas, getGoodsById, registGoods } = goodsRepository()

    //ルータ使用変数宣言
    const route = useRoute()
    const router = useRouter()
    
    //データ取得時のコールバック( GoodsRepositoryのgetGoodsByIdメソッドに渡す)
    const getCallback = ( result, data ) =>{
      if( result != "success"){
        alert( result )
      }else{
        goods.value = data
      }
    }
    
    //パラメータでIDが渡された際は、商品情報を取得する
    console.log(route.params.id)
    if( route.params.id != null ){
      console.log("getGoodsById")
      getGoodsById( route.params.id, getCallback  )
    }
    
    //登録ボタン押下時の処理
    const regist = () => {
      registGoods( goods.value, regCallback )
    }
    
    //登録ボタン押下時のコールバック( GoodsRepositoryのregistGoodsメソッドに渡す)
    const regCallback = ( result ) =>{
      if( result == "success"){
        alert("登録完了");
        router.push("/")
      }else{
        alert( result );
      }
    }
    
    //キャンセル処理 商品一覧画面へ戻る
    const cancel = () => {
      router.push("/")
    }
    
    //template上で使用する変数を宣言
    return{
      categoryDatas,
      goods,
      regist,
      cancel   
    }
  }
})

</script>

<style>
	
	.inputItemDiv{
		width:360px;
		overflow: hidden;
		margin:2px;
	}
	
	.inputTitleDiv{
		float: left;
		width:100px;
		height:25px;

	}

	.inputDiv{
		float: left;
		width:250px;
		height:25px;
	}
	
	.input{
		width:250px;
		height:20px;
	}
	
	.inputread{
		width:250px;
		height:20px;
		background-color: #ccc;
	}
	
	.select{
		width:257px;
		height:25px;
	}
	
	.button{
		width:100px;
	}
	
	.validateError{
		color:red;
		width:250px;
		font-size:12px;
	}

</style>

・GoodsRepository(商品情報のサーバへのアクセス)

import { ref } from 'vue'
import  axios from 'axios'

//商品情報の取得、登録、編集などの機能を提供する
export default function (){

  const apiGatewayUrl = 'https://kgxc1b2bea.execute-api.ap-northeast-1.amazonaws.com/prod'

  //カテゴリのマスタを宣言、refとして宣言
  const categoryDatas = ref([])
  categoryDatas.value.push({id:'001', name:'本'})
  categoryDatas.value.push({id:'002', name:'ゲーム'})


  //商品をIDで指定して取得するメソッド
  const getGoodsById = async( id, callback ) =>{

    try{
      var formData = {"ID": Number( id ) }
      //formData.append("ID", Number( id ) )
      var response = await requestServerPost( "/goodsbyid", formData )
      console.log(response.Item)
      callback("success", response.Item)
    }catch(e){
      callback(e)
    }
  }

  //商品登録、更新メソッド
  //sereraccess.jsのメソッドをコール
  const registGoods = async( regGoods, callback  ) => {
    
    try{
      var response = await requestServerPost( "/registgoods", regGoods )
      console.log( response )
      callback("success")
    }catch(e){
      callback(e)
    }
  }
  
  //商品情報取得
  const getGoods = async( callback ) => {
    try{
      //Spring Bootにaxiosで商品取得依頼
      var response = await requestServerGet( "/goodslist" )
      console.log( response.Items )
      //コールバックに結果を返す
      callback( "success", response.Items )
    }catch(e){
      console.log(e)
      callback(e)
    }
  }

  //axiosのGetメソッドでApiGatewayにアクセス
  const requestServerGet = ( url ) =>{
    return new Promise((resolve, reject) => {
      axios
        .get( apiGatewayUrl + url)
        .then(response => {
           resolve(response.data)
         }).catch(error => {
            reject(error)
         })
    }).catch((e) => {
      throw e
    })
  }
  
  //axiosのGetメソッドでApiGatewayにアクセス
  const requestServerPost = ( url, formDate ) =>{
    return new Promise((resolve, reject) => {
      axios
        .post( apiGatewayUrl + url, formDate)
        .then(response => {
           resolve(response.data)
         }).catch(error => {
            reject(error)
         })
    }).catch((e) => {
      throw e
    })
  }  
  
  //当該ライブラリが提供する変数、メソッドを宣言
  return{
    getGoods,
    getGoodsById,
    registGoods,
    categoryDatas
  }
}

・router/index.js(ルーター情報)

import { createRouter, createWebHistory } from 'vue-router'
import GoodsList from '../views/GoodsList.vue'
import EditGoods from '../views/EditGoods.vue'

const routes = [
  {
    path: '/',
    name: 'goodsList',
    component: GoodsList
  },
  {
    path: '/editGoods/:id',
    name: 'editGoods',
    component: EditGoods,
    props: true
  },
  {
    path: '/registGoods',
    name: 'registGoods',
    component: EditGoods,
  },
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

5.Amplifyに配置

では最後にAmplifyにVue.jsをデプロイしていきます。
今回は、Vue.jsのファイルをアップロードしてデプロイする方法にします。

・Vue.jsビルド、zip形式のまとめる

まず、Vue.jsプロジェクトをビルドします。

npm run build

プロジェクトルート/distディレクトリにビルド後のファイル群が配置されるので、そのファイル群はzipで纏めます。
名称はindex.zipにします。

・Amplifyアプリケーション作成、zipファイルをデプロイ

ウェブアプリケーションをホストの「使用を開始する」ボタンを押下します。

「Gitプロバイダーなしでデプロイ」を選択します。
GitHubからの選択などにするとGitHubにコミット時、自動的にデプロイしてくれるらしいので、他の記事で此方も試していたいと思います。

適当なアプリケーション名、環境名を入力し、先ほど作成したzipファイルをドラッグアンドドロップします。

デプロイが完了すると上記の画面が表示されます。DomainのURLにアクセスすると作成したWebページを見る事が出来ます。

以上で終了になります。今回はAWS上でVue.jsとLamdbaを使用したWebアプリケーション作成方法を例を1つ試してみました。
他にもロードバランサーを使用したり、ドメインを変えてと色々出来る事があるので、それは後に記事にしようと思います。

この記事をシェアする
  • Facebookアイコン
  • Twitterアイコン
  • LINEアイコン

お問い合わせ ITに関するお悩み不安が少しでもありましたら、
ぜひお気軽にお問い合わせください

お客様のお悩みや不安、課題などを丁寧に、そして誠実にお伺いいたします。

お問い合わせはこちら
お電話でのお問い合わせ 03-5820-1777(平日10:00〜18:00)
よくあるご質問