Vercel: 开发者体验即设计

Vercel如何将开发者体验做成产品:暗色模式优先设计、标签状态指示器、乐观UI、功能性空状态。包含CSS和JavaScript实现模式。

5 分钟阅读 221 字
Vercel: 开发者体验即设计 screenshot

Vercel:将开发者体验视为设计

"开发者对糟糕的用户体验极度敏感——如果'令人愉悦的引导流程'会拖慢他们的速度,他们根本不想要。"

Vercel 的设计理念是毫不妥协地以开发者为中心。Geist 设计系统将清晰度、速度和信息密度置于装饰之上。每一个像素都服务于开发者的工作流程。


为什么 Vercel 值得关注

Vercel 证明了开发者工具可以拥有卓越的设计,同时不显得"过度设计"。其仪表盘快速、信息密集,而且从不妨碍用户操作。

核心成就: - 创造了 Geist,一款专为开发者设计的字体 - 仪表盘重设计将首次有效绘制时间缩短了 1.2 秒 - 在开发者工具领域开创了深色模式优先的设计理念 - 树立了部署体验的行业标准 - 标签页图标能够反映部署状态(构建中、错误、就绪)


核心要点

  1. 深色模式是尊重,而非功能 - 开发者在终端和 IDE 中使用深色背景工作;白色仪表盘会造成刺眼的上下文切换和视觉疲劳
  2. 状态应该随处可见 - 标签页图标、页面标题、时间线圆点:无需切换焦点或打开标签页就能看到部署状态
  3. 乐观 UI 消除感知延迟 - 立即显示预期状态,后台与实际同步;开发者能察觉 300 毫秒的延迟
  4. 空状态是指引,而非插图 - 展示要执行的确切命令(git push origin main),而不是带有"开始使用"按钮的装饰性图形
  5. 性能即设计 - Vercel 仪表盘重设计将首次有效绘制时间缩短了 1.2 秒;再华丽的动画也无法弥补缓慢的加载时间

核心设计理念

以开发者为中心的原则

开发者评判产品的标准是它拖慢自己多少。Vercel 的设计正是基于这一点:

反模式(开发者厌恶的)                    VERCEL 的做法
───────────────────────────────────────────────────────────────────
增加延迟的"令人愉悦"的动画              即时响应,无过渡状态
阻碍工作的引导向导                      CLI 优先,仪表盘可选
隐藏在标签页中的密集文档                信息一目了然
每个操作都有加载转圈                    乐观更新 + SWR
仪表盘中的营销文案                      纯功能性 UI

核心洞察:开发者不想被"取悦"。他们想要发布。


模式库

1. 深色模式的卓越实践

Vercel 的深色模式不是一个开关选项,而是默认设置。设计如同手术刀般精准:纯黑与纯白创造最大对比度。

色彩理念:

:root {
  /* Vercel 调色板极其简洁 */

  /* 背景色 - 纯黑,无灰 */
  --bg-000: #000000;
  --bg-100: #0A0A0A;
  --bg-200: #111111;

  /* 前景色 - 高对比度白色 */
  --fg-100: #FFFFFF;
  --fg-200: #EDEDED;
  --fg-300: #A1A1A1;
  --fg-400: #888888;

  /* 边框 - 微妙但可见 */
  --border-100: #333333;
  --border-200: #444444;

  /* 语义色 - 部署状态 */
  --color-success: #00DC82;  /* 绿色 - 已部署 */
  --color-error: #FF0000;    /* 红色 - 失败 */
  --color-warning: #FFAA00;  /* 琥珀色 - 构建中 */
  --color-info: #0070F3;     /* 蓝色 - 队列中 */

  /* 强调色 - Vercel 的标志性配色 */
  --accent: #FFFFFF;         /* 黑底之上以白色为强调 */
}

纯黑为何有效: - 文字可读性达到最大对比度 - 终端风格的美学让开发者感到信任 - 在暗光环境中减少视觉疲劳 - 让彩色状态指示器更加醒目


2. 标签页状态指示器

Vercel 在浏览器标签页图标中反映部署状态,即使标签页未被聚焦也能看到信息。

┌─ 浏览器标签栏 ─────────────────────────────────────────────────────┐
│                                                                    │
│  [▶] acme-web - Building    [✓] blog - Ready    [✕] api - Error   │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

标签图标状态:
  ⏳ 队列中(灰色圆圈)
  ▶  构建中(动态转圈)
  ✓  就绪(绿色勾号)
  ✕  错误(红色叉号)

实现模式:

// 基于部署状态的动态 favicon
function updateFavicon(status) {
  const link = document.querySelector("link[rel~='icon']");

  const icons = {
    queued: '/favicon-queued.svg',
    building: '/favicon-building.svg',  // 动态
    ready: '/favicon-ready.svg',
    error: '/favicon-error.svg',
  };

  link.href = icons[status];
}

