mcpField
V
vercel-blob

vercel-blob

github.com/jackspace云与 DevOps1644 次安装12 周前更新
代码仓库

关于 vercel-blob

vercel-blob 是使用 @vercel/blob 在 Vercel Blob 中存储文件的技能,面向构建文件上传与管理功能的开发者。内容涵盖服务端和客户端上传,以及列表、下载、删除和大文件分段上传。

主要功能

  1. 01服务端与客户端文件上传
  2. 02列表、下载与删除文件
  3. 03大文件分段上传
  4. 04公开与私有访问设置
  5. 05上传验证与安全建议

适用场景

  1. 01上传用户头像和文档
  2. 02管理图片库并分页列出文件
  3. 03通过客户端令牌接收文件
  4. 04上传超过 500 MB 的文件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jackspace/claudeskillz --skill vercel-blob

安装后在 Claude Code 中输入/vercel-blob确认已加载。

附带文件README.mdSKILL.json

详细介绍

Vercel Blob(对象存储)

状态:生产就绪 上次更新:2025-10-29 依赖项:无 最新版本:@vercel/[email protected]


快速入门(3 分钟)

1. 创建 Blob 存储
# In Vercel dashboard: Storage → Create Database → Blob
vercel env pull .env.local

创建:BLOB_READ_WRITE_TOKEN

2. 安装
npm install @vercel/blob
3. 上传文件(Server Action)
'use server';

import { put } from '@vercel/blob';

export async function uploadFile(formData: FormData) {
  const file = formData.get('file') as File;

  const blob = await put(file.name, file, {
    access: 'public' // or 'private'
  });

  return blob.url; // https://xyz.public.blob.vercel-storage.com/file.jpg
}

重要:

  • 直接从客户端上传时使用客户端上传令牌(不要暴露 BLOB_READ_WRITE_TOKEN)
  • 设置正确的 access 级别(public 或 private)
  • 文件会通过 CDN 自动分发

5 步设置流程

第 1 步:创建 Blob 存储

Vercel 控制台:

  1. Project → Storage → Create Database → Blob
  2. 复制 BLOB_READ_WRITE_TOKEN

本地开发:

vercel env pull .env.local

创建 .env.local:

BLOB_READ_WRITE_TOKEN="vercel_blob_rw_xxx"

要点:

  • 免费套餐:每月 100GB 带宽
  • 文件大小限制:每个文件 500MB
  • 自动通过 CDN 分发
  • 公共文件会在全球范围内缓存

第 2 步:服务端上传

Next.js Server Action:

'use server';

import { put } from '@vercel/blob';

export async function uploadAvatar(formData: FormData) {
  const file = formData.get('avatar') as File;

  // Validate file
  if (!file.type.startsWith('image/')) {
    throw new Error('Only images allowed');
  }

  if (file.size > 5 * 1024 * 1024) {
    throw new Error('Max file size: 5MB');
  }

  // Upload
  const blob = await put(`avatars/${Date.now()}-${file.name}`, file, {
    access: 'public',
    addRandomSuffix: false
  });

  return {
    url: blob.url,
    pathname: blob.pathname
  };
}

API 路由(Edge Runtime):

import { put } from '@vercel/blob';

export const runtime = 'edge';

export async function POST(request: Request) {
  const formData = await request.formData();
  const file = formData.get('file') as File;

  const blob = await put(file.name, file, { access: 'public' });

  return Response.json(blob);
}

第 3 步:客户端上传(预签名 URL)

创建上传令牌(Server Action):

'use server';

import { handleUpload, type HandleUploadBody } from '@vercel/blob/client';

export async function getUploadToken(filename: string) {
  const jsonResponse = await handleUpload({
    body: {
      type: 'blob.generate-client-token',
      payload: {
        pathname: `uploads/${filename}`,
        access: 'public',
        onUploadCompleted: {
          callbackUrl: `${process.env.NEXT_PUBLIC_URL}/api/upload-complete`
        }
      }
    },
    request: new Request('https://dummy'),
    onBeforeGenerateToken: async (pathname) => {
      // Optional: validate user permissions
      return {
        allowedContentTypes: ['image/jpeg', 'image/png', 'image/webp'],
        maximumSizeInBytes: 5 * 1024 * 1024 // 5MB
      };
    },
    onUploadCompleted: async ({ blob, tokenPayload }) => {
      console.log('Upload completed:', blob.url);
    }
  });

  return jsonResponse;
}

客户端上传:

'use client';

import { upload } from '@vercel/blob/client';
import { getUploadToken } from './actions';

export function UploadForm() {
  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const form = e.currentTarget;
    const file = (form.elements.namedItem('file') as HTMLInputElement).files?.[0];

    if (!file) return;

    const tokenResponse = await getUploadToken(file.name);

    const blob = await upload(file.name, file, {
      access: 'public',
      handleUploadUrl: tokenResponse.url
    });

    console.log('Uploaded:', blob.url);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="file" name="file" required />
      <button type="submit">Upload</button>
    </form>
  );
}

第 4 步:列出、下载和删除

列出文件:

import { list } from '@vercel/blob';

