景区列表
业务背景
景区旅游是旅游行业中一个细分领域,主要涵盖了各种自然风景、人文景观、历史遗迹、博物馆等旅游景点。景区旅游具有独特的旅游价值,是人们观光、休闲、自然体验、文化交流的重要途径。
随着人们生活水平的不断提高,景区旅游的需求也在不断增长。为了满足游客的多样化需求,景区旅游业提供了各种多样的旅游产品,如导游服务、景区门票、住宿餐饮等。
景区旅游业在开展业务时,需要考虑多方面的因素,如景区规划、游客安全、环境保护、服务质量等。同时,景区旅游业也需要适应市场变化,不断提升服务水平,提高游客满意度。
在景区旅游业的发展过程中,旅游信息化技术发挥着重要作用。例如,通过景区旅游 APP 和网站,游客可以方便地预订门票和服务,同时也可以了解景区的详细信息。因此,景区旅游业也需要不断探索信息化技术,以提高服务效率
业务场景
作为一个 驴友, 我希望通过 一款小程序 查看到 目的地相关的景区列表, 从而快速检索 目的的相关的景区的关键信息
在快速检索的过程中 我希望了解到 景区的评级,用户的评分等级,往年的人流量/接待量 ...
我希望能够 按照 多个不同的维度 对信息进行排序
解决方案
对上述业务场景进行分析后,从技术的角度,景区列表这一功能可以拆分出以下子功能:
- 数据展现:向用户展示所有景点的相关信息(包括景点图片、景点名称、景点类型、门票价格等);
- 数据分页:当数据量较大时,一次性请求并渲染所有景点会导致性能问题,需要提供上拉加载更多功能,对数据进行分页展示;
- 数据过滤:不同的用户有不同的述求,需要提供筛选器,以便于用户按照景点类型、景点所在地等维度对数据进行过滤;
- 数据排序:需要提供选择器,以便于用户按照门票价格、相对距离等重要的指标对数据进行排序;
- 数据刷新:用户可能在一个页面停留较长时间,如果在这期间数据更新了,需要提供下拉刷新功能手动刷新数据。
整个解决方案会添加一个页面组件和两个业务组件,涉及的部分目录结构如下:
miniprogram
├── components
│ ├── filter
│ └── scenicSpotCard
├── pages
│ └── scenicSpotList
└── app.json
其中:
Filter
组件是顶部的选择器组,负责提供 UI 让用户选择过滤、排序的选项,并在用户选择后通知ScenicSpotList
拉取对应数据;ScenicSpotCard
组件是景点卡片,负责渲染景点的图片和相关信息;scenicSpotList
组件是景点列表,负责管理景点卡片,提供数据拉取、渲染、分页、刷新等功能。
功能点一:数据展现
一个景点列表往往包含了多个景点卡片,卡片的数量是不确定的,那么列表的高度往往会超出页面,因此需要一个可以滚动浏览的视图(通常是纵向滚动)对内容进行展示,在微信小程序中,视图容器 <scroll-view>
可以满足这一需求。
为什么使用 scroll-view 而不是 view ?
首先,不用 <scroll-view>
,开发者自己通过 <view>
也能实现这些功能。
使用 <scroll-view>
的目的在于节约造轮子所花的时间,因为这个组件提供了“滚动浏览”这一常见场景下的特殊业务功能,比如:
- 控制滚动位置,并提供过渡的动画效果
- 监听滚动至顶部/底部的对应事件
- 等等
因此,选用 <scroll-view>
而不是 <view>
可以帮助我们提升开发效率。
有了视图容器,接下来可以使用 wx:for
控制属性绑定一个数组,使用数组中的各项数据渲染景点卡片,细节参考:WXML 语法参考 / 列表渲染。
<!--pages/scenicSpotList/index.wxml-->
<scroll-view scroll-y="true">
<view class="scroll-view-item" wx:for="{{list}}" wx:key="name">
<!-- 景点卡片的内容 -->
</view>
</scroll-view>
最后完善景点卡片的内容并封装为 ScenicSpotCard
:
<!--components/scenicSportCard/index.wxml-->
<view class="card">
<image class="image" src="{{item.img}}" />
<view class="content">
<!-- ... -->
</view>
</view>
为什么要封装景点卡片?
封装景点卡片的目的主要是让代码更加利于理解和维护,以及未来在其他业务场景中复用它的可能性。
但主要还是为了可维护性,把繁杂的代码拆分为更小粒度的代码块,用合适的名称命名组件,都有利于团队成员维护这块代码。
功能点二:数据分页
在生产环境中,数据源中往往会有大量的数据,如果一次性向数据源请求所有景点数据并显示,会造成以下问题:
- 数据库要处理大量的数据,负载过高影响数据库性能;
- 网络开销大,响应速度慢,用户体验差。
所以,用分页的形式展示数据是很有必要的;有别于传统 Web 开发中分页器的形式,在移动端应用中,实现数据分页的形式往往是上拉加载更多(或者叫无限滚动列表),即在用户滚动时动态附加数据项,一次性增加的数据项称为一页数据,直到用户遍历完所有页的数据。
在微信小程序中,可以监听 scrolltolower
事件来检查何时滚动至页面底部,然后加载更多数据。
无限滚动的基础实现:
<!--pages/scenicSpotList/index.wxml-->
<scroll-view scroll-y="true" bindscrolltolower="scrolltolower">
<!-- ... -->
</scroll-view>
// pages/scenicSpotList/index.js
Page({
scrolltolower() {
// 防抖
// 更多数据检查
// 附加更多数据
},
});
可选的优化方案
在完成基础的无限滚动列表后,接下来要考虑的就是它的性能问题;当列表中的元素页数非常多时,我们可以回收一些不在视口中的元素来优化性能。
微信小程序提供了一套官方解决方案 recycle-view ,只要列表中的每一项都固定高度就可以使用,基本思路是:
- 设置最低限度的列表长度(后称 listSize),listSize 要比视口在一个时刻可以显示的最大项数要大,通常是它的 3 倍(即前一屏、当前屏、后一屏),并且一旦确定就不再动态增减元素,也就意味着要用重新渲染新数据模拟加载数据的效果;
- 监听滚动事件,在滚动至两端时重新渲染列表并调整滚动位置;
- 为了不影响用户的使用体验,每次重新渲染都要为视图容器填充相同高度的空元素(使用内边距
padding
替代空元素性能更好)。
功能点三:数据过滤
由于景点列表的数据是分页展示的,前端页面不具备全量数据,所以数据过滤功能通常由后端 API 实现,前端只需提供选择器的视图,并将参数传递给后端 API 即可。
在景点列表中添加选择器 UI:
<!--pages/scenicSpotList/index.wxml-->
<view>
<filter />
<scroll-view>
<!-- ... -->
</scroll-view>
</view>
<!--components/filter/index.wxml-->
<view>
<!-- 展开下拉列表时的遮罩 -->
<view class="mask" wx:if="{{activeKey}}" bindtap="closeSelect" />
<view class="filter-container">
<view wx:for="{{filters}}" wx:key="key">
<view class="filter-item" bindtap="openSelect" data-key="{{item.key}}">
<!-- 选择器标签,包含文本和箭头 -->
</view>
<view class="select-container" wx:if="{{activeKey===item.key}}">
<!-- 下拉选项列表 -->
</view>
</view>
</view>
</view>
以上术语对应 UI 如图所示:
最后通过 事件通信 的方式,在用户选择过滤选项后,通知景点列表组件传递对应参数并刷新列表:
<!--pages/scenicSpotList/index.wxml-->
<view>
<!-- getList 是自定义事件的名称,refresh 是重新请求数据的方法 -->
<filter bind:getList="refresh" />
<scroll-view>
<!-- ... -->
</scroll-view>
</view>
// components/filter/index.js
Component({
methods: {
selectItem(e) {
// 将用户的选择作为参数触发自定义事件 getList
this.triggerEvent('getList', params);
},
},
});
需要注意的细节:
用户可能会在滚动列表一段时间后对数据进行过滤,此时需要重置分页相关状态并重置滚动位置。
如何以较好的用户体验重置滚动位置,可以使用 <scroll-view>
中的以下属性做到:
首先,绑定一个用于控制滚动条位置的状态:
<!--pages/scenicSpotList/index.wxml-->
<view>
<filter bind:getList="refresh" />
<scroll-view scroll-y="true" scroll-top="{{top}}" scroll-with-animation="true" bindscrolltolower="scrolltolower" >
<!-- ... -->
</scroll-view>
</view>
然后,在合适的时机重置状态 top
即可:
// pages/scenicSpotList/index.js
Page({
refresh() {
// 重置分页状态
// 请求列表数据
// 重置滚动位置
this.setData({
top: 0,
});
},
});
功能点四:数据排序
数据排序的实现在功能点三:数据过滤中已经讲述,区别只是向后端传递参数不同,这里不再赘述。
结语
以上就是这篇文章的所有内容,希望这篇文章能够帮助到你!
Source · Demo
- Source:GitHub
- Demo: