likes
comments
collection
share

关于我借助Dumi2开发的几个组件与工具

作者站长头像
站长
· 阅读数 66

关于我借助Dumi2开发的几个组件与工具 关于dumi的介绍及使用,详细可见这篇文章:

下面介绍我借助dumi2开发的几个组件与工具(介绍只是概括,具体实现看源码与注释)。

一、关于项目配置的补充说明

在文章开头给出的链接文章中,关于项目的一些配置已经说明得很清楚了,这里主要是讲读者所用到的一些其它配置,若想了解更多可查阅官方文档

1、横向导航栏的配置

要想实现下面这样的横线导航栏:

关于我借助Dumi2开发的几个组件与工具

因为dumi 默认会将 src 下的文档都归类到 /components 下,所以首先去设置项目的目录:(components文件夹对应【组件栏】,tools文件夹对应【工具栏】)

关于我借助Dumi2开发的几个组件与工具

同时src\index.ts里面导出组件、工具的路径要写对:

/**
 * 组件
 */
export { default as Button } from './components/Button'
export { default as PictureClip } from './components/PictureClip'
export { default as PrimaryButton } from './components/PrimaryButton'
export { default as VirtualList } from './components/VirtualList'
/**
 * 工具
 */
// hooks
export { default as useDebounce } from './tools/useDebounce'
export { default as useMemoizedFn } from './tools/useMemoizedFn'
export { default as useThrottle } from './tools/useThrottle'

在.dumirc.ts里面进行路径的配置:

import { defineConfig } from 'dumi'

export default defineConfig({
  outputPath: 'docs-dist',
  resolve: {
    // 设置横向导航栏
    atomDirs: [
      { type: 'component', dir: 'src/components' },
      { type: 'tool', dir: 'src/tools' },
    ],
  },
  themeConfig: {
    name: 'dumi2-demo',
  },
  styles: [
    `.dumi-default-header-left {
      width: 220px !important;
    }`,
    `.dumi-default-features-item {
      text-align: center;
    }
    body .dumi-default-sidebar>dl>dt {
      font-size: 16px;
    }
    body .dumi-default-sidebar>dl>dd>a {
      color: #717484;
      font-size: 14px;
    }
    `,
  ],
})

在组件的index.md文件中书写约定式路由:

---
toc: content # 导航在内容区才显示,在左侧导航栏不显示
title: Button 按钮 # 组件的标题,会在菜单侧边栏展示
nav: 组件 # 约定式路由
---
---
title: useDebounce
toc: content
nav: 工具
group:
  title: 自定义hooks
---

二、组件

1、图片裁剪组件

这是一个灵活实现图片裁剪功能的组件,点击按钮即可上传图片进行裁剪,图片源既可以是链接,也可以是文件。该组件借助canvas画布,实现手动拉拽进行裁剪,或者输入图片尺寸进行裁剪,提供了更多的自由度和定制性。其中,组件参数和呈现效果如下所示:

关于我借助Dumi2开发的几个组件与工具

关于我借助Dumi2开发的几个组件与工具

2、虚拟滚动列表

这是一个灵活实现虚拟滚动列表的组件,除了实现常规的虚拟滚动列表功能外,还使用到了一些自定义hook,提高渲染性能的同时也提供了足够的灵活性、方便开发者根据实际需要灵活地修改组件源代码。其中,组件参数和呈现效果如下所示:

关于我借助Dumi2开发的几个组件与工具

关于我借助Dumi2开发的几个组件与工具

