多组件联动与高阶组件解决方案--list联动search和navigationBar

目录

01: 前言

02: 多组件联动注意事项与整体逻辑分析

多组件之间需要共享数据

监听数据变化的方式

整体逻辑分析 

03: 简单联动处理:navigationBar 对应 list 

04: 明确 searchBar 对应 list 处理流程

05: searchBar:搜索提示初步实现 

06: searchBar:处理防抖功能

07: searchBar:提示关键字高亮处理

08: searchBar:搜索历史处理 

09: 通用组件:confirm 应用场景

10: 通用组件:vnode+h函数+render函数 明确confirm构建思路

11: 通用组件:构建 confirm 组件

12. 通用组件:函数调用 confirm组件 

13: searchBar:热门精选模块构建

14. searchBar 联动 list 

15. 总结 


 

01: 前言

        到目前为止,我们已经实现了首页的 search、navigationBar、list 模块。只不过目前三个模块完全是独立的,没有任何的关联性。

        接下来要处理的就是让它们三个可以联动起来。也就是标题所说的:list 联动 search 和 navigationBar

         在这样的联动之中,我们应该注意哪些事情?联动的数据又应该如何进行处理?如何做可以让我们的逻辑更加清晰?高阶组件指的又是什么?如何创建和使用高阶组件?

02: 多组件联动注意事项与整体逻辑分析

        在我们的实际开发中,经常会遇到多个组件之间互相进行联动的场景。这样的场景我们应该怎么进行处理呢?

        所谓的多组件联动,其实更准确一点来说,是指:多个组件之间,存在一个或者多个共享的数据。当数据发生改变时,执行对应的逻辑。

        把这句话拆开来看,就是两部分:

        1. 多组件之间需要共享数据。

        2. 监听数据变化,并执行对应逻辑。

多组件之间需要共享数据

        多组件之间共享数据,通常有三种方式:

        1. 组件之间的数据传递 -- 常见于层级关系比较清晰的多组件之中。

                1. 父传子。

                2. 子传父。

                3. ……

        2. 依赖注入:Provide / Inject -- 嵌套层级比较深,并且子组件只需要父组件的部分内容。

        3. 全局状态管理工具:vuex -- 以上两种情况都不适用的情况下。 

        针对于我们这里的场景,层级关系比较复杂,并且需要进行复杂的逻辑操作。此时,我们在多组件之间共享数据的策略就需要通过 vuex 来实现。

监听数据变化的方式

        当组件之间共享的数据发生变化时,我们需要执行对应的逻辑操作。首先我们就需要监听到数据的变化。

        在 vue 中监听数据变化的方式,首推就是 watch。

        在刚才我们已经确定了共享的数据需要保存到 vuex 中,所以我们就需要通过 watch 监听到 vuex 中共享数据的变化。在监听到变化时,执行对应的业务逻辑。

整体逻辑分析 

        依据我们以上所说的内容,整体的实现逻辑应该为:

        1. 创建共享数据对应的 vuex modules 模块。

        2. 在 getters 中建立对应的快捷访问计算属性。

        3. 在对应的业务组件中,监听 getters,并执行对应逻辑。

03: 简单联动处理:navigationBar 对应 list 

关键点:共享数据发生变化 引起 逻辑数据发生变化。

- src/store/modules
- - app.js
// src/store/modules/app.js

import { ALL_CATEGORY_ITEM } from '@/constants'

export default {
  namespaced: true,
  state: () => ({
    // 当前选中的分类
    currentCategory: ALL_CATEGORY_ITEM,
  }),
  mutations: {
    /**
     * 切换选中分类
     */
    changeCurrentCategory(state, newCategory) {
      state.currentCategory = newCategory
    },
  }
}
// src/store/getters.js

export default {
  ……
  /**
   * category选中项
   */
  currentCategory: (state) => state.app.currentCategory,
  /**
   * category选中项下标
   */
  currentCategoryIndex: (state, getters) => {
    return getters.categorys.findIndex(
      (item) => item.id === getters.currentCategory.id
    )
  },
  ……
}
// src/views/main/components/list/index.vue

