yasutomogのブログ

Software Engineerの雑記

複数のAmplifyプロジェクトを使った開発方法

トライビートという会社の札幌オフィスで開発全般やってます。
気づいたらもう12月ですね。
AWS Amplify Advent Calendar 2021のカレンダー2は、まだ空きが多いので、今年開発していく中で得た知見をメモとして残しておきます。

qiita.com

概要

  • Amplifyの複数envについては色々な記事で情報が公開されている
  • 複数のAmplifyプロジェクトを接続する記事をあまり見かけなかったので試行錯誤した
  • 実際のプロジェクトでやりたかったこと
    • 管理者サイトと一般ユーザー向けサイトを別々のAmplifyでプロジェクトで管理したい
    • AmplifyのBackend(Auth、API、Function、Storage)は共有したい
  • サブドメインとかで別々にhostingしたいんだけど、Backendはそもそも同じデータ層を扱うので、APIやFunction等で共有できるものは一元管理したい。

f:id:yasug:20211201001010p:plain

どう解決したか

  • 一般ユーザー向けサイトを普通にAmplifyプロジェクト(FrontendとBackend)で構築。
  • 管理者サイトは、Backendを用意せず、一般ユーザー向けサイトのBackendを参照させる流れで構築。

f:id:yasug:20211201002152p:plain

  • 具体的な方法は下記の公式ドキュメントが参考になりました。

docs.aws.amazon.com

  • ざっくりとした流れ
    1. まず最初に、一般ユーザー向けサイト(Backendを含むAmplifyプロジェクト)を通常のAmplifyプロジェクト構築の流れで作成
    2. 次に管理者向けサイトはフロントエンド部分のみ実装したプロジェクトを構築し、amplify init 等はせずに、 amplify pull をしてbackendに一般ユーザー向けサイトを選択する。
    3. Backendを紐付けるときに、「Do you plan on modifying this backend?」と聞かれるのは、「No」を選択し、管理者向けプロジェクトからはBackendの変更をできないようにする。
    4. あとは、ドキュメントに従い、Amplify Console上でhostingの設定をしていく

tips

  • 管理者向けサイトを amplify delete すると、一般ユーザー向けサイトのBackendも削除される。管理者向けサイト側のAmplifyプロジェクト(hostingのみ)削除されることを期待していましたが、そうではなく、接続しているBackendも削除されました。
  • Gitは別リポジトリで管理するようにした。モノレポでディレクトリで分ける構成も検討しましたが、管理者サイトのみ更新など、片一方のみビルド〜デプロイみたいなことができなそうだったので、リポジトリ毎分けるようにしました。
  • 一般ユーザー向けサイトで追加したAPIやFunctiionを、管理者サイトのフロントエンドから利用するとき、一手間必要でした。具体的には下記。
    • aws-export.js を一般ユーザーサイトから管理者サイトへコピーしてくる
    • schema.json を一般ユーザーサイトから管理者サイトへコピーしてくる
      • 以下のコマンドで最新のschema.json が取得できそうであるが、エラーとなったので手作業で頑張ってた
      • $ aws appsync get-introspection-schema --api-id <対象の AppSync の API ID> --format JSON schema.json
    • amplify codegen を叩き、API.tsやmutations.tsやqueries.ts などを生成する

感想と課題

  • APIやFunctionを共有できるのはいいが、管理者サイトでのみ必要なAPIやFunctionを追加するときに、一般ユーザーサイト側のコードを修正するのが結構しんどい。
  • 管理者サイトとプロジェクトを分けるというのは、ユースケースとしてはよくありそうなので、みんながどういう風にやっているか気になる
  • 今回の構成と直接関係があるか調査中だが、Functionを追加したときに、Lambdaに環境変数を付与すると、管理者向けサイトのAmplifyビルドがコケる。管理者サイトは参照のみのため、しっくりこないが、なぜかそうなるので調べているところ。

microCMSでの画面操作とWebhookのパラメータの一覧

概要

  • microCMSからのWebhookを利用して独自でデータ管理するために作成
  • コンテンツの状態とユーザー操作によって、typeとstatusがどう変わるかをまとめる
  • 参考サイトではstatusが配列で定義されているため、実際にどういう値が来るか確認

