组件库(vue 仿element)

110 阅读20分钟

组件库

前置知识

了解vue各种组件通信的实现方式,以及宏定义等等

了解各种事件的绑定 v-model, v-bind,什么的

什么是组件库

组件库就是一组可以复用的UI组件集合,用来快速构建一致美观,功能完整的用户界面

monorepo架构

简单概述

定义:Monorepo架构(单体仓库)是一种将多项目,包或者模块放在同一个git仓库当中统一管理的软件开发架构

相对应的是Mutirepo (多仓库) 每个项目/包单独一个仓库

之间的对比

仓库数量1 个仓库N 个仓库
代码共享极其方便(直接 import)需要发包、npm link、私有源
依赖管理统一管理(如 pnpm workspace)各自管理,容易版本冲突
跨项目修改一次提交,原子化变更需要多个 PR,协调困难
CI/CD可统一配置,也可按项目触发每个项目独立配置
权限控制粒度较粗(整个仓库)精细(按仓库)
学习成本初期稍高低(传统方式)
适合场景中大型项目、组件库、全栈项目小型独立项目、开源生态
典型使用场景
  • 组件库开发

    • my-monorepo/
      ├── packages/
      │   ├── hy-element-core/      ← 核心组件源码
      │   ├── hy-element-vue/       ← Vue 版本适配层
      │   ├── hy-element-react/     ← React 版本适配层
      │   └── docs/                 ← VitePress 文档站点
      └── package.json
      
  • 全栈应用

    • fullstack-app/
      ├── apps/
      │   ├── web/                  ← 前端(Vue/React)
      │   └── server/               ← 后端(Nest.js/Express)
      ├── packages/
      │   ├── shared-types/         ← 共享 TypeScript 类型
      │   ├── utils/                ← 工具函数
      │   └── config/               ← ESLint / TS 配置
      └── package.json
      
  • 大型的开源项目

    • 使用monorepo架构便于管理多个子包
核心技术使用
  • 包管理器

    • pnpm(强烈推荐 对monorepo天然支持) yarn npm
  • 构建与任务运行器

    • turborepo (没用过)
    • Nx (没用过)
    • Lerna(我的组件库使用的就是这个 适合发布npm包管理工具)
  • 版本与变更管理

    • changesets (社区主流)
    • Lerna 传统方案
    • manual 手动管理
my-monorepo/
├── packages/
│   ├── button/              # 组件包
│   │   ├── src/
│   │   ├── package.json     # name: "@hy/button"
│   │   └── tsconfig.json
│   ├── utils/               # 工具包
│   │   └── package.json     # name: "@hy/utils"
│   └── docs/                # 文档站点(VitePress)
│       └── package.json     # name: "@hy/docs"
│
├── apps/
│   └── playground/          # 调试 Demo
│       └── package.json     # 依赖 @hy/button
│
├── package.json             # 根 package.json(定义 workspaces)
├── pnpm-workspace.yaml      # pnpm 工作区配置
├── turbo.json               # Turborepo 配置
└── tsconfig.json            # 全局 TS 配置
核心配置文件解析
  • pnpm-workspace.yaml

    • package: 
      	-'package/**'
      	-'app/**'
      
    • 告诉哪些目录是子包
  • package.json

    • {
        "name": "my-monorepo",
        "private": true,
        "scripts": {
          "build": "turbo run build",
          "dev": "turbo run dev --parallel",
          "docs:dev": "pnpm --filter @hy/docs run dev"
        },
        "devDependencies": {
          "turbo": "^2.0.0",
          "typescript": "^5.0.0"
        }
      }
      
  • 子包 package.json

    • {
        "name": "@hy/button",
        "version": "0.0.1",
        "main": "./dist/index.js",
        "types": "./dist/index.d.ts",
        "scripts": {
          "build": "tsc",
          "dev": "tsc --watch"
        },
        "dependencies": {
          "@hy/utils": "workspace:*"  // ← 关键!引用同仓库的包
        }
      }
      
    • 主要就是引用仓库中的包
核心优势
  1. 原子化提交

    • 一次提交涉及修改多个包,直接一个comiit就能搞定,状态一致性
  2. 无缝代码共享

    • 使用workspace中其他时随意使用
  3. 统一的依赖管理

    • 所有的包共享一份nodes_modules
    • 避免版本依赖冲突
    • 节省磁盘空间
  4. 高性能构建

    • 缓存:只构建变更的部分
    • 并行:多个包同时存在
    • 远程缓存(团队共享)
  5. 统一的工具链 & 规范

局限的地方
  • 仓库体积大
  • 权限控制难
  • 学习成本大
  • 不适合开源体验

从0搭建monorepo架构项目

初始化项目

pnpm init

创建pnpm-workspace.yaml

packages:
  - 'packages/*'
  - 'apps/*'

创建对应的包

mkdir -p packages/utils
cd packages/utils
pnpm init

编辑package.json

{
  "name": "@my/utils",
  "version": "0.0.1",
  "main": "index.js",
  "types": "index.d.ts"
}

这样上面创建的包就可以直接在另一个包当中直接引用

对于一些项目还是很友好的, 开发体验大幅提升

项目看法和难点

积累经验

