📝

ionic reactとバックエンドをamplifyで作る時に困ったことの記録

2021/01/31に公開

その1 公式ドキュメントがないので初期設定がわからない

Ionic AmplifyはIonic Angularで書かれているドキュメントになりますですので環境設定の方法自体が異なってきます。(私はReact NativeとIonicとReactのドキュメントをあっち行ったり、こっち行ったりしながら読んでいます。)

公式のドキュメントはこちら。

https://docs.amplify.aws/start/getting-started/setup/q/integration/ionic#create-a-new-ionic-app

これは何度も言うようですが(Ionic Angular)の設定です。
Ionic VueやIonic Reactでこの設定をするとpublishするときに死にます。

このおかげで私は設定を一からやり直しました

そのためamplify initとamplify deleteコマンドはたくさん打ちました

amplify initを実行するといろいろ質問形式で聞かれますがドキュメント通りにするとつまります。絶対につまります。

Enter a name for the project (todo)

# All AWS services you provision for your app are grouped into an "environment"
# A common naming convention is dev, staging, and production
Enter a name for the environment (dev)

# Sometimes the CLI will prompt you to edit a file, it will use this editor to open those files.
Choose your default editor

# Amplify supports JavaScript (Web & React Native), iOS, and Android apps
Choose the type of app that you're building (javascript)

What JavaScript framework are you using (ionic)

Source directory path (src)

Distribution directory path (build) => ここはIonic Reactはbuildディレクトに作成されますIonic AngularはCordovaも使えるためwwwディレクトリ配下に設定することになりますがReactはbuild配下になります。なのでこのコマンドはbuildになります

Build command (ionic build) => このコマンドはビルドスクリプトです。現在のIonicですとIonic buildと設定します

Start command (ionic serve)

# This is the profile you created with the `amplify configure` command in the introduction step.
Do you want to use an AWS profile

その2 Auth

基本的なプラグインは以下の2つ

import Amplify from "@aws-amplify/core";
import { withAuthenticator } from "@aws-amplify/ui-react";

この2つが必要です。特にログインのコンポーネントであるwithAuthenticatorはreact用のコンポーネントでも大丈夫です。IonicとなっていますがUIコンポーネントなのでcapacitorがいい具合に変換してエミュレーター上ではありますがXcodeとAndroid Studio上でしっかりと動作しているのは確認済みです。

ドキュメントはこちら

https://docs.amplify.aws/lib/auth/getting-started/q/platform/js#create-authentication-service

その3 日本語の記事が圧倒的に少ない

これはこの記事を書いている理由にもなります。Ionic Reactでのアプリ開発をしている人も少ないのか、本当に記事がなかったです。なので英語の記事を優先的に見ることになります。
私も可能な限り翻訳などで皆様の開発の一助になればと思っています。

参考にした記事はこちら
https://techroads.org/building-my-app-foundation-ionic-react-amplify-appsync-lambda-resolver/

https://alligator.io/ionic/ionic-4-react-aws-amplify/