参考サイト

document.microcms.io

前提

  • コンテンツIDを手で変更することは考慮しない(ユーザーにさせない運用を想定)
  • コンテンツの並び替えは、データ自体の変更には影響ないため考慮しない
  • APIによる操作も考慮しない(利用しない想定)
  • SSGは今回とは別にWebhookを設定(特にtypeやstatusの判断は不要)

一覧

操作 type contents.old.status contents.new.status
コンテンツの公開(新規) new contents.old === null ["PUBLISH"]
コンテンツの公開(下書きデータなしの状態で更新) edit ["PUBLISH"] ["PUBLISH"]
コンテンツの公開(公開データなし && 下書きデータあり で更新) edit ["DRAFT"] ["PUBLISH"]
コンテンツの公開(公開データあり && 下書きデータあり で更新) edit ["PUBLISH","DRAFT"] ["PUBLISH"]
コンテンツの非公開時(下書きに戻す) edit ["PUBLISH"] ["DRAFT"]
コンテンツの下書き保存時(新規) new contents.old === null ["DRAFT"]
コンテンツの下書き保存時(公開データなしの状態で下書き更新) edit ["DRAFT"] ["DRAFT"]
コンテンツの下書き保存時(公開データありの状態で下書き新規) edit ["PUBLISH"] ["PUBLISH","DRAFT"]
コンテンツの下書き保存時(公開データありの状態で下書き更新) edit ["PUBLISH","DRAFT"] ["PUBLISH","DRAFT"]
公開中コンテンツの削除時(下書きなしの状態) delete ["PUBLISH"] contents.new === null
公開中コンテンツの削除時(下書きありの状態) delete ["PUBLISH","DRAFT"] contents.new === null
下書きコンテンツの削除時 delete ["DRAFT"] contents.new === null
下書きコンテンツの破棄 edit ["PUBLISH","DRAFT"] ["PUBLISH"]

結論

  • typeとstatusを使うことで、処理制御は大体できそう!

Amplifyで発生したビルドエラーの原因と解決案(Jamstack&Nuxt.js)

概要

  • Nuxt.jsでSSGしたものをAmplify上でhostingするJamstack構成
  • GitHubにPushされるとAmplify上でSSGされる流れ
  • Amplifyプロジェクトを新規に構築していく中で、3件連続ビルドエラーを経験したのでまとめる

前提

  • node.js:v14.17.0
  • amplify-cli:4.51.1
  • nuxt:2.15.6

結論

  • aws-exports.jsはフロントエンド側でimportが必要になるので、amplify.ymlでfrontendのビルド前にbackendのビルドでamplify pushを実行し、aws-exports.jsを生成する。(aws-exports.jsのgitignoreはデフォルトのままgit管理しない。他のブログとかを見ているとgitignoreから外してしまっている人もいるが複数人での開発を考慮すると含めないで解決した方が良い認識)
  • amplify-cliのバージョンがAmplifyビルドで動作しているものと、ローカルで使用しているものが一致しているか確認する。
  • Amplifyビルド用のロール作成が必要

エラーNo.1

背景

  • AmplifyとGitHub連携して、Nuxt.jsでSSGしたものをhostingするところまでは問題なかった
  • その後、amplify add authして、プロジェクトにaws-amplify@aws-amplify/ui-vueモジュールをインストールし、aws-amplifyを利用するための、pluginクラスを新規作成。pluginのクラスは下記。
import Vue from 'vue'
import Amplify from 'aws-amplify'
import '@aws-amplify/ui-vue'
import awsExports from '../aws-exports'
    
export default ({ isHMR }) => {
  if (isHMR) return
  if (process.client) {
    Amplify.configure(awsExports)
    Vue.use(Amplify)
  }
}
    
  • Amplify上のSSGするタイミングでエラーが発生

エラー内容

[fatal] Nuxt build error
ERROR in ./plugins/amplify.client.js
Module not found: Error: Can't resolve '../aws-exports' in 'plugins'
  • ../aws-exports.jsが見つからないとのこと

