06-文章搜索页面

1)开 篇(添加片头)

在上一章中,我们完成了 热搜首页 的开发,虽然经历了 ”千辛万苦“ ,但是对大家来说,应该也是收获满满。

那么在这一章节,我们将会进入新的篇章,来到 文章搜索 页面的开发。那么在 文章搜索 的页面开发中,我们又会经历哪些 奇奇怪怪的 bug ,又将会获得哪些新的收获呢?

让我们一起期待吧!

2)文章搜索 - 分析文章搜索页面

在开发文章搜索页面之前,先来分析一下这个搜索页面都有哪些内容……

文章搜索

整个搜索页面分为三块大的内容:

首页 -> 点击搜索框 -> 慕课搜索页面 -> 聚焦「搜索框」 -> 搜索历史页面 -> 往输入框输入内容或不输入内容,回车 -> 搜索结果页面

  1. 【慕课热搜】
    1. 展示 8 个热搜内容
  2. 【搜索历史】
    1. 按照【从后向前】的顺序,展示搜索历史
    2. 点击【小垃圾筒】可删除历史记录 -> 可全部删除、可选择性删除
  3. 【搜索结果】
    1. 不输入内容直接回车,按照当前的 placeholder 索引
    2. 输入内容,按照当前内容索引
    3. item 的展示分为三类
      1. 无图片展示
      2. 单个图片展示
      3. 三个图片展示
    4. item 中关键字高亮
    5. 具备下拉刷新,上拉加载更多
    6. 点击 【叉号】返回【搜索历史】
    7. 点击【取消】返回【慕课热搜】

以上这些内容,就是我们整个文章搜索页面之中所具备的一些能力,接下来的开发就是围绕这三大块来进行开发!

3)文章搜索 - 使用分包,创建 search-blog 页面

创建文章搜索页面,即创建search-blog页面

但在创建这个页面的时候,我们将使用「分包」这个概念来创建我们的页面

1、分包

💡:什么是分包?

分包open in new window 指的是将小程序划分成不同的子包,在构建时打包成不同的分包,用户在使用时按需进行加载

简单来说,就是把一个完整的项目拆成了不同的几个子项目,或者你可以认为我们将一个大的 JS 文件按照模块拆成了几个小的 JS 文件 -> 当我们去构建我们这个项目的时候,就可以把这些功能打包成不同的分包,也就是所谓的模块 -> 当用户使用时就可以进行按需加载了

把「小程序」看做是一个大的 JS 文件,把「子包」看做是小的模块,那么「啥是分包」就很好理解了

💡:分包的好处

  • 可以优化小程序首次启动的下载时间 -> 如本来要下载一个大的 JS 文件(100 k),现在把它拆成十个小模块(假设每个模块是10 k),可以看到 10 k的下载速度显然要比100 k的快!
  • 在多人协作共同开发时,可以更好的解耦协作 -> 10 个人改一个 JS 文件和一个人改一个 JS 文件 -> 方便解耦协作

微信小程序提供了分包的能力,而 uni-app 也对分包的功能进行了支持!

2、实现分包

在 uni-app 中如何实现分包功能 ?

  1. 打开 pages.json,新建 subPackages 节点 -> 是一个数组,数组中每一个对象元素都是一个分包
  2. 节点中每个对象为一个分包,其中
    1. root:分包包名
    2. name:分包别名
    3. pages:分包下的关于别名的页面路径和窗口表现
      1. path:分包下的页面路径
      2. style:页面的样式
  3. 自己手动创建页面,小程序里边会根据这个path自动创建页面
    1. 新建一个subpkg目录
      1. 创建一个pages目录 -> 存放当前所有的页面
        1. 右键,新建 uni-app 页面:search-blog.vue -> 如果你对 uni-app 不爽,那你可以在 VS Code 里边通过之前安装的插件来新建页面

Demoopen in new window

分包

创建的search-blog.vue是子页面 -> 这个文件在subpkg/pages/search-blog/里边,我们得手动创建它 -> 你用 HbuilderX 创建这个页面时,会有不好的体验

💡:如何验证我们当前的分包已经实现了?

验证分包情况

微笑小程序开发者功能 -> 详情 -> 本地代码有:主包 + /subpkg/(分包)这两个包 -> 这就证明我们的分包已经创建完成了

如果没生效,请注意是不是 uni-app 的编译问题!


我看了这个项目最终完成时的分包代码:

分包

这样岂不是只分了一个包?

👇:有了这个分包页面后,就可以来开发这个search-blog页面了

4)文章搜索 - 完成跳转,渲染搜索框

