撸了一款 Vue 生态缺失的 CMD+K 类库

2022-09-29 09:38:32 +08:00
 xiaoluoboding

撸了一款 Vue 生态缺失的 CMD+K 类库

新轮子又来了,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 功能,可以大幅提升效率,以下工具都是一些实现比较好的代表。

你不妨也去试试,没准儿在哪个你正在访问的网站悄悄的支持着 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 中大部分的功能。

预览

安装

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 组件可以用了呢。

本年度其他独立项目

2145 次点击
所在节点    Vue.js
9 条回复
doommm
2022-09-29 11:00:16 +08:00
已 star
lizhenda
2022-09-29 11:26:22 +08:00
windows 不支持吗?
magichacker
2022-09-29 11:33:18 +08:00
是真能卷啊
dufu1991
2022-09-29 11:44:50 +08:00
应该贴个链接,放图片我还要手动输入项目地址。
xiaoluoboding
2022-09-29 13:18:08 +08:00
xiaoluoboding
2022-09-29 13:18:37 +08:00
@magichacker #3 全当你在夸我,就是喜欢折腾,😂
xiaojun1994
2022-09-29 13:54:19 +08:00
向大佬学习
wensonsmith
2022-09-29 17:59:26 +08:00
666 ,star 了
EndureBlaze
2022-09-30 17:53:54 +08:00
我在演示页面里面按下 ⌘K 之后浏览器的搜索框也会同步获取到焦点,也许是一个 bug ?我的浏览器是 Firefox 106 macOS

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/883717

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX