新規投稿のお知らせを受信されたい方は、サブスクリプションをご登録ください:

Cloudflare Pagesを使用したフルスタックアプリケーションの構築

2021-11-17

4分で読了
この投稿はEnglishおよび简体中文でも表示されます。

これは大々的に披露するしかないと思っていたため、私たちはCloudflare Pagesでのフルスタックアプリケーションのサポートを発表できることに興奮していました。Cloudflare Workersの支援を受けてサーバーレス関数をPages内から追加する方法を示すための、実演用の画像共有プラットフォームを構築しました。プロジェクトに1つのファイルを新たに追加するだけで、ダイナミックレンダリング、他のAPIとのやりとり、KVおよびDurable Objectsを使用したpersistデータを追加することができます。フルスタックアプリケーションの可能性とPagesの高速開発サイクルおよび無制限プレビュー環境を組み合わせることで、ほとんどすべてのアプリケーションを作成する能力が得られます。

Building a full-stack application with Cloudflare Pages

本日は、画面共有プラットフォームの例について説明します。一部の画像は秘密にしたまま、画像を友人たちと共有できるようにしたいと思います。関数を使用してJSON APIを構築(KVおよびDurable Objects上にデータを格納する)して、Cloudflare ImagesおよびCloudflare Accessと統合し、フロントエンドとしてReactを使用します。

良質のものをすぐにでも使用したい場合は、こちらにあるデモインスタンス、およびGitHub上のコードを利用できますが、ここではゆっくりしたアプローチに拘ります。

Cloudflare Pagesを使用したサーバーレス関数の構築

ファイル-ベースのルーティング

まだ慣れていない場合、Cloudflare Pagesはgitプロバイダーと接続(GitHubおよびGitLab)して、Cloudflareのネットワークに対する静的サイトのデプロイを自動化します。関数を使用するとこれらのアプリを動的データに散りばめて拡張することができます。まだお済みでない場合は、こちらから加入することができます

私たちのプロジェクトで、新しい関数を作成してみましょう。

git commitと、このファイルのプッシュにより最初のPages関数の構築とデプロイがトリガーされます。/timeに対するすべてのリクエストはこの関数によって処理され、他のすべてのリクエストはユーザーのプロジェクトのスタティックアセットに戻されます。ディレクトリ内へのFunctionsファイルの配置は予想どおり動作します。./functions/api/time.jsは/api/time、./functions/some_directory/index.jsは/some_directoryで利用可能です。

// ./functions/time.js


export const onRequest = () => {
  return new Response(new Date().toISOString())
}

また、TypeScript (./functions/time.tsが同様に動作する)、およびパラメータ化されたファイルもサポートされます

  • ./functions/todos/[id].jsのように単一角括弧を使用すると/todos/123などのすべてのリクエストと一致させられます。

  • そして./functions/todos/[[path]].jsのように二重角括弧を使用すると、任意の数のパスセグメント(例:/todos/123/subtasks)と一致させられます。

PagesFunctionの型は@cloudflare/workers-typesライブラリで宣言しますが、これは関数の型のチェックのために使用することができます。

動的データ

では、私たちの画像共有アプリに戻り、いくつかの画像をアップロード済みであると仮定し、それらをホームページ上に表示したいと思います。そのフロントエンドが呼び出すことができる、これらの画像のリストを返すエンドポイントが必要です。

観察力の優れた読者であればonRequestGetをエクスポートしていることに気が付くでしょう。これによりGETリクエストのみに応答することができます。

// ./functions/api/images.ts

export const jsonResponse = (value: any, init: ResponseInit = {}) =>
  new Response(JSON.stringify(value), {
    headers: { "Content-Type": "application/json", ...init.headers },
    ...init,
  });

const generatePreviewURL = ({
  previewURLBase,
  imagesKey,
  isPrivate,
}: {
  previewURLBase: string;
  imagesKey: string;
  isPrivate: boolean;
}) => {
  // If isPrivate, generates a signed URL for the 'preview' variant
  // Else, returns the 'blurred' variant URL which never requires signed URLs
  // https://developers.cloudflare.com/images/cloudflare-images/serve-images/serve-private-images-using-signed-url-tokens

  return "SIGNED_URL";
};

export const onRequestGet: PagesFunction<{
  IMAGES: KVNamespace;
}> = async ({ env }) => {
  const { imagesKey } = (await env.IMAGES.get("setup", "json")) as Setup;

  const kvImagesList = await env.IMAGES.list<ImageMetadata>({
    prefix: `image:uploaded:`,
  });

  const images = kvImagesList.keys
    .map((kvImage) => {
      try {
        const { id, previewURLBase, name, alt, uploaded, isPrivate } =
          kvImage.metadata as ImageMetadata;

        const previewURL = generatePreviewURL({
          previewURLBase,
          imagesKey,
          isPrivate,
        });

        return {
          id,
          previewURL,
          name,
          alt,
          uploaded,
          isPrivate,
        };
      } catch {
        return undefined;
      }
    })
    .filter((image) => image !== undefined);

  return jsonResponse({ images });
};