核心代码如下:

  /**
   * 监听滚动事件,第一个event是事件(如:click、keydown),第二个回调函数(所以不需要出参),第三个就是目标(是某个节点还是全局)
   */
  useEventListener(
    'scroll',
    () => {
      const { scrollTop, scrollHeight, clientHeight } = scrollRef.current
      // 利用二分查找到当前应该渲染的开始项,|| Math.floor(scrollTop / state.itemHeight)是为了防止出现null的情况
      state.start = state.start =
        binarySearch(state.positions, scrollTop) || Math.floor(scrollTop / state.itemHeight)
      // 计算结束项--开始项+渲染的数量
      state.end = state.start + state.renderCount + 1
      // 计算偏移量
      state.currentOffset = state.start > 0 ? state.positions[state.start - 1].bottom : 0
      // 距离底部的高度 = 滚动条的高度 - 默认的高度 - 距离顶部的高度---下拉到底部进行刷新
      const button = scrollHeight - clientHeight - scrollTop
      // 没必要==0滚动到底部才去请求数据,否则会导致渲染不及时问题(监听滚动事件才去触发的重新渲染,如果==0,就一直无法向下滚动造成体验问题)
      if (button <= 100 && onRequest) {
        onRequest()
          .then((res: any) => {
            const newArr = res.map((num: number) => num + totalList[totalList.length - 1] + 1) // 从最后一个元素的下一个位置开始插入新数
            setTotalList([...totalList, ...newArr])
          })
          .catch((err: any) => {
            console.log(err)
          })
      }
    },
    scrollRef,
  )
  /**
   * 初始化positions数组--渲染的数组发生变化时,重新计算
   */
  const initPositions = () => {
    // 只需要重新计算新增的部分
    const data = []
    for (let i = prelist.length; i < totalList.length; i++) {
      if (state.positions.length === 0) {
        data.push({
          index: i,
          height: state.initItemHeight,
          top: i * state.initItemHeight,
          bottom: (i + 1) * state.initItemHeight,
          dHeight: 0,
        })
      } else {
        data.push({
          index: i,
          height: state.initItemHeight,
          top:
            state.positions[prelist.length - 1].bottom +
            (i - prelist.length) * state.initItemHeight,
          bottom:
            state.positions[prelist.length - 1].bottom +
            (i - prelist.length + 1) * state.initItemHeight,
          dHeight: 0,
        })
      }
    }

    state.positions = [...state.positions, ...data]
    // 此时才去更新prelist
    setPrelist(totalList)
  }
  useEffect(() => {
    // 初始高度--用预估高度*总数
    initPositions()
  }, [totalList.length])
  /**
   * 监听内容的变化,更新positions数组,用实际的高度替换预估的高度
   */
  const setPostition = () => {
    const nodes = ref.current.childNodes
    if (nodes.length === 0) return
    nodes.forEach((node: HTMLDivElement) => {
      if (!node) return
      const rect = node.getBoundingClientRect() // 获取对应的元素信息
      const index = +node.id // 可以通过id,来取到对应的索引
      const oldHeight = state.positions[index].height // 旧的高度
      const dHeight = oldHeight - rect.height // 差值
      if (dHeight) {
        state.positions[index].height = rect.height //真实高度
        state.positions[index].bottom = state.positions[index].bottom - dHeight
        state.positions[index].dHeight = dHeight //将差值保留
      }
    })

    //  重新计算整体的高度
    const startId = +nodes[0].id

    const positionLength = state.positions.length
    let startHeight = state.positions[startId].dHeight
    state.positions[startId].dHeight = 0

    for (let i = startId + 1; i < positionLength; ++i) {
      const item = state.positions[i]
      state.positions[i].top = state.positions[i - 1].bottom
      state.positions[i].bottom = state.positions[i].bottom - startHeight
      if (item.dHeight !== 0) {
        startHeight += item.dHeight
        item.dHeight = 0
      }
    }

    // 重新计算子列表的高度
    state.itemHeight = state.positions[positionLength - 1].bottom
  }
  useEffect(() => {
    setPostition()
  }, [ref.current])

实现虚拟列表值得注意的点有三个:

1、渲染区域:这块部分为真正用户看到的列表区域,实际上有可视区和缓冲区共同组成,缓冲区的作用是防止快速下滑或者上滑的过程中出现空白区域

2、下拉请求数据:并不是滚动到底部才去发起请求数据,而是提前发起请求,否则会导致渲染不及时问题(监听滚动事件才去触发的重新渲染,如果滚动到底部才去发起请求数据,就一直无法向下滚动造成体验问题)

3、列表项不定高时的处理:维护一个数组,用于存储每项的信息,比如下标、高度、距离顶部高度等。初始化是用预估值进行统一设置,每渲染一项,就用实际的信息更新该项对应的数组项信息,然后可以保证:监听到滚动事件时,都可以从该数组中找到state.start(当前应该渲染的开始项,利用二分查找优化)。

三、工具

1、常用方法和类

myAxios

关于我借助Dumi2开发的几个组件与工具

这个二次封装的 Axios 功能主要是为了方便开发者发送请求。封装后的 axios 实例通过提供 get、post、put、delete 和 request 等方法,可以发送对应的 HTTP 操作。还加入了拦截器,在请求和响应前进行处理,并在请求中添加 token,以及统一封装错误信息。最终返回的是一个 Promise 对象,可以实现异步请求数据,并根据实际需要对返回的数据进行处理,提高开发效率。

myFetch

关于我借助Dumi2开发的几个组件与工具

实现了对 fetch 函数的二次封装,并添加了请求拦截器和响应拦截器的功能。通过请求拦截器,我们可以在发送请求前,对请求进行一些处理,比如添加请求头信息、更改请求参数等;而通过响应拦截器,我们可以在得到响应后,对响应进行一些处理,比如解析返回的数据、进行错误处理等。