<script setup>
    /**
     * 构建数据请求
     */
    let query = {
      page: 1,
      size: 20,
      categoryId: '',
      searchText: ''
    }

    /**
     * 通过此方法修改 query 请求参数,重新发起请求
     */
    const resetQuery = (newQuery) => {
      query = { ...query, ...newQuery }
      // 重置状态
      isFinished.value = false
      pexelsList.value = []
      // 数据为空,“加载”icon出现在屏幕中,会触发onLoad事件。秒呀!!!
    }

    /**
     * 监听 currentCategory 的变化
     */
    watch(
      () => store.getters.currentCategory,
      (currentCategory) => {
        // 重置请求参数
        resetQuery({
          page: 1,
          categoryId: currentCategory.id
        })
      }
    )
</script>

04: 明确 searchBar 对应 list 处理流程

对于 searchBar 区域,我们目前还缺少三部分内容要处理:

        1. 搜索提示

        2. 搜索历史

        3. 推荐主题

需要先把 searchBar 区域的内容开发完成,然后再处理对应的联动。

05: searchBar:搜索提示初步实现 

- src/views/components/header/header-search
- - hint.vue
<template>
  <div class="">
    <div
      v-for="(item, index) in hintData"
      :key="index"
      class="py-1 pl-1 text-base font-bold text-zinc-500 rounded cursor-pointer duration-300 hover:bg-zinc-200 dark:hover:bg-zinc-900"
      @click="onItemClick(item)"
      v-html="highlightText(item)"
    ></div>
  </div>
</template>

<script>
const EMITS_ITEM_CLICK = 'itemClick'
</script>

<script setup>
import { getHint } from '@/api/pexels'
import { ref, watch } from 'vue'
import { watchDebounced } from '@vueuse/core'

/**
 * 接收搜索数据
 */
const props = defineProps({
  searchText: {
    type: String,
    required: true
  }
})
/**
 * item 被点击触发事件
 */
const emits = defineEmits([EMITS_ITEM_CLICK])

/**
 * 处理搜索提示数据获取
 */
const hintData = ref([])
const getHintData = async () => {
  if (!props.searchText) return
  const { result } = await getHint(props.searchText)
  hintData.value = result
}
/**
 * 监听搜索文本的变化,并获取对应提示数据
 */
watchDebounced(() => props.searchText, getHintData, {
  immediate: true,
  // 每次事件触发时,延迟的时间
  debounce: 500
})

/**
 * 处理关键字高亮
 */
const highlightText = (text) => {
  // 生成高亮标签
  const highlightStr = `<span class="text-zinc-900 dark:text-zinc-200">${props.searchText}</span>`
  // 构建正则表达式,从《显示文本中》找出与《用户输入文本相同的内容》,
  // 使用《高亮标签》进行替换。
  const reg = new RegExp(props.searchText, 'gi')
  // 替换
  return text.replace(reg, highlightStr)
}

/**
 * item 点击事件处理
 */
const onItemClick = (item) => {
  emits(EMITS_ITEM_CLICK, item)
}
</script>
// 使用

<hint-vue v-show="inputValue" :searchText="inputValue" @itemClick="onSearchHandler">
</hint-vue>

06: searchBar:处理防抖功能

        所谓防抖指的是:当触发一个事件时,不去立刻执行。而是延迟一段时间,该事件变为等待执行事件。如果在这段时间之内,该事件被再次触发,则上次等待执行的事件取消。本次触发的事件变为等待执行事件。循环往复,直到某一个等待事件被执行为止。

        英文:debounce。

        vueuse 中提供了 watchDebounced ,可以使用这个 API 实现防抖的 watch。

07: searchBar:提示关键字高亮处理

核心逻辑:

        正则替换,把原先的正常文本 替换成 带有 html 标签的文本。最后通过 v-html 进行富文本渲染。 

08: searchBar:搜索历史处理 

