移动端头条类项目搜索功能

896 阅读5分钟

功能分析

  1. 点击搜素框进行路由页面跳转
  2. 当搜索框中内容为空时展示历史搜索记录,并且可以对历史搜索记录进行一键清空和删除
  3. 在搜索框中输入内容时可以根据输入的内容进行联想词汇搜索
  4. 当点击搜索或者联想的内容时要显示搜索出的结果

静态页面搭建

  • 使用了vant组件库中的搜索组件,搭建搜索页面的搜索样式,利用list组件和cell单元格组件,搭建了搜索记录联想词汇和搜索结果的面板

控制页面显示功能

  • 功能技术点:使用v-if属性控制什么时候显示合适的面板,当搜索框有内容时展示联想面板,然后定义一个变量isResultShow为false当点击搜索时改为true让搜索结果面板显示
<!-- 搜索结果 -->
<search-result v-if="isResult" />
<!-- /搜索结果 -->

<!-- 联想建议 -->
<search-suggestion v-else-if="searchText" />
<!-- /联想建议 -->

<!-- 搜索历史记录 -->
<search-history v-else />
<!-- /搜索历史记录 -->

联想词汇搜索功能

  • 功能逻辑:当搜索框输入内容时候,请求加载联想建议的数据,然后将请求得到的结果绑定到模版中
  • 功能技术点:这里需要把联想词汇搜索功能二次封装成组件,然后利用父传子的技术将父组件搜索框中的 内容传给联想词汇组件,并且在联想词汇组件中使用wacth属性监听父组件搜索框内容的变化,当内容变化时就在其中发起网络请求
watch: {
    searchText: {
      // 监视的处理函数
      handler (val) {
        this.loadSearchSuggestion(val)
      },
      // 首次监视触发
      immediate: true
    }
  },
  created () {},
  mounted () {},
  methods: {
    async loadSearchSuggestion (q) {
      try {
        const { data } = await getSearchSuggestion(q)
        this.suggestions = data.data.options
      } catch {
        this.$toast('获取失败')
      }
    }
  }
  • 功能技术点2:这里为了让用户在频繁输入内容时不重复的发送网络请求,所以做了防抖优化,使用了第三方的lodash包,调用了其中的debunce模块做防抖优化
// lodash 支持按需加载,有利于打包结果优化
import { debounce } from "lodash"
// debounce 函数
// 参数1:函数
// 参数2:防抖时间
// 返回值:防抖之后的函数,和参数1功能是一样的
handler: debounce(function (val) {
    this.loadSearchSuggestion(val)
}, 1000)
  • 功能技术点3:这里对用户搜索的关键字在联想到面板时做了高亮处理效果
  • 实现逻辑:找到需要高亮的字符,然后使用模版字符串的方式拼接一个带有高亮类名的html标签,然后new RegExp构造函数,使用gi全局匹配的方式找到所有符合条件的字符,最后将符合条件的不高亮字符替换成高亮字符
/**
     * 处理高亮文本
     * 思路:
     * 1. 想要在一个字符串中,将固定的字符特殊显示(改变颜色)
     * 2. 那么就需要在这个字符串中,找出该字符,然后为该字符设置单独的样式(span.active)
     * 拆解:
     *     1. 找出字符
     *     2. 替换字符
     *     3. 设置单独的样式比较容易(替换字符),难点在于找出字符
     * 如何找出字符:
     * 1. 那么《处理高亮文本》的问题,就变成,《如何在字符串中找出固定的字符》
     * 2. 在字符串中找出固定字符,大家首先想到的就应该是使用 -》 正则表达式
     * 3. 简单使用正则(text.replace(/匹配的内容/gi, highlightStr)) , 无法插入响应式数据
     * 4. 所以我们使用了 RegExp 对象。RegExp 构造函数创建了一个正则表达式对象,用于将文本与一个模式匹配。MDN: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp
     * 5. 通过 RegExp 来完成响应式数据的正则匹配
     */
    highlightText(text) {
      const highlightStr = `<span class="active">${this.searchText}</span>`;
      // 正则表达式 // 中间的内容都会当作匹配字符来使用,而不是数据变量
      // 如果需要根据数据变量动态的创建正则表达式,则手动 new RegExp
      // RegExp 正则表达式构造函数
      //    参数1:匹配模式字符串,它会根据这个字符串创建正则对象
      //    参数2:匹配模式,要写到字符串中
      const reg = new RegExp(this.searchText, 'gi');
      // text.replace(/匹配的内容/gi, highlightStr)
      return text.replace(reg, highlightStr);
    }
  • 最后在联想列表模版中绑定使用,这里需要用到v-html标签绑定使用才可以使用
<!-- 联想建议 -->
<van-cell-group v-else-if="searchContent">
  <van-cell
    icon="search"
    v-for="(item, index) in suggestions"
    :key="index"
    @click="onSearch(item)"
  >
    <div slot="title" v-html="highlight(item)"></div>
  </van-cell>
