1
头图

9月底,新轮子又来了,Vue Command Palette 是一个为 Vue 而生的快速、无样式、可组合的 Command Palette(CMDK)组件库。

灵感来源

这个组件的诞生的灵感来自上个月观察到一个比较火的 React 类库项目 cmdk

cmdk 是一个为 React 而生的快速、无样式、可组合的 CMDK 组件,由 Linear 工程师 Paco Coursey 和他的设计师小伙伴合力开发的,一周内获得 3k Star。

其特点是无样式的,只提供基础的功能框架,可组合的组件 API,便于扩展,这样一来,你可以基于它二次开发,编写成任何你想要的样子。

官网也做的比较用心,编写了四种样式作为例子,有 RaycastLinearVercelFramer

发现 Vue 生态内缺少一款好用的 CMDK 类库,于是决定自己造一个(chaoxi)。

如果你还不知道什么是 CMDK,这里简单介绍下。

CMDK 是一种用户体验

CMDK 是 CMD + K 的缩写,CMD 代表 Mac 系统中的键位 ⌘ ,对应 Command。CMD + K 是组合键,需要同时按下或者先后按下。

其实 CMDK 这种用户体验我们或多或少都接触过,Mac 自带的 聚焦搜索 就是这样的一个工具 ⌘ + Space 即可唤起它进行搜索,或者作为开发者查阅一些文档的时候,都会带有搜索的功能,有时候会发现都是嵌入的 algolia search, 再或者在使用 VSCode 的时候打开的命令面板(⇧ + ⌘ + P)

在去年发现 Raycast 这个 App 之后,生产力明显上升,Raycast 可以自定义很多快捷方式,可以结合一些工具打造顺滑的工作流,比如 Raycast 结合 GitHub 去 Create Issue,结合 Linear 去 Create Issue 等等。

所以我认为一个好的工具类、文档类的站点,应当内置一个好用的 CMDK 功能,可以大幅提升效率,以下工具都是一些实现比较好的代表。

  • Vercel
  • GitHub
  • Raycast
  • Linear
  • Framer
  • Algolia

你不妨也去试试,没准儿在哪个你正在访问的网站悄悄的支持着 CMDK,你敲一下 ⌘ + K 就能唤起呢。

Vue 中的命名空间组件

这次的组件设计有别于以往的组件开发方式,使用了 Vue 中的命名空间组件 的编写方式,在了解命名空间组件之前,我们先了解一下复合组件

复合组件

cmdk 类库提到了它的组件设计借鉴了《React Hooks: Compound Components》这篇文章中提到的 React 中的 复合组件 设计模式。

那什么是 复合组件 呢,它是一种组件的设计模式,一般适用于有两个或者多个组件一起工作,通常一个组件是父组件、而其他的是子组件。

我们在使用的大部分 UI 类库都会采用复合组件的设计模式去编写复杂组件,比如我们常用的 SelectMenuTable 等等组件到实现方式都是复合组件。

令我好奇的是 cmdk 这个 React 类库中采用的是 <父组件.子组件 /> 的引入方式,例如 cmdk 官网的例子:

import { Command } from 'cmdk';

<Command.Dialog open={open} onOpenChange={setOpen}>
  <Command.Input />

  <Command.List>
    {loading && <Command.Loading>Hang on…</Command.Loading>}

    <Command.Empty>No results found.</Command.Empty>

    <Command.Group heading="Fruits">
      <Command.Item>Apple</Command.Item>
      <Command.Item>Orange</Command.Item>
      <Command.Separator />
      <Command.Item>Pear</Command.Item>
      <Command.Item>Blueberry</Command.Item>
    </Command.Group>

    <Command.Item>Fish</Command.Item>
  </Command.List>
</Command.Dialog>

上面代码中出现的 <Command.Dialog><Command.List> 等组件引入方式,是在 Vue 中很少采用的方式,我在想为什么不可以呢,于是想去试试。

原生 HTML

举个例子:

例如 HTML 中的 <select><option> 标签:

<select>
  <option value="value1">key1</option>
  <option value="value2">key2</option>
  <option value="value3">key3</option>
</select>

常规组件

通常在 Vue 中实现,我们需要编写两个组件,假设是 MySelect 作为父级组件,MyOption 作为子组件

