Vant Sidebar 侧边导航组件完全指南:用法、API 与源码级原理解析
【免费下载链接】vantA lightweight, customizable Vue UI library for mobile web apps.项目地址: https://gitcode.com/GitHub_Trending/va/vant
导读
本文围绕 Vant 4 移动端 UI 库中的Sidebar(侧边导航)组件展开,它提供一种垂直展示的导航栏,用于在不同内容区域之间快速切换,常见于分类页、设置页、商品列表筛选等场景。通过本文你将掌握van-sidebar/van-sidebar-item的完整用法(基础绑定、徽标、禁用、事件监听、路由跳转与自定义插槽)、全部 Props / Events / Slots / CSS 变量 API,并从源码与测试层面理解其"父子组件通过依赖注入协同工作"的底层原理,最终能够独立完成样式定制与二次开发。
一、组件定位与引入方式
1. 组件定位
Sidebar 是 Vant 中为数不多的"垂直导航"类组件:它默认渲染一个宽度约 80px 的窄列,导航项自上而下排列,选中项左侧会显示一条主题色竖条,适合放在页面左侧(或内容区上方)配合右侧内容区联动切换。官方文档将其定位概括为:"垂直展示的导航栏,用于在不同的内容区域之间进行切换。"
在 Vant 的组件体系中,Sidebar 由两个组件协作完成:
Sidebar(容器):负责管理选中索引状态,通过v-model对外双向同步;SidebarItem(导航项):渲染单个导航条目,负责点击交互、徽标展示与路由跳转。
2. 安装与全局注册
组件随vant主包一起发布,无需单独安装。通过app.use进行全局注册:
import { createApp } from 'vue'; import { Sidebar, SidebarItem } from 'vant'; const app = createApp(); app.use(Sidebar); app.use(SidebarItem);全局注册后,模板中即可直接使用<van-sidebar>与<van-sidebar-item>标签。Vant 还支持按需引入(配合vant-auto-import-resolver或unplugin-vue-components)、手动局部注册等多种方式,更多注册方式可参考 组件注册。
二、基础用法:v-model 双向绑定选中项
Sidebar 的核心交互就是"当前选中哪一项"。通过v-model绑定当前选中项的索引(数字或字符串),默认值为0,即默认选中第一项:
<van-sidebar v-model="active"> <van-sidebar-item title="标签名称" /> <van-sidebar-item title="标签名称" /> <van-sidebar-item title="标签名称" /> </van-sidebar>import { ref } from 'vue'; export default { setup() { const active = ref(0); return { active }; }, };也可以使用<script setup>写法:
<script setup> import { ref } from 'vue'; const active = ref(0); </script> <template> <van-sidebar v-model="active"> <van-sidebar-item title="商品分类" /> <van-sidebar-item title="优惠活动" /> <van-sidebar-item title="我的订单" /> </van-sidebar> </template>源码视角:v-model 是如何工作的
在 Sidebar.tsx 中,Sidebar的 props 定义极其精简——只有一个modelValue:
export const sidebarProps = { modelValue: makeNumericProp(0), };makeNumericProp(0)表示该属性接受number | string类型,默认值为0,这与文档中v-model类型_number | string_、默认值0一一对应。
组件通过linkChildren将两个方法注入给所有子级SidebarItem:
const getActive = () => +props.modelValue; const setActive = (value: number) => { if (value !== getActive()) { emit('update:modelValue', value); emit('change', value); } }; linkChildren({ getActive, setActive });关键细节:
getActive用一元运算符+把字符串索引强制转为数字,保证内部比较与高亮判断类型一致;setActive只在值确实发生变化时才触发update:modelValue(驱动v-model更新)和change事件,避免无意义的重复触发——这一点在后面的测试用例中也有印证。
三、徽标提示:dot 与 badge
SidebarItem 内置了 Badge 徽标能力,支持两种展示形式:
dot:在标题右上角展示一个小红点(布尔值,默认false);badge:在标题右上角展示徽标内容(number | string,支持数字角标或自定义文本)。
<van-sidebar v-model="active"> <van-sidebar-item title="标签名称" dot /> <van-sidebar-item title="标签名称" badge="5" /> <van-sidebar-item title="标签名称" /> </van-sidebar>进阶:badge-props 透传 Badge 属性
如果默认徽标样式不够用,可以通过badge-props把任意 Badge 组件 的属性透传进去,例如自定义徽标颜色:
<van-sidebar v-model="active"> <van-sidebar-item title="消息" :badge="99" :badge-props="{ color: '#1989fa' }" /> <van-sidebar-item title="任务" dot /> </van-sidebar>源码视角:徽标如何渲染
在 SidebarItem.tsx 中,标题被包裹在Badge组件内部:
<Badge dot={dot} class={bem('text')} content={badge} {...props.badgeProps} > {slots.title ? slots.title() : title} </Badge>可以看到badge-props通过展开运算符{...props.badgeProps}直接透传给 Badge。测试文件 index.spec.tsx 中专门验证了这条链路:
test('should render badge-props correctly', () => { // ... <SidebarItem badge={1} badgeProps={{ color: 'blue' }} /> // ... expect(badge.style.backgroundColor).toEqual('blue'); });即badgeProps={{ color: 'blue' }}会真实作用到渲染出的.van-badge元素的背景色上。
四、禁用选项
通过disabled属性可以禁用某个导航项。被禁用的项点击无响应、不会触发切换,样式上会使用禁用态颜色并将鼠标光标变为not-allowed:
<van-sidebar v-model="active"> <van-sidebar-item title="标签名称" /> <van-sidebar-item title="标签名称" disabled /> <van-sidebar-item title="标签名称" /> </van-sidebar>源码视角:禁用如何拦截点击
SidebarItem 的点击处理逻辑非常直接:
const onClick = () => { if (props.disabled) { return; } emit('click', index.value); parent.setActive(index.value); route(); };disabled时直接return,既不会触发click事件,也不会调用parent.setActive更新选中状态。测试用例 index.spec.tsx 也验证了这一点:
test('should not update v-model when disabled SidebarItem is clicked', () => { // 点击 index=1 的 disabled 项后 expect(wrapper.vm.active).toEqual(0); // v-model 保持 0 不变 });五、监听切换事件:change 与 click
Sidebar 提供两个与交互相关的事件:
change(Sidebar 级别):选中项变化时触发,回调参数为选中项索引index: number;click(SidebarItem 级别):点击某个导航项时触发,回调参数同样为该索引。
<van-sidebar v-model="active" @change="onChange"> <van-sidebar-item title="标签名 1" /> <van-sidebar-item title="标签名 2" /> <van-sidebar-item title="标签名 3" /> </van-sidebar>import { ref } from 'vue'; import { showToast } from 'vant'; export default { setup() { const active = ref(0); const onChange = (index) => showToast(`标签名 ${index + 1}`); return { active, onChange, }; }, };源码视角:两个事件的分工与触发时机
从上面的onClick源码可以看出触发顺序为:先emit('click', index.value)(SidebarItem 的 click),再parent.setActive(index.value)(其内部再触发 Sidebar 的change)。也就是说,一次点击会依次触发click事件和change事件;但change仅在选中索引真正改变时触发(setActive内部有value !== getActive()判断)。
测试用例对这套行为做了精确断言:
test('should emit change event when active item changed', () => { // 点击第 0 项:因已是选中项,change 不触发 expect(onChange).toHaveBeenCalledTimes(0); // 点击第 1 项:change 触发且参数为 1 items[1].trigger('click'); expect(onChange).toHaveBeenCalledWith(1); }); test('should emit click event when SidebarItem is clicked', () => { wrapper.find('.van-sidebar-item').trigger('click'); expect(onClick).toHaveBeenCalledWith(0); // 点击事件始终触发(未禁用时) });实践中两者的选型建议:需要"联动内容区刷新"时监听 Sidebar 的change;需要"统计点击行为/处理单项特例"时监听 SidebarItem 的click。
六、标题插槽与路由跳转
1. title 插槽自定义内容
如果标题不只是纯文本(例如要嵌入图标、富文本或模板片段),可以使用title插槽完全接管标题渲染:
<van-sidebar v-model="active"> <van-sidebar-item> <template #title> <van-icon name="wap-home-o" /> 首页 </template> </van-sidebar-item> <van-sidebar-item title="分类" /> </van-sidebar>使用插槽时title属性会被忽略(源码中slots.title ? slots.title() : title的优先级顺序即为此意)。测试 index.spec.tsx 中也有对应快照用例。
2. 路由与链接跳转
SidebarItem 继承了 Vant 统一的routeProps(定义见 use-route.ts),支持三种跳转方式:
| 属性 | 类型 | 说明 |
|---|---|---|
url | string | 点击后跳转的链接地址(原生跳转) |
to | string \| object | 跳转的目标路由,等同于 Vue Router 的to属性 |
replace | boolean | 是否在跳转时替换当前页面历史记录,默认false |
<van-sidebar v-model="active"> <van-sidebar-item title="关于我们" url="https://example.com/about" /> <van-sidebar-item title="个人中心" :to="{ name: 'user' }" /> <van-sidebar-item title="设置" to="/settings" replace /> </van-sidebar>其底层实现在点击时调用route():
export function route({ to, url, replace, $router: router }) { if (to && router) { routerreplace ? 'replace' : 'push'; } else if (url) { replace ? location.replace(url) : (location.href = url); } }可见跳转优先级为:to(需要应用已注册 Vue Router)优先于url(原生页面跳转);replace=true时分别对应router.replace与location.replace,不会在历史栈中留下记录。
七、完整 API 参考
Sidebar Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| v-model | 当前导航项的索引 | number \| string | 0 |
Sidebar Events
| 事件名 | 说明 | 回调参数 |
|---|---|---|
| change | 切换导航项时触发 | index: number |
SidebarItem Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| title | 内容 | string | '' |
| dot | 是否显示右上角小红点 | boolean | false |
| badge | 图标右上角徽标的内容 | number \| string | - |
| badge-props | 自定义徽标的属性,透传给 Badge 组件的 props | BadgeProps | - |
| disabled | 是否禁用该项 | boolean | false |
| url | 点击后跳转的链接地址 | string | - |
| to | 点击后跳转的目标路由对象,等同于 Vue Router 的to属性 | string \| object | - |
| replace | 是否在跳转时替换当前页面历史 | boolean | false |
SidebarItem Events
| 事件名 | 说明 | 回调参数 |
|---|---|---|
| click | 点击时触发 | index: number |
SidebarItem Slots
| 名称 | 说明 |
|---|---|
| title | 自定义标题 |
类型定义
组件导出以下 TypeScript 类型,便于在业务代码中获得完整的类型提示:
import type { SidebarProps, SidebarItemProps } from 'vant';同时 Vant 还导出了样式变量类型SidebarThemeVars/SidebarItemThemeVars(见 sidebar/types.ts 与 sidebar-item/types.ts),配合 ConfigProvider 的theme-vars使用时可获得键名校验。
八、主题定制:CSS 变量全解析
Sidebar 系列组件提供了 13 个 CSS 变量用于样式定制,所有变量均在 sidebar-item/index.less 的:root/:host中声明(--van-sidebar-width声明于 sidebar/index.less),默认值引用了 Vant 全局设计变量,保证视觉体系一致:
| 名称 | 默认值 | 作用 |
|---|---|---|
--van-sidebar-width | 80px | 侧边导航整体宽度 |
--van-sidebar-font-size | var(--van-font-size-md) | 导航项文字大小 |
--van-sidebar-line-height | var(--van-line-height-md) | 导航项行高 |
--van-sidebar-text-color | var(--van-text-color) | 导航项文字颜色 |
--van-sidebar-disabled-text-color | var(--van-text-color-3) | 禁用态文字颜色 |
--van-sidebar-padding | 20px var(--van-padding-sm) | 导航项内边距 |
--van-sidebar-active-color | var(--van-active-color) | 按压(active)态背景色 |
--van-sidebar-background | var(--van-background) | 导航项背景色 |
--van-sidebar-selected-font-weight | var(--van-font-bold) | 选中项字重 |
--van-sidebar-selected-text-color | var(--van-text-color) | 选中项文字颜色 |
--van-sidebar-selected-border-width | 4px | 选中态左侧竖条宽度 |
--van-sidebar-selected-border-height | 16px | 选中态左侧竖条高度 |
--van-sidebar-selected-border-color | var(--van-primary-color) | 选中态左侧竖条颜色 |
--van-sidebar-selected-background | var(--van-background-2) | 选中项背景色 |
两种定制方式
方式一:CSS 覆盖(最简单,直接覆盖同名变量):
.van-sidebar { --van-sidebar-width: 96px; --van-sidebar-selected-border-color: #ff976a; }方式二:通过 ConfigProvider 全局定制(主题化方案,变量会作用于子树内所有组件,参见 ConfigProvider 组件):
<van-config-provider :theme-vars="themeVars"> <van-sidebar v-model="active"> <van-sidebar-item title="标签名称" /> </van-sidebar> </van-config-provider>import { ref } from 'vue'; export default { setup() { const active = ref(0); const themeVars = { sidebarWidth: '100px', sidebarSelectedBorderColor: '#ee0a24', }; return { active, themeVars }; }, };需要说明的是:选中态左侧竖条通过&--select::before伪元素实现(绝对定位于左侧、垂直居中,宽高分别由--van-sidebar-selected-border-width/--van-sidebar-selected-border-height控制),因此调整竖条粗细时只需修改这两个变量即可。
九、源码级联动原理:provide / inject 协作模型
理解 Sidebar 的内部机制,关键在两点:父组件如何管理状态、子组件如何感知父组件。
1. 父子通信链路
在 Sidebar.tsx 中,Sidebar通过@vant/use的useChildren向所有子级注入getActive/setActive:
export type SidebarProvide = { getActive: () => number; setActive: (value: number) => void; }; export const SIDEBAR_KEY: InjectionKey<SidebarProvide> = Symbol(name); // Sidebar setup 内 const { linkChildren } = useChildren(SIDEBAR_KEY); linkChildren({ getActive, setActive });在 SidebarItem.tsx 中,子组件通过useParent(SIDEBAR_KEY)反向获取父级实例与自身索引:
const { parent, index } = useParent(SIDEBAR_KEY); if (!parent) { if (process.env.NODE_ENV !== 'production') { console.error('[Vant] <SidebarItem> must be a child component of <Sidebar>.'); } return; }注意这段健壮性处理:当SidebarItem被错误地放置在Sidebar之外时,开发环境会输出明确的错误提示,避免静默失效。
2. 选中态与无障碍语义
- 选中判断:
const selected = index.value === parent.getActive();——由子组件自行比对索引并添加van-sidebar-item--select类; - 无障碍:
Sidebar根节点带role="tablist",每个SidebarItem带role="tab"、aria-selected(当前选中态)与tabindex(禁用时移除焦点能力),天然符合 ARIA 标签页(Tabs)语义,方便读屏软件识别。
3. 滚动与文本细节
- 容器样式(sidebar/index.less)设置了
overflow-y: auto与-webkit-overflow-scrolling: touch,导航项较多时容器内部可独立滚动; - 标题文本
.van-sidebar-item__text设置了word-break: break-all(对应 Vant issue #7455),避免超长标题撑破布局; - 导航项之间通过
:not(:last-child)::after绘制 1px 分隔线(利用 Vant 全局 hairline 机制)。
十、从 Demo 与测试看真实应用
1. 官方 Demo 的结构
官方演示页面 demo/index.vue 用van-grid将四个场景(基础用法 / 徽标提示 / 禁用选项 / 监听切换事件)并排展示,并配合showToast演示 change 回调,是快速上手组件形态的最佳参考。
2. 测试用例覆盖的行为契约
sidebar/test/index.spec.tsx 覆盖了组件的核心契约,可作为业务开发的"行为文档":
- 点击当前已选中项不触发
change(去重逻辑); - 切换选中项触发
change且参数为正确索引; - 点击非禁用项触发
click; v-model随点击正确更新,且change只触发一次;- 点击
disabled项不更新v-model; title插槽与badge-props透传均正常渲染。
结语
Sidebar 是 Vant 组件库中"小而精"的典型:外部 API 仅有v-model与少量 Props,内部却依托useChildren/useParent的依赖注入模型、Badge 组件复用与统一的路由封装(useRoute)实现了完整且健壮的导航交互。掌握其用法与原理后,无论是快速接入(5 分钟即可落地一个可用的侧边导航)、深度定制(CSS 变量 + ConfigProvider 主题)还是二次扩展(新增插槽、路由联动),都有清晰的实现路径可循。
相关文件索引:
- 组件实现:Sidebar.tsx、SidebarItem.tsx
- 类型定义:sidebar/types.ts、sidebar-item/types.ts
- 样式:sidebar/index.less、sidebar-item/index.less
- 测试:sidebar/test/index.spec.tsx
- 演示:sidebar/demo/index.vue
【免费下载链接】vantA lightweight, customizable Vue UI library for mobile web apps.项目地址: https://gitcode.com/GitHub_Trending/va/vant
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考