原因

  • amplifyコマンドでプロジェクト構築すると、aws-exports.jsはgitignoreに含まれている
  • Amplify上のビルドフローは、ルートディレクトリにamplify.ymlを配置しておくとで設定可能
  • 最初はhostingだけ試していたので、amplify.ymlを下記のようにしていた
version: 1
frontend:
  phases:
    preBuild:
      commands:
        - npm ci
    build:
      commands:
        - npm run generate
  artifacts:
    # IMPORTANT - Please verify your build output directory
    baseDirectory: dist
    files:
      - '**/*'
  cache:
    paths:
      - node_modules/**/*
    

解決策

  • amplify.ymlのfrontendのビルド処理の前に、backendでamplifyPush -simpleを差し込む
  • frontendの前にamplify pushが走り、これによりaws-exports.jsがAmplify上で作成され、その後のfrontendのソースからも参照が可能となる
version: 1
backend:
  phases:
    build:
      commands:
        - '# Execute Amplify CLI with the helper script'
        - amplifyPush --simple
frontend:
  phases:
    preBuild:
      commands:
        - npm ci
    build:
      commands:
        - npm run generate
  artifacts:
    # IMPORTANT - Please verify your build output directory
    baseDirectory: dist
    files:
      - '**/*'
  cache:
    paths:
      - node_modules/**/*
    

エラーNo.2

背景

  • エラーNo.1の解決策をGitHubにPushして再びAmplify上でビルドしたら別のエラーが発生

エラー内容

# Executing command: amplifyPush --simple
# Getting Amplify CLI Cloud-Formation stack info from environment cache
# Start initializing Amplify environment: main
# Initializing new Amplify environment: main (amplify init)
File project: data should NOT have additional properties: 'graphqltransformer'
JSONValidationError: File project: data should NOT have additional properties: 'graphqltransformer'

原因

解決策

  • ローカルでは、その時点の最新版4.51.1を使っていたので、Amplify上も以下のように合わせた。
  • 初期段階はPackageには何も指定されていなかった。
  • latestを指定しているが、ローカルに明示的に合わせたほうが良いかもしれない。暫く経過観察予定。

f:id:yasug:20210521002243p:plain

f:id:yasug:20210521002333p:plain

エラーNo.3

背景

  • エラーNo.2の解決策を保存して、再びAmplify上でビルドしたら別のエラーが発生

エラー内容

  • Amplifyコンソール上で以下のエラーが発生
Your app does not have a role and you're attempting to interact with AWS resources
In order for you to interact with AWS resources you need to attach a role to your app. This can be done in the General Settings page in the console

f:id:yasug:20210521002837p:plain

原因

  • Amplifyビルドするのに必要なロールがあたっていないという感じ

解決案

eslintとprettier

概要

  • eslintとprettierをGitコミット時に実行する

環境

  • node:14.14.0
  • eslint:7.23.0
  • eslint-config-prettier:8.1.0
  • husky:6.0.0
  • lint-staged:10.5.4
  • prettier:2.2.1

設定ファイル

prettierrc.json

{
  "singleQuote": true
}

eslintrc.json

{
  "env": {
      "es6": true
  },
  "globals": {
      "alert": false,
      "document": false,
      "$": false,
      "console": false
  },
  "extends": ["eslint:recommended", "prettier"]
}

.husky/pre-commit

#!/bin/sh
  #!/bin/sh
  . "$(dirname "$0")/_/husky.sh"

  npm run lint-staged

package.json

"scripts": {
  "lint-staged": "lint-staged"
},
"lint-staged": {
  "*.{js,ts,jsx,tsx}": [
      "npx eslint . --fix",
      "npx prettier --write ."
  ]
},

Nuxt.jsでPAY.JPの初期化エラー(既にインスタンス化されています)対策

概要

  • Nuxt.jsベースでJamstackなECサイトのサンプル実装しているときに、PAY.JPの初期化でエラーが出ることがあったのでメモ
  • PAY.JPを実装するにあたり、payjp(2.0.5)のnode moduleを利用
  • 画面側ではpayjpCardElementを生成し、トークンを作成して決済用のAPI呼び出す流れで実装

エラーとなる実装

