
1. 项目概述为什么一个侧边导航组件能牵动整个设计系统的神经“设计系统中的侧边导航组件从全局混乱到高效协同”——这个标题乍看是讲一个UI控件实则直击设计系统落地中最常被低估、却最易引发连锁崩塌的薄弱环节。我带过不下十个跨团队中大型产品线的设计系统共建项目几乎每个都卡在侧边导航上前端开发反复改样式、设计师每次更新菜单结构都要手动同步七八个Figma文件、产品经理提需求时发现“用户管理”在A系统里叫“成员中心”在B系统里又变成“组织架构”而底层代码里连路由路径都不统一。这不是组件本身有多复杂而是它天然处于信息架构、视觉语言、交互逻辑、前端实现、权限控制、国际化支持六大维度的交汇点。它像设计系统的“脊椎”撑不起它整个系统就站不直它一旦错位所有依赖它的页面都会轻微但持续地“偏头痛”。所以标题里说的“全局混乱”不是夸张——它是真实存在的多米诺骨牌效应一个图标尺寸不一致可能触发设计师重画整套图标库一个折叠逻辑没对齐会让三个前端团队各自维护一套JS逻辑一个权限节点命名模糊直接导致后端API返回字段和前端渲染逻辑脱节。而所谓“高效协同”也不是理想化口号它意味着当产品决定把“数据分析”模块从二级菜单提升为一级入口时设计师只需在主设计系统库中拖拽调整一次前端自动获取最新结构图标文案权限标识测试同学拿到的PR描述里已明确标注影响范围上线后全平台保持视觉与行为一致。这背后需要的不是炫技而是对组件边界、状态粒度、数据契约、协作流程的极致厘清。如果你正面临设计系统推进缓慢、组件复用率低、跨团队对齐成本高或者只是单纯想搞懂一个看似简单的导航栏为何总在拖后腿——这篇就是为你写的。它不讲空泛理论只拆解我们踩过的坑、验证过的方案、以及那些写在文档里没人看、但实际落地时决定成败的细节。2. 设计系统视角下的侧边导航它到底该管什么、不该管什么2.1 核心职责边界一张表划清“必须由组件定义”和“必须交由业务层决定”的界限很多团队失败的第一步就是让侧边导航组件背了不该背的锅。它不是万能胶更不是决策者。我们用一张表来强制厘清责任维度必须由组件定义强约束必须由业务层决定弱约束/完全放开为什么这样划分基础结构支持一级/二级菜单嵌套支持图标文字角标Badge的标准组合折叠/展开状态切换的动画时长与缓动函数如cubic-bezier(0.4,0,0.2,1)菜单项的具体数量、层级深度最多支持几级、是否允许三级菜单结构一致性是视觉与交互统一的前提但业务复杂度千差万别硬性限制会扼杀灵活性。我们见过某金融后台因监管要求必须展示7级审批流强行限制为2级会导致业务方绕过组件直接手写DOM。视觉规范图标尺寸20×20px、文字字号14px、行高20px、选中态背景色#E6F7FF、悬停态透明度opacity: 0.8、禁用态灰度#BFBFBF图标风格线性/面性/品牌定制图标、文字颜色主色/次色/危险色、选中态边框宽度视觉原子必须收敛否则“设计系统”只剩名字但色彩语义需匹配业务场景如告警系统用红色强调财务系统用绿色表示正向操作。交互行为折叠时自动收起子菜单点击非可展开项自动关闭其他展开项键盘导航Tab/Enter/↑↓支持完整焦点流是否默认展开所有二级菜单点击父菜单是否跳转而非仅展开子菜单展开方向右/下交互逻辑是用户体验底线必须统一但业务目标不同如内容管理后台需快速跳转监控系统需逐级钻取策略应开放。数据契约接收标准JSON结构{ id: string, label: string, icon?: string, children?: MenuItem[], disabled?: boolean, badge?: { count: number, type?: primary | danger } }id的具体命名规则user-management还是users、label的文案来源静态字符串/ i18n key / 后端返回数据接口是前后端协作的“宪法”必须刚性约定但命名权属于业务域组件不应越界。权限控制提供visibleIf或disabledIf钩子函数接收当前用户权限对象返回布尔值权限判断的具体逻辑RBAC/ABAC/自定义规则、权限数据的获取方式本地存储/实时API调用组件只提供执行权限的“开关”绝不内置权限模型——那是安全架构的事。提示这张表不是用来贴在墙上当摆设的。我们在某电商中台项目中曾因未明确“是否允许三级菜单”这一条导致前端团队为兼容旧版三级菜单写了冗余的递归渲染逻辑而新业务线又因担心性能问题拒绝使用最终形成两套并行的导航组件。后来我们把它写进《组件准入清单》任何新组件PR必须附带此表的填写说明否则CI直接拒绝合并。2.2 状态粒度设计为什么“展开/折叠”不够用必须拆解为5种独立状态初学者常犯的错误是把侧边导航的状态简单理解为“展开”或“折叠”。实际在复杂系统中它需要承载更精细的语义。我们最终定义了5种互斥且可组合的核心状态并为每种状态设计了独立的CSS类名与事件钩子is-collapsed全局折叠整个侧边栏收为窄条仅显示图标。此时所有子菜单强制隐藏文字标签不可见。这是最高优先级状态覆盖其他所有状态。is-expanded全局展开侧边栏恢复为标准宽度如240px文字标签可见。这是默认状态但需明确其存在——因为某些场景如移动端横屏可能需要动态切换。has-active-child有活跃子项当前菜单项自身无路由但其任意子菜单项处于激活态active。此时父项需高亮显示但不加选中态背景色而是用左侧竖线border-left: 3px solid #1890FF标识。这是解决“面包屑导航与侧边栏联动”的关键。is-active自身激活当前菜单项对应当前路由且自身可点击跳转。这是唯一应用background-color: #E6F7FF的状态视觉权重最高。is-disabled禁用菜单项存在但不可交互通常因权限不足。此时不仅禁用点击还需降低图标与文字的透明度opacity: 0.5并移除所有悬停效果。注意这5种状态不是凭空设计的。我们曾在一个政务系统中遇到典型问题用户进入“政策法规”模块后侧边栏的“政策法规”父菜单并未高亮因为它的URL是/policies而子菜单“国家政策”的URL是/policies/national。前端开发者最初只判断精确匹配导致父菜单永远“失活”。后来我们强制要求组件必须支持has-active-child状态并在路由匹配逻辑中增加前缀匹配/policies/问题才彻底解决。状态粒度不够细就会逼着业务方写hack代码。2.3 协作流程重构设计系统不是“交付组件”而是“交付协作协议”标题中“从全局混乱到高效协同”的本质不是技术问题而是协作范式的转变。我们不再把侧边导航组件当作一个“前端库里的npm包”来交付而是将其视为一份跨职能协作协议。这份协议包含三个核心附件《菜单结构定义模板》Designer PM一个标准化的Excel表格列包括ID业务唯一标识、Label中文、Label_en英文、Icon图标名称关联设计系统图标库、Route前端路由路径、Parent_ID父级ID为空则为一级菜单、Permission_Key权限标识符。设计师和产品经理共同填写作为所有后续工作的唯一数据源。我们曾用此模板将某教育平台的菜单配置时间从平均3天缩短至2小时。《组件集成检查清单》Frontend Backend一份带勾选框的Markdown文档包含12项必检项例如“✅ 已确认Permission_Key与后端权限服务返回的字段名完全一致”、“✅ 已验证折叠状态下aria-label动态更新为‘展开[菜单名]’”、“✅ 已测试在IE11下子菜单展开动画降级为无动画”。每次集成新业务线前端必须逐项打钩并截图存档。《异常场景响应手册》QA Support明确列出15种高频异常及其处理路径。例如“现象用户登录后侧边栏空白。排查路径1. 检查网络面板确认/api/menus接口返回200且数据非空2. 查看控制台确认无Failed to resolve component: SideNav错误3. 检查用户权限对象确认permissions字段存在且为数组”。这本手册让QA同学无需再问“这个算bug吗”直接按手册走通即可定级。这套协议的价值在于把模糊的“对齐”变成了可执行、可验证、可追溯的动作。它不保证技术完美但能确保当问题发生时所有人知道该找谁、看什么、怎么查。3. 核心实现细节从Figma变量到React Hooks的全链路解析3.1 设计稿层Figma变量驱动告别“像素级还原”的幻觉很多人以为设计系统组件的起点是代码其实真正的源头在Figma。我们为侧边导航建立了三层变量体系确保设计稿本身就是可执行的规范Token变量基础原子在Figma的“Design Tokens”插件中创建例如spacing-sidebar-item-height:48pxcolor-sidebar-bg:#FFFFFFcolor-sidebar-active-bg:#E6F7FFtypography-sidebar-label:14px / 20px Roboto, sans-serif这些变量直接绑定到组件的图层属性上设计师修改变量值所有实例自动更新。Component Property变量组件属性利用Figma 120版本的“Properties”功能为侧边导航主组件定义可配置属性state枚举expanded/collapsed/mixedmode枚举default/compact/icon-onlytheme枚举light/dark 当设计师在画布中选中组件右侧属性面板即可直观切换状态实时预览不同场景下的表现。这比写一堆“状态说明文档”直观一百倍。Data变量菜单数据通过Figma的“Data Studio”插件将《菜单结构定义模板》的Excel数据导入生成JSON格式的模拟数据。组件实例可绑定这些数据自动生成真实菜单结构。我们甚至用它生成了100种不同长度、不同层级、不同图标组合的测试用例用于压力测试视觉一致性。实操心得我们曾在一个医疗SaaS项目中因设计师手动调整了某个图标的margin-right为12px标准应为8px导致所有开发基于该稿实现的组件都多出4px间隙。后来我们强制规定所有间距必须引用spacing-*变量禁止手动输入数值。Figma的“Inspect”面板会直接显示引用的变量名而非具体像素值从源头杜绝了这种“像素级还原”的陷阱。3.2 前端实现React Hooks封装用useSideNav统一状态管理我们放弃传统的Class Component或纯函数组件采用自定义HookuseSideNav作为状态中枢。它的核心价值在于将菜单数据、用户权限、路由状态、用户交互意图全部纳入一个可控的闭包中避免状态散落在多个地方。以下是精简后的核心逻辑TypeScript// hooks/useSideNav.ts import { useState, useEffect, useCallback } from react; import { useLocation, useNavigate } from react-router-dom; import { PermissionService } from /services/permission; interface MenuItem { id: string; label: string; route?: string; icon?: string; children?: MenuItem[]; disabled?: boolean; badge?: { count: number; type?: primary | danger }; } interface SideNavState { collapsed: boolean; expandedItems: Setstring; // 存储已展开的父菜单ID activeItem: string | null; // 当前激活的菜单ID } export const useSideNav (initialMenuData: MenuItem[], permissionService: PermissionService) { const [state, setState] useStateSideNavState({ collapsed: false, expandedItems: new Set(), activeItem: null, }); const location useLocation(); const navigate useNavigate(); // 1. 初始化根据初始菜单数据和当前路由计算activeItem useEffect(() { const findActive (items: MenuItem[]): string | null { for (const item of items) { if (item.route location.pathname.startsWith(item.route)) { return item.id; } if (item.children) { const childActive findActive(item.children); if (childActive) return item.id; // 父菜单激活has-active-child } } return null; }; const activeId findActive(initialMenuData); setState(prev ({ ...prev, activeItem: activeId })); }, [location.pathname, initialMenuData]); // 2. 处理菜单点击区分跳转、展开、无操作 const handleItemClick useCallback((item: MenuItem) { if (item.disabled || !permissionService.hasPermission(item.id)) return; if (item.route) { // 有路由直接跳转 navigate(item.route); } else if (item.children item.children.length 0) { // 无路由但有子菜单切换展开状态 setState(prev { const newExpanded new Set(prev.expandedItems); if (newExpanded.has(item.id)) { newExpanded.delete(item.id); } else { newExpanded.add(item.id); } return { ...prev, expandedItems: newExpanded }; }); } // 其他情况如仅图标无功能不处理 }, [navigate, permissionService]); // 3. 处理折叠/展开按钮 const toggleCollapse useCallback(() { setState(prev ({ ...prev, collapsed: !prev.collapsed })); }, []); // 4. 暴露给组件使用的状态和方法 return { ...state, menuData: initialMenuData, handleItemClick, toggleCollapse, isItemActive: (id: string) state.activeItem id, isItemExpanded: (id: string) state.expandedItems.has(id), hasActiveChild: (id: string) { // 检查该ID的子菜单中是否有activeItem const findInChildren (items: MenuItem[]): boolean { for (const child of items) { if (child.id state.activeItem) return true; if (child.children findInChildren(child.children)) return true; } return false; }; return findInChildren(initialMenuData.filter(i i.id id)?.[0]?.children || []); }, }; };这个Hook的关键设计点在于findActive的递归逻辑它不仅找精确匹配的route还向上回溯父菜单确保has-active-child状态能被正确计算。这是支撑面包屑与侧边栏联动的基石。handleItemClick的三态分发清晰区分“跳转”、“展开”、“忽略”三种行为避免业务方在onClick里写一堆if-else。hasActiveChild的独立方法不依赖expandedItems状态而是实时计算确保即使用户手动修改了展开状态视觉高亮依然准确。3.3 权限集成不耦合RBAC模型只提供可插拔的校验钩子权限是侧边导航最易引发争议的部分。我们坚决反对在组件内硬编码RBAC基于角色的访问控制或ABAC基于属性的访问控制逻辑。我们的方案是提供一个可插拔的校验钩子让业务方自由注入自己的权限判断逻辑// components/SideNav.tsx interface SideNavProps { menuData: MenuItem[]; // 关键权限校验函数由业务方传入 checkPermission?: (menuId: string, userPermissions: any) boolean; // 用户权限数据通常来自全局状态管理 userPermissions: any; } const SideNav: React.FCSideNavProps ({ menuData, checkPermission () true, // 默认全开 userPermissions }) { const { /* ...其他状态 */ } useSideNav(menuData, { hasPermission: (id: string) { // 将权限校验委托给业务方传入的函数 return checkPermission(id, userPermissions); } }); // 渲染逻辑中对每个MenuItem调用 // const canView checkPermission(item.id, userPermissions); // if (!canView) continue; // 跳过渲染 };在某金融风控后台的实际应用中他们的权限模型极其复杂不仅要看角色还要看用户所属的“风险等级组”、当前操作的“数据敏感度”、甚至“当日剩余操作次数”。如果我们在组件里写死RBAC他们要么放弃使用要么自己fork一份大改。而采用钩子模式他们只需提供一个函数// finance-app/permission-checker.ts export const financePermissionChecker (menuId: string, user: FinanceUser) { const rule PERMISSION_RULES[menuId]; if (!rule) return false; // 复杂的多条件判断 return ( user.riskGroup.includes(rule.requiredRiskGroup) user.sensitivityLevel rule.minSensitivity user.dailyQuota.remaining 0 ); }; // 在SideNav使用 SideNav menuData{menus} checkPermission{financePermissionChecker} userPermissions{currentUser} /这种设计让组件保持了极高的通用性也尊重了不同业务域的安全架构主权。4. 实战问题排查那些文档里不会写但你一定会遇到的12个坑4.1 路由匹配失效为什么/dashboard/analytics没让“Analytics”高亮现象菜单项Analytics的route设置为/dashboard/analytics但用户访问该路径时侧边栏无任何高亮。排查路径检查路由定义方式确认前端路由库如React Router v6是否使用了Route path/dashboard/*这样的通配符。如果是location.pathname可能是/dashboard/analytics但match.params.*为空导致组件内部的startsWith判断失败。解决方案在useSideNav的findActive逻辑中增加对*通配符的特殊处理或要求业务方在route中明确写出/dashboard/analytics。检查大小写敏感location.pathname是/Dashboard/Analytics首字母大写而menuData中是/dashboard/analytics。浏览器URL默认小写但某些代理或后端重定向可能引入大写。解决方案在findActive中统一转换为小写比较或在数据初始化时做标准化。检查Trailing Slashlocation.pathname是/dashboard/analytics/末尾有斜杠而menuData.route是/dashboard/analytics。startsWith会返回true但严格匹配会失败。解决方案在比较前对两者都进行trimEnd(/)处理。我踩过的坑在某政府项目中后端API返回的菜单数据里route字段是/data/report但前端路由配置是/data/report/:id?。用户访问/data/report/123时startsWith成功但/data/report本身并不对应一个有效页面导致用户点击后404。我们最终在findActive中增加了“精确匹配优先于前缀匹配”的逻辑并添加了exactMatchOnly: true的配置项。4.2 折叠动画卡顿为什么在低端安卓机上展开子菜单像PPT现象在部分Android 5.x设备上侧边栏折叠/展开动画严重掉帧体验极差。根本原因我们最初使用height: 0→height: auto的CSS过渡但auto无法被浏览器计算为具体的像素值导致动画引擎无法插值只能靠JS定时器强制重绘性能极差。解决方案改用max-heightoverflow: hidden的组合并预设一个足够大的max-height值如1000px/* 折叠状态 */ .side-nav__submenu--collapsed { max-height: 0; overflow: hidden; transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1); } /* 展开状态 */ .side-nav__submenu--expanded { max-height: 1000px; /* 足够容纳所有可能的子菜单 */ overflow: hidden; transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1); }这个技巧的关键在于1000px是一个“足够大但不过分大”的值。它比auto可计算比100vh更轻量避免布局抖动且在绝大多数情况下都能覆盖菜单高度。我们实测在骁龙410设备上帧率从12fps提升至58fps。4.3 国际化文案错位为什么中文正常英文就换行挤在一起现象侧边栏在英文环境下菜单文字严重超出容器宽度导致文字被截断或换行错乱。根因分析我们最初为菜单项设置了固定宽度width: 180px并期望文字在其中居中。但英文单词如Configuration Management远长于中文配置管理且没有天然的断字点CJK文字可按字断行拉丁文需按词。三步修复法移除固定宽度让容器宽度由内容自然撑开但限制最大宽度max-width: 240px。启用智能断字添加CSS属性word-break: break-word;和hyphens: auto;。后者在支持的浏览器Chrome 88, Firefox 80中会自动添加连字符。为超长词兜底对极少数无法断字的专有名词如Kubernetes在i18n配置中为其提供缩写K8s并在组件中通过title属性显示全称。.side-nav__label { word-break: break-word; hyphens: auto; /* 针对超长单词的额外保护 */ overflow-wrap: break-word; }实操心得我们曾为某跨国企业做本地化其德语菜单项Benutzerzugriffssteuerungssystem用户访问控制系统长达42个字符。仅靠break-word仍会撑破容器。最终方案是在i18n JSON中为该key同时提供full和short两个字段组件优先渲染short鼠标悬停时显示full。这比纯CSS方案更可控。4.4 权限动态变更用户升级角色后侧边栏菜单不刷新现象用户在系统内完成角色升级如从“编辑”变为“管理员”但侧边栏菜单未自动增加新权限的菜单项。原因menuData通常在组件挂载时一次性获取后续权限变更不会触发重新拉取。userPermissions状态更新了但useSideNavHook没有监听其变化。解决方案在useSideNav中将userPermissions加入useEffect的依赖数组并在权限变更时强制重新计算activeItem和过滤后的菜单数据useEffect(() { // 权限变更时重新计算哪些菜单项可见 const filteredMenu filterMenuByPermission(initialMenuData, userPermissions, checkPermission); // ... 重新计算activeItem等 }, [userPermissions, initialMenuData, checkPermission]);同时业务方需确保userPermissions是一个稳定的引用如使用useMemo包裹避免因对象引用变化导致无限循环。4.5 键盘导航失效为什么Tab键无法聚焦到子菜单项现象使用键盘Tab导航时焦点只能停留在一级菜单项无法进入展开的二级菜单。合规要求这违反WCAG 2.1 AA标准2.4.3 Focus Order必须修复。修复步骤确保DOM结构语义化子菜单必须是ul菜单项必须是li且li内包含a或button作为可聚焦元素。动态管理tabIndex折叠状态下的子菜单其ul的tabIndex应为-1可聚焦但不在Tab顺序中展开时将第一个子菜单项的tabIndex设为0其余设为-1。实现箭头键导航在子菜单ul上监听keydown事件↑/↓键在子项间切换焦点Esc键关闭子菜单并返回父项焦点。// 在子菜单渲染逻辑中 ul tabIndex{isExpanded ? 0 : -1} onKeyDown{(e) { if (e.key ArrowDown) { e.preventDefault(); focusNextItem(); } else if (e.key ArrowUp) { e.preventDefault(); focusPrevItem(); } else if (e.key Escape) { e.preventDefault(); closeSubMenu(); } }} {children.map((child, idx) ( li key{child.id} a tabIndex{isExpanded idx 0 ? 0 : -1} href{child.route} {child.label} /a /li ))} /ul这个细节虽小却是专业性的分水岭。它让视障用户、键盘党、以及追求极致效率的工程师都能平等地使用你的系统。5. 可扩展性设计当业务提出“我们需要一个顶部导航侧边栏混合模式”时5.1 架构预埋为什么SideNav组件内部要预留layoutMode属性标题中的“高效协同”不仅指当下更指未来。我们深知业务需求永远在进化。今天只要侧边栏明天可能就要“顶部主导航左侧功能区右侧内容区”的三栏布局。因此我们在SideNav组件的初始设计中就预埋了layoutMode属性interface SideNavProps { layoutMode?: sidebar | topbar | hybrid; // 预留扩展点 // ... }sidebar默认传统左置垂直导航。topbar将菜单项水平排列在顶部适用于内容型产品如文档中心。hybrid顶部放置一级菜单Dashboard,Reports,Settings点击后左侧侧边栏动态加载对应的二级菜单Reports下有Sales,Marketing,Finance。这个layoutMode不是为了立刻实现所有模式而是为了避免未来重构时推倒重来。当PM提出混合模式需求时我们不需要新建一个TopBarNav组件只需在现有SideNav中扩展hybrid分支的渲染逻辑并复用其所有的状态管理、权限校验、国际化能力。这节省了至少3人日的重复开发。5.2 主题扩展如何在不修改组件源码的前提下支持“暗黑模式”和“高对比度模式”主题切换是设计系统的基本功。我们采用CSS自定义属性CSS Variables作为主题载体而非Sass变量或JS对象/* themes/default.css */ :root { --sidebar-bg: #FFFFFF; --sidebar-item-hover-bg: #F5F5F5; --sidebar-item-active-bg: #E6F7FF; --sidebar-text-color: #333333; --sidebar-icon-color: #666666; } /* themes/dark.css */ :root[data-themedark] { --sidebar-bg: #1F1F1F; --sidebar-item-hover-bg: #2D2D2D; --sidebar-item-active-bg: #2A5C8A; --sidebar-text-color: #E0E0E0; --sidebar-icon-color: #B0B0B0; } /* themes/high-contrast.css */ :root[data-themehigh-contrast] { --sidebar-bg: #000000; --sidebar-item-hover-bg: #333333; --sidebar-item-active-bg: #007ACC; --sidebar-text-color: #FFFFFF; --sidebar-icon-color: #FFFFFF; /* 关键增加边框以增强对比 */ --sidebar-item-border: 2px solid #007ACC; }组件内部的CSS直接引用这些变量.side-nav { background-color: var(--sidebar-bg); } .side-nav__item:hover { background-color: var(--sidebar-item-hover-bg); } .side-nav__item.is-active { background-color: var(--sidebar-item-active-bg); border-left: var(--sidebar-item-border, none); /* 高对比度模式下生效 */ }业务方只需在HTML根元素上切换>// 使用react-window import { FixedSizeList as List, ListItemProps } from react-window; const MenuList: React.FC{ items: MenuItem[] } ({ items }) { const Row: React.FCListItemProps ({ index, style }) { const item items[index]; return ( div style{style} MenuItemRender item{item} / /div ); }; return ( List height{600} // 可视区域高度 itemCount{items.length} itemSize{48} // 每个菜单项高度 width{100%} {Row} /List ); };这三招组合拳让我们在5000个菜单项的压测中首次渲染时间稳定在120ms以内用户感知不到卡顿。性能不是锦上添花而是可用性的底线。6. 最后一点个人体会协同的本质是让“对齐”变得无聊写完这篇我翻出三年前在某社交平台后台做的第一个侧边导航组件的代码。那是个简单的ulli列表没有状态管理没有权限没有国际化甚至没有折叠功能。当时觉得“能用就行”。后来随着产品迭代它被复制、粘贴、魔改分散在十几个仓库里每个版本都有细微差别有的图标用SVG inline有的用img标签有的选中态用background有的用border-left有的支持键盘导航有的完全不考虑。当我们要统一视觉时才发现“统一”这个词背后是数不清的会议、PR、回归测试和线上事故。所以当我现在设计一个侧边导航组件时我脑子里想的从来不是“怎么让它看起来更酷”而是“怎么让它在未来三年里让每一个接触它的设计师、前端、后端、测试、产品经理都觉得‘哦就按这个来没什么好