组件库改版怕截图标不清?用 Storybook 搭本地组件预览页,再借 cpolar 给前端和测试临时验收

组件库改版怕截图标不清?用 Storybook 搭本地组件预览页,再借 cpolar 给前端和测试临时验收

组件库改版怕截图标不清?用 Storybook 搭本地组件预览页,再借 cpolar 给前端和测试临时验收

组件库一改版,最怕的不是写代码,而是对齐细节:按钮 hover 后颜色深一点、禁用态文字灰一点、卡片间距差 4px、弹窗遮罩透明度不对。

这些东西靠截图聊,很快就变成“你看我标红的这里”“我手机上看到不是这样”。我更推荐的做法是:本地用 Storybook 把组件状态整理成一页页可交互 Demo,再用 cpolar 临时开一个 HTTPS 地址,让前端、UI、测试同事短时间在线验收。

这篇不讲复杂工程化,也不讲生产部署。目标很明确:在本机跑一个只监听127.0.0.1的 Storybook 预览页,里面只放按钮、卡片、表单、弹窗这些演示组件,然后用 cpolar 短时开放给同事看,验收结束马上关掉。

为什么截图越发越乱

组件库改版后的验收,经常卡在“状态”上。

按钮不是只有默认态,还有 primary、danger、disabled、loading、hover、focus;表单不是只有空白态,还有必填提示、长度校验、提交中;弹窗也不只是打开那一刻,还包括遮罩、关闭、确认、取消、滚动内容。

我踩过最典型的坑,是一轮改版里大家在群里来回发了十几张图。UI 说按钮圆角不对,前端说自己看到的是新样式,测试又补了一张低分辨率截图,最后谁也说不清当前讨论的是哪一次提交。等问题定位清楚,真正要改的只是一行 CSS。

截图能说明静态布局,但说明不了交互。更麻烦的是,同一个组件在不同页面里被业务样式污染后,截图里到底是组件库的问题,还是业务页面的问题,很难一句话讲清楚。

Storybook 的价值就在这里:把组件从业务页面里拿出来,只看组件本身。每个状态都是一个 story,同事点开链接就能切换、操作、复现,不用拉代码,也不用搭完整业务环境。

我会把它当成“组件验收桌面”:按钮放按钮区,卡片放卡片区,表单和弹窗各自独立。谁反馈问题,就直接报 story 名称和状态名。这样改版讨论会从“看这张图的左下角”变成“Button / Loading 的禁用颜色偏浅”,沟通质量完全不一样。

准备一个干净的示例项目

这里用 Vite + React 做演示。真实项目里,你可以把步骤放在组件库仓库的临时分支,也可以单独起一个 demo 项目。安全起见,我建议演示项目只放公开样式和示例组件,不要直接导入真实业务接口。

mkdir storybook-component-review cd storybook-component-review npm create vite@latest . -- --template react-ts npm install

安装 Storybook:

npx storybook@latest init

初始化完成后,项目里会出现.storybook目录和src/stories示例文件。为了让预览页只在本机监听,启动时加上 host 参数:

npm run storybook -- --host 127.0.0.1 --port 6006

浏览器打开:

http://127.0.0.1:6006

这里有个边界要守住:不要为了“方便同事访问”把 Storybook 直接监听到所有网卡。本地预览就是本地预览,对外访问交给 cpolar 的临时隧道处理,范围更清楚,也更方便收尾。

写一个按钮组件,把状态摆出来

先创建组件目录:

mkdir -p src/components/Button

新建src/components/Button/Button.tsx

import './Button.css'; type ButtonProps = { children: string; variant?: 'primary' | 'secondary' | 'danger'; disabled?: boolean; loading?: boolean; onClick?: () => void; }; export function Button({ children, variant = 'primary', disabled = false, loading = false, onClick, }: ButtonProps) { return ( <button className={`demo-button demo-button--${variant}`} disabled={disabled || loading} onClick={onClick} > {loading ? '处理中...' : children} </button> ); }

新建src/components/Button/Button.css

