找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4067

积分

0

好友

563

主题
发表于 前天 04:51 | 查看: 13| 回复: 0

自从接触 Vue 以来,我就对 Vue 官网通过 Command + K(或 Ctrl + K)调出全局关键词搜索的功能念念不忘。恰好最近项目也需要实现类似的功能,这给了我一个绝佳的“带薪学习”机会。网上的教程良莠不齐,而之前的项目中我恰好做过一个全局弹出组件的功能,于是便举一反三,为我们项目量身打造了一个全局搜索框,现在就来分享一下我的思路。

注意:本文不会一上来就教你抄代码,而是作为一个引路人,一步步引导你理解这个组件的设计思路。我会以“假如我是一个初学者,如果别人能这样教我,我就能很快理解”的角度来讲解,毕竟授人以鱼不如授人以渔。希望你在阅读时能拓展思路,最终做到举一反三。

一、文件准备

在开始编码之前,我们需要准备好三个核心文件:

  1. SearchBar.ts:负责创建和管理搜索框实例的逻辑。
  2. SearchBar.vue:搜索框的 Vue 组件,负责 UI 呈现。
  3. useSearch.ts:提供一个全局可调用的 Hook,方便在任何地方触发搜索框。

项目文件结构预览

二、搜索框的样式设计

样式并非本文重点,你可以花几分钟在 SearchBar.vue 里快速写一个简易的搜索框:用一个绝对定位的 div 包裹一个 input 标签即可,以便我们快速进入核心逻辑的学习。

首先明确思路,这个组件需要悬浮在页面最顶层,所以内部需要使用绝对定位。我们在 SearchBar.vue 中为最外层的 div 设置样式。下面的示例中使用了 UnoCSS,不熟悉也没关系,它只是样式写法,不影响核心逻辑的理解。

const searchBarStyle = computed<CSSProperties>(() => {
  return {
    position: 'absolute',
    top: '120px',
    left: 'calc(50% - 310px)'
  };
});

在模板中,将这个计算属性绑定到 div 的样式上:

<div id="searchBarWrapper" ref="searchBarWrapper" class="w-[720px] cursor-pointer rounded-[8px] shadow-[0_4px_10px_0_rgba(0,0,0,0.1)]" :style="searchBarStyle">

最终,一个基础的搜索框样式就完成了。
搜索框最终效果预览

三、理解渲染函数 hrender(关键步骤)

  1. 引入函数:打开 SearchBar.ts 文件,从 vue 中引入 hrender 函数,并引入我们写好的 SearchBar.vue 组件。
    导入 h 和 render 函数

  2. h() 函数解析:我们先从 Vue 官网看看这个函数的定义。
    Vue3 官方 h 函数类型定义
    可以看到,它的第一个参数是必填的,可以是字符串或组件 (Component)。本文重点讨论参数为组件的情况。关键点在于它的返回值:一个 VNode(虚拟节点)。你肯定对虚拟 DOM 不陌生,Vue 的渲染过程是:虚拟 DOM -> 真实 DOM

  3. 模板与渲染函数的关系:你可能更熟悉在 SearchBar.vue<template> 里写组件。但要知道,Vue 底层正是通过调用 h() 来构建虚拟 DOM 的。<template> 标签是 Vue 为了让你能用熟悉的 HTML 方式开发而提供的语法糖。

  4. 使用 h() 创建组件 VNode:既然 h() 的第一个参数可以是组件,那我们的 SearchBar.vue 不就是一个组件吗?如果不想用 <template> 展示,我们可以直接写:h(SearchBar)。这样就能得到一个代表该组件的虚拟节点。
    使用 h 函数创建 SearchBar 的 VNode