<template>
  <div id="payjp-form"></div>
</template>
<script>
  mounted() {
    const payjp = window.Payjp(process.env.PAY_JP_PK)
    const elements = this.payjp.elements()
    const cardElement = elements.create('card')
    cardElement.mount('#payjp-form')
    cardElement.on('change', async (event) => {
      if (event.complete) {
        const res = await this.$payjp.createToken(cardElement)
        this.payjpToken = res.id
        this.isValidCard = true
      }
    })
  },
</script>
  • 画面初期表示時は問題なくカードレイアウトが表示されていたが、画面遷移後、再び画面を開くと「Error: 既にインスタンス化されています」というエラーが発生。
    複数回の初期化処理はNGとのこと。

対応策(Pluginを作成し初期化を1度に制御する)

  • plugins/payjp.jsを新規作成
import Vue from 'vue';

export default ({store, isHMR}) => {

  if (isHMR) return

  if (process.client) {

    Vue.prototype.$payjp = window.Payjp(process.env.PAY_JP_PK)
  }
}
  • nuxt.config.jsにPlugin定義追加
plugins: [
  '~/plugins/payjp.js',
],
  • エラーとなっていた画面の修正
<template>
  <div id="payjp-form"></div>
</template>
<script>
  mounted() {
    const elements = this.$payjp.elements()
    const cardElement = elements.create('card')
    cardElement.mount('#payjp-form')
    cardElement.on('change', async (event) => {
      if (event.complete) {
        const res = await this.$payjp.createToken(cardElement)
        this.payjpToken = res.id
        this.isValidCard = true
      }
    })
  },
</script>

Firestoreを使用するFirebase Cloud Functionsをファイル分割した時のエラー対応

概要

  • Firebase Cloud functionsで、Firestoreを利用している
  • 当初は、index.jsに関数定義していた
  • 複数関数定義する必要があったので、関数ごとにファイル分割した
  • 1回目の関数実行時は特に問題ないが、2回目以降の呼び出し時に以下のエラーが発生 (初期化は1回にしなさいとのこと)
The default Firebase app already exists. This means you called initializeApp() more than once without providing an app name as the second argument. In most 
cases you only need to call initializeApp() once. But if you do want to initialize multiple apps, pass a second argument to initializeApp() to give each app a unique name.

エラー発生時のコード

  • index.js
const foo = require('./foo')
const bar = require('./bar')
exports.foo = foo.hoge
exports.bar = bar.fuga
  • foo.js
const functions = require('firebase-functions');
const admin = require('firebase-admin')

exports.hoge = functions.region('asia-northeast1').https.onCall(async (data, context) => {

  admin.initializeApp()
  const fireStore = admin.firestore()

  // 以下、firestoreを使った処理

})
  • bar.js
const functions = require('firebase-functions');
const admin = require('firebase-admin')

exports.fuga = functions.region('asia-northeast1').https.onCall(async (data, context) => {

  admin.initializeApp()
  const fireStore = admin.firestore()

  // 以下、firestoreを使った処理

})

エラー解決用に修正したコード

  • index.js(変更なし)
const foo = require('./foo')
const bar = require('./bar')
exports.foo = foo.hoge
exports.bar = bar.fuga
  • firestore.js(新規追加)
const admin = require('firebase-admin');

admin.initializeApp();
const fireStore = admin.firestore();

module.exports = {
  fireStore,
};
  • foo.js(変更)
const functions = require('firebase-functions');
const { fireStore } = require('./firestore')

exports.hoge = functions.region('asia-northeast1').https.onCall(async (data, context) => {

  // 以下、firestoreを使った処理

})
  • bar.js(変更)
const functions = require('firebase-functions');
const { fireStore } = require('./firestore')

exports.fuga = functions.region('asia-northeast1').https.onCall(async (data, context) => {

  // 以下、firestoreを使った処理

})

JamstackなECサイトの考察