また、KV名前空間(env.IMAGESでアクセス)を使用して、アップロード済みの画像に関する情報を格納します。Pagesプロジェクト内にバインディングを作成するには、[Settings(設定)] タブに移動します。

![](/content/images/2021/11/unnamed-15.png "Screenshot of the "Functions" page on the Pages project "Settings" tab in the Cloudflare dashboard")

他のAPIとのインターフェース

Cloudflare Imagesは画像のホスティングおよび変換のための、低価格で、ハイパフォーマンス、高機能のサービスです。複数のバリアントを作成して、様々な方法で画像をレンダリングして署名付きURLでアクセスを制御することができます。このサービスのAPIを使用するインターフェースに以下の関数を追加して、受信ファイルをCloudflare Imagesにアップロードします。

永続性データ

// ./functions/api/admin/upload.ts

export const onRequestPost: PagesFunction<{
  IMAGES: KVNamespace;
}> = async ({ request, env }) => {
  const { apiToken, accountId } = (await env.IMAGES.get(
    "setup",
    "json"
  )) as Setup;

  // Prepare the Cloudflare Images API request body
  const formData = await request.formData();
  formData.set("requireSignedURLs", "true");
  const alt = formData.get("alt") as string;
  formData.delete("alt");
  const isPrivate = formData.get("isPrivate") === "on";
  formData.delete("isPrivate");

  // Upload the image to Cloudflare Images
  const response = await fetch(
    `https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v1`,
    {
      method: "POST",
      body: formData,
      headers: {
        Authorization: `Bearer ${apiToken}`,
      },
    }
  );

  // Store the image metadata in KV
  const {
    result: {
      id,
      filename: name,
      uploaded,
      variants: [url],
    },
  } = await response.json<{
    result: {
      id: string;
      filename: string;
      uploaded: string;
      requireSignedURLs: boolean;
      variants: string[];
    };
  }>();

  const metadata: ImageMetadata = {
    id,
    previewURLBase: url.split("/").slice(0, -1).join("/"),
    name,
    alt,
    uploaded,
    isPrivate,
  };

  await env.IMAGES.put(
    `image:uploaded:${uploaded}`,
    "Values stored in metadata.",
    { metadata }
  );
  await env.IMAGES.put(`image:${id}`, JSON.stringify(metadata));

  return jsonResponse(true);
};

頻繁に読み取られてもまれにしか書き込まれない情報を格納するため、既にKVを使用しています。もう少し同機が必要な機能の場合はどうでしょうか。

各画像にダウンロードカウンターを追加しましょう。highresバリアントをCloudflare Images内に作成すると、ユーザーがリンクをリクエストするたびにカウンターをインクリメントすることができます。これにはもう少しセットアップが必要ですが、プロジェクト内のDurable Objectsの機能を解き放つだけの価値が絶対にあります。

このダウンロードカウントを保持できる、以下のDurable Objectクラスを作成して公開する必要があります。

ミドルウェア

// ./durable_objects/downloadCounter.js
ts#example---counter

export class DownloadCounter {
  constructor(state) {
    this.state = state;
    // `blockConcurrencyWhile()` ensures no requests are delivered until initialization completes.
    this.state.blockConcurrencyWhile(async () => {
      let stored = await this.state.storage.get("value");
      this.value = stored || 0;
    });
  }

  async fetch(request) {
    const url = new URL(request.url);
    let currentValue = this.value;

    if (url.pathname === "/increment") {
      currentValue = ++this.value;
      await this.state.storage.put("value", currentValue);
    }

    return jsonResponse(currentValue);
  }
}

関数を実行する前に一部のコード(認証またはロギングなど)を実行する必要がある場合のために、Pagesには使いやすいミドルウェアが用意されており、これはファイルベースのルーティングの任意のレベルで適用することができます。ディレクトリに_middleware.tsファイルを作成することで、このファイルが最初に実行され、next()が呼び出されるときにユーザーの関数を実行することがわかります。