定位到首页,完成页面跳转

  1. hot.vue -> my-search组件的包裹器view添加点击事件 -> @click="onToSearch"
  2. 用户点击这个view -> 触发onToSearch -> 使用uni.navigateTo({})方法完成页面跳转 -> 页面路径在分包路径下:'/subpkg/pages/search-blog/search-blog'
  3. search-blog页面渲染搜索框
    1. my-search组件目前值拥有一个按钮,还没有输入的能力 -> 我们需要赋予它新能力(下一节再赋予,这节只是让它渲染出来)
    2. 添加基础结构
    3. 添加基础样式 -> 有吸顶效果

效果:

效果

Demoopen in new window

5)文章搜索 - 为 my-search 组件赋予搜索的能力-1

我们希望通过my-search组件来渲染我们的搜索框,但是就目前的这个my-search组件,它并不具备搜索能力

💡:如何给my-search赋予搜索的能力?

对于 uni-app 来说,它提供了一个uni-search-bar搜索栏组件

文档:uni-search-bar 搜索栏 - DCloud 插件市场 open in new window

这个搜索栏组件具备的功能:

功能

从文档里边我们可以看到它的:

  • 兼容性
  • 基本的使用姿势
  • API
  • 事件
  • 插槽(替换icon

我们之前创建这个项目的时候,选择了uni-ui,所以这个搜索栏组件已经被安装了

搜索栏组件

因此,我们可以直接使用它

总之,我们可以利用它来完成我们的搜索功能

💡:实现逻辑

为了好调试(不要每次都在hot页面点击然后进入页面),请修改编译模式,把search-blog页面作为启动页面

  1. 定位到my-search.vue
    1. 添加uni-search-bar组件标签
    2. 「搜索按钮」和「搜索输入框」不能同时展示 -> 添加isShowInput这个props
    3. 根据想要的效果 -> 添加样式 -> 由于这个组件在很多地方都要用,所以不要把样式给写死了 -> 添加配置对象config这个props(有图标、背景色、边框等样式的配置) -> 让父组件指定my-search的样式
  2. uni-search-bar传递参数,如radiusbgColor
    1. 添加插槽 -> 指定图标:使用uni-icons组件,图标类型是clear
  3. my-search-bar添加宽度100%样式
  4. 改造输入框,让它符合最终实现效果 -> 搜索框的交互 -> 根据输入内容,点击按钮等,其底部会有视图切换
    1. 绑定valueuni-search-bar不使用双向数据绑定,因为会修改父组件传递过来的数据 -> 毕竟我们定义了一个value props -> 所以使用单向绑定
    2. 绑定事件 -> 看文档了解这些事件有啥用

Demoopen in new window

效果:

效果

💡:uni-icons的效果

uni-icons

6)文章搜索 - 为 my-search 组件赋予搜索的能力-2

💡:什么时候触发某个事件?

触发事件

confirm在输入框内回车就会触发

💡:让my-search组件具备非常强大的搜索输入框能力

my-search组件输入内容 -> 触发onInput事件 -> 把这个事件的发生通知给父组件,并且把这次输入的内容传给父组件

父组件在使用my-search组件时添加v-model

注意,可以使用这个v-model是有条件的(vue 中的基本知识):

v-model是 vue 中完成双向数据绑定的指令,如果说该指令想要应用到组件的绑定中,那么需要遵守以下条件:

  1. 子组件中,也就是my-search组件中接收到的值必须以value命名
  2. 子组件中想要修改value时,必须要发送一个叫做input的事件

满足以上两点,父组件就可以通过v-model指令把值直接传递给value这个props

Demoopen in new window

这个代码的主要功能就是把子组件触发的事件抛给父组件这一层去处理 -> 关键点:父子组件的双向数据绑定!

💡:占位内容居中展示?

居中

设计稿是居左展示

这个功能是无法通过uni-search-bar进行配置的,毕竟它没有提供可以让文本居中这样的一个属性

当组件无法提供给我们想要的能力的时候,那我们只能去修改这个组件了 -> 也就是修改uni-search-bar的源代码

.uni-searchbar__box {
  /* 处理初始 searchbar 位置 */
  /* 默认值是 center */
  justify-content: start;
}

至此,我们就完成了对整个搜索框的赋能,我们给my-search组件赋予了全新的能力:搜索输入框

本质就是间接给uni-search-bar组件添加非常多的配置项,以及对应的回调方法

并且,我们也可以通过v-model来完成父组件与子组件中绑定数据的双向绑定:

父组件search-blog<my-search v-model="searchVal"></my-search>