// 标题也反映状态
function updateTitle(projectName, status) {
  const prefixes = {
    queued: '⏳',
    building: '▶',
    ready: '✓',
    error: '✕',
  };

  document.title = `${prefixes[status]} ${projectName} - Vercel`;
}

核心洞察:开发者打开着许多标签页。在标签栏中可见的状态意味着他们无需切换标签页就能检查构建状态。


3. 部署时间线

部署检查器展示了清晰的部署流程时间线。

┌─ 部署时间线 ───────────────────────────────────────────────────────┐
│                                                                    │
│  [o] 队列中                                  12:34:56 PM           │
│  │                                                                 │
│  [o] 构建中                                  12:34:58 PM           │
│  │ └─ 安装依赖... 3.2s                                             │
│  │ └─ 构建中... 12.4s                                              │
│  │ └─ 生成静态页面... 2.1s                                         │
│  │                                                                 │
│  [o] 部署中                                  12:35:14 PM           │
│  │ └─ 上传构建产物...                                              │
│  │                                                                 │
│  [*] 就绪                                    12:35:18 PM           │
│    └─ https://acme-abc123.vercel.app                               │
│                                                                    │
│  总计: 22s                                                         │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

视觉编码:

.timeline-step {
  position: relative;
  padding-left: 24px;
}

.timeline-step::before {
  content: '';
  position: absolute;
  left: 0;
  top: 6px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--step-color);
}

/* 连接线 */
.timeline-step:not(:last-child)::after {
  content: '';
  position: absolute;
  left: 4px;
  top: 16px;
  width: 2px;
  height: calc(100% - 6px);
  background: var(--border-100);
}

/* 步骤状态 */
.timeline-step[data-status="complete"]::before {
  background: var(--color-success);
}

.timeline-step[data-status="active"]::before {
  background: var(--color-warning);
  animation: pulse 1.5s infinite;
}

.timeline-step[data-status="error"]::before {
  background: var(--color-error);
}

.timeline-step[data-status="pending"]::before {
  background: var(--fg-400);
}

4. 日志查看器设计

Vercel 的日志查看器集成在部署概览中,而非独立页面。

┌─ Build Logs ───────────────────────────────────────────────────────┐
│                                                                    │
│  Filter: [All ▼]  [Function: api/hello ▼]           [Copy] [↓]    │
│                                                                    │
├────────────────────────────────────────────────────────────────────┤
│                                                                    │
│  12:34:58.123  info   Installing dependencies...                   │
│  12:35:01.456  info   added 1234 packages in 3.2s                  │
│  12:35:01.789  info   Running build...                             │
│  12:35:14.012  info   ✓ Compiled successfully                      │
│  12:35:14.234  warn   Large bundle size: pages/index.js (245kb)    │
│  12:35:14.567  info   Generating static pages...                   │
│  12:35:16.890  info   ✓ Generated 42 pages                         │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

核心特性: - 一键复制到剪贴板 - 按函数或构建输出筛选 - 日志级别颜色编码(info、warn、error) - 毫秒级精度时间戳 - 可分享特定日志行的 URL

实现方式:

.log-line {
  display: flex;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.6;
  padding: 2px 12px;
}

.log-line:hover {
  background: var(--bg-200);
}

.log-timestamp {
  color: var(--fg-400);
  min-width: 100px;
  margin-right: 12px;
}

.log-level {
  min-width: 48px;
  margin-right: 12px;
}

.log-level[data-level="info"] { color: var(--fg-300); }
.log-level[data-level="warn"] { color: var(--color-warning); }
.log-level[data-level="error"] { color: var(--color-error); }

.log-message {
  color: var(--fg-100);
  white-space: pre-wrap;
  word-break: break-word;
}

5. 空状态设计

Vercel 的空状态注重功能性而非装饰性。它们告诉你下一步该做什么。

┌─ Empty State: No Deployments ──────────────────────────────────────┐
│                                                                    │
│                                                                    │
│                         No deployments yet                         │
│                                                                    │
│               Push to your repository to create                    │
│                    your first deployment                           │
│                                                                    │
│                                                                    │
│           git push origin main                                     │
│                                                                    │
│                                                                    │
│                         [View Documentation]                       │
│                                                                    │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

设计原则: - 无装饰性插图 - 明确的操作指引(git 命令) - 有用的文档链接 - 命令使用等宽字体(便于复制)


视觉设计系统

字体排版(Geist)

Vercel 专为开发者体验创建了 Geist 字体:

:root {
  /* Geist Sans - UI 和正文 */
  --font-sans: 'Geist', -apple-system, BlinkMacSystemFont, sans-serif;

  /* Geist Mono - 代码和技术内容 */
  --font-mono: 'Geist Mono', 'SF Mono', monospace;

  /* 字号比例 */
  --text-xs: 12px;
  --text-sm: 13px;
  --text-base: 14px;
  --text-lg: 16px;
  --text-xl: 18px;
  --text-2xl: 24px;

  /* 行高 */
  --leading-tight: 1.25;
  --leading-normal: 1.5;
  --leading-relaxed: 1.75;

  /* 字符间距 */
  --tracking-tight: -0.02em;
  --tracking-normal: 0;
  --tracking-wide: 0.02em;
}