个人觉得学习写组件库,并不是学习了很多的新的东西(主要是对框架有了更深的理解,了解了一些封装思路,经验是比较重要的),但是monorepo架构还是比较值得学习一下的

还有就是对typescript使用的更加熟练了,知道了原来根本不会使用的一些方法,反正就是理解更加深入

提高了自己的解决问题的能力,学习的过程中,遇见了很多的api报错,问ai也问不出什么东西,提高了自己查文档看文档的能力

实现的项目结构

└─packages
    ├─components // 组件目录
    ├─core // npm包的入口
    ├─docs // 文档目录
    ├─hooks // 组合式api hooks目录
    ├─play // 主要是看效果的地方
    │  ├─.vscode
    │  ├─public
    │  └─src
    │      ├─assets
    │      └─components
    ├─theme // 主题目录
    └─utils // 工具函数目录

项目难点

技术架构难点
  1. 技术架构设计

    • 难点:如何组织组件结构,是否支持多框架, 是否对项目进行拆分

    • 建议:

      • 使用monorepo架构
      • 分离不同的逻辑,分成多个区域进行操作,互不干扰
  2. typescript 支持(我最痛的点)

    • 难点:如何提供类型完善的类型定义,如何支持用户获得智能支持

    • 建议

      • 改掉使用anyscript的习惯
      • 每个组件设计设置自己的props,emits,instance类型
  3. 系统样式和主题定制

    • 难点:如何支持主题变量,如何避免样式污染,如何支持按需引入

    • 建议

      • 使用css Varible(--hy-primary-color)
      • 提供对应的接口 直接对数据进行更改就行
      • 支持自动导入样式
构建和工程化难点
  1. SSR和SSG的兼容性问题

    • 难点:vitepress是静态生成器(SSG), 构建运行在node环境,不存在window等

    • 使用window会报错

    • 根源: 组件或者主题当中直接使用了浏览器API

      • 解决方案:

        • 使用 onMouted || onBeforeMount 包裹DOM进行操作
        • 判断环境: if(typeof window !== 'undefined')
        • 使用特有的插件,延迟客户端渲染
  2. 打包和 tree shaking

    • 难点: 如何让用户按需引入组件,如何避免打包整个库(减少全量引入)

    • 建议:

      • 使用vite-plugin-dts自动生成类型 提供支持
      • 配置package.jsonexport字段支持子路径的导入
      • 提供 components 入口供给插件自动导入
    • 打包当中学习的一些经验(技巧)

      • 打包之前的rmFile操作, (使用的是rollup的原生的钩子直接实现)

        • import { each, isFunction } from "lodash-es";
          import shell from 'shelljs'
          
          export default function hooksPlugin({
              rmFiles = [],
              beforeBuild,
              afterBuild
          }: {
              rmFiles?: string[],
              beforeBuild?: Function,
              afterBuild?: Function,
          }) {
              return {
                  name: 'hooks-plugin',
                  buildStart() {
                      each(rmFiles, (fName) => shell.rm('-rf', fName))
                      isFunction(beforeBuild) && beforeBuild()
                  },
                  buildEnd(err?: Error) {
                      !err && isFunction(afterBuild) && afterBuild()
                  }
              }
          }
          // 定义两个钩子和要删除文件的名字
          
      • 如何实现分包操作

        • 分包的优点:

          • 优化加载性能,减少重复的代码, 提高缓存效率,
          • 减少首屏加载体积, 提升用户体验
          • 利用浏览器缓存 节省带宽和加载时间
          • 避免重复打包, 带来的负担
        • output: {
                          assetFileNames: (assetInfo) => {
                              if (assetInfo.name === 'style.css') return 'index.css'
                              if (assetInfo.type === 'asset' && /.(css)$/i.test(assetInfo.name as string)) {
                                  return 'theme/[name].[ext]'
                              }
                              return assetInfo.name as string
                          },
                          // 进行分包处理
                          manualChunks(id) {
                              // console.log(id)
                              if (id.includes('node_modules')) {
                                  return 'vendor'
                              }
                              if (id.includes('/packages/hooks')) {
                                  return 'hooks'
                              }
                              if (id.includes('/packages/utils') || id.includes('plugin-vue:export-helper')) {
                                  return 'utils'
                              } 
                              for (const item of getDirectoriesSync('../components')) {
                                  if (includes(id, `/packages/components/${item}`)) {
                                      return item
                                  }
                              } 
                          }
                      }
          
      • tree shaking 优化

        • 技术难点:

          • 实现 esm cjs umd等多种格式,支持tree-shaking优化
          • 构建es lib dist 三套目录结构,支持sideEffect副作用文件,实现代按需加载
          • 成果: 包从3mb优化到了800kb, 按需单文件引入只需10-50kb
        • 	"sideEffects": [
          		"./dist/index.css",
          		"./dist/theme/*.css"
          	],
          
        • 主要作用就是告诉这些文件的不要进行消除 我们需要使用这些东西

        • 不这么进行配置文件样式可能会丢失

