我们很高兴地宣布在 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 可让您通过添加动态数据来增强这些应用程序。如果您尚未注册,可在此处注册。
让我们在项目中创建一个新函数:
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 获取帮助。