二つ目の記事のようにpublishの時はS3とCloudFrontを使用しましょう。(原因はわかっていませんが、その選択以外ではページがうまく表示されませんでした。

その4 ログインコンポーネントの編集の仕方

配色はIonicもUIコンポーネントとしてsrc/theme/variables.cssに:rootがあります。
こちらにamplifyの要素を追加しましょう

/** Ionic CSS Variables **/
:root {
  /** primary **/
  --ion-color-primary: #3880ff;
  --ion-color-primary-rgb: 56, 128, 255;
  --ion-color-primary-contrast: #ffffff;
  --ion-color-primary-contrast-rgb: 255, 255, 255;
  --ion-color-primary-shade: #3171e0;
  --ion-color-primary-tint: #4c8dff;
  
  ...中略
  
  /** Amplify theme color **/
  --amplify-primary-color: #3880ff;
  --amplify-primary-tint: #4c8dff;
  --amplify-primary-shade: #3171e0;
}

こうすることによってログインのボタンの色を変更させることができます。

参考

日本語化対応

こちらの記事をご参考にください。

https://dev.classmethod.jp/articles/how-to-customize-amplify-framework-new-ui-component/

要約しますと

assets/i18n/amplify/vocabularies.jsを作成
こちら対応keyを更新しました。一部対応していないキーも存在します。こちらは現在サポートに確認中です。確認後再度編集いたします。

現在うまく動作ができていないのは
'Attributes did not conform to the schema: email: The attribute is required'
の部分になります。

export const vocabularies = {
  ja: {
    'Sign In': 'ログイン',
    'Create account': '新規ユーザー作成',
    'No account?': 'アカウントが未登録ですか?',
    'Forgot your password?': 'パスワードをお忘れですか?',
    'Reset password': 'パスワードをリセット',
    'Enter your username': 'ユーザー名を入力',
    'Enter your password': 'パスワードを入力',
    'Username *': 'ユーザー名 *',
    'Password *': 'パスワード *',
    'Sign in to your account': 'アカウントにログイン',
    'Create a new account': '新しいアカウントを作成',
    'Email Address *': 'メールアドレス',
    'Phone Number *': '電話番号',
    'Username': 'ユーザー名',
    'Password': 'パスワード',
    'Email': 'メールアドレス',
    'Create Account': 'アカウント作成',
    'Have an account?': 'アカウントをお持ちですか?',
    'Sign in': 'ログインへ',
    'Reset your password': 'パスワードのリセット',
    'Send Code': 'パスワードをリセットします',
    'Back to Sign In': 'ログインへ戻る',
    'User does not exist.': 'ユーザー名とパスワードが一致しません',
    'Username/client id combination not found.': 'ユーザー名が存在しません',
    'Username cannot be empty': 'ユーザー名が入力されていません',
    'Password cannot be empty': 'パスワードが入力されていません',
    'Invalid email address format.': 'メールアドレスのフォーマットが一致しません',
    'Password did not conform with policy: Password not long enough': 'パスワードの規約が一致しません',
    "1 validation error detected: Value at 'password' failed to satisfy constraint: Member must have length greater than or equal to 6": 'パスワードの文字列は6文字以上が必要です',
    'Attributes did not conform to the schema: email: The attribute is required': 'メールアドレスを入力してください',
    'Confirm Sign up': '認証キーの入力',
    'Confirmation Code': '認証コード',
    'Enter your code': '認証コードを入力してください',
    'Lost your code?': 'コードを紛失しましたか?',
    'Resend Code': 'コードを再送します',
    'Confirm': '確認',
    'Incorrect username or password.': 'パスワードが間違っています',
    'Cannot reset password for the user as there is no registered/verified email or phone_number': 'パスワードをリセットすることができません。メールアドレスや電話番号の認証が完了しているか確認してください',
    'Attributes did not conform to the schema: email: The attribute is required\n':'メールアドレスを入力してください',
    'Custom auth lambda trigger is not configured for the user pool.': 'カスタム認証は構成されていません',
  }
}

といった感じになります。
ここで問題となるのがKeyの探し方です。

'Sign In': 'ログイン',
'Sign in': 'サインイン',

ここは微妙に違うかと思います。さてさてそれはなぜかと言った話です。

何もない状態でinspectorを実施するとサインインの箇所はこのようになっています

この部分は新規ユーザー作成の部分です。

次にログインの部分です

inspectorで確認するとこちらはSign Inとなっています。

この部分がキーに該当する箇所になります。
そのキーに該当する箇所を言語変化させることによって多言語対応が可能になります。

その5 データの取得のタイミング

amplify経由でデータを取得する時(例えばDynamoDBからとか)ページ遷移の機能を入れていると非同期処理で実施してくれるかと思うがそうではなく普通に関数がキャンセルされます。ですので
必要に応じてawaitでAPI/メソッドの実行結果を待つか、callbackで終了の状況を確認する必要があります。

特にデータの書き込みとページ遷移などはよくあるパターンです。この辺りを意識して開発するとバグの発生が防げて開発スピードも安定します。

その6 ドメイン設定

Ionic Reactでは公式サポートがされていないためAmplify Consoleでの設定がうまくいきません。
(思っている以上に設定がややこしいです)

なので以下の条件にマッチする場合はこちらの設定方法を使われるのがよろしいかと思われます

  1. どうしてもAWSのリソースを使わないといけない状態でドメイン設定をする場合
  2. どうしてもGraphqlを使わないといけないケース(Firestoreやsupabaseではダメなケース・・・私はそのような状況に出会したことがなく、まだまだ未熟なのですが、きっと皆さんはあるのでしょう。もしありましたら教えていただけますと幸いです。)

以下設定方法
最初は直接CloudFrontを作ってドメイン設定しようかなと思ったのですが、CloudFormationを使用している場合は直接の操作はお勧めしないとのことでCloufFormationで設定してくださいとのことです。

多分ですがコピペでいけます。

template.json

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Description": "Hosting resource stack creation using Amplify CLI",
  "Parameters": {
    "env": {
      "Type": "String"
    },
    "bucketName": {
      "Type": "String"
    },
    "domainName": {
      "Type": "String"
    },
    "acmArn": {
      "Type": "String"
    }
  },
  "Conditions": {
    "ShouldNotCreateEnvResources": {
      "Fn::Equals": [
        {
          "Ref": "env"
        },
        "NONE"
      ]
    }
  },
  "Resources": {
    "S3Bucket": {
      "Type": "AWS::S3::Bucket",
      "DeletionPolicy": "Retain",
      "Properties": {
        "BucketName": {
          "Fn::If": [
            "ShouldNotCreateEnvResources",
            {
              "Ref": "bucketName"
            },
            {
              "Fn::Join": [
                "",
                [
                  {
                    "Ref": "bucketName"
                  },
                  "-",
                  {
                    "Ref": "env"
                  }
                ]
              ]
            }
          ]
        },
        "WebsiteConfiguration": {
          "IndexDocument": "index.html",
          "ErrorDocument": "index.html"
        },
        "CorsConfiguration": {
          "CorsRules": [
            {
              "AllowedHeaders": ["Authorization", "Content-Length"],
              "AllowedMethods": ["GET"],
              "AllowedOrigins": ["*"],
              "MaxAge": 3000
            }
          ]
        }
      }
    },
    "PrivateBucketPolicy": {
      "Type": "AWS::S3::BucketPolicy",
      "DependsOn": "OriginAccessIdentity",
      "Properties": {
        "PolicyDocument": {
          "Id": "MyPolicy",
          "Version": "2012-10-17",
          "Statement": [
            {
              "Sid": "APIReadForGetBucketObjects",
              "Effect": "Allow",
              "Principal": {
                "CanonicalUser": {
                  "Fn::GetAtt": ["OriginAccessIdentity", "S3CanonicalUserId"]
                }
              },
              "Action": "s3:GetObject",
              "Resource": {
                "Fn::Join": [
                  "",
                  [
                    "arn:aws:s3:::",
                    {
                      "Ref": "S3Bucket"
                    },
                    "/*"
                  ]
                ]
              }
            }
          ]
        },
        "Bucket": {
          "Ref": "S3Bucket"
        }
      }
    },
    "OriginAccessIdentity": {
      "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity",
      "Properties": {
        "CloudFrontOriginAccessIdentityConfig": {
          "Comment": "CloudFrontOriginAccessIdentityConfig"
        }
      }
    },
    "CloudFrontDistribution": {
      "Type": "AWS::CloudFront::Distribution",
      "DependsOn": ["S3Bucket", "OriginAccessIdentity"],
      "Properties": {
        "DistributionConfig": {
          "ViewerCertificate": {
            "AcmCertificateArn": {
              "Ref": "acmArn"
            },
            "MinimumProtocolVersion": "TLSv1.2_2019",
            "SslSupportMethod": "sni-only"
          },
          "Aliases": [
            {
              "Ref": "domainName"
            }
          ],
          "HttpVersion": "http2",
          "Origins": [
            {
              "DomainName": {
                "Fn::GetAtt": ["S3Bucket", "DomainName"]
              },
              "Id": "hostingS3Bucket",
              "S3OriginConfig": {
                "OriginAccessIdentity": {
                  "Fn::Join": [
                    "",
                    [
                      "origin-access-identity/cloudfront/",
                      {
                        "Ref": "OriginAccessIdentity"
                      }
                    ]
                  ]
                }
              }
            }
          ],
          "Enabled": "true",
          "DefaultCacheBehavior": {
            "AllowedMethods": [
              "DELETE",
              "GET",
              "HEAD",
              "OPTIONS",
              "PATCH",
              "POST",
              "PUT"
            ],
            "TargetOriginId": "hostingS3Bucket",
            "ForwardedValues": {
              "QueryString": "false"
            },
            "ViewerProtocolPolicy": "redirect-to-https",
            "DefaultTTL": 86400,
            "MaxTTL": 31536000,
            "MinTTL": 60,
            "Compress": true
          },
          "DefaultRootObject": "index.html",
          "CustomErrorResponses": [
            {
              "ErrorCachingMinTTL": 300,
              "ErrorCode": 400,
              "ResponseCode": 200,
              "ResponsePagePath": "/"
            },
            {
              "ErrorCachingMinTTL": 300,
              "ErrorCode": 403,
              "ResponseCode": 200,
              "ResponsePagePath": "/"
            },
            {
              "ErrorCachingMinTTL": 300,
              "ErrorCode": 404,
              "ResponseCode": 200,
              "ResponsePagePath": "/"
            }
          ]
        }
      }
    }
  },
  "Outputs": {
    "Region": {
      "Value": {
        "Ref": "AWS::Region"
      }
    },
    "HostingBucketName": {
      "Description": "Hosting bucket name",
      "Value": {
        "Ref": "S3Bucket"
      }
    },
    "WebsiteURL": {
      "Value": {
        "Fn::GetAtt": ["S3Bucket", "WebsiteURL"]
      },
      "Description": "URL for website hosted on S3"
    },
    "S3BucketSecureURL": {
      "Value": {
        "Fn::Join": [
          "",
          [
            "https://",
            {
              "Fn::GetAtt": ["S3Bucket", "DomainName"]
            }
          ]
        ]
      },
      "Description": "Name of S3 bucket to hold website content"
    },
    "CloudFrontDistributionID": {
      "Value": {
        "Ref": "CloudFrontDistribution"
      }
    },
    "CloudFrontDomainName": {
      "Value": {
        "Fn::GetAtt": ["CloudFrontDistribution", "DomainName"]
      }
    },
    "CloudFrontSecureURL": {
      "Value": {
        "Fn::Join": [
          "",
          [
            "https://",
            {
              "Fn::GetAtt": ["CloudFrontDistribution", "DomainName"]
            }
          ]
        ]
      }
    },
    "CloudFrontOriginAccessIdentity": {
      "Value": {
        "Ref": "OriginAccessIdentity"
      }
    }
  }
}