复杂组件的实现**
  • 技术难点: tree组件的递归渲染,table组件的虚拟滚动., form校验系统

  • 解决方案:使用虚拟化技术,防抖节流,异步校验引擎, 实现大数据高性能的渲染

  • 成果:tree组件支持多节点渲染,table组件支持5000+行数据

  • 举例一些复杂组件的实现

    • carousel

      • 递归组件的通信机制

        • 难点: carousel 和 carouselItem 之间的双向通信,

          • 解决办法 使用 provide/inject 实现双向通信
          // Carousel.vue
          provide('carousel', {
            activeIndex,
            itemCount,
            direction: props.direction
          })
          
          // CarouselItem.vue
          const carousel = inject('carousel', {
            activeIndex: ref(0),
            itemCount: ref(0),
            direction: 'horizontal'
          })
          
        • 动态计数管理

          // 难点:itemCount 的动态管理
          onMounted(() => {
            index.value = carousel.itemCount.value  // 获取当前索引
            carousel.itemCount.value++              // 总数加一
          })
          
          onBeforeUnmount(() => {
            carousel.itemCount.value--              // 组件销毁时减一
          })
          
        • 过渡动画状态管理

          // 难点:防止过渡期间的重复操作
          const isTransitioning = ref(false)
          
          function setActionItem(index: number) {
            if (index === activeIndex.value || isTransitioning.value) return  // 关键:防止重复操作
            isTransitioning.value = true
            // ... 执行切换
          }
          
          function onTransitionEnd() {
            isTransitioning.value = false  // 过渡结束后重置状态
          }
          
          // 挑战:
          // - 确保动画完成前不响应新操作
          // - 正确处理 transitionend 事件
          // - 防止快速点击导致的动画冲突
          
        • 自动播放机制的实现

          • 定时器管理

            // 难点:复杂的定时器控制逻辑
            const timer = ref<number | null>(null)
            
            function startTimer() {
              if (!props.autoplay || itemCount.value <= 1) return
              stopTimer()  // 重新开始前先停止
              timer.value = window.setInterval(() => {
                next()
              }, props.interval)
            }
            
            function stopTimer() {
              if (timer.value) {
                clearInterval(timer.value)
                timer.value = null
              }
            }
            
            // 挑战:
            // - 防止定时器重复创建
            // - 处理组件生命周期
            // - 支持暂停/恢复
            
          • 悬停暂停逻辑

            function handleMouseEnter() {
              isHover.value = true
              if (!props.pauseOnHover) return
              stopTimer()  // 悬停时停止
            }
            
            function handleMouseLeave() {
              isHover.value = false
              if (props.pauseOnHover && props.autoplay) {
                startTimer()  // 离开时重新开始
              }
            }
            
            // 挑战:
            // - 处理多种配置组合
            // - 避免状态冲突
            // - 确保恢复逻辑正确
            
          • 性能优化: 内存管理 渲染优化 事件处理等

    • Form

      • 复杂的组件通信逻辑

        // 难点:Form 与 FormItem 的双向通信
        // 解决方案:provide/inject + Context 模式
        
        // Form.vue
        const formCtx: FormContext = reactive({
          ...toRefs(props),
          emits,
          addField,    // 添加表单项
          removeField  // 移除表单项
        })
        provide(FORM_CTX_EKY, formCtx)
        
        // FormItem.vue  
        const ctx = inject(FORM_CTX_EKY)
        const formItemCtx: FormItemContext = reactive({
          validate,
          resetField,
          clearValidate,
          addInputId,
          removeInputId,
        })
        provide(FORM_ITEM_CTX_KEY, formItemCtx)
        
        // 挑战:
        // - 组件间的依赖注入管理
        // - Context 对象的响应式处理
        // - 避免循环引用问题
        
      • 动态规则的合并和处理(收集待校验表单项的相应的规则)

        // 难点:合并表单级规则和字段级规则
        const itemRules = computed(() => {
          const rules: FormItemRule[] = []
          
          // 1. 合并字段自身规则
          if (props.rules) {
            rules.push(...props.rules)
          }
          
          // 2. 合并表单全局规则
          const formRules = ctx?.rules
          if (formRules && props.prop) {
            const _rules = getValByProp(formRules)  // 使用 lodash get 获取嵌套属性
            if (_rules) rules.push(..._rules)
          }
          
          // 3. 处理 required 属性覆盖
          if (!isNil(required)) {
            const requiredRules = filter(
              map(rules, (rule, i) => [rule, i]),
              (item: [FormItemRule, number]) => includes(keys(item[0]), 'required')
            )
            if (size(requiredRules)) {
              // 统一更新已存在的 required 规则
              for (const item of requiredRules) {
                const [rule, i] = item as [FormItemRule, number]
                rules[i] = { ...rule, required }
              }
            } else {
              rules.push({ required })
            }
          }
          
          return rules
        })
        
        // 挑战:
        // - 规则优先级处理
        // - 嵌套对象属性访问
        // - 规则去重与合并
        
      • 异步验证和状态管理

        // 难点:复杂的异步验证状态管理
        const validateStatus: Ref<ValidateStatus> = ref('init')
        const errMsg = ref('')
        
        async function doValidate(rules: RuleItem[]) {
          const modelName = propString.value
          const validator = new Schema({ [modelName]: rules })
          
          return validator.validate({ [modelName]: innerVal.value }, { firstFields: true })
            .then(() => {
              validateStatus.value = 'success'  // 验证成功状态
              ctx?.emits('validate', props, true, '')
              return true
            })
            .catch((err: FormValidateFailuer) => {
              const { errors } = err
              validateStatus.value = 'error'    // 验证失败状态
              errMsg.value = errors && size(errors) > 0 ? errors[0].message ?? '' : ''
              ctx?.emits('validate', props, false, errMsg.value)
              return Promise.reject(err)
            })
        }
        
        // 挑战:
        // - 异步状态同步
        // - 错误信息格式化
        // - Promise 链式处理
        
      • 验证触发的机制

        // 难点:支持多种验证触发方式
        function getTriggeredRules(trigger: string) {
          const rules = itemRules.value
          if (!rules) return []
          return filter(rules, (r) => {
            if (!r?.trigger || !trigger) return true  // 无触发条件则全部验证
            if (isArray(r.trigger)) {
              return r.trigger.includes(trigger)      // 数组触发条件
            }
            return r.trigger === trigger              // 单一触发条件
          }).map(({ trigger, ...rule }) => rule as RuleItem) // 移除 trigger 属性
        }
        
        const validate: FormItemInstance['validate'] = async function (
          trigger: string,
          callback?: FormValidateCallback
        ) {
          if (isResetting || !props.prop || isDisabled.value) return false
          
          const rules = getTriggeredRules(trigger)    // 获取对应触发条件的规则
          if (!size(rules)) {
            callback?.(true)
            return true
          }
          
          validateStatus.value = 'validating'         // 验证中状态
          return doValidate(rules).then(() => {
            callback?.(true)
            return true
          }).catch((err: FormValidateFailuer) => {
            const { fields } = err
            callback?.(false, fields)
            return Promise.reject(fields)
          })
        }
        
        // 挑战:
        // - 多种触发方式统一处理
        // - 规则过滤与筛选
        // - 回调函数与 Promise 统一
        
      • 表单的状态管理

        // 字段注册和注销
        // 难点:动态管理表单项
        const fields: FormItemContext[] = []
        
        const addField: FormContext['addField'] = function (field) {
          if (!field.prop) return
          fields.push(field)  // 添加到验证队列
        }
        
        const removeField: FormContext['removeField'] = function (field) {
          if (!field.prop) return
          fields.splice(fields.indexOf(field), 1)  // 从验证队列移除
        }
        
        // 组件生命周期
        onMounted(() => {
          if (!props.prop) return
          ctx?.addField(formItemCtx)      // 注册到表单
          initialVal = innerVal.value     // 保存初始值
        })
        
        onUnmounted(() => {
          if (!props.prop) return
          ctx?.removeField(formItemCtx)   // 从表单注销
        })
        
        // 挑战:
        // - 防止重复注册
        // - 组件销毁时清理
        // - 索引查找性能
        
      • 批量验证实现

        // 难点:支持全量验证和部分验证
        async function doValidateField(fields: FormItemContext[] = []) {
          let validateErrors: ValidateFieldsError = {}
          for (const field of fields) {
            try {
              await field.validate('')  // 逐个验证
            } catch (error) {
              validateErrors = {
                ...validateErrors,
                ...(error as ValidateFieldsError)
              }
            }
          }
          if (!size(Object.keys(validateErrors))) return true
          return Promise.reject(validateErrors)
        }
        
        const validateField: FormInstance['validateField'] = async function (keys, callback) {
          try {
            const result = await doValidateField(filterFilelds(fields, keys ?? []))
            if (result === true) callback?.(result)
            return result
          } catch (error) {
            if (error instanceof Error) throw error
            const invalidField = error as ValidateFieldsError
            callback?.(false, invalidField)
            return Promise.reject(invalidField)
          }
        }
        
        // 挑战:
        // - 异步验证顺序控制
        // - 错误信息聚合
        // - 验证结果处理
        
      • 性能优化

        • 内存管理:及时的将内存进行释放,消除
        • 字段过滤优化: 高效的字段过滤 大数据量的性能, 数组查找优化 条件判断优化等等
    • message

      • 函数式的方式进行渲染**

        • 实现函数式调用而并非组件渲染

          // 难点:实现函数式调用而非组件渲染
          // 传统方式:在模板中使用 <hy-message />
          // 函数式:Message.success('提示内容')
          
          // 解决方案:使用 createApp + mount 动态创建
          import Message from "./methods"
          export const hyMessage = withInstallFunction(Message, '$message')
          
          // withInstallFunction 实现原理
          function withInstallFunction(fn: any, name: string) {
            fn.install = (app: App) => {
              app.config.globalProperties[name] = fn
            }
            return fn
          }
          
      • 动态组件的创建和挂载

        const createMessage = (props: CreateMessageProps): MessageInstance => {
            const id = useId().value
            const container = document.createElement('div')
            const destory = () => {
                const idx = findIndex(instances, { id })
                if (idx === -1) return
                instances.splice(idx, 1)
                render(null, container) // 不进行渲染
            }
            const _props: MessageProps = {
                ...props,
                id,
                zIndex: nextZindex(),
                onDestory: destory
            }
            const vnode = h(MessageConstructor, _props) // 测试尽量使用h函数进行测试,比较安全并且使用简单
            render(vnode, container)
            document.body.appendChild(container.firstElementChild!) // !保证我的不是空节点
        
            const vm = vnode.component!
            const handler: MessageHandler = {
                close: () => vm.exposed!.close()
            }
            const instance: MessageInstance = {
                id,
                vnode,
                handler,
                vm,
                props: _props
            }
            instances.push(instance)
            return instance
        }
        
      • 定位和计算偏移 计算新组件的弹出的位置, 拿到上一个组件的位置,计算得到新组件的位置

        // 难点:多个消息组件的堆叠布局
        const { topOffset, bottomOffset } = useOffset({
          getLastBottomOffset: bind(getLastBottomOffset, props),
          offset: props.offset,
          boxHeight,
        })
        
        // getLastBottomOffset 实现
        function getLastBottomOffset(props: MessageProps) {
          const messages = document.querySelectorAll('.hy-message')
          if (messages.length === 0) return 0
          
          // 获取最后一个消息组件的底部位置
          const lastMessage = messages[messages.length - 1]
          const rect = lastMessage.getBoundingClientRect()
          return rect.bottom + props.offset
        }
        
      • 还是常说的声明周期和内存管理

      • 内容渲染 vnode渲染支持

        // 难点:支持字符串、VNode、函数等多种内容类型
        <template>
          <div class="hy-message__content">
            <slot>
              <render-vnode v-if="message" :vNode="message" />  // 渲染 VNode
            </slot>
          </div>
        </template>
        
        // RenderVnode 组件实现
        export const RenderVnode = defineComponent({
          props: {
            vNode: [String, Object, Array]
          },
          render() {
            return this.vNode
          }
        })
        
      • 插槽和默认内容

        // 难点:插槽与 props 内容的优先级处理
        <template>
          <div class="hy-message__content">
            <slot>  // 优先使用插槽内容
              <render-vnode v-if="message" :vNode="message" />  // 其次使用 props
            </slot>
          </div>
        </template>
        
    • select

      • 复杂的双向系统

        // 难点:Select 与 Option 的深度通信
        // 解决方案:provide/inject + Context 模式
        
        // Select.vue
        provide<SelectContext>(SELECT_CTX_KEY, {
          handleSelect,      // 选项选择处理
          selectStates,      // 全局状态管理
          renderLabel,       // 标签渲染函数
          highlightedLine,   // 高亮行状态
        })
        
        // Options.vue
        const ctx = inject(SELECT_CTX_KEY)
        const selected = computed(() => 
          ctx?.selectStates?.selectedOption?.value === props.value
        )
        
        // 挑战:
        // - 状态同步管理
        // - 函数传递与执行
        // - 避免循环引用
        
      • 数据源双模式的支持

        // 难点:支持 options 数组和 slot 插槽两种数据源
        const hasChildren = computed(() => size(children.value) > 0)
        
        // 数组模式
        const filteredOptions = ref(props.options ?? [])
        
        // 插槽模式
        const filteredChilds: Ref<Map<VNode, SelectOptionProps>> = ref(new Map())
        
        // 数据源切换逻辑
        const hasData = computed(
          () =>
            (hasChildren.value && filteredChilds.value.size > 0) ||
            (!hasChildren.value && size(filteredOptions.value) > 0)
        )
        
        // 挑战:
        // - 两种模式的统一处理
        // - 数据结构转换
        // - 性能优化
        
      • 动态过滤和搜索

        // 难点:复杂的过滤逻辑
        const handleFilterDebounce = debounce(handleFilter, timeout.value)
        
        function handleFilter() {
          const selectKey = selectStates.inputValue
          selectStates.highlightedIndex = -1
        
          if (hasChildren.value) {
            genFilterChilds(selectKey)  // 插槽模式过滤
          } else {
            genFilterOptions(selectKey) // 数组模式过滤
          }
        }
        
        // 远程搜索处理
        async function callRemoteMethod(method: Function, search: string) {
          selectStates.loading = true
          try {
            const result = await method(search)
            return result
          } catch (error) {
            debugWarn(error as Error)
            return []
          }
        }
        
        // 挑战:
        // - 防抖处理
        // - 异步搜索
        // - 加载状态管理
        
      • 键盘导航系统的实现

        // 难点:完整的键盘交互支持
        const keyMap = useKeyMap({
          isDropdownVisible,
          controlVisible,
          selectStates,
          highlightedLine,
          handleSelect,
          hasData,
          lastIndex,
        })
        
        function handleKeydown(e: KeyboardEvent) {
          keyMap.has(e.key) && keyMap.get(e.key)?.(e)
        }
        
        // useKeyMap 实现
        function useKeyMap(config) {
          return new Map([
            ['Enter', (e) => {
              if (config.highlightedLine.value) {
                config.handleSelect(config.highlightedLine.value)
              }
            }],
            ['ArrowUp', (e) => {
              e.preventDefault()
              if (config.selectStates.highlightedIndex > 0) {
                config.selectStates.highlightedIndex--
              }
            }],
            ['ArrowDown', (e) => {
              e.preventDefault()
              if (config.selectStates.highlightedIndex < config.lastIndex.value) {
                config.selectStates.highlightedIndex++
              }
            }],
            ['Escape', (e) => config.controlVisible(false)]
          ])
        }
        
      • 过滤算法的实现

        // 难点:多种过滤方式支持
        async function genFilterOptions(search: string) {
          if (props.remote && props.remoteMethod && isFunction(props.remoteMethod)) {
            // 远程过滤
            filteredOptions.value = await callRemoteMethod(props.remoteMethod, search)
            return
          }
          if (props.filterMethod && isFunction(props.filterMethod)) {
            // 自定义过滤
            filteredOptions.value = props.filterMethod(search)
            return
          }
          // 默认过滤
          filteredOptions.value = filter(props.options, (opt) => 
            includes(opt.label, search)
          )
        }
        
      • 性能优化

        // 防抖处理
        // 难点:输入防抖与性能平衡
        const timeout = computed(() => props.remote ? 300 : 100)
        const handleFilterDebounce = debounce(handleFilter, timeout.value)
        
        // 防抖函数在输入时调用
        @input="handleFilterDebounce"
        
      • 计算属性优化

        // 难点:复杂的计算属性依赖管理
        const lastIndex = computed(() =>
          hasChildren.value
            ? filteredChilds.value.size - 1
            : size(filteredOptions.value) - 1
        )
        
        const showClear = computed(
          () =>
            props.clearable && selectStates.mouseHover && selectStates.inputValue !== ""
        )
        
    • tooltip

      • 复杂的事件策略模式 给不同的事件绑定对应的触发事件

        // 难点:支持多种触发方式的统一处理
        const triggerStrategyMap: Map<string, () => void> = new Map()
        triggerStrategyMap.set('hover', () => {
            events.value['mouseenter'] = openFinal
            outerEvents.value['mouseleave'] = closeFinal
            dropdownEvents.value['mouseenter'] = openFinal
        })
        triggerStrategyMap.set('click', () => {
            events.value['click'] = togglePopper
        })
        triggerStrategyMap.set('contextmenu', () => {
            events.value['contextmenu'] = (e) => {
                e.preventDefault()
                openFinal()
            }
        })
        
        // 挑战:
        // - 不同触发方式的事件绑定策略
        // - 事件的动态切换与重置
        // - 策略模式的可扩展性
        
      • 防抖和延迟控制

        // 难点:复杂的防抖逻辑与状态管理
        let openDebounce: DebouncedFunc<() => void> | void
        let closeDebounce: DebouncedFunc<() => void> | void
        
        function openFinal() {
            closeDebounce?.cancel()  // 取消关闭定时器
            openDebounce?.()         // 触发打开防抖
        }
        
        function closeFinal() {
            openDebounce?.cancel()   // 取消打开定时器
            closeDebounce?.()        // 触发关闭防抖
        }
        
        // 防抖函数的动态更新
        watchEffect(() => {
            openDebounce = debounce(bind(setVisible, null, true), openDelay.value)
            closeDebounce = debounce(bind(setVisible, null, false), closeDelay.value)
        })
        
        // 挑战:
        // - 防抖函数的创建与销毁
        // - 状态冲突的处理
        // - 延迟时间的动态响应
        
      • 虚拟触发器支持 实现虚拟节点的状态绑定 对应的虚拟触发

        // 难点:支持虚拟节点作为触发器
        const triggerNode = computed(() => {
            if (props.virtualTriggering)
                return (
                    ((props.virtualRef as ButtonInstance)?.ref as any) ??  // 组件实例
                    (props.virtualRef as HTMLElement) ??                   // DOM元素
                    _triggerNode.value                                     // 默认节点
                )
            return _triggerNode.value as HTMLElement
        })
        
        // 虚拟节点事件绑定
        export function useEventsToTriggerNode(
            props: TooltipProps & { virtualTriggering?: boolean },
            triggerNode: ComputedRef<HTMLElement | undefined>,
            events: Ref<Record<string, EventListener>>,
            closeMethod: () => void
        ) {
            const _eventHandleMap = new Map()  // 存储事件引用防止内存泄漏
            
            const _bindEventToVirtualTriggerNode = () => {
                const el = triggerNode.value
                isElement(el) && each(events.value, (fn, event) => {
                    _eventHandleMap.set(event, fn)
                    el?.addEventListener(event as keyof HTMLElementEventMap, fn)
                })
            }
            
            const _unbindEventToVirtualTiggerNode = () => {
                const el = triggerNode.value
                isElement(el) &&
                    each(['mouseenter', 'click', 'contextmenu'], (key) => {
                        _eventHandleMap.has(key) && el?.removeEventListener(key, _eventHandleMap.get(key))
                    })
            }
            
            // 挑战:
            // - 事件引用管理防止内存泄漏
            // - 虚拟节点与真实DOM的映射
            // - 事件的动态绑定与解绑
        }
        
      • 多区域的事件管理 将区域进行分离,方便进行管理 绑定事件 通过ref进行事件的绑定

        // 难点:三个区域的事件分离与管理
        const events: Ref<Record<string, EventListener>> = ref({})        // 触发器区域
        const outerEvents: Ref<Record<string, EventListener>> = ref({})   // 容器区域
        const dropdownEvents: Ref<Record<string, EventListener>> = ref({}) // 弹窗区域
        
        // 事件策略动态绑定
        function attachEvents() {
            if (props.disabled || props.manual) return
            triggerStrategyMap.get(props.trigger)?.()
        }
        
        function resetEvents() {
            events.value = {}
            outerEvents.value = {}
            dropdownEvents.value = {}
            attachEvents()
        }
        
      • popper 定位管理

        // 难点:Popper实例的创建与销毁管理
        let popperInstance: null | Instance
        
        watch(
            visible,
            (val) => {
                if (!val) return
                if (triggerNode.value && popperNode.value) {
                    destroyPopperInstance()
                    popperInstance = createPopper(
                        triggerNode.value,
                        popperNode.value,
                        popperOptions.value
                    )
                }
            },
            { flush: 'post' }
        )
        
        function destroyPopperInstance() {
            popperInstance?.destroy()
            popperInstance = null
        }
        
        onUnmounted(() => {
            destroyPopperInstance()
        })
        
        // 挑战:
        // - 内存泄漏防护
        // - DOM更新时机
        // - 定位精度保证
        
        // 难点:定位配置的动态更新
        const popperOptions = computed(() => ({
            placement: props.placement,
            modifiers: [
                {
                    name: 'offset',
                    options: {
                        offset: [0, 9],  // 距离触发器9px
                    },
                },
            ],
            ...props.popperOptions,  // 支持外部扩展
        }))
        
      • 交互逻辑 难点

        • 点击外部关闭

          // 难点:精确的外部点击检测
          useClickOutside(containerNode, () => {
              emits('click-outside')
              if (props.trigger === 'hover' || props.manual) return  // hover和手动模式不关闭
              visible.value && closeFinal()
          })
          
          // 挑战:
          // - 不同触发方式的差异化处理
          // - 避免误触发
          // - 事件冒泡处理
          
        • 延迟时间计算

          // 难点:根据触发方式动态计算延迟时间
          const openDelay = computed(() =>
              props.trigger === 'hover' ? props.showTimeout : 0
          )
          const closeDelay = computed(() =>
              props.trigger === 'hover' ? props.hideTimeout : 0
          )
          
          // 挑战:
          // - 条件判断的准确性
          // - 性能优化
          // - 用户体验平衡
          
        • popperJS 的使用

