1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Vue.js、AWS Amplifyおよびboto3でサンプルアプリを作ってみる(第三回:axiosでAPIコール編)

Last updated at Posted at 2020-06-03

第二回で、Amplify CLIを使ってバックエンドリソースを作成し、CICDを回してVueアプリを外部公開するところまで辿り着いた。
今回は、さらにAPIとLambda関数を用意し、Vue.jsからaxiosで呼び出すところまでをトライ。これができれば、「Vue.jsでフロントエンドを作ってAWSサービスを叩いてみる」という当初の目的を達成できたことになる。

前回の内容はこちら。
Vue.js、AWS Amplifyおよびboto3でサンプルアプリを作ってみる(第二回:Amplifyとバックエンドリソース編)

(今回)やりたいこと

  • axiosをセットアップする
  • APIとLambda関数を作る
  • Vue.jsからaxios経由でAPIを呼び出す

image.png

あえてやらないこと

AmplifyはライブラリやUIコンポーネントを備えていて、その気になればVue.js内から直接AWSサービスを操作できる様子(例:学習/デプロイ済みの機械学習モデルエンドポイントに推論リクエストを投げる、S3にオブジェクトをアップロードするなど)。
ただ、それをやるとなるとVue.jsのディレクティブやJavascript SDKにもう少し深入りする必要がありそう。あっちもこっちも手を出したくないので、今回は初志貫徹でコードはPython、SDKはboto3に留めておく。

ということで、上図の通りフロントエンドからはAPIを叩くだけにして、ロジックやSDKの使用はバックエンドで行う役割分担にする。

axiosについて

簡単に言うとHTTPクライアントで、Vue.jsのコード内からPostman的にREST APIを呼び出す方法がないかと探すうちに発見。

axios

これなら簡単にAPIの呼び出しと結果の受領ができそうだ。

以下を参考にさせていただきました。感謝。
axios を利用した API の使用
axiosを乗りこなす機能についての知見集
Vue.js+axiosでDynamo DBにAjax通信する
[axios] axios の導入と簡単な使い方

Step by Step

1. Amplifyライブラリのインストール

やらないとは言いつつも、後学のために、セットアップして使えるようにするところまでは試しておく。

% npm install aws-amplify
% npm install aws-amplify-vue 

このあたりを参照しながらインストール。
公式も参照のこと。

2. src/main.jsへの取り込み

src/main.jsを編集して、Amplifyライブラリの取り込みを指定する。

src/main.js
// Amplify
import Amplify, * as AmplifyModules from 'aws-amplify'
import { AmplifyPlugin } from 'aws-amplify-vue'
import awsconfig from './aws-exports'
Amplify.configure(awsconfig)

Vue.use(AmplifyPlugin, AmplifyModules)

aws-exportsが見つからない、というエラーが出る場合は、.gitignoreでGitのトラッキング対象外になってしまっていないかを確認する。
自分の環境では、これをコメントアウトすると動いた。
.gitignoreへの追加はAmplify自身が行っているようなので若干謎だが、とりあえず動いたので、気にせず先に進む。

.gitignore
.DS_Store
node_modules
/dist

# local env files
.env.local
.env.*.local

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

#amplify
amplify/\#current-cloud-backend
amplify/.config/local-*
amplify/mock-data
amplify/backend/amplify-meta.json
amplify/backend/awscloudformation
build/
dist/
node_modules/
# aws-exports.js <-- ここ
awsconfiguration.json
amplifyconfiguration.json
amplify-build-config.json
amplify-gradle-config.json
amplifyxc.config

3. axiosのインストール

Amplifyライブラリと同じ手順。

% npm install axios

4. src/main.jsへの取り込み

ここもAmplifyライブラリと同様だが、axoisの仕様に若干のクセがありハマった。
axiosは厳密にはプラグインでないので、main.jsでVue.use()に書いてあっても、this.axiosUndefinedとなってしまう。
代わりに以下のようにprototype.$axiosとして定義し、読み込み元では$axios.get()として呼ぶ必要がある。

// ダメな例

// Axios
import axios from 'axios'
import VueAxios from 'vue-axios'

Vue.use(AmplifyPlugin, AmplifyModules, VueAxios, axios)
// 動く例

// Axios
import axios from 'axios'
import VueAxios from 'vue-axios'

Vue.use(AmplifyPlugin, AmplifyModules, VueAxios)
Vue.prototype.$axios = axios

main.jsは最終的にこのようになる。

src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'

// Element UI
import './plugins/element.js'

// Axios
import axios from 'axios'
import VueAxios from 'vue-axios'

// Amplify
import Amplify, * as AmplifyModules from 'aws-amplify'
import { AmplifyPlugin } from 'aws-amplify-vue'
import awsconfig from './aws-exports'
Amplify.configure(awsconfig)

Vue.config.productionTip = false
Vue.use(AmplifyPlugin, AmplifyModules, VueAxios)
Vue.prototype.$axios = axios

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