概要

  • Jamstack Advent Calendar 2020 - Qiita の22日目の記事です。
  • 株式会社トライビート| TRIBEAT CO., LTD. という会社でBtoCや業務システムのWebアプリケーション構築や運用・保守をしています。
  • これまでJamstackという言葉が出てくる前から、社内独自のSSGの仕組みで試行錯誤して対応することがありました。
  • Jamstack関係の環境やサービスが充実してくる中で、ECサイト構築が現実的に良い選択になりそうかを実際に試してみたので、考察やハマったポイントを備忘録としてまとめる。(まだ検証中)

アーキテクチャの構成

  • microCMS
  • Firebase hosting
  • Firestore
  • Firebase functions
  • Firebase Authentication
  • Nuxt.js
  • Algolia
  • Stripe
  • GitHub Actions

※Next.jsとVercelへの移行も試したいが、データ層の設計がまとまったタイミングで検討する。

画面構成

ECサイト

  • 商品一覧
    • サイトのtopページ
    • algoliaから商品一覧を取得して表示
    • 検索を可能にするため、商品一覧情報を組み込んだ静的ページ化はせずSPAとする
    • 商品をクリックすることで、商品詳細画面へ遷移
  • 商品詳細画面
    • SSGで静的ファイル化したページ
    • 数量変更とカート追加が可能
    • カート追加ボタンをクリックすることで、カート画面へ遷移
  • カート画面
    • カートに追加された商品一覧を表示
    • 商品の数量を変更することが可能
    • 買い物を続けるボタンをクリックすることで商品一覧画面へ遷移
    • 購入者情報とカード情報を入力すると購入ボタンが活性化
    • 購入ボタンクリックすると、在庫チェック後、決済用のCloud Functionsをリクエス
    • カード情報入力部分はStripeのvueコンポーネントを使用
  • アカウント画面(未実装)
    • いつでも作成できるので、後回しにして進めていた画面
    • 取り急ぎは、Firebase Authentication上でユーザー作成し、簡易画面からログイン処理とログアウト処理を実装
    • アカウント情報の作成や更新できるようにする
    • ログイン、ログアウトの組み込み
    • 認証はあくまでも便利機能として考え、未認証でも購入できる想定

管理者サイト

  • プレビュー画面
    • microCMS管理画面の画面プレビューからリクエストされる想定
    • リクエストURLからmicroCMSのAPIを叩き、動的にページ作成
  • 在庫管理画面
    • 商品データに在庫数を付与したリスト画面
    • 商品データの一覧はSSGビルドで静的ファイルとして作成
    • 在庫数のみ、描画時にFirestroeから取得して当て込む
    • 在庫数の変更が可能(Firestoreを更新する)
  • 販売管理画面
    • 販売データ(日付、合計金額)とステータスを表示したリスト画面
    • FireStoreから動的にデータを取得するので、静的化せずSPAとする
    • 詳細ボタンをクリックすると、購入者情報や購入商品一覧をオーバーレイで表示