/* 数据使用等宽数字 */
.tabular-nums {
  font-variant-numeric: tabular-nums;
}

/* 或使用 Geist Mono 进行数据比较 */
.data-value {
  font-family: var(--font-mono);
}

间距系统

:root {
  /* 4px 基础单位 */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-8: 32px;
  --space-10: 40px;
  --space-12: 48px;
  --space-16: 64px;
}

边框圆角

:root {
  /* Subtle, consistent radii */
  --radius-sm: 4px;
  --radius-md: 6px;
  --radius-lg: 8px;
  --radius-xl: 12px;
  --radius-full: 9999px;
}

动画模式

乐观更新

Vercel 采用乐观 UI 更新策略,操作响应几乎瞬时完成。

// SWR pattern for realtime updates
const { data, mutate } = useSWR('/api/deployments');

async function triggerDeploy() {
  // Immediately show "deploying" state
  mutate(
    { ...data, status: 'building' },
    false  // Don't revalidate yet
  );

  // Then actually trigger
  await fetch('/api/deploy', { method: 'POST' });

  // Revalidate to get real state
  mutate();
}

微妙的加载状态

/* Skeleton loading - no spinners */
.skeleton {
  background: linear-gradient(
    90deg,
    var(--bg-200) 0%,
    var(--bg-100) 50%,
    var(--bg-200) 100%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  border-radius: var(--radius-md);
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

按钮状态

.button {
  transition: background 100ms ease, transform 100ms ease;
}

.button:hover {
  background: var(--fg-100);
}

.button:active {
  transform: scale(0.98);
}

/* No long transitions - instant feedback */

性能优化(设计驱动)

Vercel 仪表盘的重新设计包含了多项提升性能的设计决策:

采用的技术: - 预连接 API、静态资源和头像源 - 关键 API 调用获得更高的浏览器优先级 - 对 React 组件进行记忆化处理(useMemo、useCallback) - ReactDOM.unstable_batchedUpdates 减少了 20% 的重渲染 - 使用 SWR 实现高效的实时数据更新

核心洞察:性能本身就是设计。一个动画华丽但响应迟缓的仪表盘,不如一个没有动画但速度飞快的仪表盘。


对我们工作的启示

1. 深色模式作为默认

当你的用户在深色环境中工作(终端、IDE),深色模式不是一个功能——而是一种尊重。

2. 状态无处不在

标签图标、页面标题、时间线指示器:状态应该无需聚焦即可感知。

3. 默认乐观更新

立即展示预期状态,在后台同步真实结果。

4. 开发者讨厌等待

尽可能避免加载转圈。使用骨架屏、乐观更新和预加载。

5. 空状态就是操作指南

不要展示漂亮的插图,而是展示用户需要执行的命令。


常见问题

为什么 Vercel 使用纯黑色(#000000)而不是深灰色作为背景?

纯黑色为白色文字提供最大对比度,实现最佳可读性。它也与开发者日常使用的终端和代码编辑器的美学风格一致,使仪表盘感觉像是工作流程中的原生组成部分。深灰色背景往往显得"柔和",但会降低对比度,在高分辨率显示器上可能显得暗淡。

Vercel 的标签页状态指示器是如何工作的?

Vercel 根据部署状态动态更新浏览器 favicon:构建中显示转圈动画,就绪显示绿色对勾,错误显示红色叉号。页面标题也会更新表情符号前缀(▶、✓、✕)。这意味着开发者可以在多个标签页中监控多个部署而无需切换焦点——在浏览器标签栏一眼就能看到状态。

Vercel 的加载状态策略是什么?

Vercel 避免使用传统的加载转圈,而是采用乐观 UI 和骨架屏。当你触发部署时,UI 会在服务器确认之前立即显示"构建中"状态。SWR 库处理后台重新验证。这使得操作感觉是即时的,即使网络请求需要 200-500 毫秒。

什么是 Geist?为什么 Vercel 要创建自定义字体?

Geist 是 Vercel 专为开发者界面设计的字体家族。它包含用于 UI 文本的 Geist Sans 和用于代码的 Geist Mono。该设计针对仪表盘中常见的小字号(12-14px)进行了优化,包含表格数字以实现数据列对齐,并且具有独特的字符形状以防止相似字形之间的混淆(l、1、I)。

Vercel 处理空状态的方式与其他产品有何不同?

Vercel 的空状态展示可执行的命令,而不是装饰性插图。空的部署页面以等宽字体显示 git push origin main(便于复制),而不是展示一个带有"开始使用"按钮的卡通图片。其理念是开发者想要确切知道该做什么,而不是被视觉抚慰。