目录
01: 前言
02: 多组件联动注意事项与整体逻辑分析
多组件之间需要共享数据
监听数据变化的方式
整体逻辑分析
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.jsimport { ALL_CATEGORY_ITEM } from '@/constants'export default {namespaced: true,state: () => ({// 当前选中的分类currentCategory: ALL_CATEGORY_ITEM,}),mutations: {/*** 切换选中分类*/changeCurrentCategory(state, newCategory) {state.currentCategory = newCategory},}
}
// src/store/getters.jsexport 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 = falsepexelsList.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=""><divv-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) returnconst { 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-iconname="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"><divv-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-iconname="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"><divv-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"><divv-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 = falsesetTimeout(() => {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.jsimport { 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) => {// 允许只传递 contentif (title && !content) {content = titletitle = ''}// 关闭弹层事件const close = () => {render(null, document.body)}// 取消按钮事件const cancelHandler = () => {reject(new Error('取消按钮点击'))}// 确定按钮事件const confirmHandler = () => {resolve()}// 1. vnodeconst vnode = h(confirmComponent, {title,content,cancelText,confirmText,confirmHandler,cancelHandler,close})// 2. renderrender(vnode, document.body)})
}
// src/libs/index.jsexport { 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"><divclass="relative rounded w-[260px] cursor-pointer":style="{backgroundColor: randomRGB()}"><imgclass="h-full w-full object-cover rounded"v-lazy:src="themeData.big.photo"alt=""/><pclass="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]"><divv-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()}"><imgclass="w-full h-full object-cover rounded"v-lazy:src="item.photo"/><pclass="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.jsexport 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 函数