本指南详细介绍如何编写 Cloudflare Pages 项目,使其完美适配本应用的部署能力。
本应用支持将 Cloudflare Pages 项目打包为 .zip 文件后直接部署。应用会自动识别项目类型,处理 TypeScript / JSX 转换、模块间 import 路径重写、路由配置生成等工作,无需 wrangler 或 esbuild。
| 功能 | 支持状态 | 说明 |
|---|---|---|
_worker.js 单文件 | 支持 | 高级模式,原样上传 |
functions/ 目录 | 支持 | 标准模式,自动路由 |
| 纯静态网站 | 支持 | HTML / CSS / JS / 图片 |
| 文件路由 / 动态路由 / Catch-all | 支持 | 含嵌套目录 |
中间件 _middleware.js | 支持 | 含嵌套中间件 + context.next() |
| 方法级别导出 | 支持 | onRequestGet / Post / Put / Delete 等 |
TypeScript .ts | 支持 | Sucrase 类型剥离 |
JSX .jsx | 支持 | 需 @jsx pragma |
TSX .tsx | 支持 | TS + JSX 同时转换 |
| 模块间 import | 支持 | 相对路径自动重写 |
| 静态资源回退 | 支持 | env.ASSETS.fetch |
| NPM 依赖 | 不支持 | 无 bundler |
_worker.js/ 目录模式 | 不支持 | 仅支持单文件 |
| TS path aliases | 不支持 | 不读取 tsconfig.json |
将项目打包为 .zip 文件上传。Zip 文件的根目录就是项目根目录。
注意:functions/ 目录是标准模式的核心。应用检测到此目录后会自动扫描所有 .js / .ts / .tsx / .jsx 文件,生成路由配置和入口脚本。
注意:_worker.js 必须放在项目根目录(不是 public/ 内)。应用会将其原样上传为 _worker.bundle,不做任何转换。
应用按以下优先级自动检测部署模式:
项目根目录存在 _worker.js 文件时触发。应用原样上传,适合完整的自定义 Worker。
项目根目录存在 functions/ 目录时触发。自动生成路由、转换 TS/JSX、重写 import。
_worker.js 使用 ES Module 格式,必须 export default 一个对象,包含 fetch 方法:
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
if (url.pathname === '/') {
return new Response('Hello World!');
}
if (url.pathname === '/api') {
return Response.json({ message: 'API' });
}
// 静态资源回退
return env.ASSETS.fetch(request);
}
};
标准模式下,应用自动处理一切 — 路由生成、类型转换、import 重写。你只需按约定写文件即可。
// functions/hello.js
export function onRequestGet(context) {
return new Response('Hello from Functions!');
}
标准模式下,functions/ 目录中的文件路径直接映射为 URL 路由。
| 文件路径 | URL 路由 | 说明 |
|---|---|---|
functions/hello.js | /hello | 基础路由 |
functions/api/time.js | /api/time | 嵌套目录 |
functions/api/index.js | /api | index 文件 |
functions/api/user/[id].js | /api/user/:id | 动态参数 |
functions/api/[[slug]].js | /api/:slug* | Catch-all(返回数组) |
functions/_middleware.js | 所有路由 | 根中间件 |
functions/api/_middleware.js | /api/* | 嵌套中间件 |
使用 [param] 语法定义动态段,通过 context.params 获取:
// functions/api/user/[id].js
export function onRequestGet(context) {
const { id } = context.params;
return Response.json({
id: Number(id),
message: `User ${id}`
});
}
使用 [[slug]] 语法匹配多级路径,参数以数组形式返回:
// functions/api/[[slug]].js
export function onRequestGet(context) {
const { slug } = context.params;
// slug 是数组,如 /api/a/b/c → ['a', 'b', 'c']
return Response.json({ segments: slug });
}
当多个路由可能匹配同一 URL 时,按以下优先级排序:
/api/user/123 优先匹配 api/user/[id].js:id 参数:slug* 最低优先级通过导出不同名称的函数来处理不同的 HTTP 方法。一个文件可以同时导出多个方法处理函数。
| 导出函数名 | HTTP 方法 |
|---|---|
onRequestGet | GET |
onRequestPost | POST |
onRequestPut | PUT |
onRequestPatch | PATCH |
onRequestDelete | DELETE |
onRequestOptions | OPTIONS |
onRequestHead | HEAD |
onRequest | 所有方法 |
// functions/api/items.js
export function onRequestGet(context) {
return Response.json({ items: [] });
}
export function onRequestPost(context) {
return new Response('Created', { status: 201 });
}
export function onRequestDelete(context) {
return new Response(null, { status: 204 });
}
提示:同一个文件中导出的多个方法共用同一个模块,应用会自动去重,模块只上传一次。
中间件文件命名为 _middleware.js,可以拦截请求、修改响应、通过 context.data 传递数据给下游路由。
// functions/_middleware.js — 全局中间件
export function onRequest(context) {
// 在请求处理前执行
const requestId = crypto.randomUUID();
// 将数据传递给下游
context.data.requestId = requestId;
context.data.timestamp = Date.now();
// 调用下一个处理函数
const response = await context.next();
// 在响应返回前修改
response.headers.set('X-Request-ID', requestId);
return response;
}
中间件作用于其所在目录及子目录的所有路由:
中间件按从外到内的顺序执行,context.next() 将控制权交给下一层。
// functions/api/users.js
export function onRequestGet(context) {
// 读取上游中间件传递的数据
const { requestId, timestamp } = context.data;
return Response.json({
users: [],
requestId: requestId,
processedAt: Date.now() - timestamp
});
}
应用内置 Sucrase 转换器,支持 .ts 文件的类型剥离。无需 tsconfig.json,无需编译步骤。
interface / type 类型定义<T>enum 枚举asis / instanceoftype 导入:import { type User } from './types'?: 和空值合并 ??// functions/utils/types.ts
export interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
}
export enum Status {
Active = 'active',
Inactive = 'inactive',
Pending = 'pending',
}
export function isAdmin(user: User): user is User {
return user.role === 'admin';
}
// functions/api/ts.ts — 使用 TS 类型
import { type User, Status } from '../utils/types';
export function onRequestGet(): Response {
const users: User[] = [
{ id: 1, name: 'Alice', email: '[email protected]', role: 'admin' },
];
const status: Status = Status.Active;
return Response.json({ users, status });
}
应用支持 .jsx 和 .tsx 文件的 JSX 语法转换。由于 Workers 环境没有 React,必须使用 @jsx pragma 指定自定义的 jsx runtime。
关键:每个包含 JSX 语法的文件,必须在文件顶部添加 /** @jsx h */ 注释,否则 Sucrase 会默认使用 React.createElement,导致运行时报错。
/** @jsx h */
// 上面这行必须放在文件顶部,告诉 Sucrase 用 h() 而不是 React.createElement
// 自定义 jsx runtime
function h(tag, props, ...children) {
return {
tag,
props: props || {},
children: children.flat().filter(c => c != null && c !== false)
};
}
function jsxToHtml(element) {
if (typeof element === 'string') return element;
if (Array.isArray(element)) return element.map(jsxToHtml).join('');
if (typeof element === 'object' && element.tag) {
const attrs = Object.entries(element.props)
.map(([k, v]) => ` ${k}="${v}"`).join('');
return `<${element.tag}${attrs}>${element.children.map(jsxToHtml).join('')}</${element.tag}>`;
}
return '';
}
export function onRequestGet() {
const page = (
<html>
<body><h1>Hello JSX!</h1></body>
</html>
);
return new Response('<!DOCTYPE html>' + jsxToHtml(page), {
headers: { 'Content-Type': 'text/html' }
});
}
/** @jsx h */
import { type User } from './utils/types';
// 自定义 jsx runtime(带类型)
function h(tag: string, props: Record<string, unknown>, ...children: unknown[]): Record<string, unknown> {
return { tag, props, children };
}
function Card(props: { user: User }) {
return (
<div>
<h2>{props.user.name}</h2>
<p>{props.user.email}</p>
</div>
);
}
export function onRequestGet() {
const user: User = { id: 1, name: 'Alice', email: '[email protected]', role: 'admin' };
const html = jsxToHtml(Card({ user }));
return new Response(html, { headers: { 'Content-Type': 'text/html' } });
}
应用支持模块间相对路径 import。部署时自动将所有相对路径重写为 ./func_N.js 格式,匹配 Cloudflare Workers 的模块规范。
import { User, Status } from '../utils/types';
import { type User } from '../utils/types';
import helpers from '../utils/helpers';
import * as utils from '../utils/helpers';
import './polyfill';
const module = await import('../utils/helpers');
export { formatUser, paginate } from './helpers';
应用自动解析以下路径变体,无需手动指定扩展名:
| import 路径 | 尝试匹配 |
|---|---|
./types | types → types.ts → types.js → types.tsx → types.jsx → types/index.js → ... |
../utils/helpers | ../utils/helpers → ../utils/helpers.ts → ... |
./utils | ./utils → ./utils/index.ts → ./utils/index.js → ... |
提示:import 路径不要写扩展名(./types 而非 ./types.ts),应用会自动尝试所有可能的扩展名。
// functions/api/import.ts — 多模块 import
import { formatUser, paginate } from '../utils/helpers';
import { capitalize, slugify } from '../utils/shared';
import { type User, Status } from '../utils/types';
export function onRequestGet() {
const users: User[] = [/* ... */];
return Response.json({
formatted: users.map(formatUser),
page1: paginate(users, 1, 2),
slugs: users.map(u => slugify(u.name)),
status: Status.Active
});
}
静态文件放在 public/ 目录中,会与 Functions 一起部署。当请求的路径没有匹配的 Function 时,自动回退到静态资源。
_routes.json 控制哪些路径触发 Functions,哪些直接返回静态资源。如果项目没有提供,应用会自动生成。
{
"version": 1,
"include": ["/*"],
"exclude": ["/css/*", "/images/*", "/favicon.ico"]
}
include — 匹配的路径触发 Functionsexclude — 匹配的路径直接返回静态资源(节省 Function 调用次数)// 在 Function 中手动获取静态资源
export function onRequestGet(context) {
return context.env.ASSETS.fetch(context.request);
}
functions/utils/ 下,避免被当作路由(虽然没有 onRequest 导出会自动走静态回退,但语义更清晰)onRequest(通用方法)functions/utils/types.ts.jsx / .tsx 文件必须在顶部添加 /** @jsx h */h() 函数和 jsxToHtml() 函数可以在每个文件中定义,或抽取为共享模块 functions/utils/jsx-runtime.jstsconfig.json — Sucrase 直接做类型剥离package.json — 无需安装依赖import { type Foo } 语法导入纯类型,转换后会被自动移除functions/ 或 _worker.js 在 zip 的根级别public/ 目录| 功能 | 原因 | 替代方案 |
|---|---|---|
| NPM 依赖 | 无 bundler,无法解析 import React from 'react' |
将依赖代码复制到项目中,或使用 Workers 原生 API |
_worker.js/ 目录模式 |
只支持单文件 _worker.js |
使用 functions/ 标准模式 |
| TS path aliases | 不读取 tsconfig.json |
使用相对路径 ../utils/types |
| 动态 import 变量 | import(varName) 无法静态分析 |
使用静态字符串 import('./module') |
| JSON import | import data from './data.json' 不支持 |
使用 fetch() 或内联为 JS 对象 |
| CSS / 文件 import | import styles from './style.css' 不支持 |
在 HTML 中用 <link> 引用静态 CSS |
| Node.js API | Workers 是 V8 环境,不是 Node.js | 使用 Web API(crypto、fetch、TextEncoder 等) |
| TS 装饰器 | Sucrase 默认不支持 decorators transform | 使用高阶函数或普通类 |
通常是运行时 JavaScript 错误。最常见的原因是 JSX 文件缺少 /** @jsx h */ pragma,导致 Sucrase 输出 React.createElement 但 Workers 环境没有 React。在文件顶部添加 pragma 即可。
路由未匹配且没有静态资源可回退。检查文件路径是否正确,或添加 public/index.html 作为兜底页面。
import 路径未被正确重写。确保 import 使用的是相对路径(./ 或 ../),不要使用 bare specifier(如 react)。
不需要。应用使用 Sucrase 做类型剥离,不读取 tsconfig.json。TypeScript 的 paths、baseUrl 等配置也不会被处理,请使用相对路径。
不能。应用没有 bundler,无法解析 NPM 依赖。如果需要第三方库,将其源码直接复制到项目中作为本地模块。
功能上基本一致。主要区别:不支持 NPM 依赖和 TS path aliases(因为无 bundler),但支持 TS / JSX / TSX 转换和模块间 import(通过 Sucrase + 路径重写实现)。
可以。导出 onRequestGet + onRequestPost 等多个函数即可,模块只上传一次不会重复。
会被注册为路由(默认分配 onRequest),但实际访问时会走静态资源回退,不影响功能。建议放在 functions/utils/ 目录下以示区分。