AWSわからなくても動くやつ、できました。Wijmo×Amplify Gen2

AWSは、数百にのぼるクラウドサービスを提供する世界最大級のプラットフォームです。その圧倒的な機能の広さと柔軟性が、グローバルでのトップシェアを支えています。

一方で、その多彩なサービスを使いこなすには深い専門知識が求められ、AWS認定資格に代表されるように、相応の学習が必要となります

そうした学習コストをやわらげ、フロントエンド開発者が多くのAWSサービスを使いやすくするために設計されたフルスタック開発プラットフォームが「AWS Amplify Gen2(アンプリファイ ジェネレーションツー)」です。

今回の記事では、「AWS Amplify Gen2」と「Wijmo(ウィジモ)」のデータグリッド「FlexGrid(フレックスグリッド)」を利用し、WebアプリケーションにCRUD機能を追加する方法を解説します。

Amplify x Wijmo

事前準備

今回は、以下の記事で作成したWebアプリケーションに、Wijmo(FlexGrid)を利用してCRUD機能を追加していきます。
記事内に記載されている事前準備の内容や、確認用のSwagger UIページも含めて、Webアプリケーションの実装を事前に行ってください。

Wijmo(FlexGrid)でCRUD機能を実装

それでは、事前に準備したWebアプリケーションに対して、WijmoのFlexGridを活用し、CRUD操作機能を追加していきます。

Wijmoのインストール

まず、作成済みのWebアプリケーション「amplify-gen2-app」のプロジェクトをVS Codeで開き、ターミナルから以下のコマンドを実行してWijmoをインストールします。

npm install @mescius/wijmo.react.all

コンポーネント実装

つづいて、「components」フォルダを作成し、その中に「FlexGrid.tsx」を配置して、CRUD処理を行うためのコンポーネントを実装します。

componentsフォルダ

Wijmoはクライアントサイドでのみ動作するため、サーバーサイドレンダリング(SSR)に対応した「Next.js」を使用する場合は、該当コンポーネントがクライアント側で実行されることを、'use client';の記述で明示する必要があります。

今回の例では、Wijmoの「FlexGrid」を使ってデータを一覧表示し、AWS Amplify Gen2のDataクライアントを通じて、バックエンドに対するCRUD処理(データの取得・追加・更新・削除)を実装しています。

「FlexGrid.tsx」に実装するコードは以下の通りです。

'use client';

/**
 * FlexGrid.tsx
 * Wijmo FlexGridを使用した請求書管理用グリッド。
 */
import React from "react";
import "@mescius/wijmo.styles/wijmo.css";
import * as WjCore from "@mescius/wijmo";
import * as WjGrid from "@mescius/wijmo.react.grid";
import { FlexGridFilter } from "@mescius/wijmo.react.grid.filter";
import "@mescius/wijmo.cultures/wijmo.culture.ja";
import { CollectionViewNavigator } from "@mescius/wijmo.react.input";
import { generateClient } from "aws-amplify/data";
import type { Schema } from "@/amplify/data/resource";
import { Amplify } from "aws-amplify";
import outputs from "@/amplify_outputs.json";

// Amplify の設定を読み込んで初期化
Amplify.configure(outputs);

const client = generateClient<Schema>();

/**
 * 請求書レコードの型定義。
 */
export type InvoiceRecord = {
  InvoiceID: string;
  BillNo: string;
  SlipNo: string;
  CustomerID: string;
  CustomerName: string;
  Products: string;
  Number: number;
  UnitPrice: number;
  Date: string;
};

//export type InvoiceRecord 
type InvoicesQuery = {
  items: InvoiceRecord[];
  isSynced: boolean;
};

/**
 * 必須フィールドが全て存在するか検証する
 */
const isValidInvoice = (item: Partial<InvoiceRecord>): item is InvoiceRecord => {
  return !!(
    item.InvoiceID &&
    item.BillNo &&
    item.SlipNo &&
    item.CustomerID &&
    item.CustomerName &&
    item.Products &&
    typeof item.Number === 'number' &&
    typeof item.UnitPrice === 'number' &&
    item.Date
  );
};

