diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b20d0bb..cb5d239 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: strategy: matrix: - node-version: [12.x] + node-version: [14.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: diff --git a/README.md b/README.md index 2124f73..b74cf26 100644 --- a/README.md +++ b/README.md @@ -9,23 +9,109 @@ vue3-ts-util是下厨房几个vue3后台的通用函数,组件库。 # 安装 `yarn add vue3-ts-util` # 用法 -## 网络请求相关 -打开可以查看对应详细点的文档 -1. [FetchQueue](doc/io.md#FetchQueue) 请求容器,用于控制多个请求的并发,重试,意外处理,自动控制loading,可以大量减少了`try catch finally`等代码的使用 - 1. [useStrictQueueHelper](doc/io.md#strictQueue) 在FetchQueue的基础上增加了对vue的一些便利性工具 - 2. [strictQueue](doc/io.md#strictQueue) 返回一个严格的请求队列,这是针对有副作用的请求而使用的 -2. [Task](doc/io.md#Task) 针对轮训请求的控制 -3. [makeAsyncIter](doc/io.md#makeAsyncIter) 将基于游标分页的请求转成异步迭代资源 - 1. [useAntdListPagination](doc/io.md#useAntdListPagination) makeAsyncIter针对翻页做的适配,与GeneralPagation组件搭配使用 - 2. [useInfiniteScrolling](doc/io.md#无限滚动) makeAsyncIter针对无限滚动做的适配 - -## vuex相关 +见文档[doc](doc) +目录见下方 + + +## vue3 composition api的hook +- [useDomRect hook风格获取dom的rect](./doc/hooks.md#usedomrect-hook风格获取dom的rect) +- [useEmit 用于在hook内emit](./doc/hooks.md#useemit-用于在hook内emit) +- [useFetchQueueHelper/useStrictQueue/useRetryableQueue fetchqueue的hook wrapper](./doc/hooks.md#usefetchqueuehelperusestrictqueueuseretryablequeue-fetchqueue的hook-wrapper) +- [useInfiniteScrolling 无限滚动](./doc/hooks.md#useinfinitescrolling-无限滚动) +- [useResizeable 用于鼠标拖拽调整调整某个元素的大小位置](./doc/hooks.md#useresizeable-用于鼠标拖拽调整调整某个元素的大小位置) +- [useStackAlloc hook风格管理object url分配](./doc/hooks.md#usestackalloc-hook风格管理object-url分配) +- [useWatchDocument `document.addEventListener`的hook wrapper](./doc/hooks.md#usewatchdocument-documentaddeventlistener的hook-wrapper) +- [createTypedShareStateHook/useHookShareState 生成一个实例内进行状态共享的hook](./doc/hooks.md#createtypedsharestatehookusehooksharestate-生成一个实例内进行状态共享的hook) +- [useRouteId 路由参数id获取,合法判断](./doc/hooks.md#userouteid-路由参数id获取合法判断) + +## 输入输出,网络请求相关的 +- [FetchQueue 自动管理loading等的请求控制容器](./doc/io.md#fetchqueue-自动管理loading等的请求控制容器) + - [构造参数](./doc/io.md#构造参数) + - [类方法/属性](./doc/io.md#类方法属性) + - [pushAction返回的任务实例](./doc/io.md#pushaction返回的任务实例) + - [例子](./doc/io.md#例子) + - [最小化](./doc/io.md#最小化) + - [排队执行,失败自动重试](./doc/io.md#排队执行失败自动重试) + - [更多的例子见单元测试](./doc/io.md#更多的例子见单元测试) + - [衍生hooks](./doc/io.md#衍生hooks) +- [Task 轮训请求的控制](./doc/io.md#task-轮训请求的控制) + - [参数](./doc/io.md#参数) + - [返回值](./doc/io.md#返回值) + - [停止轮训](./doc/io.md#停止轮训) + - [获取轮训结果](./doc/io.md#获取轮训结果) + - [获取轮训参数](./doc/io.md#获取轮训参数) + - [一个简单的例子](./doc/io.md#一个简单的例子) +- [makeAsyncIter 分页api的迭代管理](./doc/io.md#makeasynciter-分页api的迭代管理) + - [返回参数](./doc/io.md#返回参数) + - [一个简单的例子](./doc/io.md#一个简单的例子-1) + - [控制多资源,内部状态重置](./doc/io.md#控制多资源内部状态重置) + - [中断之前的请求](./doc/io.md#中断之前的请求) + - [返回类型的约束](./doc/io.md#返回类型的约束) + - [在vue2 options api中使用](./doc/io.md#在vue2-options-api中使用) + - [在小程序中使用](./doc/io.md#在小程序中使用) + - [最小无限滚动加载收藏的例子](./doc/io.md#最小无限滚动加载收藏的例子) + - [在ts/js中获取asyncIter的状态](./doc/io.md#在tsjs中获取asynciter的状态) + - [在wxml中获取asyncIter的状态](./doc/io.md#在wxml中获取asynciter的状态) + - [通过设置回调来实现状态变化时更新 setStateUpdatedCallback](./doc/io.md#通过设置回调来实现状态变化时更新-setstateupdatedcallback) + - [简写方式 bindPage](./doc/io.md#简写方式-bindpage) + - [如何知道asyncIter引发的界面修改完成时机](./doc/io.md#如何知道asynciter引发的界面修改完成时机) + - [常用场景的使用](./doc/io.md#常用场景的使用) + - [antd表格翻页](./doc/io.md#antd表格翻页) + - [无限滚动](./doc/io.md#无限滚动) +- [useInfiniteScrolling 无限滚动](./doc/io.md#useinfinitescrolling-无限滚动) + - [探底触发](./doc/io.md#探底触发) + - [交叉触发模式](./doc/io.md#交叉触发模式) + - [hooks](./doc/io.md#hooks) +- [useAntdListPagination / GeneralPagination 翻页管理](./doc/io.md#useantdlistpagination--generalpagination--翻页管理) + - [使用参考](./doc/io.md#使用参考) + +## 其余不好分类的函数 +- [deepComputed](./doc/other.md#deepcomputed) + - [主要的使用场景](./doc/other.md#主要的使用场景) + - [性能相关](./doc/other.md#性能相关) +- [events/typedEventEmitter 类型安全的EventEmitter](./doc/other.md#eventstypedeventemitter-类型安全的eventemitter) +- [image/getImageUrl 从下厨房用的图像结构构造url](./doc/other.md#imagegetimageurl-从下厨房用的图像结构构造url) +- [assigIncrId 生成一个全局自增id](./doc/other.md#assigincrid-生成一个全局自增id) +- [unid/typedID/ID 使用symbol实现的ID生成器](./doc/other.md#unidtypedidid-使用symbol实现的id生成器) +- [delay,delayFn 延时,推迟控制流执行](./doc/other.md#delaydelayfn-延时推迟控制流执行) +- [promise2ref promsie转成ref](./doc/other.md#promise2ref-promsie转成ref) +- [promiseSetRef 在promise完成时设置某个ref](./doc/other.md#promisesetref-在promise完成时设置某个ref) +- [momentConvert 一个函数实现下厨房常用的多种时间转换](./doc/other.md#momentconvert-一个函数实现下厨房常用的多种时间转换) + +## 类型及类型推导辅助相关 +- [globalComponents](./doc/type.md#globalcomponents) +- [DeepReadonly转换一个类型为深度只读](./doc/type.md#deepreadonly转换一个类型为深度只读) + - [仅使用类型](./doc/type.md#仅使用类型) + - [也可以使用这种方式](./doc/type.md#也可以使用这种方式) +- [ok 先验条件断言](./doc/type.md#ok-先验条件断言) +- [thruthy 真值断言](./doc/type.md#thruthy-真值断言) +- [Columns 描述antd表格结构的类型](./doc/type.md#columns-描述antd表格结构的类型) +- [Image 下厨房的图像结构](./doc/type.md#image-下厨房的图像结构) +- [WithRequired 将对象部分字段转为不可空](./doc/type.md#withrequired-将对象部分字段转为不可空) +- [customPropType vue props用于推导自定义类型的辅助函数,使用interface风格写props](./doc/type.md#customproptype-vue-props用于推导自定义类型的辅助函数使用interface风格写props) + +## 本库的vue3组件 +- [GeneralPagination 翻页器和相关hook](./doc/vue3components.md#generalpagination-翻页器和相关hook) +- [SplitView 支持鼠标拖拽调整的视图分割](./doc/vue3components.md#splitview-支持鼠标拖拽调整的视图分割) + - [props](./doc/vue3components.md#props) + - [例子](./doc/vue3components.md#例子) +- [SearchSelect 支持搜索的选择,追求尽可能少的代码来描述](./doc/vue3components.md#searchselect-支持搜索的选择追求尽可能少的代码来描述) + - [props](./doc/vue3components.md#props-1) + - [例子](./doc/vue3components.md#例子-1) + +## vuex相关的 +- [mutation 生成mutation函数的辅助函数](./doc/vuex.md#mutation-生成mutation函数的辅助函数) +- [VuexPersistence 用于持久化的vuex插件](./doc/vuex.md#vuexpersistence-用于持久化的vuex插件) + - [feature](./doc/vuex.md#feature) + - [最小化例子](./doc/vuex.md#最小化例子) + + # 开发 ## 下载 ```bash git clone .... cd vue3-ts-util -yarn +yarn .... ``` ## 使用dev-watch开发新功能及debug @@ -78,8 +164,9 @@ yarn test ## 一些需要注意的地方 1. vue组件的类型声明应该使用`yarn gen-vue-type`来自动生成,而不是手写或者使用shims,使用shims会丢失props的类型信息。对于props的声明应该尽量`customPropType`,可以尽可能接近写interface的体验 2. 如果需要返回一个外部不可修改的对象可以使用`deepReadonly` -3. 尽可能足够的单元测试 -4. 如果修改了组件相关及时修改vetur下的文件,及volar所使用的`src/globalComponents.ts` +3. 修改文档后使用vscode的markdown in one更新所在文件的目录,然后使用`yarn gen-contents`生成合并的目录写入到readmeimage +4. 尽可能足够的单元测试 +5. 如果修改了组件相关及时修改vetur下的文件,及volar所使用的`src/globalComponents.ts` diff --git a/doc/hooks.md b/doc/hooks.md new file mode 100644 index 0000000..264a659 --- /dev/null +++ b/doc/hooks.md @@ -0,0 +1,83 @@ +- [useDomRect hook风格获取dom的rect](#usedomrect-hook风格获取dom的rect) +- [useEmit 用于在hook内emit](#useemit-用于在hook内emit) +- [useFetchQueueHelper/useStrictQueue/useRetryableQueue fetchqueue的hook wrapper](#usefetchqueuehelperusestrictqueueuseretryablequeue-fetchqueue的hook-wrapper) +- [useInfiniteScrolling 无限滚动](#useinfinitescrolling-无限滚动) +- [useResizeable 用于鼠标拖拽调整调整某个元素的大小位置](#useresizeable-用于鼠标拖拽调整调整某个元素的大小位置) +- [useStackAlloc hook风格管理object url分配](#usestackalloc-hook风格管理object-url分配) +- [useWatchDocument `document.addEventListener`的hook wrapper](#usewatchdocument-documentaddeventlistener的hook-wrapper) +- [createTypedShareStateHook/useHookShareState 生成一个实例内进行状态共享的hook](#createtypedsharestatehookusehooksharestate-生成一个实例内进行状态共享的hook) +- [useRouteId 路由参数id获取,合法判断](#userouteid-路由参数id获取合法判断) +desc: vue3 composition api的hook +# useDomRect hook风格获取dom的rect +```ts +const contentDom = ref() +const { rect } = useDomRect(contentDom) // rect即为contentDom的rect,尺寸发生改变时他也会改 +``` +# useEmit 用于在hook内emit +避免多传一个ctx +```ts +const { emit, emitValue, emitModal } = useEmit() +const onClick = () => { + emit('change', e) // 和ctx.emit 一致 + emitValue('hello') // 事件update:value的简写 + emitModal('world') // 事件update:modelValue的简写 +} +``` +# useFetchQueueHelper/useStrictQueue/useRetryableQueue fetchqueue的hook wrapper +1. useFetchQueueHelper, 增加了更多有用的函数, 包括vue ref风格的loading。需要传入一个队列实例 +2. useRetryableQueue, useFetchQueueHelper的可重试参数包装 +3. useStrictQueue, useFetchQueueHelper的严格参数包装 +```ts +const { loading, run, fetchQueue } = useStrictQueue() +loading.value // 队列是否在跑任务 +const res = await run(fetcRes) // pushAction的简写,更简短的方式 +const res = await fetchQueue.pushAction(fetchRes).res // 和上面那个一致 +``` +# useInfiniteScrolling 无限滚动 +见[doc/io.md#useinfinitescrolling](/doc/io.md#useinfinitescrolling) +# useResizeable 用于鼠标拖拽调整调整某个元素的大小位置 +demo状态写了一半发现用不上,但是现有的实现都能用 +```ts +const ele = ref() +const { style } = useResizable(ele, { width: 100, height: 100, x: 100, y: 100 }) +``` +```html +
+
+``` +# useStackAlloc hook风格管理object url分配 +给上传的文件视频图片等blob分配一个url,并且组件卸载后释放这个资源,不用手动释放担心内存泄露RAII +```ts +const { alloc } = useStackAlloc() +const ptr = alloc(blob) // 现在可以将ptr作为url,提供video/image查看 +``` +# useWatchDocument `document.addEventListener`的hook wrapper +在组件卸载前会删除监听器 +```ts +useWatchDocument('scroll', throttle((e) => { + console.log(e) +}, 200)) +``` + +# createTypedShareStateHook/useHookShareState 生成一个实例内进行状态共享的hook +```ts +const { useHookShareState } = createTypedShareStateHook(() => ({ count: 0 })) +const useA = () => { + const { state } = useHookShareState() + state.count++ +} +const useB = () => { + const { count } = useHookShareState().toRefs() // 使用torefs展开 + count.value++ // 若useA与useB在同一实例内,则这两个为同一个数 +} +``` +更多细节可以看相关单元测试 + + +# useRouteId 路由参数id获取,合法判断 +```ts +const id = useRouteId() +id.src // id的源,转成数字 +id.srcStr // 同上,但是不转成数字 +id.isVaild // 是否合法,条件转成数字后不是nan且不为0,一般我们是把id为0当成创建的页面 +``` diff --git a/doc/io.md b/doc/io.md index 0084ff2..6f7368e 100644 --- a/doc/io.md +++ b/doc/io.md @@ -1,8 +1,134 @@ -# FetchQueue +- [FetchQueue 自动管理loading等的请求控制容器](#fetchqueue-自动管理loading等的请求控制容器) + - [构造参数](#构造参数) + - [类方法/属性](#类方法属性) + - [pushAction返回的任务实例](#pushaction返回的任务实例) + - [例子](#例子) + - [最小化](#最小化) + - [排队执行,失败自动重试](#排队执行失败自动重试) + - [更多的例子见单元测试](#更多的例子见单元测试) + - [衍生hooks](#衍生hooks) +- [Task 轮训请求的控制](#task-轮训请求的控制) + - [参数](#参数) + - [返回值](#返回值) + - [停止轮训](#停止轮训) + - [获取轮训结果](#获取轮训结果) + - [获取轮训参数](#获取轮训参数) + - [一个简单的例子](#一个简单的例子) +- [makeAsyncIter 分页api的迭代管理](#makeasynciter-分页api的迭代管理) + - [返回参数](#返回参数) + - [一个简单的例子](#一个简单的例子-1) + - [控制多资源,内部状态重置](#控制多资源内部状态重置) + - [中断之前的请求](#中断之前的请求) + - [返回类型的约束](#返回类型的约束) + - [在vue2 options api中使用](#在vue2-options-api中使用) + - [在小程序中使用](#在小程序中使用) + - [最小无限滚动加载收藏的例子](#最小无限滚动加载收藏的例子) + - [在ts/js中获取asyncIter的状态](#在tsjs中获取asynciter的状态) + - [在wxml中获取asyncIter的状态](#在wxml中获取asynciter的状态) + - [通过设置回调来实现状态变化时更新 setStateUpdatedCallback](#通过设置回调来实现状态变化时更新-setstateupdatedcallback) + - [简写方式 bindPage](#简写方式-bindpage) + - [如何知道asyncIter引发的界面修改完成时机](#如何知道asynciter引发的界面修改完成时机) + - [常用场景的使用](#常用场景的使用) + - [antd表格翻页](#antd表格翻页) + - [无限滚动](#无限滚动) +- [useInfiniteScrolling 无限滚动](#useinfinitescrolling-无限滚动) + - [探底触发](#探底触发) + - [交叉触发模式](#交叉触发模式) + - [hooks](#hooks) +- [useAntdListPagination / GeneralPagination 翻页管理](#useantdlistpagination--generalpagination--翻页管理) + - [使用参考](#使用参考) + +desc: 输入输出,网络请求相关的 +# FetchQueue 自动管理loading等的请求控制容器 请求容器,用于控制多个请求的并发,重试,意外处理,自动控制loading,可以大量减少了`try catch finally`等代码的使用 +## 构造参数 +```ts +//最大并发数量, -1为不限制 +maxConcurrencyCount = -1, +// 最大重试次数 +maxRetryCount = 3, +// 重试间隔ms +retryInterval = 3_000, +// 错误处理方法,retry | throw +errorHandleMethod: ErrorHandleMethod = 'retry' +``` +## 类方法/属性 +```ts + /** + * 获取队列配置参数 + */ + conf: { + maxConcurrencyCount: number; + maxRetryCount: number; + retryInterval: number; + errorHandleMethod: ErrorHandleMethod; + }; + /** + * 等待直到当前的队列为空 + */ + waitUntilEmpty(): Promise; + /** + * 添加队列监听器 + */ + on (name: EventName, cb: Fn) : void; + + /** + * 是否空闲 + */ + isIdle: boolean; -# Task + /** + * 压入一个任务到资源获取队列,如果有提示两个任务的元和任务函数一次则这两次函数的运行会是同一个结果 + * @param meta 元标识,且将作为action函数的实参传入 + * @param action 资源获取函数 + */ + pushAction (action: () => Promise): ExportFetchTask; + /** + * 添加全局监听器 + */ + static on (name: EventName, cb: (target: FetchQueue, ...args: any[]) => any) ; + +``` +## pushAction返回的任务实例 +```ts +type ExportFetchTask = { + // 正在运行的是哪个任务 + readonly action: () => Promise; + // 运行结果 + readonly res: Promise; + // 任务是否正在运行 + readonly running: boolean; + // 取消当前任务 + readonly cancel: () => void; +} +``` +## 例子 +### 最小化 +```ts +const queue = new FetchQueue() +const task = queue.pushAction(fetchUser) +const user = await task.res +``` +### 排队执行,失败自动重试 +```ts +const queue = new FetchQueue(1, -1, 0) // 不并发, 不限制重试数量,重试间隔0 +queue.pushAction(action0) +queue.pushAction(action1) +queue.pushAction(action2) +queue.pushAction(action3) +await queue.waitUntilEmpty() // 将会按顺序执行所有任务,某个任务失败,会不断尝试直至完成 +``` +#### 更多的例子见单元测试 + +## 衍生hooks +在vue内的话更推荐使用包装过的几个hook,而不是裸FetchQueue。具体的文档[hooks部分](./hooks.md#usefetchqueuehelperusestrictqueueuseretryablequeue) +1. useFetchQueueHelper, 增加了更多有用的函数, 包括vue ref风格的loading。需要传入一个队列实例 +2. useRetryableQueue, useFetchQueueHelper的可重试参数包装 +3. useStrictQueue, useFetchQueueHelper的严格参数包装 + + +# Task 轮训请求的控制 Task是针对轮训请求的一个封装,主要还是用于各类分析结果的轮训获取。在之前是Task还支持定时在某个时刻去执行action,后来用不到就删除了。 ## 参数 @@ -56,8 +182,8 @@ completedTask.then(res => { console.log(res) // 这是可用的数据 }) ``` -# makeAsyncIter -将基于游标分页的请求转成异步迭代资源 +# makeAsyncIter 分页api的迭代管理 +将基于游标分页的请求转成异步迭代资源,旨在提供更高程度的抽象,逻辑层只通过next()和reset()即可完成所有操作。 从jarvis的Pagination到spam的useCursorControl再到lanfan-dashboard的makeAsyncIter对于分页资源控制的探索一直有在尝试,整体是呈现一个类型推导逐渐完善,手动管理的变量逐渐变少,不再需要手动处理意外的趋势。 @@ -67,13 +193,15 @@ completedTask.then(res => { ```ts interface R { - load: Ref, // 所有资源是否已加载完成 - async next(): void, // 向前迭代 - res: Ref, // 当前迭代到资源 - loading: Ref, // 当前是否在加载中 - cursorStack: string[], // 保存使用的所有cursor - reset (reFetch: bool): void, // 重置内部状态,多资源管理时用得到 - [Symbol.asyncIterator]: ES2018AsyncIter, // for await of 语法 + load: Ref // 所有资源是否已加载完成 + async next(): void // 向前迭代 + res: Ref // 当前迭代到资源 + abort(): void // 中断当前请求 + loading: Ref // 当前是否在加载中 + cursorStack: string[] // 保存使用的所有cursor + // 重置内部状态,多资源管理时用得到,如果当前处于迭代中,直接重置会失败,考虑使用force + reset (reFetch: boolean | { force: boolean, reFetch: boolean }): Promise + [Symbol.asyncIterator]: ES2018AsyncIter // for await of 语法 iter: { [Symbol.asyncIterator]: ES2018AsyncIter } @@ -103,7 +231,16 @@ const iter = makeAsyncIter( ) watch([keyword], () => iter.reset(true)) // keyword改变后,重置并重新获取 ``` +## 中断之前的请求 +典型场景例如 +1. tab切换,在加载还未完成时继续切换 +2. 获取远程的搜索建议,持续的输入 + + +和上面的一样,这两种场景都是需要`reset()`,但是这个是应对请求时间较长的情况,如果你直接`reset`会引发断言错误,可以先`abort`中断掉之前的请求,或者直接`reset({ force: true })`。 +但不一定需要上面那种情况,如果觉得某次迭代时间过长,也可以`abort`返回之前的状态再重新`next`。 +image ## 返回类型的约束 makeAsyncIter是针对基于游标分页的请求,为了要获取到cursor的信息,使用了对返回类型进行约束的并发,必须满足以下类型,`next,next_cursor存在一个就行,prev同样` ```ts @@ -120,14 +257,112 @@ type Response = { cursor: PageCursor } 如果对应的接口不满足,可以参考下面尝试写个转换 ```ts const apiCursorNormalizer = { cursor: customCursor }>(api: T) => { - return (...args: Parameters) => api(...args).then(resp => customCursor2PageCursor(resp.cursor)) + return (...args: Parameters) => api(...args).then(resp => ({ ...resp, curosr: customCursor2PageCursor(resp.cursor) })) } const iter = makeAsyncIter( - compose(apiCursorNormalizer, fetchRecipesCustomCursor), + apiCursorNormalizer(fetchRecipesCustomCursor), resp => resp.recipes ) ``` +## 在vue2 options api中使用 + + +makeAsyncIter是使用的composition api的风格写法,只不过因为没有钩子和useXXX所以不需要强制与setup同步运行才没有以use开头,这种写法对options api不友好,不能适合直接用需要使用reactive包一层。 +其他的基本一致 + +参考下图 +1. 在js中 + +image + +2. 在模板中 +image + + +[源码位置](https://github.com/xiachufang/jupiter/blob/master/ganymede/src/shared/util/makeAyncIter.ts), 实现和本库的有点微小差别。 + +## 在小程序中使用 +[源码位置](https://github.com/xiachufang/weapp/blob/master/source/lib/asyncIterator.ts),由于小程序和vue完全不同的响应式系统,使用起来有点差别,同时也抛弃了composition api的风格写法改用了class。 +### 最小无限滚动加载收藏的例子 +```ts +Page({ + data: {}, + iter: new AsyncIterator( + cursor => pagedCollectedBoards({ cursor, size: 10 }), + resp => resp.content.cells, + { dataUpdateStrategy: 'merge' } // 无限滚动要保留之前获取的资源所以选择merge + ), + onLoad () { + this.iter.bindPage(this) // 绑定页面,为了在迭代器状态变化时通知页面 + this.iter.next() // 进行首次加载 + }, + onReachBottom () { + this.iter.next() // 滚到底部时继续加载 + } +}) +// next(), reset() , abort() 与vue3版本一致,用法参考vue3版本 +``` +> 模板内存在res,loading,completed3个没写明在data中的变量,在后续部分会介绍 + +```html + + + + + + + + -- 到底了 -- + +``` + +### 在ts/js中获取asyncIter的状态 +直接通过`this.iter.state`来获取,有足够完善的类型推导 +![origin_img_v2_4159e30e-6ae4-4e04-b960-75dd959be45g](https://user-images.githubusercontent.com/25872019/178453985-f36ab0b0-fea5-4a34-9a83-c393b9852124.png) + +### 在wxml中获取asyncIter的状态 +小程序并没有类似vue的响应式值,所有要如何去通知页面更新这块需要单独写,这边使用最简单的回调实现。 +`asyncIter`内部有个值`stateUpdatedCallback`,在`asyncIter`状态变化后将会调用它。 +`asyncIter`的状态包括3种,任意一个改变都会触发回调 +1. loading 迭代器是否处于加载中 +2. completed 迭代器是否加载完成,对应主版本中的load +3. res 迭代后获取到资源 + +#### 通过设置回调来实现状态变化时更新 setStateUpdatedCallback +```ts +this.iter.setStateUpdatedCallback(() => { + this.setData(this.iter.state) // 将会把loading,completed,res隐式的更新到this.data上。即使你没在page.data里面写 +}) // 在模板中 {{res}} {{completed}} 使用 +// 不嫌麻烦也可以 +this.iter.setStateUpdatedCallback(() => { + const { res, laoding, completed } = this.iter.state + this.setData({ list: res, pending: loading, hasMore: !completed }) // list, pending,hasMore为data中写明 +}) // 在模板中 {{list}} {{hasMore}} 使用 +``` +#### 简写方式 bindPage +是`setStateUpdatedCallback`的进一步简化,需要注意的是`setStateUpdatedCallback`和`bingPage`同时只生效一个 +```ts +this.iter.bindPage(this) // 在模板中 {{res}} {{completed}} 使用 +this.iter.bindPage(this, 'recommend') // 在模板中 {{recommend.res}} {{recommend.completed}} 使用 +``` +然后无论是使用`setStateUpdatedCallback`还是`bingPage`,在脚本文件中我都不推荐使用`this.data.res`取获取状态,而是应该`this.iter.state.res`。 +### 如何知道asyncIter引发的界面修改完成时机 +在vue3,vue2中我们直接 +```ts +await iter.next() +await nextTick() +// 现在就已经界面更新完成 +``` +而在小程序中不是nextTick可以通过setData的回调来实现,因此可以这样 +```ts + this.iter.setStateUpdatedCallback(() => { + this.setData(this.iter.state, () => { + // do something + }) +}) +``` + ## 常用场景的使用 直接使用的makeAsyncIter的场景并不多,makeAsyncIter是对分页资源的一种可迭代的抽象。 日常中更多的是使用针对不同场景使用不同的适配,makeAsyncIter与这些的关系有点类似zrender和echarts @@ -135,8 +370,7 @@ const iter = makeAsyncIter( 参考[useAntdListPagination](#useAntdListPagination) ### 无限滚动 参考[useInfiniteScrolling](#useInfiniteScrolling) - -# useInfiniteScrolling +# useInfiniteScrolling 无限滚动 useInfiniteScrolling是针对无限滚动做的一个适配,包含了两种触发模式,探底触发和交叉触发。 ## 探底触发 探底触发适用于整个页面向下滚动,页面滚动到底部达到一定阈值是进行资源迭代,场景例如厨房装备页的滚动到底部加载。 @@ -186,7 +420,7 @@ await hooks.iterationPost?.() ``` -# useAntdListPagination +# useAntdListPagination / GeneralPagination 翻页管理 useAntdListPagination是makeAsyncIter针对翻页做的一个适配,与GeneralPagation组件搭配使用,可以很容易写的出来一个翻页的组件 ## 使用参考 ```ts diff --git a/doc/other.md b/doc/other.md new file mode 100644 index 0000000..2d13e53 --- /dev/null +++ b/doc/other.md @@ -0,0 +1,106 @@ +- [deepComputed](#deepcomputed) + - [主要的使用场景](#主要的使用场景) + - [性能相关](#性能相关) +- [events/typedEventEmitter 类型安全的EventEmitter](#eventstypedeventemitter-类型安全的eventemitter) +- [image/getImageUrl 从下厨房用的图像结构构造url](#imagegetimageurl-从下厨房用的图像结构构造url) +- [assigIncrId 生成一个全局自增id](#assigincrid-生成一个全局自增id) +- [unid/typedID/ID 使用symbol实现的ID生成器](#unidtypedidid-使用symbol实现的id生成器) +- [delay,delayFn 延时,推迟控制流执行](#delaydelayfn-延时推迟控制流执行) +- [promise2ref promsie转成ref](#promise2ref-promsie转成ref) +- [promiseSetRef 在promise完成时设置某个ref](#promisesetref-在promise完成时设置某个ref) +- [momentConvert 一个函数实现下厨房常用的多种时间转换](#momentconvert-一个函数实现下厨房常用的多种时间转换) + +desc: 其余不好分类的函数 +# deepComputed +其概念类似computed函数,使用get获取初始化值,在调用get时收集外部依赖,在外部依赖变化时重新生成自己。.value = xxx时调用set函数。不过computed仅支持最外层的变化,而deepComputed则是支持更深层次的变化。 +改用deepComputed最明显的几个好处则是不需要手动外部数据的变化,不需要在数据修改后各种烦人的dispatch('xxxx',xxx.value)或者ctx.emit('update:xxx',xxx.value)`,在数据变化后自动帮你提交。 +## 主要的使用场景 +例如一个超大型的表单拆分成数个小组件,每个小组件都通过v-model&ctx.emit与父组件通信. +```ts + // 例如是控制表单日期的部分 + const date = deepComputed({ + get: () => props.date, + set: v => ctx.emit('update:date', v) + }) +``` +或者是需要读写vuex表单,官方推荐则是对表单的所有字段写个action commit,而使用deepComputed则不需要这么麻烦,像正常的computed那样就行,并且可以严格保证flux的架构 +```ts +const recipe = deepComputed({ + get: () => { + const { createRecipe } = store.state + return createRecipe.recipe + }, + set: v => dispatch('createRecipe/setRecipe', v) + }) +``` +这两种deepComputed都能保证两处的值在操作后是一致的,因此我感觉可以理解为deepComputed结果的值是对另外一个值部分引用的持有 +## 性能相关 +在性能方面deepComputed,支持set和get时的双向防抖,默认关闭。以及set, get时的clone,默认启用。 +该函数不是简单的watch({deep:true})实现,那样会导致watch和set相互调用栈溢出,而是使用proxy实现,默认监控浅层的object以及持续的array,在vue2中不可用,与vue2的响应式实现冲突。 +![image](https://user-images.githubusercontent.com/25872019/178645084-055e3e18-8514-4df0-b0ef-8aca3015c5f7.png) +
图 替换为deepComputed前后的两种写法,二者等价
+ + +# events/typedEventEmitter 类型安全的EventEmitter +用法和node 的EventEmitter一样,不过新增了类型检查和hook风格管理的监听 useEventListen +```ts +const { eventEmitter, useEventListen } = typedEventEmitter<{ userInfoLoaded: undefined, cancelTask: number }>() +eventEmitter.emit('userInfoLoaded') // ok +eventEmitter.emit('userInfo') // 类型错误 +eventEmitter.emit('cancelTask') // 类型错误 +eventEmitter.emit('cancelTask', 1) // ok +eventEmitter.on('cancelTask', (v: string) => { // 类型错误 + /// +}) +eventEmitter.on('cancelTask', (v) => { // ok + v // 这是一个数字 +}) +useEventListen('userInfoLoaded', () => console.log('userInfoLoaded')) // 和其他hook一样,组件卸载前会清理掉,不用手动删除 +``` +image + +# image/getImageUrl 从下厨房用的图像结构构造url +```ts +const url = getImageUrl(image) +``` +# assigIncrId 生成一个全局自增id +# unid/typedID/ID 使用symbol实现的ID生成器 +使用symbol实现,相较于直接添加数字或者是字符串id,最大的好处是不会污染数据本身,id不会被序列化,key也是迭代不出来。 +适用于需要比较场合,例如v-for的key,如果是回进行数组内部的删除或者是调整顺序那么就不能使用index作为key,这时候就需要ID生成器。当然如果后端有返回id,那直接使用返回的。 + +例如以下的情况,同时编辑多种用料,用料位置可拖拽调整,可新增删除 +```ts +import { ID, unid, UniqueId, typedID, idKey } from 'vue3-ts-util' +interface Ing extends UniqueId { + name: string + amount: numebr +} +const ings: Ing[] = [] +// 正常情况下就这样,可以获取足够完善的类型提示 +const newIng = ID({ name: '胡萝卜', amount: 1 }) +// 但是如果你这样 +const newIng: Ing = ID({ name: '胡萝卜', amount: 1 }) +// 或者这样时,虽然也有类型检测,输入name时也能提示,但是相对于第一种不够好,因为比较的是id的返回值和push函数 +ing.push(ID({ name: '胡萝卜', amount: 1 })) + + +// 那么可以这样,就可以获得足够完善的类型提示 +const ingId = typedID() +const newIng = ingId({ name: '胡萝卜', amount: 1 }) +ing.push(ingId({ name: '胡萝卜', amount: 1 })) + +if (newIng[idKey] !== oldIng[idKey]) { + // 比较可以这样 +} +``` +当然如果对这种方法觉得麻烦直接`obj._id = uniqueId()` +# delay,delayFn 延时,推迟控制流执行 +```ts +await delay(300) +doSomething() // 延迟300ms执行 +``` + +# promise2ref promsie转成ref +# promiseSetRef 在promise完成时设置某个ref +# momentConvert 一个函数实现下厨房常用的多种时间转换 +使用方法见单元测试momentConvert部分 diff --git a/doc/type.md b/doc/type.md new file mode 100644 index 0000000..0ec2a36 --- /dev/null +++ b/doc/type.md @@ -0,0 +1,70 @@ +- [globalComponents](#globalcomponents) +- [DeepReadonly转换一个类型为深度只读](#deepreadonly转换一个类型为深度只读) + - [仅使用类型](#仅使用类型) + - [也可以使用这种方式](#也可以使用这种方式) +- [ok 先验条件断言](#ok-先验条件断言) +- [thruthy 真值断言](#thruthy-真值断言) +- [Columns 描述antd表格结构的类型](#columns-描述antd表格结构的类型) +- [Image 下厨房的图像结构](#image-下厨房的图像结构) +- [WithRequired 将对象部分字段转为不可空](#withrequired-将对象部分字段转为不可空) +- [customPropType vue props用于推导自定义类型的辅助函数,使用interface风格写props](#customproptype-vue-props用于推导自定义类型的辅助函数使用interface风格写props) +desc: 类型及类型推导辅助相关 + +# globalComponents +给volar提给该库组件的类型提示 +# DeepReadonly转换一个类型为深度只读 +## 仅使用类型 +image + +## 也可以使用这种方式 +```ts +function getADeepReadonlyObject() { + return deepReadonly({ + foo: 'caillo' + }) +} +``` +# ok 先验条件断言 +断言除了用于测试外还用于先验条件,即检测接下来代码的执行,如果条件不满足则断言失败抛异常或者退出进程。和异常不同,断言失败只应该发生在开发阶段,如果断言失败那么就是你代码写错了,而异常则可能是由于外界条件的不足。 +将断言用于先验条件这种做法在一些编译型语言上尤其流行,这些断言在debug编译时生效,release编译时又会被移除。 +在ts中除了先验条件外,断言还支持用于控制流的类型推导。 +```ts +ok(typeof next === 'string') +ok(ele) +``` +# thruthy 真值断言 +和非空断言有点像,好处是会更早引发断言失败 +```ts +let numMayBeNull: number | null +doSomething(numMayBeNull!) // 如果函数没校验,可能会随着多次传递而难以debug +doSomething(thruthy(numMayBeNull)) // 函数未调用就断言失败 +``` +# Columns 描述antd表格结构的类型 +主要是用于推导antd表格,方便在ts里描述antd表格的行列 +image +image + +# Image 下厨房的图像结构 +# WithRequired 将对象部分字段转为不可空 + +```ts + +type AllOptional = { + str?: string; + num?: number; + bool?: boolean +} + +type SomeOptional = WithRequired +``` +# customPropType vue props用于推导自定义类型的辅助函数,使用interface风格写props +适用于vue2/3,例子直接看本库的组件就行,如果你使用vue3的setup我更推荐使用[defineProps](https://v3.cn.vuejs.org/api/sfc-script-setup.html#defineprops-%E5%92%8C-defineemits),这个只是不能直接书写props interface时的一个workaround +```ts +props: { + seg: customPropsType(), // 非空,自定义接口 + getState: customPropsType<(id: number) => ArchState>(false), // 可空,函数 + enable: customPropsType(false, Boolean) // 可空,运行时类型检查, + direction: customPropType((): 'vertical' | 'horizontal' => 'horizontal') // 默认参数 + } +``` +image diff --git a/doc/vue3components.md b/doc/vue3components.md new file mode 100644 index 0000000..481bd87 --- /dev/null +++ b/doc/vue3components.md @@ -0,0 +1,64 @@ +- [GeneralPagination 翻页器和相关hook](#generalpagination-翻页器和相关hook) +- [SplitView 支持鼠标拖拽调整的视图分割](#splitview-支持鼠标拖拽调整的视图分割) + - [props](#props) + - [例子](#例子) +- [SearchSelect 支持搜索的选择,追求尽可能少的代码来描述](#searchselect-支持搜索的选择追求尽可能少的代码来描述) + - [props](#props-1) + - [例子](#例子-1) +desc: 本库的vue3组件 + +记得先全局导入,或者单独导入 +```ts +import { SearchSelect, SplitView, GeneralPagination } from 'vue3-ts-util' + +const app = createApp(App) +Object.entries({ + SplitView, + GeneralPagination, + SpinSection +}).forEach(args => app.component(...args)) +``` +# GeneralPagination 翻页器和相关hook +具体见[io的相关部分](./io.md#useantdlistpagination--generalpagination-翻页管理) +# SplitView 支持鼠标拖拽调整的视图分割 +本库只实现了左右分割,需要上下分割,我在这里实现了[vue-ts-util-lite](https://github.com/zanllp/vue3-ts-util-lite/blob/main/src/SplitView/index.vue) +## props +percent: 左边的视图的占比,number,默认50 +border: bool 一个浅灰色的框用来区分视图,默认关闭 +## 例子 +```html + + + + +``` + +# SearchSelect 支持搜索的选择,追求尽可能少的代码来描述 +1. 支持按照输入来搜索,关键字将会使用红色表明并按照出现顺序排序 +2. 支持虚拟列表 +3. 支持多选 +4. 把转换移到了ts,尽可能减少繁琐的模板,增加类型推导,还可以直接闭包 + +![iShot2022-07-14 17 32 11](https://user-images.githubusercontent.com/25872019/178951654-a1258dac-3084-43bd-bed7-c093c6749935.gif) +## props +options 选项数组 +conv 定义如何从选项数组转换到值以及选项的文本,key回填时显示的文本,具体见SearchSelectConv +value v-model的值,如果为多选类型则为array,否则是conv.value的返回类型 +mode 模式 多选的话multipie,单选不需要写 +## 例子 +```ts +const options = '黄瓜,土豆,胡萝卜,西红柿,茄子'.split(',').map((name,idx) => ({ id: idx + 1, name })) +const conv: SearchSelectConv<{ id: number; name: string }> = { + text: v => v.name, + value: v => v.id + // 还有optionText, key 可空 +} +const selectedID = ref() +``` +```html + +``` diff --git a/doc/vuex.md b/doc/vuex.md new file mode 100644 index 0000000..039e3b8 --- /dev/null +++ b/doc/vuex.md @@ -0,0 +1,75 @@ +- [mutation 生成mutation函数的辅助函数](#mutation-生成mutation函数的辅助函数) +- [VuexPersistence 用于持久化的vuex插件](#vuexpersistence-用于持久化的vuex插件) + - [feature](#feature) + - [最小化例子](#最小化例子) + +desc: vuex相关的 +# mutation 生成mutation函数的辅助函数 +```ts +const mutationSetter = mutation() + +const mutations = { + setTagList: mutationSetter('tagList') // 将会提供所有的key给你选择 +} + +// 等价于 + +const mutations = { + setTagList(state: AnnotationState, tagList: Tag[]) { + state.tagList = tagList + } +} +``` + +# VuexPersistence 用于持久化的vuex插件 +## feature +1. 监控mutations的commit并持久化 +2. 支持自定义序列化和反序列化 +3. 支持设置过期时间 +4. 支持设置名称空间,对微前端友好 +5. 由vuex-dispatch-infer提供类型推导,从mutation type中直接推存储键名,不需要再去思考名字 +## 最小化例子 +```ts +// src/store/app.ts +const state = (): AppState => ({ + isSuperUser: persistence.get('app/setSu').or(false) // 从持久化中获取,如果没找到则使用false +}) + +const mutationSetter = mutation() + +const mutations = { + setSu: mutationSetter('isSuperUser') +} + +export default { + namespaced: true, + state, + mutations +} + + +// src/store/index.ts +import app from './app.ts' +import { GetOverloadDict } from 'vuex-dispatch-infer' + +type StoreParams = { modules: { app: typeof app } } +type Mutations = keyof GetOverloadDict +export const persistence = new VuexPersistence('ciallo') + +export const store = createStore({ + modules: { + app + }, + plugins: [persistence.watch([ + 'app/setSu', // 最简单的用法,不过期,使用json进行序列化和反序列化,名字会帮你推出来 + { // 需要自定义可以用object的选项来替代上面的那个 + type: 'app/setSu', // type是必要的,其他的是可选的 + expire: moment.duration(1, 'h'), // 下次打开页面时如果距离上次修改超过一小时会删除 + serialize: v => v ? '1' : '', // 自定义序列化方法 + deserialize: str => !!str + } + ])] +}) + +store.commit('app/setSu', true) // 在每次commit时会保存到本地,下次打开时恢复 +``` diff --git a/package.json b/package.json index ddba025..d2fab86 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "build": "yarn clean && yarn rollup -c", "clean": "rm -rf ./dist ./es", "dev-watch": "ts-node --project tsnode.config.json scripts/index.ts --dev-watch", + "gen-contents": "ts-node --project tsnode.config.json scripts/index.ts --gen-contents", "import-optimize": "ts-node --project tsnode.config.json scripts/index.ts --import-optimize", "pre-release": "yarn build && yarn import-optimize && yarn test && nrm use npm", "test": "yarn jest", diff --git a/scripts/genContents.ts b/scripts/genContents.ts new file mode 100644 index 0000000..7166ac3 --- /dev/null +++ b/scripts/genContents.ts @@ -0,0 +1,40 @@ +import * as fs from 'fs/promises' +import path from 'path' + +/** + * 从doc文件夹下的文件的目录生成总的目录 + * 文件目录由markdown in one生成 + * https://user-images.githubusercontent.com/25872019/179443451-6c974bf3-18d8-463f-a4df-1dcb0f787086.png + */ +const reg = /(.+)/s +const readmeFilename = './README.md' +export const genContents = async () => { + const docProfiles = new Array<{ contents: string, desc: string, fileName: string }>() + const files = await fs.readdir('./doc') + for (const fileName of files) { + const file = (await fs.readFile(path.join('./doc', fileName))).toString() + const [_, contents, desc] = /^((?:.|\n)*)?desc:(.*)?\n/.exec(file)! + docProfiles.push({ + contents: contents + .trim() + .split('\n') + .map(line => line.replace(/]\((.*)\)/, (_, url) => `](./doc/${fileName}` + url + ')')) + .join('\n'), + desc, + fileName + }) + } + const readme = (await fs.readFile(readmeFilename)).toString() + const buildGeneralDoc = () => { + return docProfiles.map((doc) => { + return `## ${doc.desc} +${doc.contents} +` + }).join('\n') + } + const newReadme = readme.replace(reg, +` +${buildGeneralDoc()} +`) + await fs.writeFile(readmeFilename, newReadme) +} diff --git a/scripts/index.ts b/scripts/index.ts index 37c0249..bf9feb2 100644 --- a/scripts/index.ts +++ b/scripts/index.ts @@ -1,6 +1,7 @@ import { importOptimize } from './importOptimize' import { startGenVueType } from './generateVueType' import { devWatch } from './watch' +import { genContents } from './genContents' const { argv } = process const type = argv?.[2].substr(2) @@ -14,4 +15,7 @@ switch (type) { case 'dev-watch': devWatch() break + case 'gen-contents': + genContents() + break } diff --git a/src/SearchSelect/typedef.ts b/src/SearchSelect/typedef.ts index 556e8e2..3a1eb5d 100644 --- a/src/SearchSelect/typedef.ts +++ b/src/SearchSelect/typedef.ts @@ -33,5 +33,6 @@ export interface Props { value: unknown options: any[] conv: SearchSelectConv + mode?: 'multipie' } diff --git a/src/makeAsyncIterator.ts b/src/makeAsyncIterator.ts index 2ddcb72..1316d77 100644 --- a/src/makeAsyncIterator.ts +++ b/src/makeAsyncIterator.ts @@ -17,6 +17,7 @@ export type ResetParams = { /** * 创建异步迭代器 + * https://github.com/xiachufang/vue3-ts-util/blob/main/doc/io.md#makeasynciter * 分页资源获取,不需要手动管理cursor的迭代 * @param resFetch 资源获取函数 * @param resp2res 响应体转获取额资源 diff --git a/src/useResizable.ts b/src/useResizable.ts index cc280f6..f8281c3 100644 --- a/src/useResizable.ts +++ b/src/useResizable.ts @@ -10,6 +10,13 @@ interface Point { } const twoPointDistance = (a: Point, b: Point) => Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)) +/** + * 用于鼠标拖拽调整调整某个元素的大小位置 + * @param ele 控制的对象 + * @param initRect 初始大小 + * @param triggerWidth 触发可重置的范围大小 + * @returns + */ export const useResizable = (ele: Ref, initRect: { width: number, height: number; x: number; y: number }, triggerWidth = 50) => { const rect = reactive(initRect) const pressed = ref(false)