关于我借助Dumi2开发的几个组件与工具
关于dumi的介绍及使用,详细可见这篇文章:
下面介绍我借助dumi2开发的几个组件与工具(介绍只是概括,具体实现看源码与注释)。
一、关于项目配置的补充说明
在文章开头给出的链接文章中,关于项目的一些配置已经说明得很清楚了,这里主要是讲读者所用到的一些其它配置,若想了解更多可查阅官方文档。
1、横向导航栏的配置
要想实现下面这样的横线导航栏:
因为dumi 默认会将 src 下的文档都归类到 /components 下,所以首先去设置项目的目录:(components文件夹对应【组件栏】,tools文件夹对应【工具栏】)
同时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画布,实现手动拉拽进行裁剪,或者输入图片尺寸进行裁剪,提供了更多的自由度和定制性。其中,组件参数和呈现效果如下所示:
2、虚拟滚动列表
这是一个灵活实现虚拟滚动列表的组件,除了实现常规的虚拟滚动列表功能外,还使用到了一些自定义hook,提高渲染性能的同时也提供了足够的灵活性、方便开发者根据实际需要灵活地修改组件源代码。其中,组件参数和呈现效果如下所示:
核心代码如下:
/**
* 监听滚动事件,第一个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
这个二次封装的 Axios 功能主要是为了方便开发者发送请求。封装后的 axios 实例通过提供 get、post、put、delete 和 request 等方法,可以发送对应的 HTTP 操作。还加入了拦截器,在请求和响应前进行处理,并在请求中添加 token,以及统一封装错误信息。最终返回的是一个 Promise 对象,可以实现异步请求数据,并根据实际需要对返回的数据进行处理,提高开发效率。
myFetch
实现了对 fetch 函数的二次封装,并添加了请求拦截器和响应拦截器的功能。通过请求拦截器,我们可以在发送请求前,对请求进行一些处理,比如添加请求头信息、更改请求参数等;而通过响应拦截器,我们可以在得到响应后,对响应进行一些处理,比如解析返回的数据、进行错误处理等。
使用该封装函数 myFetch 时,用户可以传入请求的 URL 和请求参数,函数会自动根据参数发送请求,并在发送请求前和得到响应后执行相应的请求拦截器和响应拦截器,以实现对请求和响应的拦截和处理。同时,该函数也支持 Promise 的异步操作,用户可以通过 await 关键字来等待请求结果的返回。
myStorage
对本地存储 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将它们整合在了一起,需要的读者可以从项目源码中复制一份在自己的项目中使用。本项目的源码地址如下:
转载自:https://juejin.cn/post/7269703885429948435