function FlexGrid() {
  const isLicenseLoaded = useLicense(WjCore, 'wijmo');
  const [invoices, setInvoices] = React.useState<WjCore.CollectionView<InvoiceRecord>>(
    new WjCore.CollectionView<InvoiceRecord>([], {
      trackChanges: true,
      pageSize: 15,
    })
  );

  React.useEffect(() => {
    if (!isLicenseLoaded) return;
    
    client.models.Invoices.observeQuery().subscribe({
      next: ({ items, isSynced }) => {
        setInvoices(
          new WjCore.CollectionView<InvoiceRecord>(items, {
            trackChanges: true,
            pageSize: 15,
          })
        );
      },
      error: (err) => console.error("invoicesデータの取得に失敗しました:", err),
    });
  }, [isLicenseLoaded]);

  if (!isLicenseLoaded) {
    return <div>Loading license...</div>;
  }

  const handleUpdate = async () => {
    const edited = invoices.itemsEdited;
    const added = invoices.itemsAdded;
    const removed = invoices.itemsRemoved;

    if (edited.length === 0 && added.length === 0 && removed.length === 0) {
      alert("変更されたデータがありません。");
      return;
    }

    try {
      // 変更されたデータがあれば更新
      for (const item of edited) {
        if (isValidInvoice(item)) {
          await client.models.Invoices.update(item);
        }
      }

      // 追加されたデータがあれば登録
      for (const item of added) {
        if (isValidInvoice(item)) {
          await client.models.Invoices.create(item);
        }
      }

      // 削除されたデータがあれば削除
      for (const item of removed) {
        if (item.InvoiceID) {
          await client.models.Invoices.delete({
            InvoiceID: item.InvoiceID
          });
        }
      }

      if (edited.length) alert(`${edited.length}件のデータを更新しました。`);
      if (added.length) alert(`${added.length}件のデータを登録しました。`);
      if (removed.length) alert(`${removed.length}件のデータを削除しました。`);
    } catch (err) {
      console.error("更新中にエラーが発生しました。:", err);
      alert("更新中にエラーが発生しました。");
    } finally {
      invoices.clearChanges();
    }
  };

  return (
    <div>
      <div>
        <button onClick={handleUpdate} className="btn">更新</button>
        <CollectionViewNavigator headerFormat="{currentPage:n0} / {pageCount:n0} ページ" byPage={true} cv={invoices}/>
      </div>
      <div>
        <WjGrid.FlexGrid itemsSource={invoices} allowAddNew={true} allowDelete={true}>
          <FlexGridFilter />
          <WjGrid.FlexGridColumn header="ID" binding="InvoiceID" width={70} />
          <WjGrid.FlexGridColumn header="請求書番号" binding="BillNo" width={150} />
          <WjGrid.FlexGridColumn header="伝票番号" binding="SlipNo" width={150} />
          <WjGrid.FlexGridColumn header="顧客ID" binding="CustomerID" width={100} />
          <WjGrid.FlexGridColumn header="顧客名" binding="CustomerName" width={200} />
          <WjGrid.FlexGridColumn header="商品" binding="Products" width={250} />
          <WjGrid.FlexGridColumn header="数量" binding="Number" width={80} />
          <WjGrid.FlexGridColumn header="単価" binding="UnitPrice" width={100} format="c" />
          <WjGrid.FlexGridColumn header="日付" binding="Date" width={120} />
        </WjGrid.FlexGrid>
      </div>
    </div>
  );
};

export default FlexGrid;

つづいて、スタイルの設定を「app/app.css」に追加します。以下の強調表示された箇所をファイルに追加してください。

body {
    margin: 0;
    background: linear-gradient(180deg, rgb(117, 81, 194), rgb(255, 255, 255));
    display: flex;
    font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
    height: 100vh;
    width: 100vw;
    justify-content: center;
    align-items: center;
  }
  
  main {
    display: flex;
    flex-direction: column;
    align-items: stretch;
  }
  
  button {
    border-radius: 8px;
    border: 1px solid transparent;
    padding: 0.6em 1.2em;
    font-size: 1em;
    font-weight: 500;
    font-family: inherit;
    background-color: #1a1a1a;
    cursor: pointer;
    transition: border-color 0.25s;
    color: white;
  }
  button:hover {
    border-color: #646cff;
  }
  button:focus,
  button:focus-visible {
    outline: 4px auto -webkit-focus-ring-color;
  }
  
  ul {
    padding-inline-start: 0;
    margin-block-start: 0;
    margin-block-end: 0;
    list-style-type: none;
    display: flex;
    flex-direction: column;
    margin: 8px 0;
    border: 1px solid black;
    gap: 1px;
    background-color: black;
    border-radius: 8px;
    overflow: auto;
  }
  
  li {
    background-color: white;
    padding: 8px;
  }
  
  li:hover {
    background: #dadbf9;
  }
  
  a {
    font-weight: 800;
    text-decoration: none;
  }

  .wj-flexgrid {
    width: 1280px !important;
    margin: 10px;
  }

さいごに、コンポーネントを呼び出すためのページを作成する為に「app」フォルダ配下に「flexgrid」フォルダを作成し、その中に「page.tsx」を追加します。

flexglidページの追加

