这是一个 Web 需求,内容是让用户在列表里「更容易地访问」自己想要访问的资源,类似让网页收藏夹更方便地找到自己收藏的网页这种需求。

首先很容易想到的一个方案是提供一个模糊搜索功能,该方案在列表条目比较多的时候,无疑能带来很高的收益,遂保留之。不过如果用户经常访问的某个页面恰好不在可视区域内,那意味着该用户每次都要滚动视图,或者执行搜索才能访问到它,所以只靠该方案是没办法很好地解决这类用户「更容易地访问」需求的。

那么再思考一下需求本身,对于这类列表数据,什么是「更容易地访问」?

  • 需要的数据恰好就在当前的可视区域内,点过去
  • 我知道我要的那条数据大概在哪,甚至数据还没出来鼠标就先在那个位置等着了,于是朝着那个位置点过去

基于以上两点,如果列表的数据是会经常变化的,显然第一点很难办到。第二点倒是可以通过再设计一个排序方案,让用户根据顺序这个线索能够很快找到也不失为一个好办法。不过这里话又说回来了,如果你的列表数据不那么容易变化,就要谨慎地变更你的默认排序方案了,因为你每变动一次,就可能会影响到某部分用户的使用习惯,对于他们来说习惯就代表了便捷。但如果说旧的排序方案对于目前的新用户已经不够友好了了,这时候可以采用增量上线的办法,就类似于网站买东西的「按销量排序」和「按好评排序」,新用户默认成你们希望的,旧用户默认成他之前的,并让他看到有了新的,但用不用看他。

说到这里有些朋友可能会想,那既然这样,干脆出一个手动排序的功能好了!不错,在列表资源有限的情况下,手动排序是能够达到「更容易地访问」这个需求的,但如果数据量超过了一定的值,就恭喜你又收获了一个新需求,即如何「更容易地排序」?大家应该都有过手机上跨屏排序 APP 的体验吧。。。同时,如果你真的做了手动排序,前文里说的搜索功能就不能简单地只展示一个过滤结果了,因为这样用户就没办法去拿着搜索结果去排序了啊(用户想把资源 A 排在最前面,但资源 A 他想要通过搜索先找到),既然这两功能的目的是一致的,那它们就得尽可能地能做到「互相服务」。这里正确的交互大概是:自动滚动到匹配条目,高亮该条目,支持上下切换匹配条目(如果匹配到了多个项)…

交互也要讲究逻辑自洽 —— 《前端工程师的产品思维》

再然后,除了搜索功能的配套改造,手动排序的交互动画还要做吧,如果是简单的文字列表还好,大小不一不规则的小卡片什么的实现起来还是挺花费功夫的吧。。(不过这里有个大家经常会见到的偷懒方案,就是给每个条目一个「置顶/星标/收藏」功能,点来点去也就实现了排序)