钩子函数的使用**

钩子函数是软件设计中的一种扩展机制,允许在特定的执行时机插入自定义逻辑,从而无需修改核心代码

钩子函数的必要性:

  1. 代码解耦和可扩展性

    // ❌ 没有钩子函数 - 紧耦合
    class Popper {
      update() {
        // 核心逻辑
        this.computePosition()
        this.applyStyles()
        
        // 特定业务逻辑 - 与核心逻辑耦合
        if (this.options.withShadow) {
          this.addShadow()
        }
        if (this.options.withAnimation) {
          this.animateIn()
        }
      }
    }
    
    // ✅ 使用钩子函数 - 高度解耦
    class Popper {
      constructor() {
        this.hooks = {
          beforeUpdate: [],
          afterUpdate: [],
          beforePosition: [],
          afterPosition: []
        }
      }
      
      update() {
        this.runHook('beforeUpdate')
        
        this.computePosition()
        this.runHook('afterPosition')
        
        this.applyStyles()
        this.runHook('afterUpdate')
      }
      
      runHook(name, ...args) {
        this.hooks[name].forEach(fn => fn(...args))
      }
      
      addHook(name, fn) {
        this.hooks[name].push(fn)
      }
    }
    
  2. 声明周期管理 实现在特定的地方执行特定的事件

    // Popper.js 中的修饰符执行阶段
    const phaseOrder = [
      'beforeRead',    // 读取前
      'read',          // 读取阶段
      'afterRead',     // 读取后
      'beforeMain',    // 主逻辑前
      'main',          // 主逻辑
      'afterMain',     // 主逻辑后
      'beforeWrite',   // 写入前
      'write',         // 写入阶段
      'afterWrite'     // 写入后
    ]
    
    function runModifierEffects(instance) {
      phaseOrder.forEach(phase => {
        instance.options.modifiers
          .filter(modifier => modifier.phase === phase)
          .forEach(modifier => {
            if (modifier.enabled) {
              // 在特定阶段执行修饰符
              modifier.fn(instance.state, modifier.options)
            }
          })
      })
    }
    
  3. 插件系统支持

    // 插件可以通过钩子函数扩展功能
    const shadowPlugin = {
      name: 'shadow',
      phase: 'afterWrite',
      fn: ({ state }) => {
        // 在样式应用后添加阴影
        state.elements.popper.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)'
      }
    }
    
    const animationPlugin = {
      name: 'animation',
      phase: 'beforeWrite',
      fn: ({ state }) => {
        // 在应用样式前添加动画
        state.elements.popper.style.transition = 'all 0.3s ease'
      }
    }
    
    createPopper(reference, popper, {
      modifiers: [shadowPlugin, animationPlugin]
    })
    