「page.tsx」ファイルは以下のように実装します。「components/FlexGrid.tsx」ファイルを動的インポートし、その際{ ssr: false }オプションを指定することで対象ファイルをSSR(サーバーサイドレンダリング)しないように設定します。

'use client'
import dynamic from 'next/dynamic'

const FlexGrid = dynamic(
    () => {
        return import("../../components/FlexGrid");
    },
    { ssr: false }
);

export default function Home() {
    return (
        <div>
            <h1>
                Amplify Gen2 x Wijmo サンプル
            </h1>
            <FlexGrid />
        </div>
    )
}

アプリケーションの実行

ここまでの実装が完了しましたら、動作確認を行ってみます。まず、Amplifyのサンドボックスを実行するためAWS CLIでのシングルサインオン(SSO)認証設定を行います。

aws configure sso

認証情報の設定後、以下のコマンドにてサンドボックスを作成します。

npx ampx sandbox --profile <認証情報を設定したプロファイル 例:amplify-policy-xxxxxx>

サンドボックスの作成後、別のターミナルを開き、以下のコマンドでアプリケーションを実行します。

npm run dev

実行後、「http://3000/flexgrid」にブラウザからアクセスすると以下のようにFlexGridを利用したページが表示されます。動作確認を行うと、グリッド上から追加、変更、削除した内容が[更新]ボタンを押すことで、データベースに反映されます。

Create処理

Update処理

Delete処理

Amplifyへデプロイ

GitHubへコードをプッシュすると、自動的にAWS Amplifyでのデプロイが開始されます。そのため、Amplify上でも正しく動作する状態でGitコミット・プッシュする必要があります。

今回使用しているWijmoでは、製品を利用するためにライセンスキーの設定が必要です。ローカルの開発環境とAmplifyなどの実行環境では、それぞれ異なるライセンスキーを設定する必要があります。
※ ライセンスキーを設定せずにlocalhost環境で実行した場合、Wijmoはトライアル版として動作します。

ライセンスキーを環境ごとに切り替えて読み込む実装方法については、以下の記事で紹介しています。こちらの記事をもとに、作成中のアプリケーションへ処理を追加してください。

Amplifyで環境変数を使うには?

ローカル環境では、「env.local」ファイルに設定した環境変数が読み込まれ、正しくライセンス情報が設定されますが、Amplify上で環境変数を設定しても正しく読み込まれません。

Amplify上で環境変数を使うためには、プロジェクトフォルダ直下にある「amplify.yml」ファイルに以下の強調箇所を追記する必要があります。

version: 1
backend:
  phases:
    build:
      commands:
        - npm ci --cache .npm --prefer-offline
        - npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
frontend:
  phases:
    build:
      commands:
        - npm run build
        - echo "WIJMO_LICENSE_KEY=$WIJMO_LICENSE_KEY" >> .env
  artifacts:
    baseDirectory: .next
    files:
      - '**/*'
  cache:
    paths:
      - .next/cache/**/*
      - .npm/**/*
      - node_modules/**/*

上記を設定後、以下のようにAmplify管理画面から、配布ライセンスキーの環境変数を追加します。

Amplify環境に環境変数を設定1
Amplify環境に環境変数を設定2

環境変数を設定後、GitHubへコードをプッシュすると、次のようにAWS Amplifyでの自動デプロイが開始されます。

Amplify自動デプロイ

動作確認

最後に「https://デプロイ先のURL/flexgrid」にアクセスして動作確認を行います。ライセンスキーも正しく設定され、ローカル環境と同様にCRUD処理が正常に行えることを確認できました。

さいごに

今回は、フロントエンド開発者向けのフルスタック開発プラットフォーム「AWS Amplify Gen2」上で、WijmoのFlexGridを使ったCRUDアプリケーションを構築する方法をご紹介しました。

Wijmo×Amplify Gen2を活用することで、AWSの詳細な知識がなくても、すぐに動作するCRUDアプリケーションが簡単に構築できました。

次回の記事では、作成したWebアプリケーションにActiveReportsJSを使い、帳票機能を追加する方法についてもご紹介予定です。こちらもあわせてご覧ください。

製品サイトではWijmoを導入したばかりの方や、トライアル期間中の方向けに、Wijmoの概要や導入方法、基本的な使い方を紹介した「Wijmo利用ガイド」を公開しています。こちらも是非ご覧ください。

そのほか、Wijmoの機能を手軽に体験できるデモアプリケーションやトライアル版も公開しておりますので、こちらもご確認ください。

また、ご導入前の製品に関するご相談、ご導入後の各種サービスに関するご質問など、お気軽にお問合せください。

\  この記事をシェアする  /