これでようやくaxiosの事前準備が完了。

5. Lambda関数の準備

バックエンド側を作る。
そろそろAmplifyに全部やらせるのも飽きてきたので、練習も兼ねて、amplify add functionではなくスクラッチで用意する。
こんな感じのLambda関数を書く。

lambda_function.py
import boto3
import json
import os
import datetime

print('Loading function')
glue = boto3.client('glue')
gluedb = os.environ['GLUEDBNAME']

# dict内のdatetime型データをJSON対応のISOフォーマット(文字列)に変換する
def convert_datetime2iso(object):
    if isinstance(object, type(datetime)):
        return object.isoformat()

# 本体
def lambda_handler(event, context):
    operation = 'GET'
    if operation == 'GET': 
        tables = glue.get_tables(
            DatabaseName=gluedb
        )["TableList"]
        response = json.dumps(tables, default=convert_datetime2iso)
        return {
            'isBase64Encoded': False,
            'statusCode': 200,
            'headers': {
                # CORSの許可
                'Content-Type': 'application/json', 
                'Access-Control-Allow-Origin': '*' 
            },
            'body': response
        }
    else:
        response = ('Unsupported method:' + format(operation))
        return response

ファイル名がlambda_function.py、関数名がlambda_handler()なので、Lambdaからはlambda_function.lambda_handlerとして呼び出すことになる。

AWS Glueを呼び出し、DB名を渡してテーブル一覧を取得する簡単な関数で、GLUEDBNAMEをLambdaの環境変数として渡す形を取っている。今回はsh10_externalをDB名とした。よーく見る(見なくても)とメソッドがGETしか定義されてないが、お試しなのでご容赦。。

スクリーンショット 2020-06-03 02.07.35.png

実際にGlueに渡す命令はglue.get_tables()のみで、特に複雑な処理はない。

6. APIの準備

次に、axiosから呼び出すREST APIを作成する。このAPIのバックエンドとして、先程のLambda関数を実行する形。
これもamplify add apiではなくスクラッチでAPIを作成してみる。
マネジメントコンソールでAPI Gatewayの画面に移動し、以下の仕様でAPIを作成。

  • Lambdaプロキシー統合
  • /tablesリソース
  • ANYメソッド

スクリーンショット 2020-06-03 13.40.50.png

API作成まではすんなりいったものの、Lambda関数と統合して動かすまでに色々ハマった。

まず、not JSON seriarizableエラー(クライアントから見ると500エラー)が出まくる。Lambda単体では動くようになっても、APIから呼ぶとまた出る。ここで大分時間を使った。
詳細はまた項を改めるが、結論としてはGlueからのdict型のレスポンスをjson.dump()した上で、API GatewayがLambdaプロキシ統合で要求する形式に成形して返すことで、無事API様に受け取って貰えた。

ようやく関門突破かと思いきや、今度はCORS header 'Access-Control-Allow-Origin' missing'といったエラーが出る。何か見覚えある単語が。 これは、axiosというか今回のSPAが当該APIを別のオリジンから呼ぶ形になるので、CORSを許可する必要があるということだ。 CORSは一般にサーバー側で設定し、API GatewayもOPTIONS`メソッドでこれを設定するメニューがあったので設定してみたが、どうやら効いてなさそうだ。

色々調べたところ、今回使用したLambdaプロキシー統合の場合はどうもバックエンドのLambdaの方で明示的にCORSを許可するヘッダを記述してやる必要がある模様。
上記の返値の中ほど、'headers':{}の中にCORS設定を書いてやると、ようやく動いた。やれやれ。

        return {
            'isBase64Encoded': False,
            'statusCode': 200,
            'headers': {
                # CORSの許可
                'Content-Type': 'application/json', 
                'Access-Control-Allow-Origin': '*' 
            },
            'body': response
        }

7. API呼び出しとテーブルへの取り込み設定

最後は再びフロントエンド側に戻り、axiosでAPIを呼び出してデータを取得する箇所と、取得したデータをElement UIのtablesの中に成形して取り込む受け皿部分を作る。
今回のSPAではsrc/components/配下に全てのコンポーネントを配置してVue Routerからルーティングしているが、この一つとしてCatalogueTable.vueというページを用意する。

ここでのポイントは以下の三つ。

  • getDummyData()内のthis.$axios.getでaxiosを呼び出し、REST APIを叩く
  • tablesという配列でデータを受け取る
  • Element UIのel-tableコンポーネントでtablesのデータ(:data="tables")を成形する

axiosの引数となるREST APIのURIには、先程API Gatewayで作成したAPIのエンドポイントを指定する。
成形は<el-table></el-table><el-table-column></el-table-column>の中で、Element UIの書式を使ってわりと自由に行うことができる。
ここでは最低限の属性だけを設定してみた。

  • テーブル
