Cloudflare環境で構築する!Wijmo×Next.jsのサーバーレスCRUDアプリ

Cloudflareは、Webサイトの高速化を実現するCDN(コンテンツ配信ネットワーク)や、セキュリティ強化のためのWAF(Web Application Firewall)などのインフラサービスを提供する企業として設立されました。現在ではサービスがさらに追加され、静的ページホスティングの「Pages」やサーバーレスAPIの実装が可能な「Workers」、サーバーレスデータベース構築に対応する「D1」など、Webアプリ開発に必要な機能を全て網羅したフルスタックプラットフォームへと進化しています。

Webアプリケーションのバックエンドからフロントエンド、そしてデータベースまでを包括的にカバーするCloudflareの開発プラットフォームを活用したアプリケーション作成方法は以下の記事でも解説しています。

今回の記事では、Cloudflare開発プラットフォームの代表的なサービスである「Pages」、「Workers」、「D1」を使い、「Wijmo(ウィジモ)」×「Next.js」のCRUDアプリを作る手順をわかりやすく解説します。

さらに、次回の記事では、Cloudflare上に構築したWebアプリに、「ActiveReportsJS(アクティブレポートJS)」を用いた帳票機能を組み込む方法をご紹介する予定です。こちらもあわせてご覧いただけますと幸いです。

事前準備

今回は以下の記事で作成したCRUD処理用のWeb APIを利用してアプリケーションを作成します。確認用のSwagger UIページも含め、事前に以下のWebアプリケーションの実装を行ってください。

Wijmo FlexGridでCRUD機能を実装

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

Wijmoのインストール

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

npm install @mescius/wijmo.react.all

コンポーネント実装

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

componentsフォルダ

Wijmoはクライアントサイドでのみ動作するため、サーバーサイドレンダリングに対応した「Next.js」を使用する場合は、該当コンポーネントがクライアントサイドで実行されることを'use client';の指定によって明示する必要があります。「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";

const INVOICES_ENDPOINT = "/api/Invoices"; // 請求書APIのパス

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

/**
 * ページングおよびフィルタ可能なグリッドで請求書データを表示するFlexGridコンポーネント。
 * 追加、編集、削除、およびサーバーへの変更反映に対応します。
 */
function FlexGrid() {
  const [invoices, setInvoices] = React.useState<WjCore.CollectionView<InvoiceRecord>>(
    new WjCore.CollectionView<InvoiceRecord>([], {
      trackChanges: true,
      pageSize: 15,
    })
  );

  React.useEffect(() => {
    WjCore.httpRequest(INVOICES_ENDPOINT, {
      success: (xhr) => {
        const data = JSON.parse(xhr.response) as InvoiceRecord[];
        setInvoices(
          new WjCore.CollectionView<InvoiceRecord>(data, {
            trackChanges: true,
            pageSize: 15,
          })
        );
      },
      error: (err) => console.error("invoicesデータの取得に失敗しました:", err),
    });
  }, []);

  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 {
      edited.forEach(item =>
        WjCore.httpRequest(INVOICES_ENDPOINT +'/' + item.ID, { method: "PUT", data: item })
      );
      added.forEach(item =>
        WjCore.httpRequest(INVOICES_ENDPOINT, { method: "POST", data: item })
      );
      removed.forEach(item =>
        WjCore.httpRequest(INVOICES_ENDPOINT +'/' + item.ID, { method: "DELETE" })
      );

      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="ID" 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/globals.css」に追加します。以下の強調表示された箇所をファイルに追加してください。

@import "tailwindcss";

:root {
  --background: #ffffff;
  --foreground: #171717;
}

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --font-sans: var(--font-geist-sans);
  --font-mono: var(--font-geist-mono);
}

@media (prefers-color-scheme: dark) {
  :root {
    --background: #0a0a0a;
    --foreground: #ededed;
  }
}

body {
  background: var(--background);
  color: var(--foreground);
  font-family: Arial, Helvetica, sans-serif;
}

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

/* Button component */
.btn {
  @apply bg-sky-500 hover:bg-sky-600 text-white font-semibold m-2 py-2 px-6 rounded focus:outline-none focus:ring-2 focus:ring-sky-500;
}

さいごに、コンポーネントを呼び出すためのページを作成する為に「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 className="p-8">
            <h1 className="text-3xl font-bold mb-4 text-sky-500 ">
                Cloudflare x Wijmo × Next.jsサンプル
            </h1>
            <FlexGrid />
        </div>
    )
}

アプリケーションの実行

ここまでの実装が完了しましたら、動作確認を行ってみます。以下のコマンドでアプリケーションを実行してください。

npm run preview

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

Create処理

Update処理

Delete処理

Cloudflareにデプロイ

ライセンスキー設定処理の実装

ここまででWijmoのFlexGridを使ったCRUD処理アプリの作成が完了しました。これからCloudflare環境へデプロイしますが、その前にWijmoのライセンスキーを設定する処理を追加します。

今回利用するWijmoや、次回の記事で紹介予定のActiveReportsJSでは、ローカル開発環境と実行環境でそれぞれ異なるライセンスキーの設定が必要です。そのため、コードにキーを埋め込む場合、ローカル環境用と実行環境用で別々のファイルを用意する必要があります。
※ ライセンスキーを設定せずにlocalhost環境で実行した場合トライアル版として動作します。

今回は、コードを別ファイルに分けずに済むよう、ローカル環境・実行環境それぞれの環境変数からライセンスキーを取得するAPIを実装し、そのAPIを経由してライセンスキーの設定処理を行うように実装を行います。

環境変数の追加

まず、ローカル環境に、以下の環境変数を追加します。

環境変数の追加

追加したファイルには、以下のようにライセンスキーを設定します。