四、编写 SearchBarCreator 类和 present 方法

  1. 创建类:回到 SearchBar.ts,我们创建一个类。
    创建 SearchBarMaker 类

  2. 定义显示与隐藏方法:这个搜索框肯定需要有“显示”和“隐藏”的方法,我们命名为 presentdismiss
    定义 present 和 dismiss 方法

  3. 创建 VNode:根据上面的学习,我们可以在 present 方法里先创建组件的 VNode。
    在 present 方法中调用 h 函数

  4. 理解 render() 函数:有了虚拟 DOM,如何变成真实 DOM?Vue 提供了 render() 函数。它的类型是 RootRenderFunction
    Vue3 的 RootRenderFunction 类型
    注意看它的第二个参数:container: HostElement。我们换个思路,打开项目的 main.ts 文件,看看 mount 方法的定义。
    查看 mount 函数调用
    Vue App 接口部分定义
    发现了吗?虽然我们不清楚 HostElement 的具体类型,但你知道 mount 函数里填的参数是什么吧?没错,就是那个全局唯一的、id 为 app 的真实 DOM 元素。
    index.html 中的 app 容器
    简单来说,render() 函数会将你的虚拟 DOM“转化”为真实 DOM,但你需要给它一个真实的“容器”DOM,告诉它渲染到哪里。

  5. 将元素插入 Body:既然搜索框是全局的,且需要覆盖在所有组件之上,最简单的办法就是让它成为 body 的第一个子元素。因为通常我们所有的页面内容都位于 body 内一个 id=“app”div 中。所以,我们的思路是:按下搜索按钮时,在 <div id=“app”> 元素之前插入我们的组件。
    在 present 方法中执行渲染与插入 DOM
    index.html 结构示意
    修改 index.html 以说明思路

  6. 测试显示功能:我们来测试一下。在 App.vue 里写一个按钮,调用 SearchBarCreator 实例的 present 方法(注:后文将 SearchBarMaker 更名为 SearchBarCreator,仅名称变化,逻辑不变)。

    <script setup lang="ts">
    import SearchBarCreator from "./components/SearchBar/SearchBar";
    function openSearchBar() {
      const searchBar = new SearchBarCreator();
      searchBar.present();
    }
    </script>
    <template>
      <div class="h-[100vh] w-[100vw] bg=[#2ec1cc]">
        <button @click="openSearchBar">搜索</button>
      </div>
    </template>

    在 App.vue 中调用 present 方法

  7. 实现隐藏 (dismiss) 方法:让搜索框消失很简单,在合适的时机移除这个 DOM 元素即可。这里需要注意,searchBar 实例需要提升到文件作用域,不能在每次 openSearchBar 里都 new 一个。
    dismiss 方法实现
    优化后的调用方式:

    <script setup lang="ts">
    import SearchBarCreator from "/components/SearchBar/SearchBar";
    const searchBar = new SearchBarCreator();
    function openSearchBar() {
      searchBar.present();
    }
    function closeSearchBar() {
      searchBar.dismiss();
    }
    </script>
    <template>
      <div class="h-[100vh] w-[100vw] bg-#2ec1cc">
        <button @click="openSearchBar()">搜索</button>
        <button @click="closeSearchBar()">关闭</button>
      </div>
    </template>

    优化后的 App.vue 调用逻辑

五、优化 SearchBarCreator 逻辑(单一实例控制)

写到这里你会发现一个问题:连续点击搜索按钮,会弹出多个搜索框。我们希望全局同时只存在一个搜索框。换个思路,就是同一时间,这个 SearchBar 实例只能展示一个。我们可以增加一个状态变量 isShowing 来控制。

思路是:如果搜索框正在展示 (isShowingtrue),当用户再次调用 present 时,我们先调用自身的 dismiss 方法将其关闭。
增加 isShowing 状态控制的 SearchBarCreator 类
测试一下,问题完美解决。

六、封装全局调用 Hook

上面的方式只能在 App.vue 里调用。但如果我想在项目的任意一个 XXX.vue 文件里调用呢?难道每个地方都要重新引入和 new 一次吗?当然不,程序员可不会写这种重复代码。

  1. 创建全局 Hook:打开之前准备的 useSearch.ts 文件。我们把生成 SearchBar 实例和其方法的逻辑封装在这里,并暴露给外部。

    import SearchBarCreator from "./SearchBar/SearchBar";
    const searchBar = new SearchBarCreator();
    function openSeachBar() {
      searchBar.present();
    }
    function closeSeachBar() {
      searchBar.dismiss();
    }
    export default {
      openSeachBar,
      closeSeachBar,
    };

    useSearch.ts 文件内容

  2. 在组件中使用 Hook:现在我们可以在任意组件中引入并使用这个 Hook 了。以 App.vue 为例:

    <script setup lang="ts">
    import useSearch from "./components/useSearch";
    const { openSearchBar, closeSearchBar } = useSearch;
    </script>
    <template>
      <div class="h-[100vh] w-[100vw] bg-[#2ec1cc]">
        <button @click="openSearchBar">搜索</button>
        <button @click="closeSearchBar">关闭</button>
      </div>
    </template>

    使用 useSearch Hook 的 App.vue
    这样一来,我们就能在项目任何地方方便地调用这个“唯一的搜索框”了。通过这种方式管理全局组件,正是现代 Vue.js 应用开发的常见模式。

