景区列表

业务背景

景区旅游是旅游行业中一个细分领域,主要涵盖了各种自然风景、人文景观、历史遗迹、博物馆等旅游景点。景区旅游具有独特的旅游价值,是人们观光、休闲、自然体验、文化交流的重要途径。

随着人们生活水平的不断提高,景区旅游的需求也在不断增长。为了满足游客的多样化需求,景区旅游业提供了各种多样的旅游产品,如导游服务、景区门票、住宿餐饮等。

景区旅游业在开展业务时,需要考虑多方面的因素,如景区规划、游客安全、环境保护、服务质量等。同时,景区旅游业也需要适应市场变化,不断提升服务水平,提高游客满意度。

在景区旅游业的发展过程中,旅游信息化技术发挥着重要作用。例如,通过景区旅游 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>

为什么要封装景点卡片?

封装景点卡片的目的主要是让代码更加利于理解和维护,以及未来在其他业务场景中复用它的可能性。

但主要还是为了可维护性,把繁杂的代码拆分为更小粒度的代码块,用合适的名称命名组件,都有利于团队成员维护这块代码。

功能点二:数据分页

在生产环境中,数据源中往往会有大量的数据,如果一次性向数据源请求所有景点数据并显示,会造成以下问题:

  1. 数据库要处理大量的数据,负载过高影响数据库性能;
  2. 网络开销大,响应速度慢,用户体验差。

所以,用分页的形式展示数据是很有必要的;有别于传统 Web 开发中分页器的形式,在移动端应用中,实现数据分页的形式往往是上拉加载更多(或者叫无限滚动列表),即在用户滚动时动态附加数据项,一次性增加的数据项称为一页数据,直到用户遍历完所有页的数据。

在微信小程序中,可以监听 scrolltolower 事件来检查何时滚动至页面底部,然后加载更多数据。

scroll-tolower

无限滚动的基础实现:

<!--pages/scenicSpotList/index.wxml-->
<scroll-view scroll-y="true" bindscrolltolower="scrolltolower">
  <!-- ... -->
</scroll-view>
// pages/scenicSpotList/index.js
Page({
  scrolltolower() {
    // 防抖
    // 更多数据检查
    // 附加更多数据
  },
});

可选的优化方案

在完成基础的无限滚动列表后,接下来要考虑的就是它的性能问题;当列表中的元素页数非常多时,我们可以回收一些不在视口中的元素来优化性能。

微信小程序提供了一套官方解决方案 recycle-view 在新窗口打开,只要列表中的每一项都固定高度就可以使用,基本思路是:

  1. 设置最低限度的列表长度(后称 listSize),listSize 要比视口在一个时刻可以显示的最大项数要大,通常是它的 3 倍(即前一屏、当前屏、后一屏),并且一旦确定就不再动态增减元素,也就意味着要用重新渲染新数据模拟加载数据的效果;
  2. 监听滚动事件,在滚动至两端时重新渲染列表并调整滚动位置;
  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 如图所示:

scenic-filter

最后通过 事件通信在新窗口打开 的方式,在用户选择过滤选项后,通知景点列表组件传递对应参数并刷新列表:

<!--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>在新窗口打开 中的以下属性做到:

scroll-topscroll-animation

首先,绑定一个用于控制滚动条位置的状态:

<!--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

参考