V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
xiaodaiguaray
V2EX  ›  前端开发

优雅使用 vue3.x + lodash 在项目中管理、封装指令

  •  
  •   xiaodaiguaray ·
    XiaoDaiGua-Ray · 2023-09-22 15:16:15 +08:00 · 618 次点击
    这是一个创建于 432 天前的主题,其中的信息可能已经有所发展或是发生改变。

    vue3.x directives

    vue 指令是一种特殊的 vue.js 特性,用于在 DOM 元素上添加特定的行为或功能。指令通过在元素上使用特定的指令名称和参数来实现,可以用于操作 DOM、响应事件、绑定数据等。今天简述一下如何在项目中优雅的管理、封装一些常用的指令。

    后面代码基本上会逐行进行注释,以便于大家阅读。

    管理指令

    统一管理

    1. 统一包管理:通过创建 directives 文件夹,我们可以将所有与指令相关的文件集中在一个位置,使其更易于查找和维护。这种统一的包管理结构有助于组织和管理指令相关的代码。
    2. 模块化指令:在 directives/modules 文件夹中,我们为每个指令创建一个独立的文件夹。这种模块化的结构使每个指令的逻辑和功能被封装在一个独立的文件中,方便单独管理和维护。每个指令文件夹中的出口文件 index.ts 可以用于导出指令逻辑,使其在项目中得以统一注册和使用。
    3. 规范化命名:指令文件夹的命名需要言简意赅,能够清晰表达指令的用途。通过规范化的命名,可以使项目中的指令更易于理解和识别,提高代码的可读性和可维护性。
    4. 可扩展性:通过这种管理方式,我们可以轻松地添加新的指令。只需在 directives/modules 文件夹中创建一个新的指令文件夹,并在其中编写相应的指令逻辑即可。这种结构使项目具有良好的可扩展性,方便我们根据需要添加、修改或删除指令。
    ·
    ├── directives
    ├──├── index.ts
    ├──├── modules
    ├──├──├── some directive
    ├──├──├──├── index.ts type.ts ...
    

    统一注册

    每个指令都使用 index.ts 文件统一暴露一个 CustomDirectiveFC 类型的函数,并且在根目录的 index.ts 中自动搜集 modules 文件夹中的所有文件并注册这些指令。

    实现

    公共类型( type.ts )

    import type { Directive } from 'vue'
    import type { App } from 'vue'
    
    export type { DebounceBindingOptions } from './modules/debounce/type'
    export type { ThrottleBindingOptions } from './modules/throttle/type'
    
    export type CustomDirectiveFC<T, K> = () => Directive<T, K>
    
    export interface DirectiveModules extends Object {
      default: CustomDirectiveFC<unknown, unknown>
    }
    
    export type AppType = App<Element>
    

    搜集、注册指令

    利用 import.meta.glob 方法(如果是 webpack 则是使用 require.context),获取所有指令文件夹的路径。然后,遍历这些文件夹,找到包含 index.ts 文件的位置。在每个 index.ts 文件中,你可以导入 CustomDirectiveFC 函数。通过这种方式,你可以自动搜集所有指令,并进行注册。

    警告 该方式会搜集 modules 中所有的文件夹,并且以文件名称作为指令名称。如果是多个单词,请使用小写单词并以 - 连接。

    import type { DirectiveModules, CustomDirectiveFC } from '@/directives/type'
    
    export const combineDirective = <
      T extends Record<string, DirectiveModules>,
      K extends keyof T,
    >(
      directiveModules: T,
    ) => {
      const directives = Object.keys(directiveModules).reduce((pre, curr) => {
        const fc = directiveModules[curr]?.default
    
        if (typeof fc === 'function') {
          pre[curr] = fc
    
          return pre
        } else {
          throw new Error('directiveModules[curr] is not function')
        }
      }, {} as Record<K, CustomDirectiveFC<unknown, unknown>>)
    
      return directives
    }
    
    import { combineDirective } from './helper/combine'
    import { forIn } from 'lodash-es'
    
    import type { App } from 'vue'
    import type { DirectiveModules } from '@/directives/type'
    
    /**
     *
     * 初始化全局自定义指令
     *
     * 该方法会将 modules 下每个文件夹视为一个指令
     * 并且会将文件夹名称识别为指令名称
     * 每个文件下的 index.ts 文件视为每个指令的入口(也就是指令的处理逻辑, 需要暴露出一个 Directive 类型的对象)
     */
    export const setupDirectives = (app: App<Element>) => {
      // 获取 modules 包下所有的 index.ts 文件
      const directiveRawModules: Record<string, DirectiveModules> =
        import.meta.glob('@/directives/modules/**/index.ts', {
          eager: true,
        })
      // 将所有的包提取出来(./modules/[file-name]/index.ts)
      const directivesModules = combineDirective(directiveRawModules)
      // 提取文件名(./modules/copy/index.ts => copy)
      const regexExtractDirectiveName = /(?<=modules\/).*(?=\/index\.ts)/
      // 匹配合法指令名称
      const regexDirectiveName = /^([^-]+-)*[^-]+$/
    
      forIn(directivesModules, (value, key) => {
        const dname = key.match(regexExtractDirectiveName)?.[0]
    
        if (typeof dname === 'string' && regexDirectiveName.test(dname)) {
          app.directive(dname, value?.())
        } else {
          console.error(`[setupDirectives] ${dname} is not a valid directive name`)
        }
      })
    }
    

    main.ts 文件中导入并且调用 setupDirectives 方法,即可完成自定义指令的搜集与注册。然后即可在项目全局中使用。

    自定义指令

    准备工作我们已经完成了,现在只需要开发自定义指令即可。现在我们来封装几个常用指令热热手。

    v-throttle

    实现

    import type { ThrottleSettings } from 'lodash-es'
    import type { AnyFC } from '@/types/modules/utils'
    
    export interface ThrottleBindingOptions {
      func: AnyFC
      trigger?: string
      wait?: number
      options?: ThrottleSettings
    }
    
    import { throttle } from 'lodash-es'
    import { on, off } from '@use-utils/element'
    
    import type { ThrottleBindingOptions } from './type'
    import type { AnyFC } from '@/types/modules/utils'
    import type { DebouncedFunc } from 'lodash-es'
    import type { CustomDirectiveFC } from '@/directives/type'
    
    const throttleDirective: CustomDirectiveFC<
      HTMLElement,
      ThrottleBindingOptions
    > = () => {
      let throttleFunction: DebouncedFunc<AnyFC> | null
    
      return {
        beforeMount: (el, { value }) => {
          const { func, trigger = 'click', wait = 500, options } = value
    
          if (typeof func !== 'function') {
            throw new Error('throttle directive value must be a function')
          }
    
          throttleFunction = throttle(func, wait, Object.assign({}, options))
    
          on(el, trigger, throttleFunction)
        },
        beforeUnmount: (el, { value }) => {
          const { trigger = 'click' } = value
    
          if (throttleFunction) {
            throttleFunction.cancel()
            off(el, trigger, throttleFunction)
          }
    
          throttleFunction = null
        },
      }
    }
    
    export default throttleDirective
    

    使用

    <template>
      <p>我执行了{{ count }}次</p>
      <p>该方法 1s 内仅会执行一次</p>
      <button
        v-throttle="{
          func: handleClick,
          trigger: 'click',
          wait: 1000,
          options: {},
        }"
      >
        节流按钮
      </button>
    </template>
    
    <script setup lang="ts">
    const count = ref(0)
    
    const handleClick = () => {
      count.value++
    }
    </script>
    

    v-debounce

    实现

    import type { DebounceSettings } from 'lodash-es'
    import type { AnyFC } from '@/types/modules/utils'
    
    export interface DebounceBindingOptions {
      func: AnyFC
      trigger?: string
      wait?: number
      options?: DebounceSettings
    }
    
    import { debounce } from 'lodash-es'
    import { on, off } from '@use-utils/element'
    
    import type { DebounceBindingOptions } from './type'
    import type { AnyFC } from '@/types/modules/utils'
    import type { DebouncedFunc } from 'lodash-es'
    import type { CustomDirectiveFC } from '@/directives/type'
    
    const debounceDirective: CustomDirectiveFC<
      HTMLElement,
      DebounceBindingOptions
    > = () => {
      let debounceFunction: DebouncedFunc<AnyFC> | null
    
      return {
        beforeMount: (el, { value }) => {
          const { func, trigger = 'click', wait = 500, options } = value
    
          if (typeof func !== 'function') {
            throw new Error('debounce directive value must be a function')
          }
    
          debounceFunction = debounce(func, wait, Object.assign({}, options))
    
          on(el, trigger, debounceFunction)
        },
        beforeUnmount: (el, { value }) => {
          const { trigger = 'click' } = value
    
          if (debounceFunction) {
            debounceFunction.cancel()
            off(el, trigger, debounceFunction)
          }
    
          debounceFunction = null
        },
      }
    }
    
    export default debounceDirective
    

    使用

    <template>
      <p>我执行了{{ count }}次</p>
      <p>该方法将延迟 1s 执行</p>
      <button
        v-throttle="{
          func: handleClick,
          trigger: 'click',
          wait: 1000,
          options: {},
        }"
      >
        防抖按钮
      </button>
    </template>
    
    <script setup lang="ts">
    const count = ref(0)
    
    const handleClick = () => {
      count.value++
    }
    </script>
    

    自定义指令目录结构

    ·
    ├── directives
    ├──├── index.ts type.ts
    ├──├── modules
    ├──├──├── throttle
    ├──├──├──├── index.ts type.ts
    ├──├──├── debounce
    ├──├──├──├── index.ts type.ts
    

    按照上述步骤以后,你将会得到这样的一个目录结构。

    最后

    所有的代码源码都来自于 Ray Template,可以自行点击查看源码。如果觉得对您有帮助,也可以给模板点一个小星星~~~

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3204 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 13:10 · PVG 21:10 · LAX 05:10 · JFK 08:10
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.