订阅以接收新文章的通知:

使用 Cloudflare Pages 构建全栈应用

2021-11-17

4 分钟阅读时间
这篇博文也有 English日本語版本。

我们很高兴地宣布在 Cloudflare Pages 中支持全栈应用程序,我们知道,我们需要对此进行全面展示。我们构建了一个示例图像分享平台,演示如何在 Cloudflare Workers 的帮助下,直接从 Pages 中添加无服务器功能。只需添加一个新文件到您的项目,您就可以添加动态呈现、与其他 API 交互,以及使用 KV 和 Durable Objects 保存数据。全栈应用程序的可能性,结合 Pages 的快速部署周期和无限制的预览环境,让您几乎可以创建任何应用程序。

Building a full-stack application with Cloudflare Pages

今天,我们将介绍我们的示例图像分享平台。我们希望能够和朋友分享图片,同时又能将一些图片保持私密。我们使用 Functions 构建一个JSON API(在 KV 和 Durable Objects 上储存数据),与 Cloudflare Images 和 Cloudflare Access 集成,并为前端使用 React。

如果您想直接深入挖掘好东西,我们的演示实例发布在此处代码位于 GitHub 上,不过继续读下去可以了解更温和的方法。

使用 Cloudflare Pages 构建无服务器函数

基于文件的路由

如果您尚不熟悉,Cloudflare Pages 可以连接您的 git 提供商(GitHub 和 GitLab),并自动将您的静态站点部署到 Cloudflare 的网络。Functions 可让您通过添加动态数据来增强这些应用程序。如果您尚未注册,可在此处注册

让我们在项目中创建一个新函数:

git commit-ing 和推送此文件会触发构建并部署您的第一个 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)。

我们在 @cloudflare/workers-types 库中声明 PagesFunction 类型,您可以用来对您的 Functions 进行类型检查。

动态数据

回到我们的图像分享应用程序,我们假设已经上传一些图像,且想要在主页上展示这些图像。我们需要一个返回图像列表的端点,前端可以调用该端点:

目光敏锐的读者会注意到,我们正在导出 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 项目中创建绑定,请导航至“设置”选项卡。

![](/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 来存储经常读取但鲜少写入的信息。但那些需要更多同步性的功能怎么办呢?

让我们向每张图像添加一个下载计数器。我们可以在 Cloudflare Images 中创建一个 highres 变量,每当用户请求链接时就增加计数。这需要更多一点设置,但是值得我们去做,因为可以解锁项目中 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 App 作为一个易于使用的示例构建了这个图像分享平台的前端,但是 Cloudflare Pages 本身集成了不断增长的框架数(当前为 23 个),您始终能够配置您自己的整个自定义构建命令

您的前端仅需调用我们已配置的 Functions 并呈现该数据。我们使用 SWR 来简化此过程,但您可以依据自身偏好使用整个 vanilla JavaScript fetch-es 来执行此操作。

本地开发

// ./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>
  );

}

无论速度多快,如果您需要推送每一个更改来测试其效果,那么像这样的项目迭代可能会令人十分痛苦。我们发布了一个与 wrangler 的一流集成,用于 Pages 项目的本地开发,包括对 Functions、Workers、密码、环境变量和 KV 的全面支持。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 上。您可以自己进行挖掘并进行部署!这里有一个五分钟的安装向导,您需要用到 Cloudflare Images、Access、Workers 和 Durable Objects。

我们对于 Pages 平台的未来寄予希望,对于大家构建的产品也十分期待!可前往 #what-i-built channel 炫耀您的全栈应用程序,或前往 Discord 服务器上的 #pages-help channel 获取帮助。

我们保护整个企业网络,帮助客户高效构建互联网规模的应用程序,加速任何网站或互联网应用程序抵御 DDoS 攻击,防止黑客入侵,并能协助您实现 Zero Trust 的过程

从任何设备访问 1.1.1.1,以开始使用我们的免费应用程序,帮助您更快、更安全地访问互联网。要进一步了解我们帮助构建更美好互联网的使命,请从这里开始。如果您正在寻找新的职业方向,请查看我们的空缺职位
Full Stack WeekCloudflare PagesCloudflare WorkersFull StackCloudflare ImagesCloudflare Access开发人员Developer Platform

在 X 上关注

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

相关帖子

2024年10月31日 13:00

Moving Baselime from AWS to Cloudflare: simpler architecture, improved performance, over 80% lower cloud costs

Post-acquisition, we migrated Baselime from AWS to the Cloudflare Developer Platform and in the process, we improved query times, simplified data ingestion, and now handle far more events, all while cutting costs. Here’s how we built a modern, high-performing observability platform on Cloudflare’s network. ...