mcpField
N
netlify-development

netlify-development

github.com/mindrally云与 DevOps287669 次安装3 周前更新
代码仓库

关于 netlify-development

面向 Netlify 项目开发者的构建与部署指南,介绍函数、数据存储、图像 CDN 和配置。它按函数类型说明适用场景与运行约束,并涵盖 Blobs 的存储范围和一致性。

主要功能

  1. 01Serverless、Edge、后台及定时函数指导
  2. 02函数路径与配置建议
  3. 03Netlify Blobs 存储实践
  4. 04Image CDN 参数与用法
  5. 05部署配置及运行限制说明

适用场景

  1. 01为 API 端点选择函数类型
  2. 02实现边缘请求处理与个性化
  3. 03使用 Blobs 保存应用数据
  4. 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、fill
  • fm:格式(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
};

应避免的常见问题

  1. 在 @netlify/functions 导入语句中添加版本号
  2. 在没有明确需要时添加 CORS 标头
  3. 为使用场景选择错误的函数类型
  4. 忘记为后台函数添加 -background 后缀
  5. 在后台函数中未使用 Blobs 进行持久化存储
  6. 忽略后台函数的 15 分钟超时限制
  7. 未验证无服务器函数中的输入
  8. 将环境变量硬编码
  9. 未在边缘节点妥善处理错误
  10. 使用无服务器函数执行更适合边缘函数的任务
  11. 提交 .netlify 目录,而不是将其加入 git 忽略
  12. 除非用户提出要求,否则在新站点中向 netlify.toml/_redirects/_headers 添加重定向或自定义标头
  13. 将非生产数据写入全局 Blobs 存储,而不是部署专用存储

评论

支持 Markdown