从去年底把通讯录模块重构完到这次把城市选择器、商品分类页全部收敛到同一个索引列表方案上前前后后差不多磨了半年。这半年里RcIndexList 从最初一个只能“点字母跳分组”的雏形慢慢长成了现在既能处理多级分组、又能和双列表联动、还扛得住大数据量滚动刷新的通用组件。趁着这次把案例整理成册把过程中踩过的坑、想通的设计、以及那些文档里根本不会写的细节一并沉淀下来。这篇进阶篇面向的是已经用过 IndexList、或者至少对 ArkUI 列表类组件有基本认识的开发者。如果你只是刚接触 HarmonyOS 开发建议先跑一遍基础组件的官方示例再回来看这篇否则很多优化点和坑点会缺少上下文。我尽量把每个案例拆到“为什么要这么做”的层面而不是只给代码。1. 为什么需要一个叫 RcIndexList 的索引列表组件1.1 通讯录场景的三个硬需求最初的需求其实只有一个做一个带字母索引的通讯录。听起来简单但把需求拆开之后就变成三个绕不开的硬指标。第一右侧字母索引条必须常驻。用户在快速找人时肌肉记忆会直接去点屏幕右侧的字母如果索引条要滑动才出现体验上就废了一半。第二列表滚动时索引条上的高亮字母必须同步变化而且不能滞后。第三按压索引条时弹出的字母气泡IndexPopup要跟手拖动到哪个字母气泡就得立刻切到哪个字母。这三条分别对应了索引条的绘制、列表滚动状态的双向绑定、以及手势层级的处理。系统自带的 IndexList 能解决一部分但做到“跟手”和“同步”这两件事时自定义程度明显不够。IndexList 内部把索引条和列表绑定得比较死想改索引条的布局、想接入双列表联动、想在滚动时做额外的状态分发都会觉得施展不开。于是我们决定自己封装底层列表仍用原生组件索引条和锚点定位逻辑自己做对外暴露一套简洁的分组数据模型和回调。这样既保留原生列表的滚动性能又把控制权收回到手中。1.2 组件边界做什么不做什么封装组件最容易犯的错是“什么都管”。RcIndexList 的设计初期就有三件事被明确排除在外不做数据源管理、不做网络加载、不做业务占位样式。它只做一件事把“分组数据”渲染成“可滚动、可定位、索引同步”的列表。数据源由调用方传入组件内部只维护分组结构。调用方把数据按组整理好比如 contacts: { A: [张三, 张四], B: [李五], …… }组件负责遍历渲染、计算锚点、响应索引条事件。这样做最直接的好处是接入成本低任何场景只要你能把数据整理成分组结构就能直接用。底层技术上走的是“List 容器 sticky 标题 自定义 overlay 索引条”的组合。为什么不直接用系统 IndexBar因为我们要的索引条在某些场景下是“热”字、在某些场景下是“#”号甚至有些场景还要支持双列索引。系统的 justifyContent 和布局规则没法覆盖这么多变体自己画反而成本更低。// RcIndexList 对外暴露的最小接口示意 Component export struct RcIndexList { Prop groups: RcIndexGroup[]; // 分组数据 Builder itemBuilder: (group: RcIndexGroup, index: number) void; // 列表项渲染 Builder indexBarBuilder?: (groups: RcIndexGroup[], activeKey: string) void; // 索引条渲染 Event onIndexChange?: (key: string) void; // 索引变化回调 Event onScrollToIndex?: (groupIndex: number) void; // 滚动定位回调 }2. 案例一通讯录模块的字母索引与锚点定位2.1 数据分组与索引 key 的生成通讯录的分组逻辑看似简单——按姓氏拼音首字母分组——但真正落地时有两个坑多音字和特殊字符。多音字的典型例子是“单”姓读“shàn”时归入 S 组读“dān”时归入 D 组。项目里最终采用的方案是引入一个简化的多音字姓氏映射表只覆盖常见的高频姓氏匹配不到就按当前拼音库的结果走。这个表一开始只加了十几个姓后来通过用户反馈逐步扩充最终覆盖了市面上绝大多数通讯录场景。特殊字符和数字开头的联系人则统一归到一个“#”组放在列表末尾。注意这里有个细节当没有特殊字符联系人时“#”组不应该出现在索引条上。所以索引条的数据源是“由实际分组数据推导出来的”而不是定义死的 26 个字母。interface ContactModel { name: string; phone: string; pinyin: string; // 预计算拼音 } function groupContacts(contacts: ContactModel[]): RcIndexGroup[] { const map new Mapstring, ContactModel[](); const special #; // 特殊字符组名 for (const contact of contacts) { const char contact.name.charCodeAt(0); const isLetter (char 65 char 90) || (char 97 char 122); let groupKey: string; if (isLetter) { groupKey contact.pinyin.charAt(0).toUpperCase(); } else { groupKey special; } if (!map.has(groupKey)) { map.set(groupKey, []); } map.get(groupKey)!.push(contact); } // 按字母序排列# 组固定放在最后 const keys Array.from(map.keys()).sort((a, b) { if (a special) return 1; if (b special) return -1; return a.localeCompare(b); }); return keys.map(key ({ key, items: map.get(key)!, isSpecial: key special })); }分组数据的 key 生成建议在数据进列表之前完成不要在渲染时临时算拼音。通讯录这种场景数据量不大但商品类和城市类场景可能有上万条数据渲染时临时计算会造成可感知的卡顿。2.2 悬挂标题与索引条联动实现通讯录的列表项需要“字母标题在滚动时吸顶”这个在 ArkUI 里直接用 List 的 sticky 属性就能做List({ space: 0, scroller: this.scroller }) { ForEach(this.groups, (group: RcIndexGroup, groupIndex: number) { ListItem() { Text(group.key) .fontSize(14) .width(100%) .padding({ left: 16, top: 8, bottom: 8 }) .backgroundColor(#F5F5F5) } .sticky(StickyStyle.Header) // 标题吸顶关键属性 ForEach(group.items, (item: ContactModel) { ListItem() { // 联系人行的 UI 渲染 } }) }) }sticky 属性加在 ListItem 上指定 Header 样式即可实现吸顶效果。这比自己在滚动回调里计算偏移再补位要省事得多也是官方推荐做法。索引条联动是这部分的重点。右侧索引条用 overlay 叠加在 List 上方不参与布局。核心逻辑分成两块点击/拖动索引条时跳转到对应分组列表滚动时反向更新索引条高亮。跳转分组的实现依赖 scroller 的 scrollToIndex 方法因为 List 的 item 顺序和分组顺序是严格对应的所以分组序号可以直接映射成 item 序号。这里有个精妙处由于 sticky 标题占据了额外的布局空间直接 scrollToIndex 会让目标组停留在列表顶部恰好和吸顶标题的位置重合视觉上完全无感。scrollToGroup(groupIndex: number) { // 需要补偿第一个标题的位置否则标题会恰好顶在顶部被遮住 const targetIndex groupIndex * (itemsPerGroup 1); this.scroller.scrollToIndex(targetIndex, true, ScrollAlign.START); }2.3 字母气泡IndexPopup的表现细节按压索引条时的字母气泡是体验好坏最直观的地方。具体到实现气泡本质是一个跟随手指位置移动的浮层里面显示当前命中的字母。这里有三点控制直接决定手感气泡跟随不跟手的主要原因是浮层更新和手势事件的频率不对齐。手指滑动的 onTouch 事件频率远高于 UI 刷新的能力所以要在浮层里做一层缓冲记录上一次显示的字母只有字母变化时才更新状态避免每帧都触发重新渲染。气泡的尺寸不要太小。我们试过 64vp 直径在高速滑动时人眼容易丢失焦点最终定在 84vp 左右视觉上既不抢眼又能让余光稳定捕捉到字母变化。气泡的圆角、阴影、背景透明度尽量做成参数暴露不要写死在组件里。城市选择器里我们用了完全不同的气泡风格方角、无阴影同一个组件的可配置性在这一刻体现出了价值。// 索引条按压浮层的核心交互逻辑 .onTouch((event: TouchEvent) { if (event.type TouchType.Down || event.type TouchType.Move) { const y event.touch[0].y; const groupIndex this.getGroupIndexByPosition(y); if (groupIndex ! this.currentGroupIndex) { this.currentGroupIndex groupIndex; this.activeKey this.groups[groupIndex].key; this.popupVisible true; this.popupKey this.activeKey; } } else if (event.type TouchType.Up || event.type TouchType.Cancel) { this.popupVisible false; } })3. 案例二城市选择器与多级分组数据3.1 热门城市与字母分组的双层结构城市选择器的数据结构和通讯录有一个本质区别顶部多了一个“热门城市”区域。这个区域不是普通分组它固定悬浮在列表上方不参与滚动也不出现在右侧索引条上。如果简单地把热门城市作为列表的第一个分组那么“回到顶部”这个操作会先经过热门区再回到 A 组交互上多一步。更好的做法是热门区作为一个独立的容器放在 RcIndexList 外面滚动事件只作用于下方列表。这样顶部区域天然固定下方的索引定位也完全不需要知道热门区的存在。具体实现时页面的 Column 结构为顶部热门区高度自适应不滚动 中间状态栏可选 下方 RcIndexList占满剩余空间。城市列表沿用分组模型但有一个特殊处理索引条的最顶部增加一个“”符号代表“热门”。点击“”时列表滚动回顶部。这比把“热门”作为第一个分组更合理因为“热门”不是字母也不是首字母分组的自然成员它在语义上属于“快捷入口”而不是“分组”。3.2 特殊索引字符与分组偏移计算城市选择器的另一个变体需求是索引条除了 A-Z 之外开头有“*”热门结尾通常还有“#”其他区县、港澳台或特殊地名。这意味着索引条的总高度、每个索引项的高度分配都跟通讯录场景不一样。RcIndexList 在处理索引条时不能写死“每项高度相等”。实测下来当城市数量多、索引项密的时候“*”和“#”如果和其他字母一样高手指定位时容易误触。我们把特殊字符的项高度设置为字母项的 1.2 倍并在命中判断时单独处理点击区域和视觉区域保持一致。分组偏移量计算是城市选择器最容易出 bug 的地方。列表假设有 36 个分组26 个字母 热门 数字 特殊 其他其中“热门”并没有占据列表的滚动位置而是悬浮在上方。因此索引条上“热门”对应的分组序号是 -1点击时直接让列表滚动到 offset 为 0 的位置而不是 scrollToIndex(-1)否则会崩溃或定位错乱。onIndexBarSelect(key: string) { if (key *) { this.listScroller.scrollEdge(Edge.Top); // 回到顶部 } else { const groupIndex this.groups.findIndex(g g.key key); if (groupIndex 0) { this.listScroller.scrollToIndex(this.getListItemIndex(groupIndex), true); } } }一个更隐蔽的问题是当热门区高度变化比如不同城市数量导致热门区显示 4 个或 6 个城市时列表的滚动区域高度不变但锚点位置会受影响吗不会。因为热门区在列表外面列表内部的滚动锚点是独立计算的只需关注当前分组在列表内的 item 序号即可。3.3 大列表全量刷新与数据重建开销城市数据通常是省市区三级结构一份全量数据大约两三千条。在用户切换省份时需要重新生成城市列表的分组数据。直接重建数组并推给 List会引发全量刷新代价不小。实测下来有三个优化点收益最明显。分组数据生成后用不可变数据结构存储每次切换省份只替换整个分组数组的引用而不是修改已有数组的项。单元格的 key 用稳定 ID城市编码而不是索引。这样即使列表重建相同 key 的单元格也能被框架复用不会出现闪烁或位移。切换到省份时如果当前已经有列表位置优先尝试保持当前位置的视觉连续性。做法是把当前的滚动偏移按旧数据的总高度折算成新数据的近似偏移再 scrollToIndex。这个做过和没做过用户的感知差异非常大。onProvinceChange(newProvince: ProvinceModel) { const oldFirstVisible this.listScroller.currentOffset().yOffset; const oldTotalHeight this.totalListHeight; // 重新生成分组数据 this.groups this.buildGroups(newProvince.cities); // 换算新的近似偏移 const ratio this.totalListHeight / oldTotalHeight; const approximateY oldFirstVisible * ratio; // 先恢复到近似位置再平滑调整 this.listScroller.scrollTo({ yOffset: approximateY, animated: false }); }这里要注意 ratio 的边界当旧列表高度为 0首次加载时ratio 会变成无穷大要兜底处理。这类边界条件肉眼很难发现建议写单元测试覆盖。4. 案例三商品分类页的双列表联动4.1 左右布局与滚动方向的定义商品分类页是电商类应用的标配左侧是分类轴右侧是对应分类下的商品列表。这里的“分类轴”在视觉效果上跟字母索引条很像但本质上是另一个 List。也就是说这个页面是“两个 List 联动”一个横向意义上的状态选择器一个纵向滚动的商品流。RcIndexList 在这个场景下做了一个特殊的变体右侧列表复用索引列表的分组模型但索引条不再浮在右侧而是变成左侧的独立列表。这也是当初设计时把索引条抽象成 builder 插槽的原因——同一个定位逻辑可以驱动完全不同的视觉形态。右侧列表的分组和通讯录一样每组一个标题 若干商品项。左侧列表则是简化版的一维 List。联动关系只有一句话点击左侧分类右侧滚动到对应分组锚点右侧滚动经过分组标题左侧高亮同步切换到对应分类。4.2 锚点滚动的两种实现对比实现右侧列表“滚到对应分组”有两种路线。第一种是把滚动偏移换算成分组在列表中的 item 索引然后 scrollToIndex。这也是 RcIndexList 通讯录场景的默认做法。问题在于当商品列表项高度不一致不同商品卡片高度不同还有横幅、活动位穿插时单纯按索引定位可能不够精确需要额外测量高度。第二种是直接 scrollToOffset把目标分组的标题偏移量作为 yOffset。实现这种方式需要预先测量每个分组在滚动容器中的偏移量商品数据变化后还要重新测量。代码复杂度高但定位精度是绝对的。实测下来的结论是只要商品项不是异形高度差异特别大scrollToIndex 的精度完全够用而且实现和调试成本都低得多。最终的落地方案是默认走 scrollToIndex并在分组高度波动剧烈的特殊页面里用 scrollToOffset 作为策略插件的另一套实现。// 策略插件的接口设计 interface ScrollStrategy { scrollToGroup(groupIndex: number, scroller: Scroller): void; } class IndexScrollStrategy implements ScrollStrategy { scrollToGroup(groupIndex: number, scroller: Scroller) { const targetItemIndex groupIndex * (ITEMS_PER_GROUP 1); scroller.scrollToIndex(targetItemIndex, true, ScrollAlign.START); } } class OffsetScrollStrategy implements ScrollStrategy { private groupOffsets: number[] []; // 需要初始化时测量每个分组的位置 scrollToGroup(groupIndex: number, scroller: Scroller) { const offset this.groupOffsets[groupIndex] ?? 0; scroller.scrollTo({ yOffset: offset, animated: true }); } }4.3 左右状态同步的节流与边界处理右侧滚动时左侧高亮要跟着变。这个逻辑准确说来自右侧列表的 onScrollIndex 回调。onScrollIndex 会返回当前显示的第一个 item 的索引但触发频率很高直接 setState 会发生肉眼可见的抖动和性能下降。节流方案不是简单的“定时器限频”而是结合“当前可见项发生变化才更新状态”去重。实践代码中维护一个 lastGroupIndex只有 onScrollIndex 推导出的分组号发生变化时才更新左侧高亮。此外还要加 200ms 的防抖保护当快速连续滚动经过多个分组时最后一次停留超过 200ms 才允许左侧高亮跟随避免视觉上“跳跳跳”的割裂感。边界处理中有两个极容易被忽略的场景。第一个是列表滚动到最底部时最后一个分组因为内容不足一屏onScrollIndex 可能永远无法触达它的标题序号导致左侧最后一个分类一直无法高亮。解决办法是判断滚动到底部的回调一旦触发就直接把左侧高亮切到最后分类。第二个是左侧分类点击后右侧的滚动动画还没结束用户又快速点击了下一个分类。如果不做打断处理动画会互相干扰列表最终停在错误位置。处理方式是每次发起新的 scrollToIndex 调用的同时设置一个标志位在下一个 onScrollIndex 回调里消费掉并清除标志确保动画执行的连续性。onRightListScroll(index: number) { const currentGroup this.getGroupIndexByItemIndex(index); if (currentGroup this.lastGroupIndex) return; // 去重 this.lastGroupIndex currentGroup; // 防抖更新左侧高亮 if (this.highlightTimer) clearTimeout(this.highlightTimer); this.highlightTimer setTimeout(() { this.leftActiveGroup currentGroup; }, 200); }5. RcIndexList 的核心机制与关键 API 深度解析5.1 分组数据模型与锚点映射机制索引列表能工作的本质是维护了一张“分组 key → 列表 item 序号”的映射表。RcIndexList 内部每次数据更新时会重建这张表映射关系需要保证 O(1) 查询否则索引条高频拖动时会卡。映射表的生成依赖分组数据每个分组是一个 RcIndexGroup包含 groupKey 和 items 数组。列表的 item 序号并不是连续的分组序号而是要考虑“分组标题行”占据的 item 位置。比如第一个分组有 10 个联系人那么整个列表的 item 布局是标题 0、联系人 1-10、标题 11、联系人 12-……。所以从分组序号到 item 序号的计算公式是itemIndex groupIndex sum(itemsCount[0..groupIndex-1])这个计算在数据量大时可以提前做好存放在锚点映射数组里。索引条点击响应时直接用数组下标取出目标 itemIndex不需要遍历。性能测试中3000 条数据、36 个分组的情况下整个查找过程耗时低于 0.1ms完全可以支撑高频拖动。5.2 索引条自绘与浮层技术索引条为什么必须自绘系统组件能显示字母但我们还需要显示“*”“#”“热”等特殊字符需要自定义背景色、按压圆角、气泡定位参考点甚至在某些场景要支持分段颜色高亮。与其被限制不如用 Row/Column 布局自己画。核心绘制逻辑就是一个 Column遍历分组 key 列表逐个渲染文本项。自绘索引条需要处理一个关键问题触摸事件。索引条悬浮在 List 上方如果不处理事件冲突手指按住索引条时 List 也会收到滑动事件导致列表跟着滚动。这里要在索引条上使用 priorityGesture 抢占手势并在手势识别成功后阻止 List 的事件分发。ArkUI 中手势优先级从高到低依次是 priorityGesture 普通 gesture 子组件默认手势。把 onTouch 绑定在索引条上配合 gesture 的并行识别可以保证索引条手势优先响应列表滚动。IndexBar() .priorityGesture(Gesture.create(GestureType.Touch)) .gesture( Gesture.create(GestureType.Touch) .onAction((event: GestureEvent) { // 索引条自身的手势处理 }) )需要注意的是priorityGesture 虽然优先识别但默认情况下并不会阻断 List 的滚动事件。想要阻断需要在手势的 onTouch 事件里手动处理“列表不再响应”的状态或者在 overlay 浮层上增加一块透明区域拦截事件。前者实现简单后者视觉表现更细腻索引条以外的半透明遮罩区域也能响应关闭。5.3 Builder 插槽与数据刷新机制RcIndexList 的易用性很大程度来自 Builder 插槽设计。分组标题、列表项、索引条、气泡浮层四件事全部可以外部注入渲染。这样组件本身只负责状态和联动具体长什么样由调用方决定。这里有一个性能上的建议Builder 插槽的内容如果包含复杂业务组件务必用 Builder 函数声明而不是直接写成子组件节点。实测中Builder 的编译期优化能让列表项渲染性能提升 30% 左右尤其对 1000 条以上数据的场景效果明显。数据刷新方面RcIndexList 内部对分组数据的变更做了“整组替换而不是逐项修改”的约定。调用方传新的 groups 数组时组件先比对 groupKey 列表是否一致。一致则只刷新对应组内的 items不一致则整个列表重建。这个逻辑用简单的引用比较 浅层 diff 就能显著降低无效刷新。Watch(onGroupsChange) groups: RcIndexGroup[] []; onGroupsChange() { const newKeys this.groups.map(g g.key); if (this.groupKeys.length newKeys.length this.groupKeys.every((k, i) k newKeys[i])) { this.updateItemMap(); // 分组结构不变只更新锚点映射 } else { this.rebuildAll(); // 分组结构变化整体重建 } }6. 半年踩坑实录这些偏离文档的细节最费时间6.1 分组项无法准确跳到列表项的根因这个坑断断续续折腾了两天才定位。现象是点击索引条的“W”分组列表滚到了 X 组或者 V 组附近就是不落在 W 组标题上。排查后发现根因是 scrollToIndex 的目标 index 算错了。通讯录场景的分组映射公式没有问题但商品分类页的水平布局里左侧分类列表本身也参与滚动计算导致右侧列表的起始偏移不为 0。锚点映射表只计算了右侧列表内部的 item 序号却忽略了外部容器的滚动偏移。最终解决方案是给 RcIndexList 增加一个 optional 的 startOffset 参数当容器存在顶部偏移时动态修正目标偏移量。这个问题也给后续所有嵌套滚动场景提了个醒锚点计算永远要基于“列表内容在滚动容器中的绝对位置”不能假设内容从 0 开始。6.2 索引条高亮跳变与防抖阈值设置滚动列表时索引条的高亮字母偶发地快速左右跳动。比如明明滚动经过的是 B 组高亮却瞬间闪到 C 组又跳回 B 组。定位过程发现 onScrollIndex 的回调频率在某些设备尤其是低端机上会低至每帧一次同时也存在一帧内多次回调。当回调序号正好落在相邻分组的交界处时分组判断函数会在两个分组之间反复横跳。处理方式不只是加防抖而是给“判断当前分组”加上变化容忍度只有当前分组连续 3 次回调都指向同一个新分组时才正式切换状态。这个改动让高亮变得稳定代价是切换延迟仅有约两帧肉眼基本无感。6.3 自绘索引条点击事件和 List 滚动的冲突自己画索引条后一个必然遇到的问题就是按住索引条上下滑动时下方的 List 也同时在滚动。RcIndexList 最初版本的做法是索引条手势识别后手动设置 List 的 enabled(false)抬手再恢复。这个方案简单粗暴但用户体验有明显顿挫感。更好的做法是用 ArkUI 的并行手势 事件拦截。让索引条区域的手势事件直接进入自己的处理逻辑同时阻止 List 接收触摸事件不改变 List 的可用状态。这样 List 不会产生惯性滚动抬手后的停留位置也是精确的。另外还要处理一个细节按压索引条时如果弹出气泡气泡要覆盖在索引条本身上方更高 zIndex如果不弹气泡索引条自身要做按压态背景反馈否则用户感受不到“已经按住了”。6.4 懒加载与全量刷新的矛盾处理引入 LazyForEach 后分组数据量大时滚动性能确实上来了但随之而来的问题是“数据更新后旧位置的锚点失效”。比如分组 B 增加了 10 条数据列表项序号整体后移之前缓存的分组偏移表全部过期。这里我的建议是不要试图在懒加载模式下做复杂的局部增量映射更新这在复杂度上不值得。分组数据的变动频率远低于列表项数据的变动频率所以每次分组变动直接重建锚点映射表即可。因为锚点映射表只是一组整数数组重建成本远低于重新渲染列表。真正的性能热点在列表项的渲染不在映射表的生成。此外LazyForEach 的数据源需要实现 ID 生成规则。如果 ID 不稳定比如根据 items 数组重建时每个对象都生成新 IDLazyForEach 会退化成全量刷新。稳定 ID 的做法是给每个数据实体绑定一个持久化的业务主键城市用 adcode联系人用手机号商品用 skuId。class ContactDataSource extends DataSource { public totalCount(): number { return this.contacts.length; } public getData(index: number): ContactModel { return this.contacts[index]; } public getDataId(index: number): string { // 用业务主键作为稳定 ID避免全量失效 return this.contacts[index].phone; } }7. 后续还能怎么扩展RcIndexList 目前已经在多个核心页面落地但它的扩展空间还很大。一个是“索引条搜索模式”在索引条顶部加一个放大镜图标点击后列表平滑切换到搜索输入框。这本质上是在索引条事件里增加一个特殊 key 的分支处理组件结构完全兼容。另一个是“自定义排序规则”默认按字母但某些场景需要按热度、按价格段、按时间轴分组。分组 key 的生成逻辑不在组件内部所以只要调用方在分组数据中给出正确的 keyRcIndexList 就能原样渲染索引条照样工作。还有一个真正有价值的方向把锚点映射表暴露出来让外部组件也能利用“分组 key 定位列表”的能力。比如商家门店列表和商品列表联动、多 Tab 场景中的子页联动。这半年打磨下来我最深的体会有两点一是索引列表的核心竞争力在“状态同步”而不在“单点定位”任何一处没有考虑滚动边界都会在真机的高频交互里露馅二是组件封装不要贪多数据层、业务层和表现层的边界划清楚了后面加减功能都会很轻松。希望这份案例集能给正在做同类需求的开发者一些参考少走几个我已经趟平的坑。