目录
[01: 前言](#01: 前言)
[02: 多组件联动注意事项与整体逻辑分析](#02: 多组件联动注意事项与整体逻辑分析)
[03: 简单联动处理:navigationBar 对应 list](#03: 简单联动处理:navigationBar 对应 list)
[04: 明确 searchBar 对应 list 处理流程](#04: 明确 searchBar 对应 list 处理流程)
[05: searchBar:搜索提示初步实现](#05: searchBar:搜索提示初步实现)
[06: searchBar:处理防抖功能](#06: searchBar:处理防抖功能)
[07: searchBar:提示关键字高亮处理](#07: searchBar:提示关键字高亮处理)
[08: searchBar:搜索历史处理](#08: searchBar:搜索历史处理)
[09: 通用组件:confirm 应用场景](#09: 通用组件:confirm 应用场景)
[10: 通用组件:vnode+h函数+render函数 明确confirm构建思路](#10: 通用组件:vnode+h函数+render函数 明确confirm构建思路)
[11: 通用组件:构建 confirm 组件](#11: 通用组件:构建 confirm 组件)
[12. 通用组件:函数调用 confirm组件](#12. 通用组件:函数调用 confirm组件)
[13: searchBar:热门精选模块构建](#13: searchBar:热门精选模块构建)
[14. searchBar 联动 list](#14. searchBar 联动 list)
[15. 总结](#15. 总结)
01: 前言
到目前为止,我们已经实现了首页的 search、navigationBar、list 模块。只不过目前三个模块完全是独立的,没有任何的关联性。
接下来要处理的就是让它们三个可以联动起来。也就是标题所说的:list 联动 search 和 navigationBar。
在这样的联动之中,我们应该注意哪些事情?联动的数据又应该如何进行处理?如何做可以让我们的逻辑更加清晰?高阶组件指的又是什么?如何创建和使用高阶组件?
02: 多组件联动注意事项与整体逻辑分析
在我们的实际开发中,经常会遇到多个组件之间互相进行联动的场景。这样的场景我们应该怎么进行处理呢?
所谓的多组件联动,其实更准确一点来说,是指:多个组件之间,存在一个或者多个共享的数据。当数据发生改变时,执行对应的逻辑。
把这句话拆开来看,就是两部分:
-
多组件之间需要共享数据。
-
监听数据变化,并执行对应逻辑。
多组件之间需要共享数据
多组件之间共享数据,通常有三种方式:
-
组件之间的数据传递 -- 常见于层级关系比较清晰的多组件之中。
-
父传子。
-
子传父。
-
......
-
依赖注入:Provide / Inject -- 嵌套层级比较深,并且子组件只需要父组件的部分内容。
-
全局状态管理工具:vuex -- 以上两种情况都不适用的情况下。
针对于我们这里的场景,层级关系比较复杂,并且需要进行复杂的逻辑操作。此时,我们在多组件之间共享数据的策略就需要通过 vuex 来实现。
监听数据变化的方式
当组件之间共享的数据发生变化时,我们需要执行对应的逻辑操作。首先我们就需要监听到数据的变化。
在 vue 中监听数据变化的方式,首推就是 watch。
在刚才我们已经确定了共享的数据需要保存到 vuex 中,所以我们就需要通过 watch 监听到 vuex 中共享数据的变化。在监听到变化时,执行对应的业务逻辑。
整体逻辑分析
依据我们以上所说的内容,整体的实现逻辑应该为:
-
创建共享数据对应的 vuex modules 模块。
-
在 getters 中建立对应的快捷访问计算属性。
-
在对应的业务组件中,监听 getters,并执行对应逻辑。
03: 简单联动处理:navigationBar 对应 list
关键点:共享数据发生变化 引起 逻辑数据发生变化。
bash
- src/store/modules
- - app.js
javascript
// 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
},
}
}
javascript
// 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
)
},
......
}
javascript
// 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 区域,我们目前还缺少三部分内容要处理:
-
搜索提示
-
搜索历史
-
推荐主题
需要先把 searchBar 区域的内容开发完成,然后再处理对应的联动。
05: searchBar:搜索提示初步实现
bash
- src/views/components/header/header-search
- - hint.vue
javascript
<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>
javascript
// 使用
<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:搜索历史处理
bash
- src/store/modules
- - search.js
javascript
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 = []
}
}
}
javascript
// 有了 modules 之后,注意在 index.js 中注册、缓存,并声明 getters。
// 代码省略。
bash
- src/views/layout/components/header/header-search
- - history.vue
javascript
<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构建思路
想要搞明白这一点,我们就需要了解一些比较冷僻的知识点,那就是 渲染函数。在渲染函数中,我们需要了解如下概念:
-
虚拟 dom:通过 js 来描述 dom。
-
vnode 虚拟节点:告诉 vue 页面上需要渲染什么样子的节点。
-
h 函数:用来创建 vnode 的函数,接收三个参数(要渲染的 dom、attrs 对象、子元素)。
-
render 函数:可以根据 vnode 来渲染 dom。
根据以上所说我们知道:通过 h 函数可以生成一个 vnode,该 vnode 可以通过 render 函数被渲染。
据此我们就可以得出 confirm 组件的实现思路:
-
创建一个 confirm 组件。
-
创建一个 confirm.js 模块,在该模块中返回一个 promise。
-
同时利用 h 函数生成 confirm.vue 的 vnode。
-
最后利用 render 函数,渲染 vnode 到 body 中。
依据此思路,即可实现对应的 confirm 渲染。
11: 通用组件:构建 confirm 组件
bash
- src/libs
- - confirm
- - - index.vue
- - - index.js
javascript
<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组件
javascript
// 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)
})
}
javascript
// src/libs/index.js
export { confirm } from './confirm'
使用:
javascript
import { confirm } from '@/libs'
confirm('要删除所有历史记录吗?').then(() => {
store.commit('search/deleteAllHistory')
})
13: searchBar:热门精选模块构建
bash
- src/views/layout/components/header/header-search
- - theme.vue
javascript
<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知识点:
14. searchBar 联动 list
javascript
// src/store/modules/app.js
export default {
namespaced: true,
state: () => ({
// 搜索的文本
searchText: '',
}),
mutations: {
/**
* 修改 searchText
*/
changeSearchText(state, newSearchText) {
state.searchText = newSearchText
},
}
}
javascript
// 创建 getters。
// 在 index modules 中注册。
javascript
// src/views/layout/components/headr/header-search/index.vue
// 触发 searchText 变化
store.commit('app/changeSearchText', val)
javascript
// src/views/main/components/list/index.vue
/**
* 监听搜索内容项的变化
*/
watch(
() => store.getters.searchText,
(val) => {
// 重置请求参数
resetQuery({
page: 1,
searchText: val
})
}
)
15. 总结
本篇文章核心内容包含两部分:
-
多组件联动逻辑
-
confirm 通用组件
-
vnode
-
h 函数
-
render 函数