在围绕电商的开发中,都免不了围绕商品来做各类数据的打标,集成,联动。
商品选择,自然也是电商B端系统中一个最为 常见 且 通用 的交互流程之一。而在我们的系统中,商品选择这一交互动作之前大多由业务自行开发比如(营销域),这类交互说简单也简单,但要说起体验、说起细节,复杂起来让人头皮发麻 😖
首先,说到体验,我认为我们团队不缺从零到一的人,整个阿里都不缺;我们缺的是真正投入进去做细节、打磨产品的人。
做一个组件,做一个平台,这个事情很快,但这样的成品如果给到另外一个同样水平的同学,别人会进来用吗?打磨了200个小时的组件和总耗时2小时的组件,体验是不一样的,被其他开发者的采纳率也是不一样的。
想想看,我们为什么会用 Antd
的 Button 代替原生的 Button,正是因为它的体验好,怎么评价体验好?因为它里面的细节多,而做这些细节需要大量的精力,我们一时半会考虑不到这么全,所以我们要用它。
所以,我认为体验完全可以和细节挂钩,做体验不如说是做细节!
我们团队目前主要负责的则是商品域的体验提升,那么就商品选择这一话题,我想根据我们的经验和实践来谈谈其中的体验细节及最佳实践。
商品选择,从功能上划分主要就可以分为:1. 选择;2. 商品;之所以这么划分,是因为这两项能力其实是相互独立,然后再通过组件结合的。把选择部分去掉,就可以做独立的商品列表;把商品部分换成订单,就可以变成订单选择。
通过拆分后,我们逐一来看看,这里面我们做了哪些细节。
选择
选择这是一个非常常见的交互行为,要表现一个完整的选择交互,首先考虑的是对状态和逻辑的处理。
选择状态(State) - 32 h
而对于状态逻辑的处理,我们将其处理为了一个 React Hook
以保证复杂的选择逻辑不受UI组件的限制。从接口定义上可以看到其所拥有的功能,除了基础的选择能力以外,我们还需要 单选、多选,最大选择数量 等,所以我们的入参类型定义如下:
export interface ISelectionOptions = Record> {
primaryKey: string;
mode?: 'single' | 'multiple';
selectedRowKeys?: string[];
maxSelection?: number;
onChange?: (selectedRowKeys: string[], records?: Array, ...args: any[]) => void;
onSelect?: (selected: boolean, record: T, records: Array, ...args: any[]) => void;
onSelectAll?: (selected: boolean, keys: string[], records: Array, ...args: any[]) => void;
getProps?: (record: T, index: number) => { disabled?: boolean; [key: string]: any };
}
export function useSelection(dataSource: Array, options?: ISelectionOptions): ISelectionState
除此以外,最重要的就是通过其内部处理返回给我们的 状态能力 和 方法,可以仔细看一下类型定义和描述:
export interface ISelectionState = Record> {
selectedRowKeys: string[]; // 已选择的主键
mode: 'single' | 'multiple'; // 多选、单选
noneSelected: boolean; // 没有选中任何值
allSelected: boolean; // 全部选中
indeterminate: boolean; // 半选中
isDisabled: (key: string | number) => boolean; // 对应主键值是否禁用态
isSelected: (record: string | number | T) => boolean; // 对应值是否被选中
set: (selectedRowKeys: string[]) => void; // 设置全量值
select: (selected: boolean, record: T) => void; // 选择某个值
selectAll: (selected: boolean) => void; // 选择、反选全部
getSelectedRecords: (keys?: string[]) => T[]; // 获取已选择的Record完整数据
}
拥有这些状态,我们再结合UI就可以做出完整的选择能力组件。但其实这里面是有细节的,我举例说明下:
- 禁用:禁用是一个延伸出来的选择需求,但却很常见。在设置了最大选择后,如果选择数量已经到达最大数量,那么就应该禁用其他项目不让其选择;也允许用户下传 getProps 自定义其禁用选择逻辑,如: 库存为零的项目不能够被选择。
- 最大数量:最大数量在正常的单选交互流程中很好限制,但是如果存在于类似 全选 或 批量设置时,则需要做截断处理,以保证选择数量不超过最大数量。
- 获取完整数据:需要这么一个选项是因为 dataSource 一般是一个 Array
而这里的细节则是,我们需要处理 已选择数据的快照,原因是防止 dataSource 源数据发生变化时,如果没有对已选中的源数据部分做快照,则无法通过现有 dataSource 获取对应的源数据。
😉 怎么样,上面这些细节如果是你来处理,你会考虑到吗?这只是个开始,故事继续。
选择组件(Component) - 6 h
有了上述关于状态的严密处理,就可以拿着上面那些靠谱的状态和方法进行选择的交互行为实现了,我们这里用到的 选择UI承载器 是 表格弹窗。
大体实现,不再赘述,主要说明下其中我们所实现的体验细节:
- 整行选择:用户进入这个场景主要的目的是选择,所以我们需要提供整行点选能力,这里定制了鼠标移动过程的高亮及光标样式以便客户明白整行是可以点击的。
- 已选数量:因为存在跨页选择场景,用户在使用过程中很容易忘记已选数量;如果缺失已选数量透出,遇到有最大选择限制的场景时,就会对突如其来的无法选择不知所措。
💡 那么关于选择这一点结束了吗?业务:不好意思,我们还需要SKU选择!
树形选择(Tree Select) - 80 h
如上,作为一个电商通用能力解决方案,只是商品选择还无法满足业务诉求,当业务诉求上出现了树形选择诉求后,之前所做的选择能力就需要大量改造了。
首先是 useSelection Hook
的树形选择能力支持,然后是表格的树形分组渲染能力。这里面我们花了大量时间处理原先 Fusion 本不支持的交互样式,为了和原先 Fusion 的用法匹配,我们几乎是阅读了 Fusion Table 源码的每一行,最后通过一个巧妙的方式重写并覆盖其子组件,最小范围的支持了这项功能。从而沉淀了可独立使用的树形表格组件:TreeTable
其中实现过程,暂且不表,总之这个过程真实的来回投入了(80h 以上),挑一些其中涉及到一些交互细节讲讲:
- Tree型最大选择:上面提到最大选择这一块需要注意全选过程中的最大数量截断。那么在树形选择过程中这里也需要同样的逻辑应用在父节点的选择动作,因为父节点选择过程中都是属于批量选择,如果批量选择过程出现超出上限的情况同样需要处理截断。
- 树链快照:同样是上面商品选择提到的数据快照问题,当应用在树形结构时,则需要我们快照整个树形链上的数据。如选择到SKU,则同样需要快照其父节点的数据,并合并至 dataSource 中,从而完成 TreeTable 的跨页选择。
- 树形数学库:考虑到开发者在需求开发过程中,可能还会有一些列对树形结构的数据处理。我们也对外暴露了树形结构处理的数学库,其中包含了对于这类树形结构较为常见的处理逻辑。
- 索引:根据dataSource,建立 p2n , v2n 两大常用索引表
- 增:新增子节点,包含其父节点的查询合并逻辑
- 删:根据主键删除子节点,如果父节点没有其他子元素则同样移除当前父级节点
- 改:根据主键修改某节点数据
- 查:根据主键查找节点数据;根据主键查找父节点数据
总之,对于树形结构的数据及交互处理,直接给该组件原有的复杂程度提升了一个量级。最主要的是需要保证上下游的 接口合理、分层清晰、性能可靠 及 稳定安全。
为此我们先是对数学库做了较为完整的单元测试,然后投入了一个具体的业务项目,收集下游使用过程中所面临的调用问题,以保证接口设计的开发者友好度。对外暴露一个完整数学库也可以让开发者在二次开发过程中,针对Tree型数据处理做到开箱即用,一步到位。
以上关于选择部分的各类抽象,不论是常规选择还是树型选择,不论是状态封装,还是表格承接,我们每一步的设计都遵循了高内聚低耦合的设计原则,相互之间可以独立组合使用。如果有一天树形选择的组件变成了另外一种组件的交互形式(如: 卡片),这套选择逻辑状态封装依旧可以快速复用进去。
商品
关于商品这一部分,依旧有非常多可圈可点的体验细节,且听咱们娓娓道来。
数据模型(Data model) - 40h
首先,在商品选择这一业务场景时,业务方大多只是在业务上需要做商品数据的关联。
也就是对于业务方来说,其实查询商品详细信息(如库存,商品图片)都是为了给前端提供必要数据,从而获取业务所需关联的商品ID而已,更何况需要查询一个完整的商品信息也没有表面上看起来那么简单。
如果需要提供一个功能完善的商品数据查询接口,在服务端也有诸多细节需要处理:
- 类目查询:类目数据往往是一个树形的数据。而我们作为一个国际化电商业务,不同国家所设定的类目、语言也不一样。另外类目本身也会存在关键字查询的能力
- 查询性能:业务侧作为二方接入放,一般通过微服务形式调用商品搜索服务,对于商品查询有数量限制,而作为服务转发多少也会有些性能损耗在里面。
- 混合查询:业务上为了更好的用户体验,往往会提供一个混合搜索的选项给用户,用户不管是商品ID、商品标题、SKU ID都可以直接输入,然后在后端识别搜索后返回具体匹配内容
- 批量查询:一些KA卖家往往都是在自己的系统中维护商品,常常提供一组商品ID进行匹配,所以商品管理页已经支持了使用 “逗号” 作为分隔符的批量搜索能力
- 库存查询:库存看似是一个商品的属性,实际上在系统设计上面还有多仓设计,多仓、活动仓、平台仓 每个仓储需要独立查询和判断,如果加上库存排序就更加难搞了。
那么如果我们联合商品域服务端推出一套内置官方API的端到端解决方案,岂不是可以一站式解决二方业务的接入问题。遇到业务上只是需要获得一个商品ID关联 的场景,以前可能对于服务端需要接入一圈 “商品搜索”、“库存(活动库存,多仓库存)”、“类目搜索”等服务,现在只需要在前端侧使用我们提供的商品选择组件即可使用最稳定,最全面,最可靠,性能最好的商品调用链路。
当然上面所说的只是解决了数据模型里面数据源的问题,其实对于数据模型还有很多细节需要处理,如:
- 价格:某个商品价格是多少?这实际上取决于SKU的销售价格(非原价)。所以对于商品价格来说,首先需要列出所有SKU的销售价格 (促销价 ?? 原价),然后如果所有SKU价格都相同,则以单个 “货币 + 数字” 显示;如果SKU价格不同,则以 “货币 + 最小价格 ~ 最大价格” 的范围显示。
- 库存:库存这个问题看似很简单,实际在我们这样一个国际化业务的场景中却存在着较为复杂的细节逻辑,某一个库存可能是来自 多仓、活动仓、平台仓、海外仓 等多个属性,每个仓对于可售库存的影响方式是不同的。对于数据模型中,我们不能含糊的让服务端提供一个可售库存就完事,因为在业务场景中,不同业务对于库存的处理方式可能不一样。需要提供完整库存数据模型的同时,再提供计算逻辑及可售库存这个终态数据。
有了稳定,完善的数据源是保证可用性的第一步,接下来就是组件体验细节了,毕竟 单元化细节决定整体品质。
类目筛选项(Category Selector) - 48h
要做商品选择,类目筛选往往是最常见又最难搞定的部分。为什么这么说呢?因为类目是个树形结构,而且在团队分工上,类目服务 和 商品服务 是两个相互之间独立的不同团队负责。
正是因为上面的两点技术和组织架构上的问题,让我们一开始走了一条错误的路线,背离了真实的客户体验,接下来我就来具体讲讲我们的 错误实践。
我们最完整的类目选择是什么样的?
一开始,我们花了非常大的精力做了上面这种看上去非常完美,见了就无可挑剔的复杂类目选择组件作为商品搜索的类目筛选。
正在我们洋洋得意的时候,突如其来的客诉如晴天霹雳惊醒了我们:
❗️❓ 我就选择个商品啊!我早都忘记了我的商品在你们的什么细分分类里面!
商品选择这个场景里面,类目只是一个筛选项。可能卖家一共就只在3个类目下发过10个商品,展示全部类目这一点都无法解决卖家的实际筛选诉求。筛选,顾名思义应当是从已有数据中进行筛选,从全部类目中筛选商品无异于大海捞针,现在回想起来,多么愚蠢。
对于卖家已有商品的数据,筛选类目其实用一层结构的下拉搜索选择框就够了。
接着,业务兴冲冲的反馈给二方业务服务端同学后。他们却表示很为难,因为商品和类目他们各自独立,提供各自独立的微服务调用方法,本来这个类目查询就跟他们本身的业务关联不大,他们只是服务之间调用转发而已。这种联合聚合查询对于他们还有上游来说会有很大的服务端压力,也不应该放在他们业务这一侧来处理。
非常真实 又 如教科书般的经验教训。上面的故事,就是典型的:
没有深度思考就行动,然后不断为一个错误的问题寻找解法的例子。
可以说,类目数据获取如果直接由业务方调用类目团队获取,一定无法用作筛选目的。既然是筛选,就应该由商品团队通过商品数据源洗出具体的 1~5级类目从而来做类目筛选。
而这一步,我们既然已经趟过一遍坑,如果能从前端侧就直接提供了端到端的最佳实践,也是可以竟可能避免业务再次进坑,浪费大量无效精力。
互斥筛选项(Search Select) - 24h
商品ID、SKU ID、商品名称,我们通常认为这些筛选项为互斥筛选项,卖家只需要输入一个应该就可以定位到对应商品。
这不是一个商品选择特有的筛选场景,我们通过二次封装做了一个独立组件 SearchSelect
:
该组件可以最大空间使用率的完成卖家搜索需求,同时该组件实际上还存在一些体验细节:
- 自动聚焦:经过一遍遍打磨,通常切换筛选项之后,搜索框因为鼠标点击会失去焦点,用户需要再次点击输入框以后才可以进行输入,我们在这个过程中则做了自动聚焦的逻辑处理。
- 保持数据:这里也是经过和交互设计师打磨的交互细节,用户在切换筛选项后,通常数据上会处理处理一次切换,而此时因为数据切换,输入框内容会被清空;而 用户的输入结果通常在交互设计上认为是非常宝贵的,一切数据上的操作行为,都需要保留用户的输入结果。所以这里看上去一个切换筛选项过程,实际上在数据上是把一个筛选项的输入结果搬到了另外一个筛选项中。
- 筛选关键字高亮:通常用户搜索后,筛选结果不一定只有一个,这个时候高亮搜索关键字是一个对于卖家快速聚焦内容非常好的交互。我们在该组件中增加
HightLightWords
子组件,共享父组件上下文,从而帮助开发者快速处理关键字高亮行为。 - 节流自动搜索:通常用户输入后,不用点击任何地方,可以立马看到搜索结果,这样的交互肯定是最好的;但因为服务器压力等问题,通常在B端系统中利用 “Search” 按钮或者 “回车” 方式触发搜索事件。在该组件接口设计中,我们提供输入 立即执行,回车执行,失焦执行 3种方式,又开发者根据场景选择触发方式,并且已经处理了节流能力,只有当用户停止输入动作后才会发起真正的交互动作。
商品、SKU信息(Product Cell) - 40h
商品信息通常有商品图片,标题,ID组成,并且各自都有非常多的细节:
商品图片:
这里的商品图片可以分为,图片预览 + 图片 两个组件组合。其实图片这一块做细了有很多细节,先了解下商品图片在当前场景中的作用:
在商品列表的场景中,商品图片主要作用是用来帮助卖家快速定位商品,卖家对于商品主图是什么大致是有色彩、结构上的预期的;其次则是卖家会放大图片来查看图片细节,进而区分具体数据。
根据上述对图片的分析,我们的优化细节则可以分为以下几项:
- 尺寸:首先在商品列表中,商品图片是按照 1:1 尺寸展示,但数据源给的商品尺寸却不一定都是1:1 的尺寸,为了让图片布满整个正方形区域,从而达到最佳的 整体视觉体验,需要对图片进行缩放处理,常见的图片缩放处理模式为4种,详情查看。这里就是是使用了
aspectFill
模式对图片做了缩放处理展示,以便不论接受到什么尺寸的图片,商品图片区域都可以用图片填满。 - 性能:在商品列表中,需要展示很多商品图片,如果每张图片都加载原图,对用户和服务器的流量都是一种浪费,每个图片加载所用的时间当然也就更长。因为我们常规链路发品的图片是放在阿里云的CDN,本身有图片处理的能力,如质量压缩,尺寸压缩,webp格式支持等。所以针对商品列表中所展示的图片,我们做了根据图片URL自动判别是否做加载优化的能力。通过优化可以做到在商品列表页的预览中只加载质量较差,尺寸较小的商品图片,单张商品图片流量平均节省500倍以上(2mb → 3.4kb)。
- 放大:上面经过我们的处理相当于把图片做小了,满足用户大致预览识别、快速加载、节省流量诉求的同时,但也同时需要考虑如何让用户看到 “高清大图”,为此我们又处理了图片画廊浏览能力。
- 加载态:因为网络原因,即使我们已经处理了图片的大小,在绝大多数的情况下图片都是秒出,但还是会存在个别情况下图片需要加载较长时间才可以加载完全,所以我们针对图片加载过程做了加载态骨架图,这样在列表图片加载过程中,用户会有预期这里会存在一个图片。
- 异常:不可避免的,图片有各种原因可能会加载失败。此处,我们为该组件做了业务特有的Logo作为兜底图,一旦图片加载失败,至少有一个兜底图(SVG)来作为替代图片展示。
商品标题:
商品标题,这里主要需要考虑的细节主要是 商品标题不得超出两行,超出部分用省略号代替,如果文字超出则需要有Hover态的气泡弹出,显示完整的商品标题。
商品、SKU ID:
商品和SKU ID的显示这里也是有细节处理,ID这类信息通常展示给卖家主要作用也是被卖家复制作为索引进行搜索的场景。所以对于这类信息,我们通常按照以下方式进行布局:
标签(label) | ID | 复制图标(icon) |
---|---|---|
此处体验细节处理有:
- 复制:除 label 区域以外,整个区域可点击,点击后触发复制ID到剪切板动作。鼠标Hover过程需要高亮展示 ID 及 icon。
- 弹性宽度:这里的宽度是根据内容的弹性宽度,但最大宽度不能超过父节点的弹性宽度,整行展示不得换行,宽度不够则使用省略号处理ID显示部分,但仍需保留复制按钮在一行末尾处。写过CSS的同学应该知道这其中需要处理的细节难度,这里的处理完全可以作为一道CSS面试题了。
价格、数字(Currency Text) - 24h
实际上价格组件就是 货币 + 数字,为什么要单独抽离数字组件呢?当然还是为了细节!
为了让用户更容易的读懂数字,我们需要对数字做 本地化格式处理,但各国对于数字显示格式,还有货币符号是不同的,我们需要根据不同的国家站点显示不同的货币符号及数字显示格式。
另外,缩写 也是在部分场景中使用到货币的常用场景,因为印尼货币单位较大的问题,他们国家对于钱的概念大多以 千、万 单位来计数,那么对于统计类场景时大多会按照缩写来显示数字。更加需要注意的是,他们连缩写的单位也与英语不同 😭
en_US | id_ID | |
---|---|---|
thousand | K | rb |
million | M | jt |
billion | B | M |
trillion | T | Y |
总结
通过以上所有的细节汇总,其总开发耗时294+ Hours
的打磨沉淀,才让我们产出了一个相对来说体验还不错的商品、SKU选择组件。
目前我们可以做到任何一个二方业务,只要在我们的系统中就可以开箱即用的使用我们的商品选择方案。同时,我们保证了上述过程中所提及的组件都可单独抽离使用,其实很多组件也并不是为了做这个商品选择组件才做的。很多组件实际上是经过2、3年的打磨才做到这么细致,而商品选择组件也只是在该场景下非常符合这种使用场景进而将他们组合起来。
所以,每一次细节的极致追求,实际上会继续带动无数个相关的产品质量提升,这是非常典型的 协同效应,反之也是如此。
如果你也想在业务中使用商品选择组件,欢迎查看以下链接,我们也非常欢迎与期待和你的交流!