实际应用场景

  1. 数据处理管道 对数据进行标准格式化处理
  2. 流程渲染控制, 不同的流程的时机创建不同的钩子函数

钩子函数实现模式

  1. 事件监听模式 监听事件顺序 执行对应的钩子 (注册 => 使用)

    简单易用 异步支持 解耦性强 性能优秀 广泛支持Node 浏览器 原生支持 使用场景: UI交互 用户操作响应 DOM事件

    class EventEmitter {
      constructor() {
        this.events = {}
      }
      
      // 注册钩子
      on(event, callback) {
        if (!this.events[event]) {
          this.events[event] = []
        }
        this.events[event].push(callback)
      }
      
      // 移除钩子
      off(event, callback) {
        if (this.events[event]) {
          const index = this.events[event].indexOf(callback)
          if (index > -1) {
            this.events[event].splice(index, 1)
          }
        }
      }
      
      // 触发钩子
      emit(event, ...args) {
        if (this.events[event]) {
          this.events[event].forEach(callback => {
            callback(...args)
          })
        }
      }
    }
    
    // 使用示例
    const emitter = new EventEmitter()
    
    // 注册多个钩子
    emitter.on('beforeSave', (data) => {
      console.log('验证数据:', data)
    })
    emitter.on('beforeSave', (data) => {
      console.log('格式化数据:', data)
    })
    
    // 触发钩子
    emitter.emit('beforeSave', { name: 'test' })
    
  2. 回调队列模式 (注册 => 存在比较严格的执行顺序)

    生命周期明确: before after等阶段清晰 顺序可控: 添加顺序执行顺序 错误处理: 内置错误处理机制 上下文传递: 支持上下文对象传递 便于展开独立测试 使用场景: 异步任务管理 批量处理 任务调度

    class HookQueue {
      constructor() {
        this.hooks = {
          before: [],
          after: [],
          error: []
        }
      }
      
      add(type, callback) {
        if (this.hooks[type]) {
          this.hooks[type].push(callback)
        }
      }
      
      async execute(type, context) {
        for (const hook of this.hooks[type]) {
          await hook(context)
        }
      }
      
      async run(context) {
        try {
          await this.execute('before', context)
          context.result = await this.coreLogic(context)
          await this.execute('after', context)
        } catch (error) {
          context.error = error
          await this.execute('error', context)
        }
      }
      
      coreLogic(context) {
        return Promise.resolve('核心逻辑结果')
      }
    }
    
  3. 中间件模式 和koa的中间件机制差不多 洋葱模型: 支持双向处理 (进入/退出) 中断控制: 可以通过不调用next中断流程 顺序执行:严格按照顺序执行, 上下文共享 可组合性: 中间件可以独立的进行开发 使用场景: 请求处理流水线 权限验证 日志记录等

    class Middleware {
      constructor() {
        this.stack = []
      }
      
      use(fn) {
        this.stack.push(fn)
      }
      
      async execute(context) {
        let index = 0
        
        const dispatch = async (i) => {
          if (i === this.stack.length) return
          if (i <= index) throw new Error('next() called multiple times')
          
          index = i
          const fn = this.stack[i]
          
          if (!fn) return
          
          await fn(context, () => dispatch(i + 1))
        }
        
        await dispatch(0)
      }
    }
    
    // 使用示例
    const middleware = new Middleware()
    
    middleware.use(async (ctx, next) => {
      console.log('中间件 1: 开始')
      await next()
      console.log('中间件 1: 结束')
    })
    
    middleware.use(async (ctx, next) => {
      console.log('中间件 2: 开始')
      ctx.data = 'processed'
      await next()
      console.log('中间件 2: 结束')
    })
    
  4. 发布订阅模式(Pub/Sub) 完全解耦: 发布者和订阅者完全独立 动态订阅: 运行时候可以添加/移除订阅者 一对多通信: 一个事件可以通过多个订阅者 异步友好: 天然支持异步处理 可管理性强 使用场景:一对多通信 解耦组件 跨模块通信

    class PubSub {
      constructor() {
        this.topics = {}
        this.subUid = -1
      }
      
      subscribe(topic, callback) {
        if (!this.topics[topic]) {
          this.topics[topic] = []
        }
        
        const token = (++this.subUid).toString()
        this.topics[topic].push({
          token,
          callback
        })
        
        return token
      }
      
      publish(topic, args) {
        if (!this.topics[topic]) {
          return
        }
        
        this.topics[topic].forEach(subscription => {
          subscription.callback(args)
        })
      }
      
      unsubscribe(token) {
        for (const m in this.topics) {
          if (this.topics[m]) {
            for (let i = 0, j = this.topics[m].length; i < j; i++) {
              if (this.topics[m][i].token === token) {
                this.topics[m].splice(i, 1)
                return token
              }
            }
          }
        }
        return false
      }
    }
    

    还有很多的钩子机制...这里不再一一展示

钩子函数的优势

  • 高度解耦:不将业务的什么逻辑直接写在核心的方法当中, 将功能放在钩子里面执行实现解耦
  • 可扩展性
  • 运行时灵活:根据条件动态切换逻辑,钩子执行
  • 模块化开发:指定不同模块的钩子函数
  • 测试友好,向后兼容
  • 性能优化

组件库定义的钩子的使用

image-20250924102353960

组件库我使用的一些钩子

  1. useClickOutside 当点击指定元素的外部元素的时候,执行回调函数,用来关闭弹窗,菜单的
  2. useDisabledStyle用来添加禁用样式的钩子
  3. useEventListener用来对事件进行监听或者卸载监听等的操作
  4. useFocusController 用来执行focus blur等事件
  5. .... 当我们发现代码高度重合的时候我们就可以考虑封装一层或者看是否适合创建钩子函数,将对应的功能独立出来,实现低耦合