私たちのアプリケーションでは、未認証ユーザーが画像をアップロード(/api/admin/upload)または画像を削除(/api/admin/delete)できないようにしたいと思います。Cloudflare Accessではすべてまたは一部のアプリケーションに対するロールベースのアクセス制御を適用することができ、ユーザーが必要なのはサーバーレス関数に統合する単一ファイルだけです。私たちは./functions/api/admin/_middleware.tsを作成し、これは/api/admin/*の受信リクエストすべてに適用されます:

ミドルウェアは自由に使える強力なツールで、Cloudflare Accessを使用するアプリケーションのパーツを容易に保護、またはHoneycombおよびSentryなどの可観測性およびエラーロギングプラットフォームと素早く統合することができます。

// ./functions/api/admin/_middleware.ts

const validateJWT = async (jwtAssertion: string | null, aud: string) => {
  // If the JWT is valid, return the JWT payload
  // Else, return false
  // https://developers.cloudflare.com/cloudflare-one/identity/users/validating-json

  return jwtPayload;
};

const cloudflareAccessMiddleware: PagesFunction<{ IMAGES: KVNamespace }> =
  async ({ request, env, next, data }) => {
    const { aud } = (await env.IMAGES.get("setup", "json")) as Setup;

    const jwtPayload = await validateJWT(
      request.headers.get("CF-Access-JWT-Assertion"),
      aud
    );

    if (jwtPayload === false)
      return new Response("Access denied.", { status: 403 });

    // We could also use the data object to pass information between middlewares
    data.user = jwtPayload.email;

    return await next();
  };

export const onRequest = [cloudflareAccessMiddleware];

Jamstackとしての統合

"Jamstack"の"Jam"はJavaScript、API、Markupを表します。Cloudflare Pagesは以前からこの「J」と「M」を提供していましたが、中間にWorkersがあると、本当にフルスタックのJamstackを使用することができます。

取り組みやすい例として、Create Reactアプリでこの画像共有プラットフォームのフロントエンドを構築しましたが、Cloudflare Pagesは増加の一途をたどるフレームワークと自然に統合し(現在は23)、いつでも独自の完全なカスタムビルドコマンドを構成することができます

ユーザーのフロントエンドは私たちが構成済みの関数を呼び出して、そのデータをレンダリングするだけです。私たちはSWRを使用して物事を単純化しますが、ユーザーが望めば、全くありきたりなJavaScriptの fetchを使用してこれを実行することができます。

ローカルデプロイ

// ./src/components/ImageGrid.tsx

export const ImageGrid = () => {
  const { data, error } = useSWR<{ images: Image[] }>("/api/images");

  if (error || data === undefined) {
    return <div>An unexpected error has occurred when fetching the list of images. Please try again.</div>;
  }


  return (
    <div>
      {data.images.map((image) => (
        <ImageCard image={image} key={image.id} />
      ))}
    </div>
  );

}

動作状況をテストするためにすべての変更をプッシュする必要がある場合、それが高速であるとしても、このようなプロジェクト上の反復は手間がかかります。関数、Workers、機密情報、環境変数、KVに対する完全なサポートを含む、Pagesプロジェクトのローカルデプロイのための、Wranglerとの素晴らしい統合がリリースされました。Durable Objectsのサポートは間もなく開始されます。

npmからのインストール:

静的アセットのフォルダの提供をするか、既存のツールをプロキシします:

npm install wrangler@beta

前進、そして作成!

# Serve a directory
npx wrangler pages dev ./public

# or integrate with your other tools
npx wrangler pages dev -- npx react-scripts start

子犬がお好きでしたら、こちらに画像共有アプリケーションを展開してあります。コードがお好きでしたら、GitHub上に用意されています。ご自分で自由にフォークしてデプロイしてください! 5分間のセットアップウィザードがありますが、Cloudflare Images、Access、Workers、Durable Objectsが必要になります。

私たちはPagesプラットフォームの将来に胸を高鳴らせており、あなたが構築しているものについて聞きたいです!当社のDiscordサーバー上の#what-i-builtチャンネルであなたのフルスタックアプリケーションを公開するか、#pages-helpチャンネルでサポートを受けてください。

Cloudflareは企業ネットワーク全体を保護し、お客様がインターネット規模のアプリケーションを効率的に構築し、あらゆるWebサイトやインターネットアプリケーションを高速化し、DDoS攻撃を退けハッカーの侵入を防ぎゼロトラスト導入を推進できるようお手伝いしています。

ご使用のデバイスから1.1.1.1 にアクセスし、インターネットを高速化し安全性を高めるCloudflareの無料アプリをご利用ください。

より良いインターネットの構築支援という当社の使命について、詳しくはこちらをご覧ください。新たなキャリアの方向性を模索中の方は、当社の求人情報をご覧ください。
Full Stack WeekCloudflare PagesCloudflare WorkersFull StackCloudflare ImagesCloudflare Access開発者Developer Platform

Xでフォロー

Greg Brimble|@GregBrimble
Obinna Ekwuno|@Obinnaspeaks
Cloudflare|@cloudflare

関連ブログ投稿