const { blobs } = await list({
  prefix: 'avatars/',
  limit: 100
});

// Returns: { url, pathname, size, uploadedAt, ... }[]

分页列出:

let cursor: string | undefined;
const allBlobs = [];

do {
  const { blobs, cursor: nextCursor } = await list({
    prefix: 'uploads/',
    cursor
  });
  allBlobs.push(...blobs);
  cursor = nextCursor;
} while (cursor);

下载文件:

// Files are publicly accessible if access: 'public'
const url = 'https://xyz.public.blob.vercel-storage.com/file.pdf';
const response = await fetch(url);
const blob = await response.blob();

删除文件:

import { del } from '@vercel/blob';

await del('https://xyz.public.blob.vercel-storage.com/file.jpg');

// Or delete multiple
await del([url1, url2, url3]);

第 5 步:流式传输和分段上传

流式上传:

import { put } from '@vercel/blob';
import { createReadStream } from 'fs';

const stream = createReadStream('./large-file.mp4');

const blob = await put('videos/large-file.mp4', stream, {
  access: 'public',
  contentType: 'video/mp4'
});

分段上传(大于 500MB 的文件):

import { createMultipartUpload, uploadPart, completeMultipartUpload } from '@vercel/blob';

// 1. Start multipart upload
const upload = await createMultipartUpload('large-video.mp4', {
  access: 'public'
});

// 2. Upload parts (chunks)
const partSize = 100 * 1024 * 1024; // 100MB chunks
const parts = [];

for (let i = 0; i < totalParts; i++) {
  const chunk = getChunk(i, partSize);
  const part = await uploadPart(chunk, {
    uploadId: upload.uploadId,
    partNumber: i + 1
  });
  parts.push(part);
}

// 3. Complete upload
const blob = await completeMultipartUpload({
  uploadId: upload.uploadId,
  parts
});

重要规则

务必做到

✅ 客户端上传时使用客户端上传令牌 - 切勿向客户端暴露 BLOB_READ_WRITE_TOKEN

✅ 设置正确的访问级别 - public(CDN)或 private(经过身份验证的访问)

✅ 验证文件类型和大小 - 上传前检查 MIME 类型和大小

✅ 按路径名组织文件 - 使用 avatars/、uploads/、documents/ 等目录结构

✅ 处理上传错误 - 网络故障、大小限制、令牌过期等

✅ 清理旧文件 - 删除未使用的文件以控制存储成本

✅ 显式设置 content-type - 确保浏览器正确处理文件(视频、PDF)

切勿这样做

❌ 切勿向客户端暴露 BLOB_READ_WRITE_TOKEN - 使用 handleUpload() 处理客户端上传

❌ 切勿跳过文件验证 - 上传前务必验证类型、大小和内容

❌ 切勿在未使用分段上传的情况下上传大于 500MB 的文件 - 大文件请使用分段上传

❌ 切勿使用通用文件名 - file.jpg 容易冲突,请使用 ${timestamp}-${name} 或 UUID

❌ 切勿假定上传一定成功 - 务必处理错误(网络、配额等)

❌ 切勿以未加密的方式存储敏感数据 - 如有需要,请先加密再上传

❌ 切勿忘记删除临时文件 - 旧上传文件会占用配额


已知问题预防

此技能可预防 10 个已记录的问题:

问题 #1:缺少环境变量

错误:Error: BLOB_READ_WRITE_TOKEN is not defined 来源:https://vercel.com/docs/storage/vercel-blob 原因:未在环境中设置令牌 预防措施:运行 vercel env pull .env.local,并确保 .env.local 已加入 .gitignore。

问题 #2:客户端上传令牌暴露

错误:安全漏洞,可能导致未经授权的上传 来源:https://vercel.com/docs/storage/vercel-blob/client-upload 原因:在客户端代码中直接使用 BLOB_READ_WRITE_TOKEN 预防措施:使用 handleUpload() 生成带有限制条件的客户端专用令牌。

问题 #3:超出文件大小限制

错误:Error: File size exceeds limit(500MB) 来源:https://vercel.com/docs/storage/vercel-blob/limits 原因:未使用分段上传就上传大于 500MB 的文件 预防措施:上传前验证文件大小;上传大文件时使用分段上传。

问题 #4:Content-Type 错误

错误:浏览器下载文件而不是显示文件(例如,PDF 被当作文本打开) 来源:生产环境调试 原因:未设置 contentType 选项,Blob 猜测的类型不正确 预防措施:始终设置 contentType: file.type 或明确指定 MIME 类型。

问题 #5:公共文件未缓存

错误:文件传输速度慢,出口流量费用高 来源:Vercel Blob 最佳实践 原因:对本应公开的文件使用 access: 'private' 预防措施:对可公开访问的文件使用 access: 'public'(CDN 缓存)。

问题 #6:未处理列表分页

错误:仅返回前 1000 个文件,遗漏其他文件 来源:https://vercel.com/docs/storage/vercel-blob/using-blob-sdk#list 原因:大型文件列表未使用游标迭代 预防措施:在循环中使用基于游标的分页,直到 cursor 未定义。

问题 #7:删除静默失败