<template>
  <fieldset>
    <legend>currentValue: {{selected}}</legend>
    <MySelect v-model="selected">
      <MyOption :value="1">One</MyOption>
      <MyOption :value="2">Two</MyOption>
      <MyOption :value="3">Three</MyOption>
    </MySelect>
  </fieldset>
</template>

<script setup>
import { ref } from 'vue'
import MySelect from './MySelect.vue'
import MyOption from './MyOption.vue'

const selected = ref('1')
</script>
💻 在演练场中尝试一下

命名空间组件

其实 Vue 官方文档也有说明,把带 . 的组件叫做命名空间组件(Namespaced Components)

命名空间组件:可以使用带 . 的组件标签,例如 <Foo.Bar> 来引用嵌套在对象属性中的组件。这在需要从单个文件中导入多个组件的时候非常有用

假设将上面例子中的组件改为命名空间组件,会是这样写:

<template>
  <fieldset>
    <legend>currentValue: {{selected}}</legend>
    <NSelect v-model="selected">
      <NSelect.Option :value="1">One</NSelect.Option>
      <NSelect.Option :value="2">Two</NSelect.Option>
      <NSelect.Option :value="3">Three</NSelect.Option>
    </NSelect>
  </fieldset>
</template>

<script setup>
import { ref } from 'vue'
import { NSelect } from './packages.js'

const selected = ref('1')
</script>
💻 在演练场中尝试一下

在验证了可行后,于是决定以 命名空间组件 的方式编写 Vue Command Palette

命名空间组件特点:

  • 导入一个根组件即可使用全部子组件
  • 可以选择性的引入子组件
  • 让整个组件库看起来更加有一体性

Vue Command Palette

vue-command-palette 组件就是以 命名空间组件 方式编写的,基本上实现了 cmdk 中大部分的功能。

预览

preview

安装

yarn add vue-command-palette
# or
pnpm add vue-command-palette

使用

注意,此组件提供的是具有 CMDK 功能的骨架,没有任何样式,需要单独引入样式文件。

!-- <template> -->
<Command.Dialog :visible="visible" theme="custom">
  <template #header>
    <Command.Input placeholder="Type a command or search..." />
  </template>
  <template #body>
    <Command.List>
      <Command.Empty>No results found.</Command.Empty>

      <Command.Group heading="Letters">
        <Command.Item>a</Command.Item>
        <Command.Item>b</Command.Item>
        <Command.Separator />
        <Command.Item>c</Command.Item>
      </Command.Group>

      <Command.Item>Apple</Command.Item>
    </Command.List>
  </template>
</Command.Dialog>

引入一个 Command 根组件,即可选择性的去组合使用其他子组件

// <script lang="ts" setup>
import { ref } from 'vue'
import { Command } from 'vue-command-palette'

const visible = ref(false)

主题

cmdk 同样,组件库不提供任何样式,每个组件都以特殊声明的以 command- 开头的 data-attribute 命名,你可以使用它作为选择器来定制样式。

比如:

div[command-root=""] {
  // your style
}

vue-command-palette 可以传递一个名为 theme 的Props,这样你可以在主题的 class 选择器 中编写你的样式。

比如:提供的 theme 为 my-theme

// my-theme.css
.my-theme {
  // your theme
}

组件库虽然是无样式的,但是这样一来你就可以随心所欲的定制主题,考虑到对伸手党不太友好,这里提供了几种主题仅供参考:

嵌套子面板

有时候有一种需求是在命令面板中需要下钻访问子菜单中的项目,这时候需要手动控制,建议使用动态组件的方式实现

详见 Vercel 的例子

事件处理

Command.Item 的实现中提供了 @select 事件绑定,便于触发选中事件

参考实现

快捷键绑定

快捷键的绑定实现,这里参考了另一个 cmdk 类库 kbar 的实现方式,主动声明快捷键 shortcut 和响应事件 perform 的关系

const preferenceItems = [
  {
    icon: SunIcon,
    label: 'Toggle Dark Mode',
    shortcut: ['G', 'T'],
    perform: () => toggleDarkmode()
  }
]
参考实现

参考

感谢上面的组件库提供的灵感,实际上我可能是一个“组件库翻译者”,将 React 生态的 cmdk 搬运成了 Vue 生态的 vue-command-palette,或许以后你的 Vue 项目有好用的 CMDK 组件可以用了呢。

本年度其他独立项目


xiaoluoboding
2.1k 声望503 粉丝

I 💗 Web Dev, 💻 I'm a Senior Frontend Engineer