属性 内容
:data データソース
stripe ストライプ表示にする
style="width: 100%" 横幅の長さ
align="center" 中央揃え
属性 内容
prop 列名
label 列の表示名
width 列の長さ
sortable ソート可能な列として指定
src/components/CatalogueTable.vue
<template>
  <div class="cataloguetable">
    <p></p>
    <h2>テーブル一覧</h2>
    <el-button type="primary" @click="getDummyData" :loading="false">実行</el-button>
    <el-table
      :data="tables"
      stripe
      style="width: 100%"
      align="center">
      <el-table-column
        prop="Name"
        label="Table"
        width="250"
        sortable>
      </el-table-column>
      <el-table-column
        prop="DatabaseName"
        label="Database"
        width="200"
        sortable>
      </el-table-column>
      <el-table-column
        prop= "TableType"
        label="Type"
        width="200"
        sortable>
      </el-table-column>
      <el-table-column
        prop="PartitionKeys[0][Name]"
        label="Partition Key 1"
        width="150"
        sortable>
      </el-table-column>
      <el-table-column
        prop="PartitionKeys[1][Name]"
        label="Partition Key 2"
        width="150"
        sortable>
      </el-table-column>
    </el-table>
  </div>
</template>

<script>
export default {
  data() {
    return {
      input: '',
      tables: []
    }
  },
  methods: {
    getDummyData() {
      let uri = 'https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/prod/tables';
      this.$axios.get(uri)
        .then((response) => {
          this.tables = response.data
        })
        .catch((e) => {
          alert(e);
        });
    }
  }
}
</script>

これを呼び出す側のApp.vueはこんな感じで記述する。
(最終的には色々やりたいので、Element UIを使ったコンポーネントのガラだけはたくさん作ってあるが、CatalogueTable.vue以外の中身はまだ空に近い)。

src/App.vue
<template>
  <div id="app">
    <div>
    <img alt="Vue Logo" src="@/assets/gluelogo.png/" width="60" height="60">
      <h1>Data Catalogue Explorer</h1>
      <el-menu :default-active="activeIndex" mode="horizontal" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b" router>
        <el-menu-item index="home" :route="{ name:'Home' }">ホーム</el-menu-item>
        <el-menu-item index="dataset" :route="{ name:'Dataset' }">データセットの一覧を見る</el-menu-item>
        <el-menu-item index="finder" :route="{ name:'Finder' }">データセットを探す</el-menu-item>
        <el-menu-item index="adhoc" :route="{ name:'Adhoc' }">アドホック検索</el-menu-item>
        <el-submenu index="catalogue">
            <template slot="title">システムカタログ</template>
            <el-menu-item index="catalogue-database" :route="{ name:'CatalogueDatabase' }">データベース</el-menu-item>
            <el-menu-item index="catalogue-table" :route="{ name:'CatalogueTable' }">テーブル</el-menu-item>
            <el-menu-item index="catalogue-crawler">クローラー</el-menu-item>
            <el-menu-item index="catalogue-job">ジョブ</el-menu-item>
            <el-menu-item index="catalogue-jdbc">JDBC接続</el-menu-item>
        </el-submenu>
        <el-menu-item index="api" :route="{ name:'API' }">API実行</el-menu-item>
        <el-menu-item index="element" :route="{ name:'Element' }">Element UI</el-menu-item>
        <el-menu-item index="about" :route="{ name:'About' }">Vue.js</el-menu-item>
        <el-menu-item index="resources" :route="{ name:'Resources' }">リソース</el-menu-item>
      </el-menu>
      <router-view />
    </div>
  </div>
</template>

<script>
export default {
  name: 'app',
  data () {
    return {
      activeIndex: this.$route.name
    }
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Vue Routerにもこのコンポーネントへのルートを忘れずに追加してやる必要がある。

router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
......
# ここと
import CatalogueTable from '@/components/CatalogueTable.vue'
......

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
......
  # ここ
  {
    path: '/cataloguetable',
    name: 'CatalogueTable',
    component: CatalogueTable
  },
......

最後にここまでの内容をコミットし、レポジトリにプッシュ(このままAmplifyの方で自動ビルドが回り、ホスティングされている内容が更新される。詳しくは前回の記事を参照)。

% git add .
% git commit -m "axios defined"
% git push -u origin master

9. 動作確認

Amplifyがデプロイを回してサイトの更新を完了するまで、待つこと数分。
ブラウザ、スマホでアクセスしてみると、どうやら無事動作している模様。

スクリーンショット 2020-06-03 16.07.57.png

10. 落ち穂拾い

  • Amplifyマネージドのビルドで、なぜかconsole.logが失敗する。
    • 構文チェックツールのESlint設定が何かおかしいのかも、と疑って、このあたりを参考にあれこれ検証してみるも解決に至らず。
      console.logを出さなければいいだけの話なので、ここではいったん忘れることにした。いずれ解決したい。

ポスト三個分の長丁場になってしまったが、ようやくタイトル通りの自習が完了。
追々、APIを追加したりUIをいじってみたりと、色々遊んでみたい。

1
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?