parameters.jsonにはacmArnを追加してください。
ACMを作成してくださいね。
https://aws.amazon.com/jp/certificate-manager/

team-provider-info.jsonのcategoriesにhostingを追加

"hosting": {
  "S3AndCloudFront": {
     "domainName": "xxxxxxx.xxx"
  }
}

付加価値を産まない重労働が多いので違う案件では使わないかなというのが率直な感想です。
私の仕事が研究職であれば問題ないと思うのですが、私の仕事がお客様に変わってアプリケーションの開発をすることを仕事としている以上、差別化が測れないのであれば正直開発速度に依存するので、ご理解いただけますと幸いです。

また他のエンジニアの皆さんの技術選定理由の一助になれば幸いです。

グローバルセカンダリーインデックスは一つずつ追加しましょう

@key(
    name: "SortByTimestamp"
    fields: ["type", "timestamp"]
    queryField: "listPostsSortedByTimestamp"
  )
  @key(
    name: "BySpecificOwner"
    fields: ["owner", "timestamp"]
    queryField: "listPostsBySpecificOwner"
  )

こんな感じでGSIを2つ同時追加しようとすると

Failed to start API Mock endpoint LimitExceededException: Subscriber limit exceeded: Only 1 online index can be created or deleted simultaneously per table

Failed to start API Mock endpoint LimitExceededException: サブスクライバの制限を超えました。テーブルごとに同時に作成または削除できるオンラインインデックスは1つだけです。

となります。
なので

こちらのワークショップを実施している人は必ず詰まります。

https://amplify-sns.workshop.aws/ja/30_mock/25_post_back_end_key.html

なので

  @key(
    name: "SortByTimestamp"
    fields: ["type", "timestamp"]
    queryField: "listPostsSortedByTimestamp"
  )
boyaki% amplify mock api
  @key(
    name: "BySpecificOwner"
    fields: ["owner", "timestamp"]
    queryField: "listPostsBySpecificOwner"
  )
boyaki% amplify mock api

としましょう。

まとめ 今現時点で動作確認ができていること

  1. init設定
  2. Auth
  3. GraphQLAPI設定

です。PinpointのPush通知は実施してみたのですが、うまく行っておらずFirebase Cloud Messageをそのまま使っています。今後のどのように設定すればいいかわかりましたら追記していこうと思います。

それでは皆さん良い1日を

Discussion