子组件my-searchvalue + this.$emit('input',val)

7)文章搜索 - 显示推荐搜索

需要发送请求 -> 看「返回默认搜索内容」这个接口

  1. 创建api/search.js -> 和搜索相关的接口都在这儿
    1. 添加getDefaultText函数
  2. search-blog.vue里边定义loadDefaultText函数 -> 用来加载数据 -> 在created里边调用

效果

Demoopen in new window

8)文章搜索 - 创建三个业务组件

创建我们开头所说的三个组件:

  • 慕课热搜
  • 搜索历史
  • 搜索结果

毕竟我们整个文章搜索页面,说白了就是由这三个组件来组成的

目前这个代码的效果有个小问题:

抖动

为啥会抖动?

抖动

默认情况下是有5px,当你输入时,text变成了input,这5px就消失了

💡:创建三个组件

  • search-hot-list:搜索列表
  • search-history:搜索历史
  • search-result-list:搜索结果

search-blog页面里边渲染这三个基本组件

效果

Demoopen in new window

正常情况下,这三个组件可不会一同展示!

👇:控制这个三个组件的展示!

9)文章搜索 - 控制业务组件的展示效果

按照不同的业务逻辑,分别展示这三个组件:

展示组件

  • 默认展示「搜索列表」 -> 点击了输入框的取消按钮时,也会展示「搜索列表」
  • 输入框获取到焦点时,就会展示「搜索历史」 -> 点击输入框清空按钮时,也会展示「搜索历史」
  • 用户点击热搜列表 item 或者用户点击搜索历史 item 或者输入框回车或输入内容后回车,就会展示「搜索结果」页面

组件的展示逻辑搞明白后,就得通过代码来实现这个展示逻辑了!

💡:实现过程

  1. 通过一个数据showType来决定展示哪个组件
    1. 012表示吗? -> 这很不清晰,得用常量
    2. HOT_LIST SEARCH_HISTORYSEARCH_RESULT
    3. 默认值是:HOT_LIST
  2. 在 HTML 中使用常量(注意:template里边,只可以访问data中定义的数据)
    1. 让这三个常量都作为data
  3. 控制showType的值 -> 也就是那几个事件要干的事儿
    1. 失去焦点啥也不干 -> 这个处理函数无用
    2. 注意这个clear事件 -> 此时展示搜索结果,你点击清空按钮,也得返回搜索历史

实现过程

Demoopen in new window

👇:实现这三个组件

10)热搜列表 - 数据获取

分析热搜列表的业务:

热搜列表

如何实现? -> 获取热搜列表数据 -> 看接口文档

效果:

效果

组件在被v-if切换时,是会销毁组件的! -> 所以这个请求也会重新发起 -> 毕竟又重新渲染组件了!

Demoopen in new window

👇:把获取到的数据进行一个基本的展示

11)热搜列表 - 数据展示

  • 卡片展示样式 -> 很多地方用到 -> 定义到全局样式里边
  • 渲染 HTML 和 对应的 CSS
  • HTML 结构
    • 标题:search-hot-title
    • 列表:search-hot-item
      • 使用hot-ranking组件
      • 文本:单行展示
      • 图标:hot-icon -> 只展示前三个
  • 样式实现

效果:

效果

Demoopen in new window

👇:对热搜列表中,关于item项的点击事件

12)热搜列表 - 热搜点击处理

点击 item 项会有一个搜索效果 -> 相当于是你往搜索输入框输入你所点击的这个 item 的标题

实现:

  1. 给 item 项添加点击事件 -> 我们不希望在这个子组件里边处理这个事件 -> 而是交给父组件search-blog去处理
    1. 为啥这样做? -> 因为最终的搜索结果是在父组件里边进行的
    2. 抛出一个onSearch事件给父组件 -> 参数是这个 item 的标题
  2. 父组件监听这个onSearch事件 -> 直接触发了那个onSearchConfirm回调

效果:

效果

Demoopen in new window

至此,这个热搜列表就已经完成了 -> 热搜列表是「文章搜索」中最简单的一个内容,只需要获取数据,然后展示数据,处理一下 item 事件就可以了

👇:处理搜索历史的业务逻辑

13)搜索历史 - 渲染基本结构

换一种套路来写这个组件 -> 分成两步来实现这个「搜索历史」:

  1. 实现基本的 HTML 和 CSS
  2. 实现业务逻辑

分析完成后的结果:

结果