.demo-button { border: 0; border-radius: 8px; padding: 10px 18px; color: #fff; cursor: pointer; font-size: 14px; } .demo-button--primary { background: #2563eb; } .demo-button--secondary { background: #64748b; } .demo-button--danger { background: #dc2626; } .demo-button:hover:not(:disabled) { filter: brightness(0.92); } .demo-button:disabled { cursor: not-allowed; opacity: 0.55; }

再写 story:src/components/Button/Button.stories.tsx

import type { Meta, StoryObj } from '@storybook/react'; import { Button } from './Button'; const meta: Meta<typeof Button> = { title: '组件验收/Button 按钮', component: Button, args: { children: '保存设置' }, }; export default meta; type Story = StoryObj<typeof Button>; export const Primary: Story = { args: { variant: 'primary' } }; export const Danger: Story = { args: { variant: 'danger', children: '删除' } }; export const Disabled: Story = { args: { disabled: true, children: '不可点击' } }; export const Loading: Story = { args: { loading: true, children: '提交' } };

这样 UI 同事验收按钮时,不需要你发四张图。一个页面里就能看到默认、危险、禁用、加载状态。

卡片布局:把间距和内容长度测清楚

卡片最容易出现“设计稿看着挺好,真实文案一长就炸”的问题。我们给卡片写两种状态:短标题和长标题。

src/components/Card/Card.tsx

import './Card.css'; type CardProps = { title: string; desc: string; tag?: string; }; export function Card({ title, desc, tag = '组件库' }: CardProps) { return ( <section className="demo-card"> <span className="demo-card__tag">{tag}</span> <h3>{title}</h3> <p>{desc}</p> </section> ); }

src/components/Card/Card.css

.demo-card { width: 320px; border: 1px solid #e2e8f0; border-radius: 14px; padding: 18px; background: #fff; box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08); } .demo-card__tag { display: inline-block; margin-bottom: 10px; color: #2563eb; font-size: 12px; } .demo-card h3 { margin: 0 0 8px; font-size: 18px; } .demo-card p { margin: 0; color: #64748b; line-height: 1.7; }

src/components/Card/Card.stories.tsx

import type { Meta, StoryObj } from '@storybook/react'; import { Card } from './Card'; const meta: Meta<typeof Card> = { title: '组件验收/Card 卡片', component: Card, }; export default meta; type Story = StoryObj<typeof Card>; export const Normal: Story = { args: { title: '基础信息卡片', desc: '用于展示一段简短说明。' }, }; export const LongText: Story = { args: { title: '组件库改版后的长标题卡片展示效果', desc: '这里放一段稍长的演示文案,用来检查换行、间距、阴影和边框是否符合验收要求。', }, };

这一步很适合给 UI 看。对方可以直接指出:标题行高、卡片宽度、阴影强度、标签颜色哪里不对。比在聊天窗口里圈图要省心很多。

表单校验:别只截空表单

表单验收一定要看错误态。下面写一个最小登录表单,不接真实接口,只在前端本地做演示校验。

src/components/LoginForm/LoginForm.tsx

import { useState } from 'react'; import './LoginForm.css'; export function LoginForm() { const [email, setEmail] = useState(''); const [touched, setTouched] = useState(false); const invalid = touched && !email.includes('@'); return ( <form className="demo-form" onSubmit={(e) => e.preventDefault()}> <label>邮箱</label> <input value={email} onBlur={() => setTouched(true)} onChange={(e) => setEmail(e.target.value)} placeholder="demo@example.com" /> {invalid && <p className="demo-form__error">请输入正确的邮箱格式</p>} <button type="submit">提交演示</button> </form> ); }

src/components/LoginForm/LoginForm.stories.tsx

import type { Meta, StoryObj } from '@storybook/react'; import { LoginForm } from './LoginForm'; const meta: Meta<typeof LoginForm> = { title: '组件验收/LoginForm 表单', component: LoginForm, }; export default meta; type Story = StoryObj<typeof LoginForm>; export const Basic: Story = {};

测试同事打开后,可以亲自输入一段错误邮箱,检查提示文案、红色样式、输入框焦点状态。这里全程没有真实登录,没有请求后端,也没有 token,更适合作为远程临时验收页面。

弹窗交互:把打开和关闭跑一遍

弹窗截图最容易漏掉遮罩、关闭按钮、确认按钮这类细节。写一个本地 Demo 就够了。

src/components/ConfirmModal/ConfirmModal.tsx

import { useState } from 'react'; import { Button } from '../Button/Button'; import './ConfirmModal.css'; export function ConfirmModal() { const [open, setOpen] = useState(false); return ( <> <Button onClick={() => setOpen(true)}>打开弹窗</Button> {open && ( <div className="demo-modal__mask"> <div className="demo-modal"> <h3>确认提交改版方案?</h3> <p>这是演示弹窗,只用于检查遮罩、间距和按钮布局。</p> <div className="demo-modal__actions"> <button onClick={() => setOpen(false)}>取消</button> <button onClick={() => setOpen(false)}>确认</button> </div> </div> </div> )} </> ); }

src/components/ConfirmModal/ConfirmModal.stories.tsx

import type { Meta, StoryObj } from '@storybook/react'; import { ConfirmModal } from './ConfirmModal'; const meta: Meta<typeof ConfirmModal> = { title: '组件验收/ConfirmModal 弹窗', component: ConfirmModal, }; export default meta; type Story = StoryObj<typeof ConfirmModal>; export const Basic: Story = {};

跑到这里,本地 Storybook 已经能覆盖按钮状态、卡片布局、表单校验、弹窗交互四类高频验收点。

本机先验一遍,再发给别人

发送链接前,我会先在本机做一遍快速检查:

npm run storybook -- --host 127.0.0.1 --port 6006

检查清单很简单:

  • Storybook 左侧分组是否清楚,比如统一放在“组件验收”下面;
  • 按钮的禁用、加载、危险状态是否都能看到;
  • 卡片长文案是否撑破布局;
  • 表单错误提示是否能触发;
  • 弹窗打开、取消、确认是否能正常关闭;
  • 页面里没有真实客户名称、内部域名、接口地址和凭据。

确认没问题后,再进入 cpolar 环节。

用 cpolar 生成临时 HTTPS 地址

cpolar 的作用不是替代部署,而是给本机服务开一个短时入口。Storybook 仍然只监听127.0.0.1:6006,对外访问通过 cpolar 转发。

这个区别要讲清楚:Storybook 不变成线上站点,cpolar 也不承担长期访问。它只是把你本机那一份演示页面,临时递给远程同事验收。验收窗口结束,隧道关闭,链接失效,事情就收住了。

如果本机已经安装并登录 cpolar,直接执行:

cpolar http 127.0.0.1:6006

命令运行后,终端会显示一个 HTTPS 访问地址,格式类似:

https://xxxx.cpolar.top

把这个地址发给前端、UI、测试同事即可。建议附上一句说明,减少误用:

这是组件库改版验收临时链接,只包含按钮、卡片、表单、弹窗 Demo。 请在今天 18:00 前查看交互状态,验收结束后链接会关闭。

手机端同事也可以直接打开这个 HTTPS 地址,检查移动端宽度下的卡片和弹窗。测试同事可以点击表单、触发错误提示,前端同事可以核对按钮状态和组件行为。

这里一定要守住安全边界

这类临时预览页最容易犯的错,是顺手把真实业务环境也带进来。我的规则很硬:只放演示组件和公开样式,不接真实业务接口。

具体边界如下:

  • 不导入真实客户数据,文案全部用demo@example.com、示例标题、虚构描述;
  • 不调用登录、订单、支付、用户资料等真实接口;
  • 不把数据库端口、缓存服务、Docker 控制接口、后台管理页挂到 cpolar;
  • 不在 story 里写 token、cookie、内部域名、源码仓库私密路径;
  • 不把完整业务页面当作“组件预览”直接开放;
  • cpolar 链接只给参与验收的人,限定时间使用。

Storybook 适合展示组件,不适合展示秘密。把这个边界讲清楚,团队用起来会更放心。

远程验收时怎么收反馈

我一般会让同事按组件分组反馈,不要混在一条消息里:

Button:危险按钮 hover 后颜色过深,disabled 透明度 OK。 Card:长标题两行时底部间距偏小。 LoginForm:错误提示文案 OK,输入框红框需要加粗。 ConfirmModal:遮罩透明度 OK,确认按钮位置需要和设计稿对齐。

这里还有一个小技巧:每次改动后,在群里只说“已更新 Button / Danger 和 Card / LongText”。不要把整个组件库都重新拉进讨论。验收对象越小,反馈越准,返工越少。

这样前端改起来很快。每改一轮,本地热更新会刷新 Storybook,同事刷新临时链接就能继续看。整个过程不需要反复导出截图,也不需要让测试拉分支启动项目。

如果验收跨设备,记得让手机端同事也看一次。组件库的问题经常藏在小屏幕里,尤其是弹窗宽度、按钮换行和卡片内容溢出。

临时开放后的收尾清单

验收结束后不要把链接晾着。我的收尾动作固定做一遍:

  • 关闭 cpolar 进程,撤回这次临时 HTTPS 链接;
  • 停止 Storybook 服务,确认127.0.0.1:6006不再提供预览;
  • 在群里说明临时链接已关闭,旧链接不再用于验收;
  • 清理演示账号、演示分支、演示数据和临时文案;
  • 检查 story 文件,保留本地自用边界,只留下组件演示和公开样式;
  • 如果组件 Demo 还要长期保留,把它纳入正常代码评审,不把 cpolar 链接当长期入口。

说白了,Storybook 负责把组件状态讲清楚,cpolar 负责把本机预览短时间递到远程同事面前。两者配合起来,刚好解决“截图标不清、交互验不了”的老问题。

这套流程不重,但很实用。尤其是组件库改版这种细节密集的活,把按钮、卡片、表单、弹窗拆开给大家看,反馈会清楚很多,沟通成本也会降下来。