七、添加快捷键支持(Command/Ctrl + K)

  1. 事件监听位置:回想一下,main.ts 中是把 App.vue 挂载到全局的。那么如果在 App.vue 挂载时,给 window 添加全局键盘事件监听,就能实现快捷键功能。
    main.ts 入口文件

  2. 选择键盘事件:要监听组合键,我们使用 keydown 事件。

    onMounted(() => {
      window.addEventListener("keydown", (e) => {
        console.log("keydown", e);
      });
    });

    添加 keydown 事件监听

  3. 识别按键:按下 Command 键(Mac)时,事件对象的 metaKey 属性为 true;按下 Ctrl 键时,ctrlKey 属性为 true
    KeyboardEvent 对象属性,显示 metaKey

  4. 识别组合键keydown 事件支持识别多个按键。当我们同时按下 CommandK 时,会触发事件。虽然没有 k: true 这样的属性,但事件对象的 key 属性值就是字符串 "k"
    组合键事件的属性
    事件对象的 key 属性

  5. 实现判断逻辑:JavaScript 允许我们这样判断是否同时按下了 Command/CtrlK

    onMounted(() => {
      window.addEventListener("keydown", (e) => {
        if ((e.metaKey || e.ctrlKey) && e.key === "k") {
          openSearchBar(); // 调用之前封装好的打开方法
        }
      });
    });

    实现快捷键判断逻辑
    整合到 App.vue 中并测试:
    整合了快捷键监听的完整 App.vue

八、为弹出添加动画效果

为了让搜索框的弹出不那么生硬,我们可以添加一个简单的滑入动画。

  1. 定义 CSS 动画:在 App.vue<style> 中定义一个关键帧动画。

    @keyframes searchInput {
      from {
        transform: translateY(50px);
      }
      to {
        transform: translateY(0px);
      }
    }
    .searchInput {
      animation: searchInput 0.3s ease-out;
    }

    在 App.vue 中定义 CSS 动画

  2. 为组件根元素添加 Ref:在 SearchBar.vue 中,为最外层的 div 设置一个 ref,方便我们获取其 DOM 实例。

    <div id="searchBarWrapper" ref="searchBarWrapper" ... >

    SearchBar.vue 模板中的 ref

  3. 动态添加动画类名:思路是,在 SearchBar.tspresent 方法中,调用 render 函数后,组件已成为真实 DOM(但还未插入文档)。此时我们可以通过 querySelector 获取到这个 DOM 元素,并为其添加动画类名,然后再执行插入操作。

    present() {
      if (this.isShowing) {
        // ... 省略重复触发处理逻辑
      } else {
        const searchBar = h(SearchBar);
        render(searchBar, this.container);
        // 获取渲染后的 DOM 元素并添加动画类
        const searchBarWrapperDOM = this.container.querySelector("#searchBarWrapper");
        searchBarWrapperDOM?.classList.add("searchInput");
        document.body.insertBefore(this.container, document.body.firstChild);
        this.isShowing = true;
      }
    }

    在 present 方法中动态添加 CSS 类

九、实现输入框自动聚焦

为了让用户体验更好,搜索框弹出后,输入光标应自动聚焦。这在 Vue 组件内实现非常简单,只需在 SearchBar.vue 组件挂载后(或显示后)的 nextTick 中,调用 input 元素的 focus() 方法即可。

<script setup lang="ts">
import { nextTick, ref } from 'vue';
const inputModal = ref<HTMLInputElement>();
// ... 其他逻辑
const handleShow = () => {
  nextTick(() => {
    // 清空搜索词并聚焦
    cleanSearchKeyword();
    inputModal.value?.focus();
  });
};
</script>

在 SearchBar.vue 中实现自动聚焦

总结与思考

我倾向于在文章中多用截图而非直接贴出完整代码,是因为我发现在搜索教程时,自己也总想直接复制最终代码,却忽略了动手实现的过程才是真正理解的关键。

因此,我尽量将每个功能的实现代码简化到几行,希望你能带入自己的思考,一步步体会实现过程,从而真正做到举一反三。如果你认真理解了本文,或许就能明白现在许多UI组件库中全局 Dialog、Modal 等组件的底层实现原理与此类似。理解设计思路远比复制粘贴更重要。

这个全局搜索框还有很多可以优化的方向,例如:

  1. 如何保存和展示搜索历史?
  2. 如何实现实时搜索联想(防抖/节流)?

欢迎大家在 云栈社区 交流你的实现想法和优化方案。与君共勉,才是我的初衷。




上一篇:2025智能眼镜市场数据解读:小米生态如何颠覆传统品牌格局?
下一篇:彤程新材再闯港交所:半导体光刻胶与橡胶助剂双线并进,362亿巨头的新征途
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-10 10:18 , Processed in 0.616613 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表