Functions(API

  • 決済用関数
    • 在庫チェックと金額改ざんチェック後、StripeのAPI呼び出し
    • 在庫数変更(Firestore)
    • 販売情報の登録(Firestore)

現状の簡易フロー図

f:id:yasug:20201218003238j:plain
jamstack_ec_flow

実装時の考察

ECサイトの機能について

  • 当初、商品一覧画面はページング付きですべてSSGしたもので進めていたが、やはり検索したいということからAlgoliaを利用したSPAとした
  • ログイン情報の状態管理はVuexとlocalstorageを同期させた管理方法で実装(詳細は下に別途書く)
  • ブラウザから直接Firestoreへwriteすることはやめ、Cloud Functionsを間に挟むように実装(ReadはOK)
  • カートの状態管理も認証と同様にVuexで管理
  • カート画面から購入時の在庫チェックは、その後のCloud Functions内でもするが一応フロント側でも見るようにした(要検討)

管理者サイトの機能について

  • リアルタイム性が必要になるデータを扱うことが多く利用ユーザも限定されるため、SSGに意固地になりすぎずSPAでの設計も積極的に検討する
  • Firestoreへのwrite系処理もCloud Functionsが間に挟めなくて良しとする(要検討)
  • プレビュー画面ではmicroCMSのAPI_KEYがソースから見えるが良しとする(要検討)

データ層について

  • ここが一番迷っていて検討要素が多い(Stripeでどこまでデータ管理すべきか見えていない)
  • 在庫データ、商品データ、販売データをFirestoreで管理して作成を進めた(要検討)
    • 在庫データと販売データはStripe側で持たせるほうがいいのか?
    • 商品データはStripeに連携してそちらでも管理する方がいいのか?
  • 商品データはmicroCMSからAPIで取得もでき2重管理になるがFirestoreにも入れた(要検討)
    • 決済時のCloud Functionsで金額チェックをしたい 具体的には、商品マスタ情報の金額と画面から渡された数量が、画面から渡された合計金額に一致するかチェックしたい
    • 上記チェック時の金額判断するためには、microCMSから更新された商品データの履歴が必要になるため、履歴管理をどこかで持たせたい
    • 商品データもStripeでもたせる方がいいのか?
  • 顧客データ(要検討)
    • 認証周りは簡易実装しかしていないので、現状はFirebase Authenticationに固定ユーザー作って試している ユーザー情報が登録されたときに、StripeとFirestoreどちらで管理すべきか?

SSG(ビルド)について

  • 各商品ページをSSG
  • 上記で、microCMSのAPIから商品データ一式取得するので、AlgoliaとFirestoreへ必要なデータ更新するように実装
    • Firestoreへの更新をトリガーにしたCloud Functionsを用意してAlgoliaのデータと同期するようにした方がいいか迷っている。
    • microCMSから商品データ更新(追加・削除)された場合に、Firestoreのデータを全て変更する必要はないので、microCMSで設定するカスタムのWebhookでやってもいいものか迷っている(詳細は下に別途書く)

ハマったポイントと解決案

1. GitHub Actionsでデプロイ時にfirebaseコマンドがないと言われた

背景

  • GitHub ActionsのFirebaseデプロイでは、よく以下を利用したサンプルを見かけるが、まずは理解のためにも自分でfirebaseコマンドを叩くように書いてみた

    github.com

  • デプロイ用のymlでは以下のように、stripeのシークレットKEYを設定し、デプロイコマンドを叩いていた

- name: deploy to Firebase Hosting
      run: |
        firebase functions:config:set stripe.secretkey=${{secrets.STRIPE_SECRET_KEY}}
        firebase deploy
      env:
        FIREBASE_TOKEN: ${{secrets.FIREBASE_TOKEN}}
  • GitHub Actions内のデプロイ処理で、firebase command not foundエラーが発生していた

解決案

  • デプロイの前処理でビルド処理が実行されるので、そこの npm install 時にfirebase-toolsを、-gでインストール
npm install -g firebase-tools

2. ある日突然、firebaseのデプロイがコケるようになった

背景

  • 12/16になったらデプロイがコケるようになった。
  • GitHub Actionsのエラー内容は以下
Error: An unexpected error has occurred.
Error: Process completed with exit code 2.

解決案

  • firebase-toolsのバージョンチェック

github.com

  • プロジェクト生成時は8系で進めていたが、12/16に9系に上がっていた!
  • とりあえずバージョンを8系にするように変更して正常動作することを確認できた
npm install -g firebase-tools@^8.0.0
  • 9系になったことで設定ファイル系のフォーマットなど変わったのかも、まだ詳細調査はできていない。後日する予定。

3. Firebase Auth使った認証状態の保持

背景

  • 認証自体はFirebase Authenticationを利用して簡易実装していたが、どう状態保持するか悩んだ
  • 最初、middlewareにauth処理を実装してvuexで状態保持を検討した
    • npm run devで確認しているとうまく動くように見えるが、npm run generate & npm run startだとうまくいかない
    • middlewareはF5(page refresh)時には呼ばれないため、Jamstack構成では使えないと判断
  • 次に、共通のヘッダコンポーネントを用意していたので、そこのbeforeCreateでfirebaseの認証チェックするようにした
    • 商品ページの切り替え時に、その都度認証確認リクエストが飛ぶことにモヤモヤ
    • 今後ページが増えていく中で、ヘッダが全て共通になるのかモヤモヤ
    • カートの状態も何かしらの方法で保持しなくてはいけないことに気づき、使えないと判断

解決案

  • 最終的には、以下のpluginを導入しlocalstorage管理することで対応

github.com

4. GitHub Actionsのパフォーマンス検討

背景

  • GitHub Actionsの処理全体で約4〜5分掛かっている
  • 内訳としては、ビルド(npm install含む)とfirebaseのデプロイでそれぞれ2分くらい
  • ビルド処理は常に1からインストールしているので、対策ないか調査

解決案

  • 探すとすぐに以下が見つかった。

github.com

  • ただ、使ってみたけど特に大きくパフォーマンスが改善されることはなく、ここについては後日調査

5. nuxt run generate 時にFirestoreへデータ入れようとして発生したワーニング

背景

  • build処理時、nuxt.config.jsのgenerateで以下のような感じでFirestoreにデータ更新していた
  generate: {
    async routes() {

      const { data } = await axios.get(
        `https://xxx.microcms.io/api/v1/items?limit=100`,
        { headers: { 'X-API-KEY': API_KEY } }
      )

      const fsStocks = firestore.collection('items')
      const batch = firestore.batch()

      const pages = []
      data.contents.forEach((content) => {
        pages.push({
          route: `/${content.id}`,
          payload: content
        })

        const nycRef = fsStocks.doc(content.id)
        batch.set(nycRef, content)
      })
      await batch.commit()

      return pages
    }
  },
  • npm run generate すると一応SSGは正常完了するものの、以下のようなワーニングが表示される
⚠ Nuxt Warning

The command 'nuxt generate' finished but did not exit after 5s
This is most likely not caused by a bug in Nuxt
Make sure to cleanup all timers and listeners you or your plugins/modules start. 
Nuxt will now force exit

DeprecationWarning: Starting with Nuxt version 3 this will be a fatal error

解決案

  • generate完了時に明示的にFirestoreを終了させてやる必要がある
  • nuxt.config.jsのhooksに以下を追記
  hooks: {
    generate: {
      done(builder) {
        firestore.terminate().then()
      }
    }
  },

6. Firestoreへデータ登録かmicroCMSのWebhookで迷った

背景

  • ビルド時にmicroCMSから全データ取得しているので、その流れでFirestoreも一律全データ更新するようにしていた
  • microCMSで商品データ更新(追加、削除)された場合に、全データ更新ではなく、更新データのみFirestoreに反映した方が効率よいと思った
  • microCMSではWebhookにカスタム通知が設定できるので、それを利用しCloud Functionsを呼び出せば、1件ごとのFirestore更新ができるのではないかと思った

解決案

  • 以下のようなCloud Functionsを用意し、カスタム通知にURL設定することでWebhook連携の接続までは簡単にできることを確認
exports.updateCms = functions.region('asia-northeast1').https.onCall(async (data, context) => {
  functions.logger.log("called updateCms.")
})

exports.updateCms2 = functions.region('asia-northeast1').https.onRequest((request, response) => {
  functions.logger.log("called updateCms2.")
})
  • 関数内でリクエストパラメータを取得しFirestore更新まではいけるものの、セキュリティが心配に。
  • microCMSのカスタム通知で渡せるパラメータは決まっているため、認証用トークンなどは含めれないはず?
  • カスタム通知に設定するURLにクエリストリングを付与して、それをトークンとして扱うことも検討したが、一旦はそのままビルド時に全データ更新で落ち着いた。

結論

  • 細かい部分、特にデータ層の役割分担については検討も作り込みも必要だが、実際に書いてみた感触的には中小規模のECサイトであれば可能。
  • Jamstackな構成にすることで、様々なSaaSを利用することになるため、それについても少し考えてみた。
    • SaaS利用について、慎重に選択する必要はあるが利用そのものに消極的になりすぎる必要はない
    • パフォーマンス、スケーラビリティ、アベイラビリティ等の非機能要件への対策を、SaaS側に移譲することは合理的に感じている
      • SaaS提供企業側としては、如何にパフォーマンスよく安定したサービスを提供するかがビジネスに直結している
      • SaaS利用企業側からすると、対策にコストが掛かる非機能要件を移譲することで本来の目的となるビジネスロジックに集中することが可能