- src/store/modules
- - search.js
export default {
  namespaced: true,
  state: () => ({
    historys: []
  }),
  mutations: {
    /**
     * 1. 新增的历史记录位于头部
     * 2. 不可出现重复的记录
     */
    addHistory(state, newHistory) {
      const isFindIndex = state.historys.findIndex(
        (item) => item === newHistory
      )
      // 剔除旧数据
      if (isFindIndex !== -1) {
        state.historys.splice(isFindIndex, 1)
      }
      // 新增记录
      state.historys.unshift(newHistory)
    },
    /**
     * 删除指定数据
     */
    deleteHistory(state, index) {
      state.historys.splice(index, 1)
    },
    /**
     * 删除所有历史记录
     */
    deleteAllHistory(state) {
      state.historys = []
    }
  }
}
// 有了 modules 之后,注意在 index.js 中注册、缓存,并声明 getters。
// 代码省略。
- src/views/layout/components/header/header-search
- - history.vue
<template>
  <div class="">
    <div class="flex items-center text-xs mb-1 text-zinc-400">
      <span>最近搜索</span>
      <m-svg-icon
        name="delete"
        class="w-2.5 h-2.5 ml-1 p-0.5 cursor-pointer duration-300 rounded-sm hover:bg-zinc-100"
        fillClass="fill-zinc-400"
        @click="onDeleteAllClick"
      ></m-svg-icon>
    </div>

    <div class="flex flex-wrap">
      <div
        v-for="(item, index) in $store.getters.historys"
        :key="item"
        class="mr-2 mb-1.5 flex items-center cursor-pointer bg-zinc-100 px-1.5 py-0.5 text-zinc-900 text-sm font-bold rounded-sm duration-300 hover:bg-zinc-200"
        @click="onItemClick(item)"
      >
        <span>{{ item }}</span>
        <m-svg-icon
          name="input-delete"
          class="w-2.5 h-2.5 p-0.5 ml-1 duration-300 rounded-sm hover:bg-zinc-100"
          @click.stop="onDeleteClick(index)"
        ></m-svg-icon>
      </div>
    </div>
  </div>
</template>

<script>
const EMITS_ITEM_CLICK = 'itemClick'
</script>

<script setup>
import { useStore } from 'vuex'
import { confirm } from '@/libs'

const emits = defineEmits([EMITS_ITEM_CLICK])

const store = useStore()

/**
 * 删除所有记录
 */
const onDeleteAllClick = () => {
  confirm('要删除所有历史记录吗?').then(() => {
    store.commit('search/deleteAllHistory')
  })
}

/**
 * 删除单个记录
 */
const onDeleteClick = (index) => {
  store.commit('search/deleteHistory', index)
}

/**
 * item 点击触发事件
 */
const onItemClick = (item) => {
  emits(EMITS_ITEM_CLICK, item)
}
</script>

<style lang="scss" scoped></style>

09: 通用组件:confirm 应用场景

        目前当我们点击 删除全部 历史记录时,会直接删除。这样的体验并不好。我们期望的是能够给用户一个 提示,也就是 confirm。 

        期望:构建一个 confirm 组件。

        对于 confirm 这类组件而言,我们不希望它通过标签的形式进行使用。而是期望可以像 element-plus 中的 confirm 一样,可以直接通过方法的形式进行调用,这样就太爽了。 

10: 通用组件:vnode+h函数+render函数 明确confirm构建思路

        想要搞明白这一点,我们就需要了解一些比较冷僻的知识点,那就是 渲染函数。在渲染函数中,我们需要了解如下概念:

        1. 虚拟 dom:通过 js 来描述 dom。

        2. vnode 虚拟节点:告诉 vue 页面上需要渲染什么样子的节点。

        3. h 函数:用来创建 vnode 的函数,接收三个参数(要渲染的 dom、attrs 对象、子元素)。

        4. render 函数:可以根据 vnode 来渲染 dom。

        根据以上所说我们知道:通过 h 函数可以生成一个 vnode,该 vnode 可以通过 render 函数被渲染。 

        据此我们就可以得出 confirm 组件的实现思路:

        1. 创建一个 confirm 组件。

        2. 创建一个 confirm.js 模块,在该模块中返回一个 promise。

        3. 同时利用 h 函数生成 confirm.vue 的 vnode。

        4. 最后利用 render 函数,渲染 vnode 到 body 中。

        依据此思路,即可实现对应的 confirm 渲染。