</van-cell-group>
<!-- /联想建议 -->

搜索结果功能

  • 功能实现逻辑:使用了vant组件库内置的搜索组件,该组件绑定了onSearch事件,当按下回车时触发搜索功能,这个时候需要把输入框里的值作为形参传递给定义好的searchText使用父传子的方式传给搜索结果组件,并在搜索结果组件中发送网络请求,请求数据并渲染页面
  • 功能技术点:使用了vant组件库的list组件,该组件给数据的方式不是直接给空数组赋值,而是通过push的方式将数据添加进空数组,并且该组件的loading属性的控制是组件生效的关键,当触发事件时组件会自动将loading属性改为true,当数据添加玩后需要手动改回false否则会一直加载,当数据加载完成时需要将finish属性设置为true
  methods: {
   async onLoad () {
    if(this.searchText == ""){
        return this.loading = false
    }
       // 1. 请求获取数据
      const { data } = await getSearch({
        page: this.page, // 页码
        per_page: this.perPage, // 每页大小
        q: this.searchText // 搜索关键字
      })
      // 2. 将数据添加到列表中
      const { results } = data.data
      this.list.push(...results)
         // 3. 设置加载状态结束
      this.loading = false
       // 4. 判断数据是否加载完毕
        if (results.length) {
        this.page++ // 更新获取下一页数据的页码
        } else {
        this.finished = true // 没有数据了,将加载状态设置结束,不再 onLoad
      }
    }

搜索历史记录功能

  • 功能技术点1:这里需要在父组件中定义一个空数组来储存搜索的历史记录,但是由于历史记录不可重复,所以首先需要对该数组进行查重的判断,这里完使用了数组的indexOf方法,当数组中元素重复时会返回一个-1所以我只需要判断值为-1时将该元素从数组中删除即可,如果不为-1那就表示不重复,就可以正常添加数据
onSearch (val) {
  // 更新文本框内容
  this.searchText = val

  // 存储搜索历史记录
  // 要求:不要有重复历史记录、最新的排在最前面
  const index = this.searchHistories.indexOf(val)     
  if (index !== -1) {
    this.searchHistories.splice(index, 1)
  }
  this.searchHistories.unshift(val)

  // 渲染搜索结果
  this.isResultShow = true
},
  • 功能技术点2:需要使用父传子的技术,将数据传给搜索历史记录的组件,在组件中完成渲染页面和删除的后续操作
<!-- 搜索历史 -->
<search-history v-else-if="!value" :searchHistory="searchHistory" />

props: {
    searchHistory: {
        type: Array,
        required: true
    }
}
  • 功能技术点3:在删除操作前需要对按钮做出判断,当按钮为删除图标时点击变成完成,然后执行删除操作,当为完成状态时点击变为删除图标,不可执行删除操作,这里为定义了一个布尔变量,使用v-if属性控制面板的变化
<!-- 历史记录 -->
<van-cell-group v-else>
  <van-cell title="历史记录">
    <template v-if="isDeleteShow">
      <span @click="searchHistories = []">全部删除</span>
      &nbsp;&nbsp;
      <span @click="isDeleteShow = false">完成</span>
    </template>
    <van-icon v-else name="delete" @click="isDeleteShow = true"></van-icon>
  </van-cell>
  <van-cell
    :title="item"
    v-for="(item, index) in searchHistories"
    :key="index"
    @click="onSearch(item)"
  >
    <van-icon
      v-show="isDeleteShow"
      name="close"
      @click="searchHistories.splice(index, 1)"
    ></van-icon>
  </van-cell>
</van-cell-group>
<!-- /历史记录 -->




data () {
  return {
    ...
    isDeleteShow: false
  }
}
  1. 删除单条数据:给数据绑定一个点击事件,当为可以删除状态时点击使用数组的splice方法删除对应数据,否则则使用$emit子传父的方式重新调用搜索的方法
onHistoryClick (item, index) {
  // 如果是删除状态,则执行删除操作
  if (this.isDeleteShow) {
    this.searchHistory.splice(index, 1)
  } else {
    // 否则执行搜索操作
    this.$emit('search', item)
  }
}

2:删除全部历史记录:这里要注意,由于vue单项数据流的特性,所以如果给数组重新赋值,我们需要在父组件中操作,所以这里还是需要使用$emit的子传父方法,在点击事件调用时在父组件中将数组重新设置为空

  <span @click="$emit('clear-search-history')" style="margin-right: 10px;">全部删除</span>
  • 功能技术点4:最后需要做一个数据持久化的处理,需要使用vue的watch属性监听输入框中值的变化,当值变化时,使用本地存储将输入的值存储到本地中,并且在data中定义的数据从一开始就从本地中取值,就可以完成数据的持久化处理
watch: {
  searchHistories (val) {
    // 同步到本地存储
    setItem('serach-histories', val)
  }
},
data () {
  return {
    ...
    searchHistories: getItem('serach-histories') || [],
  }
}