netlify-development
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseNetlify Development Best Practices
Netlify开发最佳实践
Overview
概述
This skill provides comprehensive guidelines for building and deploying projects on Netlify, covering serverless functions, edge functions, background functions, scheduled functions, Netlify Blobs, Image CDN, and deployment configuration.
本技能提供了在Netlify上构建和部署项目的全面指南,涵盖无服务器函数、边缘函数、后台函数、定时函数、Netlify Blobs、Image CDN以及部署配置。
Core Principles
核心原则
- Use in-code configuration via exported objects (preferred over netlify.toml)
config - Never add version numbers to imported Netlify packages
- Only add CORS headers when explicitly required
- Leverage appropriate function types for different use cases
- Use Netlify Blobs for state and data storage
- 通过导出的对象使用代码内配置(优先于netlify.toml)
config - 切勿为导入的Netlify包添加版本号
- 仅在明确需要时添加CORS头
- 根据不同用例选择合适的函数类型
- 使用Netlify Blobs进行状态和数据存储
Function Types Overview
函数类型概述
| Type | Use Case | Timeout | Path Convention |
|---|---|---|---|
| Serverless | Standard API endpoints | 10s (26s Pro) | |
| Edge | Request/response modification | 50ms CPU | Custom paths |
| Background | Long-running async tasks | 15 minutes | |
| Scheduled | Cron-based tasks | 10s (26s Pro) | Configured schedule |
| 类型 | 适用场景 | 超时时间 | 路径约定 |
|---|---|---|---|
| Serverless | 标准API端点 | 10秒(专业版26秒) | |
| Edge | 请求/响应修改 | 50ms CPU时长 | 自定义路径 |
| Background | 长时间异步任务 | 15分钟 | 后缀为 |
| Scheduled | 基于Cron的定时任务 | 10秒(专业版26秒) | 配置定时规则 |
Serverless Functions
无服务器函数
Basic Structure
基本结构
typescript
// 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',
};typescript
// 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',
};Configuration Options
配置选项
typescript
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,
},
};typescript
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,
},
};Path Conventions
路径约定
- Default path:
/.netlify/functions/{function_name} - Custom paths via config completely replace the default
- Use custom paths for cleaner API URLs
- 默认路径:
/.netlify/functions/{function_name} - 通过config配置的自定义路径会完全替代默认路径
- 使用自定义路径以获得更简洁的API URL
Edge Functions
边缘函数
Use Cases
适用场景
- Modify requests before they reach the origin
- Modify responses before returning to users
- Geolocation-based personalization
- A/B testing
- Authentication at the edge
- 在请求到达源站之前修改请求
- 在响应返回给用户之前修改响应
- 基于地理位置的个性化定制
- A/B测试
- 边缘节点处的身份验证
Implementation
实现示例
typescript
// 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/*'],
};typescript
// 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/*'],
};Response Modification
响应修改
typescript
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,
});
};typescript
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,
});
};Background Functions
后台函数
Key Characteristics
核心特性
- 15-minute timeout (wall clock time)
- Immediately return 202 status code
- Return values are ignored
- Must have suffix
-background
- 15分钟超时(挂钟时间)
- 立即返回202状态码
- 返回值会被忽略
- 必须带有后缀
-background
Implementation
实现示例
typescript
// 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',
};typescript
// 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',
};Retrieving Background Results
获取后台函数执行结果
typescript
// 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 });
};typescript
// 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 });
};Scheduled Functions
定时函数
Configuration
配置示例
typescript
// 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
};typescript
// 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
};Schedule Patterns
定时规则示例
typescript
// 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
};typescript
// Common patterns
export const config = {
schedule: '@hourly', // 每小时执行一次
schedule: '@daily', // 每天午夜执行
schedule: '@weekly', // 每周执行一次
schedule: '*/15 * * * *', // 每15分钟执行一次
schedule: '0 9 * * 1-5', // 工作日上午9点执行
};Netlify Blobs
Netlify Blobs
Basic Usage
基础用法
typescript
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();typescript
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();Binary Data
二进制数据处理
typescript
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' });typescript
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' });Deploy-specific vs Site-wide
站点级与部署级存储
typescript
// 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,
});typescript
// 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
Netlify Image CDN
Usage
使用示例
html
<!-- Basic optimization -->
<img src="/.netlify/images?url=/images/hero.jpg&w=800&q=80" alt="Hero">
<!-- With fit and format -->
<img src="/.netlify/images?url=/images/hero.jpg&w=400&h=300&fit=cover&fm=webp" alt="Hero">html
<!-- Basic optimization -->
<img src="/.netlify/images?url=/images/hero.jpg&w=800&q=80" alt="Hero">
<!-- With fit and format -->
<img src="/.netlify/images?url=/images/hero.jpg&w=400&h=300&fit=cover&fm=webp" alt="Hero">Parameters
参数说明
- : Source image path (required)
url - : Width in pixels
w - : Height in pixels
h - : Quality (1-100)
q - : cover, contain, fill
fit - : Format (webp, avif, auto)
fm
- : 源图片路径(必填)
url - : 图片宽度(像素)
w - : 图片高度(像素)
h - : 图片质量(1-100)
q - : 适配方式(cover, contain, fill)
fit - : 图片格式(webp, avif, auto)
fm
Programmatic Usage
程序化调用
typescript
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}`;
}typescript
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}`;
}Environment Variables
环境变量
Access in Functions
在函数中访问环境变量
typescript
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
};typescript
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
};Context Variables
上下文变量
typescript
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);
};typescript
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);
};Build Configuration
构建配置
netlify.toml
netlify.toml示例
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 = 5173toml
[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 = 5173File-based Uploads
基于文件的上传
Direct Upload to Functions
直接上传至函数
typescript
// 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' });
};typescript
// 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' });
};Site Management
站点管理
Creating and Linking Sites
创建与关联站点
bash
undefinedbash
undefinedInitialize new site
Initialize new site
netlify init
netlify init
Link existing site
Link existing site
netlify link
netlify link
Deploy manually
Deploy manually
netlify deploy
netlify deploy
Deploy to production
Deploy to production
netlify deploy --prod
undefinednetlify deploy --prod
undefinedLocal Development
本地开发
Netlify Dev
Netlify Dev使用
bash
undefinedbash
undefinedStart local development server
Start local development server
netlify dev
netlify dev
With specific port
With specific port
netlify dev --port 8888
netlify dev --port 8888
With live reload
With live reload
netlify dev --live
undefinednetlify dev --live
undefinedTesting Functions Locally
本地测试函数
bash
undefinedbash
undefinedInvoke function directly
Invoke function directly
netlify functions:invoke hello --payload '{"name": "World"}'
netlify functions:invoke hello --payload '{"name": "World"}'
Serve functions only
Serve functions only
netlify functions:serve
undefinednetlify functions:serve
undefinedError Handling Best Practices
错误处理最佳实践
Structured Error Responses
结构化错误响应
typescript
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',
});
}
};typescript
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',
});
}
};Security Guidelines
安全指南
Input Validation
输入验证
typescript
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;
};typescript
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;
};Authentication
身份验证
typescript
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
};typescript
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
};Common Pitfalls to Avoid
需避免的常见陷阱
- Adding version numbers to imports
@netlify/functions - Adding CORS headers when not explicitly needed
- Using wrong function type for the use case
- Forgetting suffix for background functions
-background - Not using Blobs for persistent storage in background functions
- Ignoring the 15-minute timeout for background functions
- Not validating input in serverless functions
- Hardcoding environment variables
- Not handling errors appropriately at the edge
- Using serverless functions for tasks better suited to edge functions
- 为导入添加版本号
@netlify/functions - 在不需要时添加CORS头
- 为用例选择错误的函数类型
- 后台函数忘记添加后缀
-background - 后台函数中未使用Blobs进行持久化存储
- 忽略后台函数的15分钟超时限制
- 无服务器函数中未验证输入
- 硬编码环境变量
- 边缘节点处未妥善处理错误
- 使用无服务器函数处理更适合边缘函数的任务