使用该封装函数 myFetch 时,用户可以传入请求的 URL 和请求参数,函数会自动根据参数发送请求,并在发送请求前和得到响应后执行相应的请求拦截器和响应拦截器,以实现对请求和响应的拦截和处理。同时,该函数也支持 Promise 的异步操作,用户可以通过 await 关键字来等待请求结果的返回。

myStorage

关于我借助Dumi2开发的几个组件与工具

对本地存储 sessionStorage 进行封装,类型推导,自动序列化,类型转换,使用起来更方便,也便于维护管理本地存储。

2、自定义HOOKS

useMemoizedFn

用来处理函数缓存的 Hook。

源代码如下:

import { useMemo, useRef } from 'react'

/**
 * @description // 封装了一个useMemoizedFn的hook,用于缓存函数
 * @param fn // 需要缓存的函数
 * @returns // 返回一个缓存的函数
 */

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type noop = (this: any, ...args: any[]) => any

// 通过 PickFunction 类型来确保某个函数类型符合我们的要求,从而避免类型错误。
type PickFunction<T extends noop> = (
  this: ThisParameterType<T>,
  ...args: Parameters<T>
) => ReturnType<T>

function useMemoizedFn<T extends noop>(fn: T) {
  const fnRef = useRef<T>(fn)
  // 使用 useMemo 缓存一个可用的函数引用,每次 fn 改变时更新缓存
  fnRef.current = useMemo(() => fn, [fn])

  const memoizedFn = useRef<PickFunction<T>>()
  if (!memoizedFn.current) {
    // eslint-disable-next-line func-names
    memoizedFn.current = function (this, ...args) {
      return fnRef.current.apply(this, args)
    }
  }

  // 返回一个 memoizedFn 的引用
  return memoizedFn.current as T // 把返回类型断言为 T 类型
}

export default useMemoizedFn

useDebounce

用来处理函数防抖的 Hook。

源代码如下:

import { useMemoizedFn } from 'dumi2-demo'
import { useEffect, useRef } from 'react'

/**
 * @description  //防抖hook
 * @param fn // 需要防抖的函数
 * @param delay // 防抖的时间
 * @returns // 返回一个防抖的函数
 */
const useDebounce = <T extends (...args: any[]) => any>(fn: T, delay: number): T => {
  // 用 useRef 来保存函数和定时器
  const { current } = useRef<{ fn: T; timer: NodeJS.Timeout | null }>({ fn, timer: null })

  // 当传入的 fn 更新时,更新 useRef 中保存的 fn
  useEffect(() => {
    current.fn = fn
  }, [fn])

  // 使用 useMemoizedFn 缓存一个可用的函数
  return useMemoizedFn((...args) => {
    if (current.timer) {
      clearTimeout(current.timer)
    }
    current.timer = setTimeout(() => {
      current.fn.call(null, ...args)
    }, delay)
  }) as unknown as T // 函数返回值为 useMemoizedFn 的返回值,但是 return 中的函数需要断言成 T 类型(先断言为一个未知类型,以防止编译器给出错误的类型标注)
}

export default useDebounce

useThrottle

用来处理函数节流的 Hook。

源代码如下:

import { useMemoizedFn } from 'dumi2-demo'
import { useEffect, useRef } from 'react'

/**
 * @description //节流hook
 * @param fn // 需要节流的函数
 * @param delay // 节流的时间
 * @returns // 返回一个节流的函数
 */
const useThrottle = <T extends (...args: any[]) => any>(fn: T, delay: number): T => {
  // 用 useRef 来保存函数和时间
  const { current } = useRef<{ fn: T; lastUpdated: number }>({ fn, lastUpdated: Date.now() })

  // 当传入的 fn 更新时,更新 useRef 中保存的 fn
  useEffect(() => {
    current.fn = fn
  }, [fn])

  // 使用 useMemoizedFn 缓存一个可用的函数
  return useMemoizedFn((...args: any) => {
    if (current.lastUpdated + delay <= Date.now()) {
      current.lastUpdated = Date.now()
    } else {
      current.fn(...args)
    }
  }) as unknown as T // 函数返回值为 useMemoizedFn 的返回值,但是 return 中的函数需要断言成 T 类型(先断言为一个未知类型,以防止编译器给出错误的类型标注)
}

export default useThrottle

四、总结

本篇文章介绍了几个笔者在日常开发中实现的几个组件和工具,并通过dumi2将它们整合在了一起,需要的读者可以从项目源码中复制一份在自己的项目中使用。本项目的源码地址如下:

XC0703/dumi2-demo: 关于我借助Dumi2开发的几个组件与工具 (github.com)

转载自:https://juejin.cn/post/7269703885429948435
评论
请登录