Subscribe to receive notifications of new posts:

Subscription confirmed. Thank you for subscribing!

使用 Cloudflare Pages 构建全栈应用

Loading...

Building a full-stack application with Cloudflare Pages

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

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

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

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

基于文件的路由

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

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

// ./functions/time.js


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

git commit-ing 和推送此文件会触发构建并部署您的第一个 Pages 函数。对 /time 的任何请求都将由此函数提供服务,所有其他请求将回退到项目的静态资产中。将 Functions 文件放置在目录中将按您的预期工作:./functions/api/time.js 将在 /api/time 处可用,./functions/some_directory/index.js 将在 /some_directory 处可用。

我们还支持 TypeScript(./functions/time.ts 将以相同方式工作)以及参数化文件:

  • 含有具有单个方括号对的 ./functions/todos/[id].js 将匹配类似 /todos/123 的所有请求;
  • 具有两个方括号对的 ./functions/todos/[[path]].js 将匹配任何数量的路径段的请求(例如 /todos/123/subtasks)。

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

动态数据

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

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

目光敏锐的读者会注意到,我们正在导出 onRequestGet,这可让我们仅响应 GET 请求。

我们还使用 KV 命名空间(通过 env.IMAGES 访问)来存储有关已上传图像的信息。要在您的 Pages 项目中创建绑定,请导航至“设置”选项卡。

与其他 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/* 处的所有传入请求:

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

中间件是一款可任意使用的强大工具,让您能够使用 Cloudflare Access 轻松保护应用程序的部分,或与 Honeycomb 和 Sentry 等可观察性和错误记录平台快速集成。

集成为 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 [email protected]

并服务静态资产文件夹或代理您的现有工具:

# 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 Week (CN) Cloudflare Pages (CN)) Cloudflare Workers (CN) Cloudflare Access (CN) 产品新闻

Follow on Twitter

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

Related Posts

December 19, 2021 1:59PM

在 Cloudflare 上构建您的下一个视频应用程序

过去,构建视频应用程序十分困难。在录制、编码和播放视频背后有许多复杂的技术。幸运的是,Cloudflare Stream 分担走了所有困难的部分,现在您可以轻松构建自定义视频和流媒体应用程序。让我们看一下,我们可以如何结合 Cloudflare Stream、Access、Pages 和 Workers,使用极少的代码创建一个高性能的视频应用程序。...

November 17, 2021 3:03PM

Cloudflare Pages 现已提供对 GitLab 的支持

在我们对 Pages 形成概念的早期阶段,我们着手构建能够与现有工作流程无缝集成的平台,为开发人员提供顺畅体验。但是,在宣布 Pages 正式发布后,我们意识到这个平台可能实际上无法由每个开发人员使用。今天之前,只有将 GitHub 用作源代码管理工具的开发人员才能享有 Pages 体验。...