实现:

  1. search-history
    1. search-history-title-box
      1. search-history-title
      2. 逻辑判断,切换视图 -> 默认不展示×按钮,展示「辣鸡箱」
        1. 辣鸡箱 icon
        2. 「全部删除」「完成」
    2. search-history-box
  2. 给假的搜索历史数据源

效果:

效果

Demoopen in new window

至此,搜索历史的 HTML 就完成了,整个 HTML 非常简单,没有涉及到任何的业务逻辑

👇:把 HTML 对应的 CSS 搞定,再去看剩下的复杂内容

14)搜索历史 - 美化基本样式

实现:

  1. 把选择器先写下来
  2. 写样式

效果:

效果

Demoopen in new window

👇:完成搜索历史的业务逻辑 -> 会面临很多困难

15)搜索历史 - 保存历史数据到 searchData

完成搜索历史的切换逻辑

方案:把搜索历史保存到本地,而不是发送请求

💡:怎样的数据算是搜索历史数据?

当用户对该数据进行了搜索之后,那么该数据就算是搜索历史的数据了!

💡:实现

  1. 在父组件存储搜索历史数据,而不是在原先的子组件 -> 在子组件里边存储不方便! -> 父组件把数据传给子组件
  2. 用户在输入框内回车 -> 调用onSearchConfirm方法 -> 调用saveSearchData(保存这个输入内容到搜索历史数据中)
  3. 搜索旧数据,会把这个旧数据从历史记录里边删除,然后再重新放到左上角

实现

效果:

效果

Demoopen in new window

👇:实现searchData中的删除操作

16)搜索历史 - 处理 searchData 的删除操作

删除分为两种:

  1. 删除指定数据
  2. 删除全部数据

文档:交互反馈 - uni-app 官网open in new window

uni.showModal:显示模态弹窗,可以只有一个确定按钮,也可以同时有确定和取消按钮。类似于一个 API 整合了 html 中:alert、confirm。

实现过程:

  1. 子组件 -> 给「全部删除」和搜索历史 item 绑定点击事件
    1. onClearAll
    2. onHistoryItemClick
  2. onClearAll的逻辑:
    1. uni.showModal({})
      1. 删除全部 -> 确定 -> 抛出removeAllSearchData通知 -> 切换回「垃圾箱」
      2. 点击取消按钮 -> 啥也不干
  3. onHistoryItemClick的逻辑:
    1. 判断当前是否要删除? -> 是 -> 证明有「×」按钮 -> 抛出removeSearchData通知,传当前被点击的item索引
  4. 父组件 -> 接收到事件通知
    1. removeSearchData事件 -> 从历史数据里边删除这条
    2. removeAllSearchData事件 -> 清空历史数据

效果:

效果

Demoopen in new window

💡:抛出一个通知,会先执行这个通知所对应的事件处理函数

通知

17)搜索历史 - 找出现在的问题

到目前,我们已经完成了 搜索历史 的展示和删除的功能,但是还有一个 数据持久化 的功能未实现

未实现的功能我们先不着急,我们先回头看一下我们现在的代码

现在的问题

在现在的代码中,我们在 search-blog 中通过 searchData 保存了所有的搜索历史数据

而在真正的搜索历史页面中,反而是通过 props 接收了 父组件传递过来的数据

添加、删除搜索历史的功能,都放到了 search-blog 页面里进行了实现,反而把 删除的激活 操作放到了 search-history 组件中

这样的一系列操作,我们光描述都要花费上 一分钟 的时间,更不用说让别人去读你的代码了

如果我们在这样的代码基础之上,再去实现 数据持久化 的功能,那么咱们的代码就会的更加复杂,难以理解了

所以说,我们现在迫切需要做一件事情,那就是:search-blogsearch-history 解耦,让 searchData 和 组件 解耦 !

那么这个事情,我们怎么做呢?

这里请允许我先卖一个关子

欲知后事如何,请见下一章《全局状态管理

18)总结

本章节中,我们完成了 部分 的文章搜索功能

  1. 使用 分包 创建了 search-blog 页面
  2. 分析页面得到了 三个组件
    1. 热搜列表:search-hot-list
    2. 搜索历史:search-history
    3. 搜索结果:search-result-list
  3. 定义了规则,控制了三个组件的展示规律
  4. 完成了 热搜列表 的功能
  5. 在完成 搜索历史 时,遇到了问题:search-blogsearch-historysearchData 和 组件 之间(产生了)强耦合 (关系)

那我们该如何解决这个「强耦合」问题呢?

想要解决这个问题,那么我们需要用到一个新的东西,叫做 《全局状态管理工具》,那么这个东西怎么用呢?它有什么样的价值呢?

我们下一章再见!