关于 netlify-development
面向 Netlify 项目开发者的构建与部署指南,介绍函数、数据存储、图像 CDN 和配置。它按函数类型说明适用场景与运行约束,并涵盖 Blobs 的存储范围和一致性。
主要功能
- 01Serverless、Edge、后台及定时函数指导
- 02函数路径与配置建议
- 03Netlify Blobs 存储实践
- 04Image CDN 参数与用法
- 05部署配置及运行限制说明
适用场景
- 01为 API 端点选择函数类型
- 02实现边缘请求处理与个性化
- 03使用 Blobs 保存应用数据
- 04配置定时任务和图像优化
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mindrally/ skills --skill netlify-development安装后在 Claude Code 中输入/netlify-development确认已加载。
详细介绍
Netlify 开发最佳实践
概述
本技能提供在 Netlify 上构建和部署项目的全面指南,涵盖无服务器函数、边缘函数、后台函数、计划函数、Netlify Blobs、Image CDN 和部署配置。
核心原则
- 使用代码中的配置,即导出的
config对象(优先于 netlify.toml) - 导入 Netlify 软件包时绝不添加版本号(使用
@netlify/functions,不要使用@netlify/[email protected]) - 仅在明确需要时添加 CORS 标头
- 根据不同用例选择合适的函数类型
- 使用 Netlify Blobs 存储状态和数据
.netlify文件夹是生成的工具输出,而非用户代码——将其保留在.gitignore中- 绝不要将无服务器函数或边缘函数放在发布目录或公共目录中
- 除非收到明确要求,否则不要更改默认的 functions 或 edge-functions 目录
函数类型概览
| 类型 | 用例 | 超时 | 路径约定 |
|---|---|---|---|
| 无服务器 | 标准 API 端点 | 10s(Pro 为 26s) | /.netlify/functions/name |
| 边缘 | 修改请求/响应 | 50ms CPU | 自定义路径 |
| 后台 | 长时间运行的异步任务 | 15 分钟 | -background 后缀 |
| 计划 | 基于 Cron 的任务 | 30s | 配置的计划 |
函数和边缘函数的源代码目录默认为 netlify/functions 和 netlify/edge-functions,但可在 netlify.toml 中覆盖:
[functions]
directory = "my_functions"
[build]
edge_functions = "my-edge-functions"
netlify.toml 设置会覆盖 UI 中的构建设置配置。
无服务器函数
基本结构
// netlify/functions/hello.mts
import type { Context } from '@netlify/functions';
export default async (request: Request, context: Context) => {
try {
// Validate request
if (request.method !== 'POST') {
return new Response('Method Not Allowed', { status: 405 });
}
const body = await request.json();
// Business logic
const result = await processData(body);
return Response.json(result);
} catch (error) {
console.error('Function error:', error);
return Response.json({ error: 'Internal Server Error' }, { status: 500 });
}
};
export const config = {
path: '/api/hello',
};
配置选项
export const config = {
// Custom path (instead of /.netlify/functions/name)
path: '/api/users',
// HTTP methods (optional, allows all by default)
method: ['GET', 'POST'],
// Rate limiting
rateLimit: {
windowSize: 60,
windowLimit: 100,
},
};
路径约定
- 默认路径:
/.netlify/functions/{function_name} - 通过配置设置的自定义路径会完全替代默认路径
- 使用自定义路径可让 API URL 更简洁
Context 参数
无服务器函数和边缘函数都会将 context 对象作为第二个参数接收:
{
account: { id: string }; // Netlify team account ID
cookies: {
get: (name: string) => string | undefined;
set: (options: { name: string; value: string; path?: string; domain?: string; secure?: boolean; httpOnly?: boolean; expires?: Date }) => void;
delete: (nameOrOptions: string | { name: string; path?: string; domain?: string }) => void;
};
deploy: { context: string; id: string; published: boolean };
geo: {
city: string;
country: { code: string; name: string };
latitude: number;
longitude: number;
subdivision: { code: string; name: string };
timezone: string;
postalCode: string;
ip: string;
};
params: Record<string, string>; // route params from the path config
requestId: string;
server: { region: string };
site: { id: string; name: string; url: string };
}
Netlify 全局对象
所有无服务器函数和边缘函数的全局作用域中均提供此对象——在运行时读取/写入环境变量时,优先使用 Netlify.env.*,而不是 process.env:
{
context: object | null; // same as the function's second arg; null outside a handler
env: {
get: (name: string) => string | undefined;
has: (name: string) => boolean;
set: (name: string, value: string) => void;
delete: (name: string) => void;
toObject: () => Record<string, string>;
};
}
边缘函数
用例
- 在请求到达源站前修改请求
- 在向用户返回响应前修改响应
- 基于地理位置的个性化
- A/B 测试
- 在边缘进行身份验证
实现
// netlify/edge-functions/geo-redirect.ts
import type { Context } from '@netlify/edge-functions';
export default async (request: Request, context: Context) => {
const country = context.geo.country?.code || 'US';
// Redirect based on country
if (country === 'DE') {
return Response.redirect(new URL('/de', request.url));
}
// Continue to origin
return context.next();
};
export const config = {
path: '/*',
excludedPath: ['/api/*', '/_next/*'],
};
修改响应
export default async (request: Request, context: Context) => {
// Get response from origin
const response = await context.next();
// Modify headers
response.headers.set('X-Custom-Header', 'value');
// Transform HTML
const html = await response.text();
const modifiedHtml = html.replace('</body>', '<script>...</script></body>');
return new Response(modifiedHtml, {
status: response.status,
headers: response.headers,
});
};
运行时和模块支持
边缘函数运行在 Deno 上,而不是 Node.js 上,可引入多个生态系统中的代码:
- Node.js 内置模块:
import { randomBytes } from 'node:crypto' - Deno 模块:URL 导入,例如
import React from 'https://esm.sh/react' - npm 软件包(Beta):通过
npm install安装并按名称导入——包含原生二进制文件的软件包(例如 Prisma)或使用动态 require 的软件包可能无法正常工作 - 导入映射可让你使用简短名称而非完整 URL 引用第三方模块;在
netlify.toml中通过[functions] deno_import_map = "./import_map.json"声明映射文件路径
可用的 Web API 比 Node.js 更有限:fetch/Request/Response/URL、TextEncoder/TextDecoder(及其流变体)、Web Crypto(randomUUID、getRandomValues、SubtleCrypto)、WebSocket、setTimeout/setInterval、Streams API 和 URLPattern。
其他配置选项
除 path 和 excludedPath 外,边缘函数配置还支持:
{
pattern?: RegExp | RegExp[]; // regex alternative to `path`
excludedPattern?: RegExp | RegExp[];
method?: string | string[];
onError?: 'continue' | 'fail' | 'fallback';
cache?: 'manual'; // opt in to edge response caching
}
多个边缘函数的执行顺序
当多个边缘函数都能匹配同一路径时,请在 netlify.toml 中声明它们,以明确从上到下的执行顺序(无论位置如何,缓存函数始终最后运行):
[[edge_functions]]
path = "/admin"
function = "auth"
[[edge_functions]]
path = "/admin"
function = "injector"
cache = "manual"
执行顺序:基于配置的函数先于框架生成的函数运行,后者先于用户定义的内联函数运行;非缓存函数先于缓存函数运行;多个内联函数按文件名的字母顺序运行。使用 context.next() 继续函数链,而不要通过 fetch()/URL() 重新触发函数(这会启动新的请求链)。
限制和不兼容项
- 压缩后的代码大小上限为 20 MB,每次部署的内存上限为 512 MB,每个请求的 CPU 时间上限为 50ms,响应头超时时间为 40s
- 与拆分测试、
_headers/netlify.toml自定义标头,或边缘函数服务路径上的预渲染不兼容 - 只能将请求重写到同站点 URL — 对于外部内容,请使用
fetch() - 不包含在 Netlify 符合 HIPAA 要求的托管服务中
后台函数
主要特性
- 超时时间为 15 分钟(墙上时钟时间)
- 立即返回 202 状态码
- 返回值会被忽略
- 必须带有
-background后缀
实现
// netlify/functions/process-video-background.mts
import type { Context } from '@netlify/functions';
import { getStore } from '@netlify/blobs';
export default async (request: Request, context: Context) => {
const { videoId } = await request.json();
// Long-running processing
const result = await processVideo(videoId);
// Store result for later retrieval
const store = getStore('processed-videos');
await store.setJSON(videoId, result);
// Return value is ignored
return new Response('Processing complete');
};
export const config = {
path: '/api/process-video',
};
获取后台函数结果
// netlify/functions/get-video-status.mts
import { getStore } from '@netlify/blobs';
export default async (request: Request, context: Context) => {
const url = new URL(request.url);
const videoId = url.searchParams.get('id');
const store = getStore('processed-videos');
const result = await store.get(videoId, { type: 'json' });
if (!result) {
return Response.json({ status: 'processing' });
}
return Response.json({ status: 'complete', data: result });
};
定时函数
主要特性
- CRON 表达式按 UTC 计算,最短间隔为 1 分钟
- 执行时间上限为 30 秒;函数不会返回响应正文(返回值会被忽略)
- 请求正文是一个 JSON 编码对象,其中包含
next_run字段 — 即下次计划调用的 ISO-8601 时间戳 - 定时函数仅在已发布的部署中运行 — 不会在部署预览或分支部署中运行
- 通过同时运行
netlify dev和netlify functions:invoke <name>在本地测试 - 为与其他配置保持一致,计划也可以写在
netlify.toml中,但更推荐在代码中使用config.schedule:[functions."daily-cleanup"] schedule = "@daily"
配置
// netlify/functions/daily-cleanup.mts
import type { Context } from '@netlify/functions';
export default async (request: Request, context: Context) => {
console.log('Running daily cleanup...');
// Cleanup logic
await cleanupOldRecords();
return new Response('Cleanup complete');
};
export const config = {
schedule: '@daily', // or '0 0 * * *' for midnight UTC
};
计划模式
// Common patterns
export const config = {
schedule: '@hourly', // Every hour
schedule: '@daily', // Every day at midnight
schedule: '@weekly', // Every week
schedule: '*/15 * * * *', // Every 15 minutes
schedule: '0 9 * * 1-5', // 9 AM on weekdays
};
Netlify Blobs
除非数据需要关系结构或搜索功能,否则优先选择 Blobs,而不是搭建新的数据库 — Blobs 无需预配,且所有计算类型共用同一套 API。限制:存储名称 ≤64 字节,对象键 ≤600 字节,对象大小上限为 5GB。
一致性和存储范围
- 默认情况下,存储为最终一致性 — 读取速度快,写入约在 60 秒内传播。向
getStore传入{ consistency: 'strong' },即可立即读取到写入结果,但读取速度会变慢。 - 没有内置的并发控制 — 最后一次写入会生效。如果并发写入者不能相互覆盖,请自行添加锁定机制。
- 部署专属存储(
getDeployStore)会与某次部署同步,并在该部署被删除时一并移除;全局存储(getStore)会在所有分支间持久保留,且不会自动清理。构建插件和基于文件的上传必须写入部署专属存储。 - 写入全局存储时,请根据
context.deploy.context进行分支处理,防止非生产数据泄漏:function getBlobStore(...args: Parameters<typeof getStore>) { return context.deploy.context === 'production' ? getStore(...args) : getDeployStore(...args); }
基本用法
import { getStore } from '@netlify/blobs';
// Get a store
const store = getStore('my-store');
// Store data
await store.set('key', 'string value');
await store.setJSON('json-key', { foo: 'bar' });
// Retrieve data
const value = await store.get('key');
const jsonValue = await store.get('json-key', { type: 'json' });
// Delete data
await store.delete('key');
// List keys
const { blobs } = await store.list();
二进制数据
import { getStore } from '@netlify/blobs';
const store = getStore('files');
// Store binary data
const arrayBuffer = await file.arrayBuffer();
await store.set('uploads/file.pdf', arrayBuffer, {
metadata: { contentType: 'application/pdf' },
});
// Retrieve binary data
const blob = await store.get('uploads/file.pdf', { type: 'blob' });
部署专属与站点级
// Site-wide store (persists across deploys)
const siteStore = getStore({
name: 'user-data',
siteID: context.site.id,
});
// Deploy-specific store (scoped to deployment)
const deployStore = getStore({
name: 'cache',
deployID: context.deploy.id,
});
Netlify Image CDN
用法
<img src="/.netlify/images?url=/images/hero.jpg&w=800&q=80" alt="Hero">
<img src="/.netlify/images?url=/images/hero.jpg&w=400&h=300&fit=cover&fm=webp" alt="Hero">
参数
url:源图像路径(必填)w:宽度,单位为像素h:高度,单位为像素q:质量(1-100)fit:cover、contain、fillfm:格式(webp、avif、auto)
编程方式调用
function getOptimizedImageUrl(src: string, options: ImageOptions) {
const params = new URLSearchParams({
url: src,
w: String(options.width),
q: String(options.quality || 80),
fm: 'auto',
});
return `/.netlify/images?${params}`;
}
远程图片、自定义路由和缓存
- 外部图片域名必须加入允许列表后才能进行转换,并且只需要使用绝对 URL:
[images] remote_images = ["https://externalexample.com/.*"] - 如果
url参数指向远程图片,请对其值进行 URI 编码。 - 若要通过比
/.netlify/images更简洁的 URL 提供转换功能,请定义重定向(参数可以保持动态,也可以固定):[[redirects]] from = "/transform-my-images/*" to = "/.netlify/images?url=/:splat&w=50&h=50" status = 200 - 只有托管在同一域名上的图片才能通过
[[headers]]/_headers设置自定义标头(例如Cache-Control),且只有在明确要求时才设置。 - 转换后的图片和源图片会缓存在边缘节点;新的部署会使缓存失效,因此如果源图片发生变化,图片会重新处理。
- 框架集成会自动或通过最少配置启用 Image CDN:Angular 的
NgOptimizedImage、Astro 的<Image />、Nuxt 的nuxt/image、Next.js 通过next.config.js中的remotePatterns,以及 Gatsby 通过NETLIFY_IMAGE_CDN=true。
环境变量
在 Functions 中访问
export default async (request: Request, context: Context) => {
// Access environment variables
const apiKey = Netlify.env.get('API_KEY');
const dbUrl = process.env.DATABASE_URL;
if (!apiKey) {
console.error('API_KEY not configured');
return Response.json({ error: 'Configuration error' }, { status: 500 });
}
// Use variables
};
上下文变量
export default async (request: Request, context: Context) => {
// Available context
const { site, deploy, geo, ip, requestId } = context;
console.log('Site ID:', site.id);
console.log('Deploy ID:', deploy.id);
console.log('Country:', geo.country?.code);
console.log('Request ID:', requestId);
};
管理变量
可以通过 Netlify UI(站点配置 > 环境变量)、CLI、API 或 netlify.toml 设置变量;优先级为 netlify.toml > UI/CLI/API,且站点专用变量优先于共享变量。使用 CLI 前,必须先关联站点(netlify link):
netlify env:set API_KEY "value"
netlify env:set API_KEY "secret-value" --secret
netlify env:unset API_KEY
netlify env:import .env
netlify env:list --plain --context production
Netlify 构建不会直接读取 .env 文件——请改用 env:import 导入。只有在确认 .env.local 已被 git 忽略后,才导出生产上下文的 .env.local,并且绝不要提交该文件。
netlify.toml 可以按部署上下文(production、deploy-preview、branch-deploy、dev 或特定分支名称)设置变量的作用范围:
[context.production.environment]
NODE_VERSION = "20"
[context.deploy-preview.environment]
NOT_PRIVATE_ITEM = "not so secret"
避免以这种方式提交敏感值——机密信息应优先通过 UI 或 CLI 设置。
构建配置
netlify.toml
[build]
command = "npm run build"
publish = "dist"
functions = "netlify/functions"
[build.environment]
NODE_VERSION = "20"
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"
[functions]
node_bundler = "esbuild"
[dev]
command = "npm run dev"
port = 3000
targetPort = 5173
基于文件的上传
直接上传到 Functions
// netlify/functions/upload.mts
import { getStore } from '@netlify/blobs';
export default async (request: Request, context: Context) => {
const formData = await request.formData();
const file = formData.get('file') as File;
if (!file) {
return Response.json({ error: 'No file provided' }, { status: 400 });
}
const store = getStore('uploads');
const key = `${Date.now()}-${file.name}`;
await store.set(key, await file.arrayBuffer(), {
metadata: {
contentType: file.type,
originalName: file.name,
},
});
return Response.json({ key, message: 'Upload successful' });
};
站点管理
检查 .netlify/state.json 中是否有已填写的 siteId,即可确认站点是否已关联——如果缺少该文件,netlify init 会引导你将项目连接到 Netlify(包括在需要时设置仓库);如果站点已存在于 Netlify 上,netlify link 只会提示输入凭据,以便将本地文件夹连接到该站点。
创建和关联站点
# Initialize new site
netlify init
# Link existing site
netlify link
# Deploy manually
netlify deploy
# Deploy to production
netlify deploy --prod
本地开发
Netlify Dev
# Start local development server
netlify dev
# With specific port
netlify dev --port 8888
# With live reload
netlify dev --live
在本地测试 Functions
# Invoke function directly
netlify functions:invoke hello --payload '{"name": "World"}'
# Serve functions only
netlify functions:serve
错误处理最佳实践
结构化错误响应
interface ErrorResponse {
error: string;
code: string;
details?: unknown;
}
function errorResponse(status: number, error: ErrorResponse): Response {
return Response.json(error, { status });
}
export default async (request: Request, context: Context) => {
try {
// Validation
const body = await request.json();
if (!body.email) {
return errorResponse(400, {
error: 'Email is required',
code: 'MISSING_EMAIL',
});
}
// Business logic
const result = await processRequest(body);
return Response.json(result);
} catch (error) {
console.error('Function error:', error);
return errorResponse(500, {
error: 'Internal server error',
code: 'INTERNAL_ERROR',
});
}
};
安全指南
输入验证
import { z } from 'zod';
const RequestSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
});
export default async (request: Request, context: Context) => {
const body = await request.json();
const result = RequestSchema.safeParse(body);
if (!result.success) {
return Response.json(
{ error: 'Validation failed', details: result.error.issues },
{ status: 400 }
);
}
// Use validated data
const { email, name } = result.data;
};
身份验证
async function verifyToken(request: Request): Promise<User | null> {
const auth = request.headers.get('Authorization');
if (!auth?.startsWith('Bearer ')) {
return null;
}
const token = auth.slice(7);
// Verify token logic
return verifyJWT(token);
}
export default async (request: Request, context: Context) => {
const user = await verifyToken(request);
if (!user) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
// Authenticated request handling
};
应避免的常见问题
- 在
@netlify/functions导入语句中添加版本号 - 在没有明确需要时添加 CORS 标头
- 为使用场景选择错误的函数类型
- 忘记为后台函数添加
-background后缀 - 在后台函数中未使用 Blobs 进行持久化存储
- 忽略后台函数的 15 分钟超时限制
- 未验证无服务器函数中的输入
- 将环境变量硬编码
- 未在边缘节点妥善处理错误
- 使用无服务器函数执行更适合边缘函数的任务
- 提交
.netlify目录,而不是将其加入 git 忽略 - 除非用户提出要求,否则在新站点中向
netlify.toml/_redirects/_headers添加重定向或自定义标头 - 将非生产数据写入全局 Blobs 存储,而不是部署专用存储