WIJMO_LICENSE_KEY = "取得した開発用ライセンスキー"

ライセンスキー生成APIの作成

つづいて、環境変数をロードして出力するAPIを作成していきます。まず「app/api」の配下に「license」フォルダを作成、さらに配下に「[Products]」フォルダを作成し、その中に「route.ts」ファイルを追加します。

licenseAPIの追加

このようにAPIルーティングを構成することで、Wijmoだけでなく、クライアントサイドで動作するActiveReportsJSからも、api/license/(製品名) の形式でライセンスキーを取得し、各ライブラリに設定できるようになります。

「route.ts」の実装内容としては、パラメータとして定義された製品名に応じて処理を分岐し、各製品に対応する環境変数からライセンスキーを返すように処理を行っています。

  import { NextResponse, NextRequest } from 'next/server';

  export async function GET(
    request: NextRequest,
    { params }: { params: Promise<{ Products: string }> }
  ) {
    const product = (await params).Products;
    let licenseKey: string;

    switch (product) {
      case 'wijmo':
        licenseKey =
          process.env.WIJMO_LICENSE_KEY ?? 'default-wijmo-license-key';
        break;
      default:
        licenseKey = 'default-license-key';
    }

    return NextResponse.json({ licenseKey });
  }

カスタムフックの作成

つづいて、APIを呼び出して実際にライセンス設定を行うためのカスタムフックを作成します。コンポーネントから直接APIを呼び出してライセンスキーを設定することも可能ですが、ActiveReportsJSからも使いやすくするために、カスタムフックを作成します。

まず、以下のように「hooks」フォルダを作成、その中に「useLicense.ts」を追加します。

licenseAPIの呼び出し

「useLicense.ts」の処理では、APIからライセンスキーを取得する製品名と、ライセンスキーを登録するためのモジュール(今回はWjCoreが渡される)をパラメータとして定義し、カスタムフック内でライセンスキーの登録を行ったうえで、キーの設定結果をtrueまたはfalseで返します。

import { useState, useEffect } from 'react';

/**
 * 汎用ライセンスキーを読み込み適用するカスタムフック。
 * @param licenseModule ライセンスモジュール(setLicenseKey メソッドを持つオブジェクト)
 * @param product API パラメータ名(例: 'wijmo')
 * @returns ライセンスが読み込み・適用されたら true を返します。
 */
export const useLicense = (
  licenseModule: { setLicenseKey: (key: string) => void },
  product: string,
): boolean => {
  const [isLoaded, setIsLoaded] = useState(false);
  const LICENSE_ENDPOINT = "/api/license"; // ライセンスキーAPIのパス

  useEffect(() => {
    if (isLoaded) return;

    const loadLicense = async () => {
      try {
        const res = await fetch(LICENSE_ENDPOINT + '/'+ product);
        const data = (await res.json()) as { licenseKey: string };
        licenseModule.setLicenseKey(data.licenseKey);
      } catch (err) {
        console.error('ライセンスの取得に失敗しました', product, err);
      } finally {
        setIsLoaded(true);
      }
    };

    loadLicense();
  }, [isLoaded, licenseModule, product]);

  return isLoaded;
};

export default useLicense;

コンポーネントにライセンスキー設定処理を追加

作成したカスタムフックを利用して、「components/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 useLicense from '../hooks/useLicense';

~~~中略~~~

/**
 * ページングおよびフィルタ可能なグリッドで請求書データを表示するFlexGridコンポーネント。
 * 追加、編集、削除、およびサーバーへの変更反映に対応します。
 */
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;
    WjCore.httpRequest(INVOICES_ENDPOINT, {
      success: (xhr) => {
        const data = JSON.parse(xhr.response) as InvoiceRecord[];
        setInvoices(
          new WjCore.CollectionView<InvoiceRecord>(data, {
            trackChanges: true,
            pageSize: 15,
          })
        );
      },
      error: (err) => console.error("invoicesデータの取得に失敗しました:", err),
    });
  }, [isLicenseLoaded]);

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

~~~中略~~~
};

export default FlexGrid;

動作確認

先ほどと同様に、npm run previewのコマンドで実行してみると、正しくライセンスが適用され、トライアル版の表記が消えました。

ライセンスキーの適用

デプロイ

ライセンスキー設定処理の追加も行い、各環境でデプロイする準備が整いました。以下のコマンドでCloudflareにデプロイします。

npm run deploy

デプロイが成功すると、以下のようにメッセージとデプロイ先のURLが表示されます。

デプロイ成功

Cloudflare環境に環境変数を設定

デプロイ後、Cloudflare環境に配布用ライセンスキーを設定する為、環境変数を追加します。

Cloudflareのダッシュボードにログインし、サイドメニューより「Workers & Pages」を選択。画面遷移後、デプロイした「cf-crud-app」を選択します。

Cloudflare環境に環境変数を設定1

「cf-crud-app」の画面に遷移後、「設定」を選択。

Cloudflare環境に環境変数を設定2

「設定」画面の「変数とシークレット」から「追加」を選択し、環境変数を追加します。

Cloudflare環境に環境変数を設定3

ローカル環境で設定した変数名「WIJMO_LICENSE_KEY」を設定し、値に配布ライセンスキーを設定し、[デプロイ]を押します。

Cloudflare環境に環境変数を設定4

デプロイすると、設定画面上に環境変数が追加されます。

Cloudflare環境に環境変数を設定5

動作確認

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

さいごに

今回は、Cloudflareの代表的なサーバーレス環境「Pages」、「Workers」、「D1」上で、
WijmoのFlexGridを使ったNext.jsのCRUDアプリケーションを構築する方法をご紹介しました。

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

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

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

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

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