11: 通用组件:构建 confirm 组件

- src/libs
- - confirm
- - - index.vue
- - - index.js
<template>
  <div>
    <!-- 蒙版 -->
    <transition name="fade">
      <div
        v-if="isVisable"
        @click="close"
        class="w-screen h-screen bg-zinc-900/80 z-40 fixed top-0 left-0"
      ></div>
    </transition>
    <!-- 内容 -->
    <transition name="up">
      <div
        v-if="isVisable"
        class="w-[80%] fixed top-1/3 left-[50%] translate-x-[-50%] z-50 px-2 py-1.5 rounded-sm border dark:border-zinc-600 cursor-pointer bg-white dark:bg-zinc-800 xl:w-[35%]"
      >
        <!-- 标题 -->
        <div class="text-lg font-bold text-zinc-900 dark:text-zinc-200 mb-2">
          {{ title }}
        </div>
        <!-- 内容 -->
        <div class="text-base text-zinc-900 dark:text-zinc-200 mb-2">
          {{ content }}
        </div>
        <!-- 按钮 -->
        <div class="flex justify-end">
          <m-button type="info" class="mr-2" @click="onCancelClick">{{
            cancelText
          }}</m-button>
          <m-button type="primary" @click="onConfirmClick">{{
            confirmText
          }}</m-button>
        </div>
      </div>
    </transition>
  </div>
</template>

<script setup>
// confirm 组件会以方法形式调用,自动全局注册的组件无法在其中使用。
// 在方法调用的组件中,需要主动导入组件。
import mButton from '../button/index.vue'
import { ref, onMounted } from 'vue'
const props = defineProps({
  // 标题
  title: {
    type: String
  },
  // 描述
  content: {
    type: String,
    required: true
  },
  // 取消按钮文本
  cancelText: {
    type: String,
    default: '取消'
  },
  // 确定按钮文本
  confirmText: {
    type: String,
    default: '确定'
  },
  // 取消按钮事件
  cancelHandler: {
    type: Function
  },
  // 确定按钮事件
  confirmHandler: {
    type: Function
  },
  // 关闭 confirm 的回调
  close: {
    type: Function
  }
})

// 控制显示处理
const isVisable = ref(false)
/**
 * confirm 展示
 */
const show = () => {
  isVisable.value = true
}
/**
 * render 函数的渲染,会直接进行,无动画效果。
 * 页面构建完成之后,再执行动画。保留动画效果。
 */
onMounted(() => {
  show()
})

// 关闭动画执行时间。0.5s 和css transition 写法保持一致。
const duration = '0.5s'
/**
 * confirm 关闭,保留动画执行时长
 */
const close = () => {
  isVisable.value = false
  setTimeout(() => {
    if (props.close) {
      props.close()
    }
  }, parseInt(duration.replace('0.', '').replace('s', '')) * 100)
}

/**
 * 取消按钮点击事件
 */
const onCancelClick = () => {
  if (props.cancelHandler) {
    props.cancelHandler()
  }
  close()
}

/**
 * 确定按钮点击事件
 */
const onConfirmClick = () => {
  if (props.confirmHandler) {
    props.confirmHandler()
  }
  close()
}
</script>

<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
  transition: all v-bind(duration);
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

.up-enter-active,
.up-leave-active {
  transition: all v-bind(duration);
}

.up-enter-from,
.up-leave-to {
  opacity: 0;
  transform: translate3d(-50%, 100px, 0);
}
</style>

注意: 

        使用 状态驱动 css 概念绑定响应式数据到 css 中。

        confirm 组件会以方法形式调用,自动全局注册的组件无法在其中使用。在方法调用的组件中,需要主动导入组件。 例如:confirm 组件要主动导入 mButton 组件,mButton 也要主动导入m-svg-icon。否则会报警。

12. 通用组件:函数调用 confirm组件 

// src/libs/confirm/index.js

import { h, render } from 'vue'
import confirmComponent from './index.vue'