错误:文件未删除,存储配额被占满 来源:https://github.com/vercel/storage/issues/150 原因:使用了错误的 URL 格式,未找到 Blob 预防措施:使用 put() 响应中的完整 Blob URL,并检查删除结果。

问题 #8:上传超时(大文件)

错误:文件 >100MB 时出现 Error: Request timeout 来源:Vercel 函数超时限制 原因:无服务器函数超时(免费套餐为 10s,专业套餐为 60s) 预防措施:对于大文件,使用客户端上传并调用 handleUpload()。

问题 #9:文件名冲突

错误:文件被覆盖,数据丢失 来源:生产环境调试 原因:多次上传使用相同文件名 预防措施:添加时间戳/UUID:`uploads/${Date.now()}-${file.name}`,或设置 addRandomSuffix: true。

问题 #10:缺少上传回调

错误:上传已完成,但应用状态未更新 来源:https://vercel.com/docs/storage/vercel-blob/client-upload#callback-after-upload 原因:未实现 onUploadCompleted 回调 预防措施:在 handleUpload() 中使用 onUploadCompleted 更新数据库/状态。


配置文件参考

package.json
{
  "dependencies": {
    "@vercel/blob": "^2.0.0"
  }
}
.env.local
BLOB_READ_WRITE_TOKEN="vercel_blob_rw_xxxxx"

常见模式

模式 1:头像上传
'use server';

import { put, del } from '@vercel/blob';

export async function updateAvatar(userId: string, formData: FormData) {
  const file = formData.get('avatar') as File;

  // Validate
  if (!file.type.startsWith('image/')) {
    throw new Error('Only images allowed');
  }

  // Delete old avatar
  const user = await db.query.users.findFirst({ where: eq(users.id, userId) });
  if (user?.avatarUrl) {
    await del(user.avatarUrl);
  }

  // Upload new
  const blob = await put(`avatars/${userId}.jpg`, file, {
    access: 'public',
    contentType: file.type
  });

  // Update database
  await db.update(users).set({ avatarUrl: blob.url }).where(eq(users.id, userId));

  return blob.url;
}
模式 2:受保护的文件上传
'use server';

import { put } from '@vercel/blob';
import { auth } from '@/lib/auth';

export async function uploadDocument(formData: FormData) {
  const session = await auth();
  if (!session) throw new Error('Unauthorized');

  const file = formData.get('document') as File;

  // Upload as private
  const blob = await put(`documents/${session.user.id}/${file.name}`, file, {
    access: 'private' // Requires authentication to access
  });

  // Store in database with user reference
  await db.insert(documents).values({
    userId: session.user.id,
    url: blob.url,
    filename: file.name,
    size: file.size
  });

  return blob;
}
模式 3:带分页的图片库
import { list } from '@vercel/blob';

export async function getGalleryImages(cursor?: string) {
  const { blobs, cursor: nextCursor } = await list({
    prefix: 'gallery/',
    limit: 20,
    cursor
  });

  const images = blobs.map(blob => ({
    url: blob.url,
    uploadedAt: blob.uploadedAt,
    size: blob.size
  }));

  return { images, nextCursor };
}

依赖项

必需:

  • @vercel/blob@^2.0.0 - Vercel Blob SDK

可选:

  • sharp@^0.33.0 - 上传前处理图像
  • zod@^3.24.0 - 文件验证架构

官方文档


软件包版本(已于 2025-10-29 验证)

{
  "dependencies": {
    "@vercel/blob": "^2.0.0"
  }
}

生产环境示例

  • 电子商务:产品图片、用户上传(50 万多个文件)
  • 博客平台:特色图片、作者头像
  • SaaS:文档上传、PDF 生成、CSV 导出
  • 错误:0(已预防所有 10 个已知问题)

故障排除

问题:BLOB_READ_WRITE_TOKEN is not defined

解决方案:运行 vercel env pull .env.local,确保 .env.local 位于 .gitignore 中。

问题:文件大小超出限制(>500MB)

解决方案:使用 createMultipartUpload() API 进行分段上传。

问题:客户端上传因令牌错误而失败

解决方案:确保在服务端使用 handleUpload(),不要向客户端暴露读写令牌。

问题:文件未删除

解决方案:使用 put() 响应中的确切 URL,并检查 del() 返回值。


完整设置清单

  • 已在 Vercel 控制面板中创建 Blob 存储
  • 已设置 BLOB_READ_WRITE_TOKEN 环境变量
  • 已安装 @vercel/blob 软件包
  • 已实现文件验证(类型、大小)
  • 客户端上传使用 handleUpload()(而非直接使用令牌)
  • 已为上传设置内容类型
  • 访问级别正确(public 与 private)
  • 已实现删除旧文件的功能
  • 列表分页能够处理游标
  • 已在本地和生产环境中测试文件上传、下载和删除

有疑问或遇到问题?

  1. 查看官方文档:https://vercel.com/docs/storage/vercel-blob
  2. 确认已设置环境变量
  3. 确保客户端上传使用客户端上传令牌
  4. 在 Vercel 控制面板中监控存储用量

评论

支持 Markdown