这里其实在讨论的就是需求实现的成本问题了,这里看起来手动排序的成本是不小,而且收益很一般:用户需要主动地排序来达到方便访问的目的。那有没有可能改进一下这个方案?想想如果你是用户,你会怎样手动排序?如果大部分人的排序逻辑都是一致的,那这个过程显然可以做成自动的。而对于我这个需求,大部分用户会选择把经常访问的资源排在前面,于是答案呼之欲出:那就做个「常访问资源」的功能吧!显然用户在经常访问什么,我们是可以知道的(此处应有一个隐私数据使用声明。

所以常言说得好啊,很多时候功能并不是越全越自由越好,那样反而代表了产品本身的「不作为」,某种意义上来说,帮用户去做决定,甚至去培养一种新的使用习惯才是最难的,但却是对用户和产品来说是最好的。

好的,又啰嗦了。继续聊聊这个「常访问资源」的功能,它在我那个网站里可以被实现成两种形式:

  • 在原本的列表页上方展示若干个最常访问的资源
  • 作为一个新的排序规则,使用该规则就会把所有资源按照访问频率排序

我们再来逐个分析这两个方案:

第一个的好处显然是它不会引入新的排序规则,就是一个独立的 Feature,但坏处是会占用一定的列表展示区域,同时得考虑到一些边界问题,比如某些用户的列表里就那么几条一眼就能看完的数据,你还展示个「常访问资源」,反而让他一眼看不完了。。还有「若干个」到底是几个?定少了需求解决的就不够彻底,定多了必然又会更多地占用显示空间。最后其实这里还有一些挺有意思的技术问题:

——- 技术分析部分开始,不感兴趣的欢迎跳过 ——-

一个最简单的模型:列表中有 A、B、C 三个资源,然后最常访问记录只保留 2 个,按照访问频率排序。假设现在记录的内容是:A 访问过 3 次,B 访问过 2 次。如果不依赖后端接口的话,这些数据你得存在 LocalStorage 里,然后从性能和资源使用的角度考虑,你不应该在 LocalStorage 存大量的数据,那对于上面那个模型你应该存几条呢?

假设 LocalStorage 中就保存 2 条,然后你第一次访问 C 的时候,可以在内存中去保存和排序,这时候 C 的访问次数为 1,还并不能取代 B 的地位,于是你又访问了一下 C,好!这时候 C 的访问次数和 B 相等了,应该把 B 给取代吗?这里我给的答案是应该,因为这时候 C 是相对与 B 更新一些的数据,在访问次数一致的情况下,新数据可以替换旧数据。

data.sort(({ num }, { num: preNum }) => preNum - num - 1) // 排序代码

所以这时候 LocalStorage 中存的数据变成了:A 访问过 3 次,C 访问过 2 次;内存中的数据为:B 访问过 2 次。

但是操蛋的情况发生了,用户此刻关闭了网页,又重新打开了网页并访问 B,但之前内存中保存的 B 的 2 次访问记录没了,这意味着用户至少还得再访问 1 次 B,才能重新让其取代现在 C 的位置被记录成「最常访问」。同样地,甚至假设还存在资源 D,它也至少需要访问 2 次,这样访问统计就失去了公平性,越用到最后,新的最常访问就越难进入队列。

不止这个问题,其实还有。假设某用户很无聊,疯狂刷新某资源页,导致该资源的访问记录飙升到了一个可怕的数字,那第二名基本永远都无法排在第一位了。除此之外,其实还有一个杞人忧天的隐患是:数字的精度都是有限的,对于这种无限累加应该堤防数值溢出的问题。

那再重新审视一下这个技术方案,记录访问次数只是为了得出一个访问频率的排序结果,那排在第一位的访问了 1 万次,和访问了 2 次又有什么区别呢?只要比排在第二名的大就行了。因此这个问题有个特别简单的解决办法,就是每次写入 LocalStorage 的时候检测第一名的访问次数是否大于你要保存的所有资源数目,如果是,那就使它等于该数目。这样即可以用上限为资源数目的访问次数来保证所有资源的热度排序。不过如果这样也有个问题,热度值上限过低可能会导致前几名的排序经常变化,这样不利于达到

我知道我要的那条数据大概在哪,甚至数据还没出来鼠标就先在那个位置等着了,于是朝着那个位置点过去

这条优化目的。所以实际情况你可以令最大值等于保存的资源数 + 一个固定值。至于内存中的排序丢失问题,其实没有完美的解决方案,只能根据实际情况,在 LocalStorage 中保存尽可能多的条目吧,在条目够多的情况下,尾部数据的舍弃就不会那么敏感了。

同时,还要注意的是因为网页是支持多标签打开的,若想让统计的数据尽可能地准确,应当同步每个标签页面的记录值。

还有一个值得一提的优化点是:必要的话你还可以引入时间这个维度,如果某项资源长时间没有访问,它的热度也会按照一定的策略来下降。

——- 技术分析部分结束 ——-

再来分析第二个方案,也是我最终采用的方案。原因有两点:

  • 列表项包含的信息比较多,所以占用的空间比较大,如果采用第一个方案就得重新设计一个小号的展示,或只能展示极少量的条数
  • 每个用户列表的数据量不是很大,且 LocalStorage 中只会记录访问过的资源,因此不用担心日积月累导致的数据溢出问题

不过因为原本列表是有一定的默认排序策略的,故采用的是前文提到的增量上线,作为一个新的排序开关在列表顶部出现,同时在 LocalStorage 中保存该开关的状态。

最后总结一下:通过访问频率排序,不仅可以实现让用户最常访问的资源出现在可视区域内,而且它们的热度可以保证它们呆在一个固定的位置很久,从而可以达到前文说的两个优化目标。每一个需求,都把自己带入到用户角色下模拟各种使用场景,不停地跟自己杠来杠去。然后多看,多想,多模仿,好的用户体验大概就是这么来的吧。


 评论