V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
xiaoluoboding
V2EX  ›  Vue.js

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

  •  1
     
  •   xiaoluoboding ·
    xiaoluoboding · 2022-09-29 09:38:32 +08:00 · 2387 次点击
    这是一个创建于 779 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

    本年度其他独立项目

    9 条回复    2022-09-30 17:53:54 +08:00
    doommm
        1
    doommm  
       2022-09-29 11:00:16 +08:00
    已 star
    lizhenda
        2
    lizhenda  
       2022-09-29 11:26:22 +08:00
    windows 不支持吗?
    magichacker
        3
    magichacker  
       2022-09-29 11:33:18 +08:00
    是真能卷啊
    dufu1991
        4
    dufu1991  
       2022-09-29 11:44:50 +08:00
    应该贴个链接,放图片我还要手动输入项目地址。
    xiaoluoboding
        5
    xiaoluoboding  
    OP
       2022-09-29 13:18:08 +08:00
    xiaoluoboding
        6
    xiaoluoboding  
    OP
       2022-09-29 13:18:37 +08:00
    @magichacker #3 全当你在夸我,就是喜欢折腾,😂
    xiaojun1994
        7
    xiaojun1994  
       2022-09-29 13:54:19 +08:00
    向大佬学习
    wensonsmith
        8
    wensonsmith  
       2022-09-29 17:59:26 +08:00
    666 ,star 了
    EndureBlaze
        9
    EndureBlaze  
       2022-09-30 17:53:54 +08:00
    我在演示页面里面按下 ⌘K 之后浏览器的搜索框也会同步获取到焦点,也许是一个 bug ?我的浏览器是 Firefox 106 macOS
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2678 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 11:37 · PVG 19:37 · LAX 03:37 · JFK 06:37
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.