/**
 *
 * @param {*} title 标题
 * @param {*} content 文本
 * @param {*} cancelText 取消按钮文本
 * @param {*} confirmText 确定按钮文本
 * @returns
 */
export const confirm = (
  title,
  content,
  cancelText = '取消',
  confirmText = '确定'
) => {
  return new Promise((resolve, reject) => {
    // 允许只传递 content
    if (title && !content) {
      content = title
      title = ''
    }

    // 关闭弹层事件
    const close = () => {
      render(null, document.body)
    }

    // 取消按钮事件
    const cancelHandler = () => {
      reject(new Error('取消按钮点击'))
    }

    // 确定按钮事件
    const confirmHandler = () => {
      resolve()
    }

    // 1. vnode
    const vnode = h(confirmComponent, {
      title,
      content,
      cancelText,
      confirmText,
      confirmHandler,
      cancelHandler,
      close
    })
    // 2. render
    render(vnode, document.body)
  })
}
// src/libs/index.js

export { confirm } from './confirm'

使用: 

import { confirm } from '@/libs'

confirm('要删除所有历史记录吗?').then(() => {
  store.commit('search/deleteAllHistory')
})

13: searchBar:热门精选模块构建

- src/views/layout/components/header/header-search
- - theme.vue
<template>
  <div class="">
    <div class="text-xs mb-1 text-zinc-400">热门精选</div>
    <div class="flex h-[140px]" v-if="themeData.list.length">
      <div
        class="relative rounded w-[260px] cursor-pointer"
        :style="{
          backgroundColor: randomRGB()
        }"
      >
        <img
          class="h-full w-full object-cover rounded"
          v-lazy
          :src="themeData.big.photo"
          alt=""
        />
        <p
          class="absolute bottom-0 left-0 w-full h-[45%] flex items-center backdrop-blur rounded px-1 text-white text-xs duration-300 hover:backdrop-blur-none"
        >
          # {{ themeData.big.title }}
        </p>
      </div>
      <div class="flex flex-wrap flex-1 max-w-[860px]">
        <div
          v-for="item in themeData.list"
          :key="item.id"
          class="h-[45%] w-[260px] text-white text-xs relative ml-1.5 mb-1.5 rounded"
          :style="{
            backgroundColor: randomRGB()
          }"
        >
          <img
            class="w-full h-full object-cover rounded"
            v-lazy
            :src="item.photo"
          />
          <p
            class="backdrop-blur absolute top-0 left-0 w-full h-full flex items-center px-1 rounded cursor-pointer duration-300 hover:backdrop-blur-none"
          >
            # {{ item.title }}
          </p>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { getThemes } from '@/api/pexels'
import { randomRGB } from '@/utils/color'

// 处理主题数据
const themeData = ref({
  big: {},
  list: []
})
const getThemeData = async () => {
  const { themes } = await getThemes()
  themeData.value = {
    big: themes[0],
    list: themes.splice(1, themes.length)
  }
}
getThemeData()
</script>

<style lang="scss" scoped></style>

CSS知识点:

        1. CSS object-fit 属性 | 菜鸟教程 

        2. CSS backdrop-filter | 菜鸟教程

14. searchBar 联动 list 

// src/store/modules/app.js

export default {
  namespaced: true,
  state: () => ({
    // 搜索的文本
    searchText: '',
  }),
  mutations: {
    /**
     * 修改 searchText
     */
    changeSearchText(state, newSearchText) {
      state.searchText = newSearchText
    },
  }
}
// 创建 getters。
// 在 index modules 中注册。
// src/views/layout/components/headr/header-search/index.vue

// 触发 searchText 变化
store.commit('app/changeSearchText', val)
// src/views/main/components/list/index.vue

/**
 * 监听搜索内容项的变化
 */
watch(
  () => store.getters.searchText,
  (val) => {
    // 重置请求参数
    resetQuery({
      page: 1,
      searchText: val
    })
  }
)

15. 总结 

本篇文章核心内容包含两部分:

        1. 多组件联动逻辑

        2. confirm 通用组件

                1. vnode 

                2. h 函数

                3. render 函数

  • 26
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chengbo_eva

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值