├── .eslintignore ├── __test__ ├── utils │ ├── types.ts │ └── index.ts ├── setup │ ├── setupComposition.ts │ └── setupTest.ts ├── useScroll.spec.ts ├── shared │ └── utils.spec.ts ├── useDebounceRef.spec.ts ├── useThrottleRef.spec.ts ├── useThrottleFn.spec.ts ├── useDebounceFn.spec.ts ├── useVirtualList.spec.ts ├── useBoolean.spec.ts ├── useSku.spec.ts ├── useHistoryTravel.spec.ts ├── useLocalStorage.spec.ts ├── useStorage.spec.ts ├── useSessionStorage.spec.ts ├── useDrop.spec.ts ├── useClickOutside.spec.ts └── useRequest.spec.ts ├── docs ├── .vuepress │ ├── mock │ │ ├── index.js │ │ ├── request.js │ │ └── sku.js │ ├── public │ │ └── imgs │ │ │ └── logo.png │ ├── enhanceApp.js │ ├── components │ │ ├── useScroll.vue │ │ ├── useDebounceRef.vue │ │ ├── useThrottleRef.vue │ │ ├── useEventlistener.vue │ │ ├── useLocalStorage.vue │ │ ├── useClickOutside.vue │ │ ├── useDebounceFn.vue │ │ ├── useThrottleFn.vue │ │ ├── useSessionStorage.vue │ │ ├── requestBase.vue │ │ ├── useBoolean.vue │ │ ├── useSku.vue │ │ ├── useSkuCustom.vue │ │ ├── useVirtualList.vue │ │ ├── useDrop.vue │ │ └── useHistoryTravel.vue │ ├── styles │ │ └── index.styl │ └── config.js ├── info │ └── index.md ├── sideEffect │ ├── useDebounceFn.md │ ├── useThrottleFn.md │ ├── useThrottleRef.md │ └── useDebounceRef.md ├── event │ ├── useClickOutside.md │ ├── useScroll.md │ └── useEventlistener.md ├── state │ ├── useLocalStorage.md │ ├── useSessionStorage.md │ ├── useHistoryTravel.md │ └── useBoolean.md ├── README.md ├── UI │ ├── useVirtualList.md │ └── useDrop.md ├── work │ └── useSku.md └── async │ └── useRequest.md ├── src ├── useDrop │ ├── index.ts │ ├── useDrag.ts │ └── useDrop.ts ├── shared │ ├── type.ts │ └── utils.ts ├── useRequest │ ├── utils │ │ ├── fetchProxy.ts │ │ ├── cache.ts │ │ ├── types.ts │ │ └── fetch.ts │ ├── index.ts │ └── useAsync.ts ├── index.ts ├── useDebounceFn │ └── index.ts ├── useThrottleRef │ └── index.ts ├── useThrottleFn │ └── index.ts ├── useDebounceRef │ └── index.ts ├── useClickOutside │ └── index.ts ├── useSku │ └── index.ts ├── useLocalStorage │ └── index.ts ├── useSessionStorage │ └── index.ts ├── useBoolean │ └── index.ts ├── useEventListener │ └── index.ts ├── useScroll │ └── index.ts ├── useStorage │ └── index.ts ├── useHistoryTravel │ └── index.ts └── useVirtualList │ └── index.ts ├── commitlint.config.js ├── .prettierrc.js ├── babel.config.js ├── .travis.yml ├── .github ├── build-ci.yml ├── workflows │ ├── lint-test-ci.yml │ ├── codecov-ci.yml │ └── deploy-doc.yml ├── API.md ├── CONTRIBUTING.zh-CN.md ├── gitflows.md └── dev.md ├── tsconfig.json ├── jest.config.js ├── LICENSE ├── scripts ├── build.js └── release.js ├── .eslintrc.js ├── README.md ├── .gitignore ├── rollup.config.js ├── package.json └── CHANGELOG.md /.eslintignore: -------------------------------------------------------------------------------- 1 | **/dist/** 2 | 3 | **/node_modules/** -------------------------------------------------------------------------------- /__test__/utils/types.ts: -------------------------------------------------------------------------------- 1 | export type noop = (...args: any[]) => any 2 | -------------------------------------------------------------------------------- /docs/.vuepress/mock/index.js: -------------------------------------------------------------------------------- 1 | export * from './sku' 2 | export * from './request' -------------------------------------------------------------------------------- /src/useDrop/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useDrag' 2 | export * from './useDrop' 3 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | } 4 | -------------------------------------------------------------------------------- /docs/.vuepress/public/imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuasir/vue-reuse/HEAD/docs/.vuepress/public/imgs/logo.png -------------------------------------------------------------------------------- /src/shared/type.ts: -------------------------------------------------------------------------------- 1 | export type BasicType = string | number | boolean 2 | 3 | export type QuoteType = Record | any[] 4 | -------------------------------------------------------------------------------- /__test__/setup/setupComposition.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueComposition from '@vue/composition-api' 3 | 4 | Vue.use(VueComposition) 5 | -------------------------------------------------------------------------------- /docs/.vuepress/enhanceApp.js: -------------------------------------------------------------------------------- 1 | import VueCompositionAPI from '@vue/composition-api' 2 | 3 | export default ({ Vue }) => { 4 | Vue.use(VueCompositionAPI) 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | useTabs: false, 4 | tabWidth: 2, 5 | semi: false, 6 | singleQuote: true, 7 | trailingComma: 'none' 8 | } 9 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript', 5 | ], 6 | plugins: [ 7 | '@babel/plugin-proposal-optional-chaining' 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '12' 4 | 5 | cache: 6 | yarn: true 7 | 8 | install: 9 | - yarn 10 | 11 | script: 12 | - yarn docs:build 13 | - yarn build 14 | 15 | branches: 16 | only: 17 | - master 18 | - feat 19 | -------------------------------------------------------------------------------- /src/useRequest/utils/fetchProxy.ts: -------------------------------------------------------------------------------- 1 | export function fetchProxy( 2 | ...args: [RequestInfo, RequestInit | undefined] 3 | ): Promise { 4 | return fetch(...args).then((res: Response) => { 5 | if (res.ok) { 6 | return res.json() 7 | } 8 | throw new Error(res.statusText) 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /docs/.vuepress/mock/request.js: -------------------------------------------------------------------------------- 1 | export const service = (p) => 2 | new Promise((resolve) => { 3 | setTimeout(() => { 4 | resolve(p) 5 | }, 2000) 6 | }) 7 | 8 | export const serviceError = (p) => 9 | new Promise < ((_, reject) => { 10 | setTimeout(() => { 11 | reject(p) 12 | }, 2000) 13 | }) -------------------------------------------------------------------------------- /docs/info/index.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 3 | `vue-reuse`是一款基于`Vue-Composition-API`的逻辑复用函数库,它致力于拆分业务中可能出现的可复用逻辑。 4 | 5 | > 在开始之前,您可能需要熟悉`Composition API`的基本使用,访问[链接](https://composition-api.vuejs.org/zh/)即可开始`Vue3 Composition API`的学习。 6 | 7 | ### 按需引用 8 | 9 | 打包后的库支持`ESM`的模式导出,您可以直接像如下的引用,以达到`Tree Shaking`的效果. 10 | 11 | ```javascript 12 | import { useXXX } from '@xus/vue-reuse' 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/.vuepress/components/useScroll.vue: -------------------------------------------------------------------------------- 1 | 7 | 19 | -------------------------------------------------------------------------------- /src/shared/utils.ts: -------------------------------------------------------------------------------- 1 | export const isBrowser = typeof window !== 'undefined' 2 | 3 | export function createStorage(): Storage { 4 | let state: Record = {} 5 | return { 6 | getItem: (x) => state[x], 7 | setItem: (x, v) => (state[x] = v), 8 | removeItem: (x) => delete state[x], 9 | clear: () => (state = {}), 10 | length: Object.keys(state).length, 11 | key: (index) => Object.keys(state)[index] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /__test__/useScroll.spec.ts: -------------------------------------------------------------------------------- 1 | import { renderComposable } from './utils' 2 | import { useScroll } from '../src/useScroll' 3 | 4 | describe('test use scroll ', () => { 5 | test('can be render ', async () => { 6 | const { vm } = renderComposable(() => { 7 | const [pos] = useScroll(document.body) 8 | return { 9 | pos 10 | } 11 | }) 12 | await vm.$nextTick() 13 | expect(vm.$data.pos.x).toBe(0) 14 | expect(vm.$data.pos.y).toBe(0) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /docs/.vuepress/components/useDebounceRef.vue: -------------------------------------------------------------------------------- 1 | 7 | 19 | -------------------------------------------------------------------------------- /__test__/shared/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStorage } from '../../src/shared/utils' 2 | 3 | describe('test utils ', () => { 4 | test(`test createStorage`, () => { 5 | const store = createStorage() 6 | expect(store.length).toBe(0) 7 | store.setItem('1', '1') 8 | expect(store.getItem('1')).toBe('1') 9 | store.setItem('2', '2') 10 | expect(store.key(1)).toBe('2') 11 | store.removeItem('1') 12 | expect(store.key(0)).toBe('2') 13 | store.clear() 14 | expect(store.length).toBe(0) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /__test__/useDebounceRef.spec.ts: -------------------------------------------------------------------------------- 1 | import { useDebounceRef } from '../src' 2 | 3 | type noop = (...args: any[]) => any 4 | 5 | describe('test useDebounceRef', () => { 6 | const nextTask = (fn: noop, time: number) => setTimeout(fn, time) 7 | test('ref will be debounce', () => { 8 | const debounceRef = useDebounceRef(0, 500) 9 | debounceRef.value = 1 10 | debounceRef.value = 2 11 | expect(debounceRef.value).toBe(0) 12 | nextTask(() => { 13 | expect(debounceRef.value).toBe(2) 14 | }, 550) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useSku' 2 | export * from './useDebounceFn' 3 | export * from './useDebounceRef' 4 | export * from './useThrottleFn' 5 | export * from './useThrottleRef' 6 | export * from './useHistoryTravel' 7 | export * from './useLocalStorage' 8 | export * from './useSessionStorage' 9 | export * from './useEventListener' 10 | export * from './useClickOutside' 11 | export * from './useScroll' 12 | export * from './useDrop' 13 | export * from './useVirtualList' 14 | export * from './useBoolean' 15 | export * from './useRequest' 16 | -------------------------------------------------------------------------------- /docs/.vuepress/components/useThrottleRef.vue: -------------------------------------------------------------------------------- 1 | 7 | 20 | -------------------------------------------------------------------------------- /docs/.vuepress/components/useEventlistener.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | -------------------------------------------------------------------------------- /docs/.vuepress/components/useLocalStorage.vue: -------------------------------------------------------------------------------- 1 | 7 | 24 | -------------------------------------------------------------------------------- /docs/.vuepress/components/useClickOutside.vue: -------------------------------------------------------------------------------- 1 | 7 | 24 | -------------------------------------------------------------------------------- /docs/.vuepress/components/useDebounceFn.vue: -------------------------------------------------------------------------------- 1 | 7 | 24 | -------------------------------------------------------------------------------- /docs/.vuepress/components/useThrottleFn.vue: -------------------------------------------------------------------------------- 1 | 7 | 24 | -------------------------------------------------------------------------------- /docs/.vuepress/components/useSessionStorage.vue: -------------------------------------------------------------------------------- 1 | 7 | 24 | -------------------------------------------------------------------------------- /src/useDebounceFn/index.ts: -------------------------------------------------------------------------------- 1 | type ReturnValue = { 2 | run(...args: T): any 3 | cancel(): void 4 | } 5 | 6 | export function useDebounceFn( 7 | fn: (...args: T) => any, 8 | wait = 0 9 | ): ReturnValue { 10 | let timer: any = null 11 | 12 | function run(...args: T) { 13 | cancel() 14 | timer = setTimeout(() => { 15 | fn(...args) 16 | }, wait) 17 | } 18 | 19 | function cancel() { 20 | if (timer) { 21 | clearTimeout(timer) 22 | } 23 | } 24 | 25 | return { 26 | run, 27 | cancel 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/useThrottleRef/index.ts: -------------------------------------------------------------------------------- 1 | import { Ref, customRef } from 'vue-demi' 2 | 3 | export function useThrottleRef(value: T, wait = 0): Ref { 4 | let rawValue = value 5 | let timer: any = null 6 | return customRef((track, trigegr) => { 7 | return { 8 | get() { 9 | track() 10 | return rawValue 11 | }, 12 | set(val) { 13 | if (!timer) { 14 | rawValue = val 15 | trigegr() 16 | timer = setTimeout(() => { 17 | timer = null 18 | }, wait) 19 | } 20 | } 21 | } 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /__test__/useThrottleRef.spec.ts: -------------------------------------------------------------------------------- 1 | import { useThrottleRef } from '../src' 2 | 3 | type noop = (...args: any[]) => any 4 | 5 | describe('test useThrottleRef', () => { 6 | const nextTask = (fn: noop, wait: number) => setTimeout(fn, wait) 7 | 8 | test('should be run first time when run', () => { 9 | const throttleValue = useThrottleRef(0, 500) 10 | throttleValue.value = 1 11 | throttleValue.value = 2 12 | throttleValue.value = 3 13 | expect(throttleValue.value).toBe(1) 14 | nextTask(() => { 15 | expect(throttleValue.value).toBe(1) 16 | }, 550) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /docs/.vuepress/styles/index.styl: -------------------------------------------------------------------------------- 1 | button { 2 | padding 5px 10px 3 | color black 4 | background-color #fff 5 | border 1px solid #3eaf7c 6 | border-radius 4px 7 | outline none 8 | cursor pointer 9 | } 10 | 11 | .block { 12 | display block 13 | padding 10px 14 | } 15 | 16 | input { 17 | width 300px 18 | outline none 19 | border 1px solid #3eaf7c 20 | border-radius 4px 21 | padding 5px 10px 22 | margin 5px 23 | } 24 | 25 | .wrap { 26 | display flex 27 | } 28 | .right { 29 | display inline-flex 30 | flex 1 31 | justify-content center 32 | align-items center 33 | } -------------------------------------------------------------------------------- /src/useThrottleFn/index.ts: -------------------------------------------------------------------------------- 1 | type ReturnValue = { 2 | run(...args: T[]): void 3 | cancel(): void 4 | } 5 | 6 | export function useThrottleFn( 7 | fn: (...args: T[]) => any, 8 | wait = 0 9 | ): ReturnValue { 10 | let time: any = null 11 | function run(...args: T[]) { 12 | if (!time) { 13 | time = setTimeout(() => { 14 | fn(...args) 15 | time = null 16 | }, wait) 17 | } 18 | } 19 | 20 | function cancel() { 21 | if (time) { 22 | clearTimeout(time) 23 | } 24 | } 25 | 26 | return { 27 | run, 28 | cancel 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/build-ci.yml: -------------------------------------------------------------------------------- 1 | name: Build CI 2 | 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | 7 | strategy: 8 | matrix: 9 | node-version: [15.x] 10 | 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: Use Node.js ${{ matrix.node-version }} 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - name: yarn intall, build 18 | run: | 19 | npm install yarn -g 20 | yarn 21 | yarn build 22 | yarn docs:build 23 | env: 24 | CI: true 25 | -------------------------------------------------------------------------------- /docs/.vuepress/components/requestBase.vue: -------------------------------------------------------------------------------- 1 | 7 | 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/", "__test__/"], 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "sourceMap": true, 10 | "removeComments": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "checkJs": false, 14 | "allowJs": false, 15 | "rootDir": "./", 16 | "baseUrl": "./", 17 | "paths": { 18 | "@xus/vue-reuse": ["src/"] 19 | }, 20 | "skipLibCheck": true, 21 | "forceConsistentCasingInFileNames": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/useDebounceRef/index.ts: -------------------------------------------------------------------------------- 1 | import { Ref, customRef } from 'vue-demi' 2 | 3 | export function useDebounceRef(value: T, wait = 0): Ref { 4 | let rawValue = value 5 | let timer: any = null 6 | function clear() { 7 | if (timer) { 8 | clearTimeout(timer) 9 | } 10 | } 11 | return customRef((track, trigger) => { 12 | return { 13 | get() { 14 | track() 15 | return rawValue 16 | }, 17 | set(val) { 18 | clear() 19 | timer = setTimeout(() => { 20 | rawValue = val 21 | trigger() 22 | }, wait) 23 | } 24 | } 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /docs/sideEffect/useDebounceFn.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 一个帮你处理生产一个`debounce`过的代理新`function` 3 | 4 | ## 代码演示 5 | #### 基本使用 6 | 7 | #### 代码 8 | ::: details 点击查看代码 9 | <<< @/docs/.vuepress/components/useDebounceFn.vue 10 | ::: 11 | 12 | 13 | ## API 14 | ```ts 15 | const { run, cancel } = useDebounceFn(func, wait): 16 | ``` 17 | 18 | #### RetrunValue 19 | | 参数 | 说明 | 类型 | 20 | | --- | --- | --- | 21 | | `run` | 代理原函数的新`debounce`函数 | `function` | 22 | | `cancel` | 在`wait`时间到达之前,提供一个取消当前`run`的方法 | `function` | 23 | 24 | #### Params 25 | | 参数 | 说明 | 类型 | 26 | | --- | --- | --- | 27 | | `func` | 原始的函数 | `function` | 28 | | `wait` | debounce的时长 | `number` | 29 | -------------------------------------------------------------------------------- /docs/sideEffect/useThrottleFn.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 一个帮你处理生产一个`throttle`过的代理新`function` 3 | 4 | ## 代码演示 5 | #### 基本使用 6 | 7 | #### 代码 8 | ::: details 点击查看代码 9 | <<< @/docs/.vuepress/components/useThrottleFn.vue 10 | ::: 11 | 12 | 13 | ## API 14 | ```ts 15 | const { run, cancel } = useThrottleFn(func, wait): 16 | ``` 17 | 18 | #### RetrunValue 19 | | 参数 | 说明 | 类型 | 20 | | --- | --- | --- | 21 | | `run` | 代理原函数的新`throttle`函数 | `function` | 22 | | `cancel` | 在`wait`时间到达之前,提供一个取消当前`run`的方法 | `function` | 23 | 24 | #### Params 25 | | 参数 | 说明 | 类型 | 26 | | --- | --- | --- | 27 | | `func` | 原始的函数 | `function` | 28 | | `wait` | throttle的时长 | `number` | 29 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | preset: 'ts-jest', 5 | rootDir: __dirname, 6 | globals: { 7 | __VUE2__: false, 8 | }, 9 | collectCoverage: true, 10 | collectCoverageFrom: ['/src/**/**.ts'], 11 | coverageDirectory: path.resolve(__dirname, 'coverage'), 12 | // coverageReporters: ['html', 'text'], 13 | moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], 14 | watchPathIgnorePatterns: ['node_modules'], 15 | testMatch: ['/__test__/**/*spec.[jt]s?(x)'], 16 | setupFiles: [ 17 | '/__test__/setup/setupComposition.ts', 18 | '/__test__/setup/setupTest.ts', 19 | ], 20 | } 21 | -------------------------------------------------------------------------------- /src/useClickOutside/index.ts: -------------------------------------------------------------------------------- 1 | import { ref, Ref } from 'vue-demi' 2 | import { useEventListener } from '../useEventListener' 3 | type DomParam = HTMLElement | (() => HTMLElement) 4 | 5 | export function useClickOutside( 6 | callback: (event: MouseEvent) => void, 7 | dom?: DomParam 8 | ): Ref { 9 | const element = ref(null) 10 | const handler = (event: MouseEvent) => { 11 | const targetElement = typeof dom === 'function' ? dom() : dom 12 | const el = targetElement || element.value 13 | if (!el || el.contains(event.target as Node)) { 14 | return 15 | } 16 | callback(event) 17 | } 18 | useEventListener('click', handler, { capture: false }) 19 | return element 20 | } 21 | -------------------------------------------------------------------------------- /docs/event/useClickOutside.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 一个帮你处理点击事件发生在指定元素外的`hooks` 3 | > 1. 可自动在组件卸载时清除事件监听 4 | > 2. 可直接采用`ref`方式监听`Dom`事件 5 | 6 | ## 代码演示 7 | #### 基本使用 8 | 9 | #### 代码 10 | ::: details 点击查看代码 11 | <<< @/docs/.vuepress/components/useClickOutside.vue 12 | ::: 13 | 14 | 15 | ## API 16 | ```ts 17 | 18 | 19 | const elRef = useClickOutside(handler, dom?) 20 | ``` 21 | 22 | #### RetrunValue 23 | | 参数 | 说明 | 类型 | 24 | | --- | --- | --- | 25 | | `elRef` | 一个指定事件绑定元素的`ref`对象 | `Ref` | 26 | 27 | #### Params 28 | | 参数 | 说明 | 类型 | 29 | | --- | --- | --- | 30 | | `handler` | 监听事件的回调函数 | `(evt: MouseEvent) => any` | 31 | | `dom` | 需要检测的`Dom` | `HTMLElement | (() => HTMLElement)` | 32 | -------------------------------------------------------------------------------- /src/useSku/index.ts: -------------------------------------------------------------------------------- 1 | import { shallowRef, Ref } from 'vue-demi' 2 | import { 3 | createSkuSelector, 4 | SpecInstanceType, 5 | SpecLineInstanceType, 6 | SpuOps 7 | } from '@xuguo/sku' 8 | 9 | type SKU = { 10 | specTap(spec: SpecInstanceType): void 11 | skuList: Ref 12 | } 13 | 14 | type SpuList = { 15 | [key: string]: T[] | any 16 | } 17 | 18 | export function useSku(spu: SpuList, spuOps?: Partial): SKU { 19 | const judger = createSkuSelector(spu, spuOps) 20 | const skuList = shallowRef(judger.specGroup.specLines) 21 | const specTap = (spec: any) => { 22 | judger.specTap(spec) 23 | skuList.value = judger.specGroup.specLines.concat() 24 | } 25 | return { 26 | specTap, 27 | skuList 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/lint-test-ci.yml: -------------------------------------------------------------------------------- 1 | name: Lint Test CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - feat 8 | pull_request: 9 | branches: 10 | - master 11 | - feat 12 | 13 | jobs: 14 | lint: 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [15.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v1 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - name: yarn intall, lint, and test 28 | run: | 29 | npm install yarn -g 30 | yarn 31 | yarn lint 32 | yarn test 33 | env: 34 | CI: true 35 | -------------------------------------------------------------------------------- /.github/workflows/codecov-ci.yml: -------------------------------------------------------------------------------- 1 | name: Codecov CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - feat 8 | pull_request: 9 | branches: 10 | - master 11 | - feat 12 | 13 | jobs: 14 | codecov: 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [15.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v1 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - name: codecov 28 | run: | 29 | npm install yarn -g 30 | yarn 31 | yarn test --coverage 32 | bash <(curl -s https://codecov.io/bash) 33 | env: 34 | CI: true 35 | -------------------------------------------------------------------------------- /docs/state/useLocalStorage.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 一个帮你处理将数据响应式持久化到`localStorage`中的`hooks` 3 | > 1. 可自动在组件卸载时清除事件监听 4 | > 2. 以`ref`形态返回数据 5 | > 3. 以正常对`ref`访问方式即可自动完成持久化数据 6 | 7 | ## 代码演示 8 | #### 基本使用 9 | ::: tip 10 | 1. 数据将会持久化的存储在`localstorage`,刷新页面依旧能保持 11 | 2. 将`num`设置成`undefined`时会自动清除`localstorage`的存储 12 | ::: 13 | 14 | #### 代码 15 | ::: details 点击查看代码 16 | <<< @/docs/.vuepress/components/useLocalStorage.vue 17 | ::: 18 | 19 | 20 | ## API 21 | ```ts 22 | const valueRef = useLocalStorage(key, initialValue) 23 | ``` 24 | 25 | #### RetrunValue 26 | | 参数 | 说明 | 类型 | 27 | | --- | --- | --- | 28 | | `valueRef` | 响应式的数据对象 | `Ref` | 29 | 30 | #### Params 31 | | 参数 | 说明 | 类型 | 32 | | --- | --- | --- | 33 | | `key` | 存储的键值 | `string` | 34 | | `initialValue` | 初始值 | `T` | 35 | -------------------------------------------------------------------------------- /docs/state/useSessionStorage.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 一个帮你处理将数据响应式持久化到`sessionStorage`中的`hooks` 3 | > 1. 可自动在组件卸载时清除事件监听 4 | > 2. 以`ref`形态返回数据 5 | > 3. 以正常对`ref`访问方式即可自动完成持久化数据 6 | 7 | ## 代码演示 8 | #### 基本使用 9 | ::: tip 10 | 1. 数据将会持久化的存储在`sessionStorage` 11 | 2. 将`num`设置成`undefined`时会自动清除`sessionStorage`的存储 12 | ::: 13 | 14 | #### 代码 15 | ::: details 点击查看代码 16 | <<< @/docs/.vuepress/components/useSessionStorage.vue 17 | ::: 18 | 19 | 20 | ## API 21 | ```ts 22 | const valueRef = useSessionStorage(key, initialValue) 23 | ``` 24 | 25 | #### RetrunValue 26 | | 参数 | 说明 | 类型 | 27 | | --- | --- | --- | 28 | | `valueRef` | 响应式的数据对象 | `Ref` | 29 | 30 | #### Params 31 | | 参数 | 说明 | 类型 | 32 | | --- | --- | --- | 33 | | `key` | 存储的键值 | `string` | 34 | | `initialValue` | 初始值 | `T` | 35 | -------------------------------------------------------------------------------- /__test__/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { mount, Wrapper } from '@vue/test-utils' 2 | import { noop } from './types' 3 | 4 | export function renderComposable( 5 | cb: () => any, 6 | attachTo?: Element | string 7 | ): Wrapper { 8 | return mount( 9 | { 10 | setup() { 11 | return cb() 12 | }, 13 | render(h) { 14 | return h('div') 15 | } 16 | }, 17 | { attachTo } 18 | ) 19 | } 20 | 21 | export function nextTask(fn: noop, wait = 0): void { 22 | setTimeout(fn, wait) 23 | } 24 | 25 | export function waitTime(time = 0): Promise { 26 | return new Promise((resolve) => { 27 | setTimeout(() => { 28 | resolve() 29 | }, time) 30 | }) 31 | } 32 | 33 | export const nextTick: () => Promise = () => Promise.resolve().then() 34 | 35 | export * from './types' 36 | -------------------------------------------------------------------------------- /docs/event/useScroll.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 一个帮你处理`scroll`事件的`hooks` 3 | > 1. 可自动在组件卸载时清除事件监听 4 | > 2. 可直接采用`ref`方式监听`Dom`事件 5 | > 3. 返回响应式的`pos`位置信息 6 | 7 | ## 代码演示 8 | #### 基本使用 9 | 10 | #### 代码 11 | ::: details 点击查看代码 12 | <<< @/docs/.vuepress/components/useScroll.vue 13 | ::: 14 | 15 | 16 | ## API 17 | ```ts 18 |
scroll X: {{ pos.x }}
19 |
scroll Y: {{ pos.y }}
20 | 21 | const [pos, elRef] = useScroll() 22 | const [pos] = useScroll(dom?) 23 | ``` 24 | 25 | #### RetrunValue 26 | | 参数 | 说明 | 类型 | 27 | | --- | --- | --- | 28 | | `pos` | 响应式的位置信息对象 | `{ x: number, y: number }` | 29 | | `elRef` | 一个指定事件绑定元素的`ref`对象 | `Ref` | 30 | 31 | #### Params 32 | | 参数 | 说明 | 类型 | 33 | | --- | --- | --- | 34 | | `dom` | 需要检测的`Dom` | `HTMLElement | (() => HTMLElement)` | 35 | -------------------------------------------------------------------------------- /__test__/useThrottleFn.spec.ts: -------------------------------------------------------------------------------- 1 | import { useThrottleFn } from '../src' 2 | 3 | type noop = (...args: any[]) => any 4 | 5 | describe('test useThrottleFn', () => { 6 | const nextTask = (fn: noop, wait: number) => setTimeout(fn, wait) 7 | 8 | test('fn should be called when run', () => { 9 | const fn = jest.fn() 10 | const { run } = useThrottleFn(fn, 500) 11 | run(1) 12 | run(2) 13 | run(3) 14 | nextTask(() => { 15 | expect(fn).toBeCalledTimes(1) 16 | expect(fn).lastCalledWith(1) 17 | }, 550) 18 | }) 19 | 20 | test('fn should not be called when cancelled', () => { 21 | const fn = jest.fn() 22 | const { run, cancel } = useThrottleFn(fn, 500) 23 | run() 24 | run() 25 | run() 26 | run() 27 | cancel() 28 | nextTask(() => { 29 | expect(fn).not.toBeCalled() 30 | }, 550) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: vue-reuse 3 | home: true 4 | heroImage: /imgs/logo.png 5 | actionText: 开始 → 6 | actionLink: ./info/ 7 | features: 8 | - title: Composable 9 | details: 利用Vue3 composition API 来构建逻辑复用函数,极大化了逻辑的组合能力 10 | - title: TypeScript支持 11 | details: 全部采用TypeScript实现,有良好的类型推导支持 12 | footer: MIT License Copyright (c) 2020 happycoder 13 | description: 基于composition API的可组合逻辑复用函数集 14 | meta: 15 | - name: og:title 16 | content: vue-reuse 17 | - name: og:description 18 | content: 基于composition API的可组合逻辑复用函数集 19 | --- 20 | 21 | ### 安装和使用 22 | 23 | - `@xus/vue-reuse`实现基于`Vue3 composition API`的 hooks 函数集 24 | 25 | ```JavaScript 26 | // 安装依赖 27 | npm install @xus/vue-reuse @xuguo/sku vue-demi 28 | // 如果您使用的是vue2.x 您还需要安装并使用 composition API 29 | npm install @vue/composition-api 30 | 31 | // 使用 32 | import { useScroll } from '@xus/vue-reuse' 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/sideEffect/useThrottleRef.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 3 | 一个帮你处理生产一个`throttle`过的代理新`Ref` 4 | 5 | > 1. 可自动在组件卸载时清除事件监听 6 | > 2. 以`ref`形态返回数据 7 | > 3. 以正常对`rawRef`访问方式即可自动`throttle`新的代理`Ref` 8 | 9 | ## 代码演示 10 | 11 | #### 基本使用 12 | 13 | 14 | #### 代码 15 | ::: details 点击查看代码 16 | <<< @/docs/.vuepress/components/useThrottleRef.vue 17 | ::: 18 | 19 | ## API 20 | 21 | ```ts 22 | const throttleRef = useThrottleRef(rawValue, wait): 23 | ``` 24 | 25 | #### RetrunValue 26 | 27 | | 参数 | 说明 | 类型 | 28 | | ------------- | --------------------------- | ---------------------- | 29 | | `throttleRef` | 代理`rawValue`的新`Ref`对象 | `Ref` | 30 | 31 | #### Params 32 | 33 | | 参数 | 说明 | 类型 | 34 | | ------ | --------------------- | -------- | 35 | | `raw` | `throttleRef`的初始值 | `any` | 36 | | `wait` | throttle 的时长 | `number` | 37 | -------------------------------------------------------------------------------- /__test__/useDebounceFn.spec.ts: -------------------------------------------------------------------------------- 1 | import { useDebounceFn } from '../src' 2 | 3 | type noop = (...args: any[]) => any 4 | 5 | describe('test useDebounceFn', () => { 6 | const nextTask = (fn: noop) => setTimeout(fn) 7 | 8 | test('fn will be called when run', () => { 9 | const fn = jest.fn() 10 | const { run } = useDebounceFn(fn, 500) 11 | run(1) 12 | run() 13 | nextTask(() => { 14 | expect(fn).toBeCalledTimes(1) 15 | expect(fn).toBeCalledWith() 16 | }) 17 | }) 18 | 19 | test('fn will not be called when cancel', () => { 20 | const fn = jest.fn() 21 | const { run, cancel } = useDebounceFn(fn, 500) 22 | run(1) 23 | run(2) 24 | nextTask(() => { 25 | expect(fn).toBeCalledTimes(1) 26 | expect(fn).toBeCalledWith(2) 27 | }) 28 | 29 | run(3) 30 | cancel() 31 | nextTask(() => { 32 | expect(fn).toBeCalledTimes(1) 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /docs/sideEffect/useDebounceRef.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 3 | 一个帮你处理生产一个`debounce`过的代理新`Ref` 4 | 5 | > 1. 可自动在组件卸载时清除事件监听 6 | > 2. 以`ref`形态返回数据 7 | > 3. 以正常对`rawRef`访问方式即可自动`debounce`新的代理`Ref` 8 | 9 | ## 代码演示 10 | 11 | #### 基本使用 12 | 13 | 14 | #### 代码 15 | ::: details 点击查看代码 16 | <<< @/docs/.vuepress/components/useDebounceRef.vue 17 | ::: 18 | 19 | ## API 20 | 21 | ```ts 22 | const debounceRef = useDebounceRef(rawValue, wait): 23 | ``` 24 | 25 | #### RetrunValue 26 | 27 | | 参数 | 说明 | 类型 | 28 | | ------------- | --------------------------- | ---------------------- | 29 | | `debounceRef` | 代理`rawValue`的新`Ref`对象 | `Ref` | 30 | 31 | #### Params 32 | 33 | | 参数 | 说明 | 类型 | 34 | | ---------- | --------------------- | -------- | 35 | | `rawValue` | `debounceRef`的初始值 | `any` | 36 | | `wait` | debounce 的时长 | `number` | 37 | -------------------------------------------------------------------------------- /src/useRequest/utils/cache.ts: -------------------------------------------------------------------------------- 1 | type Timer = ReturnType 2 | type CacheKey = string 3 | type CacheData = { 4 | data: any 5 | timer: Timer | undefined 6 | startTime: number 7 | } 8 | 9 | const cache = new Map() 10 | 11 | export function getCache( 12 | key: CacheKey 13 | ): Pick> { 14 | const cached = cache.get(key) 15 | return { 16 | data: cached?.data, 17 | startTime: cached?.startTime as number 18 | } 19 | } 20 | 21 | export function setCache(key: CacheKey, data: any, cacheTime: number): void { 22 | const cached = cache.get(key) 23 | if (cached?.timer) { 24 | clearTimeout(cached.timer) 25 | } 26 | let timer: Timer | undefined 27 | if (cacheTime > -1) { 28 | timer = setTimeout(() => { 29 | cache.delete(key) 30 | }, cacheTime) 31 | } 32 | 33 | cache.set(key, { 34 | data: data, 35 | timer, 36 | startTime: new Date().getTime() 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /.github/API.md: -------------------------------------------------------------------------------- 1 | #### 返回值 2 | 3 | - 无返回值 4 | 5 | - 仅存在一个值 6 | 7 | ```js 8 | const xxx = useXXX() 9 | ``` 10 | 11 | - 存在值与其相关的一种行为 12 | 13 | ```js 14 | const [xxx, changeXXX] = useXXX() 15 | ``` 16 | 17 | - 存在值与其相关的多种行为 18 | 19 | ```js 20 | const [xxx, { changeXX, updateXXX }] = useXXX() 21 | ``` 22 | 23 | - 多个值的情况 24 | 25 | ```js 26 | const { a, b, ... } = useXXX() 27 | ``` 28 | 29 | - 多值多行为 30 | 31 | ```js 32 | const { changeXX, updateXXX, a, b, ... } = useXXX() 33 | ``` 34 | 35 | - 对接组件`Props` 36 | 37 | ```js 38 | const [props, { changeXXX, a, b, ... }] = useXXX() 39 | ``` 40 | 41 | #### 参数 42 | 43 | - 无参数 44 | 45 | - 单个参数 46 | 47 | ```js 48 | useXXX(a) 49 | ``` 50 | 51 | - 多个必选参数 52 | 53 | ```js 54 | // 两个 55 | useXXX(a, b) 56 | // 两个以上 57 | useXXX({ a, b, c, ... }) 58 | ``` 59 | 60 | - 多个非必选 61 | 62 | ```js 63 | useXXX({ a?, b?, c?, ... }) 64 | ``` 65 | 66 | - 必选与多选 67 | 68 | ```js 69 | useXXX(a, b?) 70 | useXXX(a, { b?, c? }) 71 | ``` 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 happycoder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/useDrop/useDrag.ts: -------------------------------------------------------------------------------- 1 | type DragProps = { 2 | draggable: 'true' 3 | key: T extends unknown ? (key: any) => string : string 4 | onDragStart: T extends unknown 5 | ? (key: any) => (evt: DragEvent) => void 6 | : (evt: DragEvent) => void 7 | } 8 | 9 | type DragFn = (key?: T) => DragProps 10 | 11 | export function useDrag(): DragFn { 12 | function getProps(): DragProps 13 | function getProps(key: any): DragProps 14 | function getProps(key?: any): any { 15 | if (typeof key === 'undefined') { 16 | return { 17 | draggable: 'true' as const, 18 | key: (customKey: any) => JSON.stringify(customKey), 19 | onDragStart: (customKey: any) => (evt: DragEvent) => { 20 | evt.dataTransfer?.setData('custom', JSON.stringify(customKey)) 21 | } 22 | } 23 | } else { 24 | return { 25 | draggable: 'true' as const, 26 | key: JSON.stringify(key), 27 | onDragStart: (evt: DragEvent) => { 28 | evt.dataTransfer?.setData('custom', JSON.stringify(key)) 29 | } 30 | } 31 | } 32 | } 33 | return getProps 34 | } 35 | -------------------------------------------------------------------------------- /src/useLocalStorage/index.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from 'vue-demi' 2 | import { useStorage } from '../useStorage' 3 | import { isBrowser, createStorage } from '../shared/utils' 4 | 5 | export function useLocalStorage(key: string, defaultValue?: undefined): Ref 6 | export function useLocalStorage(key: string, defaultValue: null): Ref 7 | export function useLocalStorage(key: string, defaultValue: string): Ref 8 | export function useLocalStorage(key: string, defaultValue: number): Ref 9 | export function useLocalStorage( 10 | key: string, 11 | defaultValue: boolean 12 | ): Ref 13 | export function useLocalStorage>( 14 | key: string, 15 | defaultValue: T 16 | ): Ref 17 | export function useLocalStorage( 18 | key: string, 19 | defaultValue: T 20 | ): Ref 21 | export function useLocalStorage< 22 | T extends string | number | boolean | Record | [] | null 23 | >(key: string, initialValue: T): Ref { 24 | return useStorage( 25 | isBrowser ? localStorage : createStorage(), 26 | key, 27 | initialValue as any 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /docs/state/useHistoryTravel.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 一个能静默的帮你记录`ref`曾经被赋予过的值的`hooks` 3 | > 对于代理过后拥有记录历史能力的`ref`你仅需要想使用普通`ref`一样使用它 4 | > 并且还提供了在历史记录中穿梭的能力 5 | 6 | ## 代码演示 7 | #### 基本使用 8 | ::: tip 9 | 1. 我通过一个历史记录列表来展示所有曾到达的记录,通过高亮标识当前的值为哪一个 10 | 2. 通过前进、后退和`go`我们能达到任何历史状态 11 | 3. 也可以通过`forwardLength`和`backLength`来实时获取可前进后退的步数 12 | ::: 13 | 14 | #### 代码 15 | ::: details 点击查看代码 16 | <<< @/docs/.vuepress/components/useHistoryTravel.vue 17 | ::: 18 | 19 | 20 | ## API 21 | ```ts 22 | const { 23 | current, 24 | backLength, 25 | forwardLength, 26 | back, 27 | forward, 28 | go } = useHistoryTravel(initialValue?) 29 | ``` 30 | 31 | #### RetrunValue 32 | | 参数 | 说明 | 类型 | 33 | | --- | --- | --- | 34 | | `current` | 拥有记录历史能力的`Ref` | `Ref` | 35 | | `backLength` | 可后退步数 | `ComputedRef` | 36 | | `forwardLength` | 可前进步数 | `ComputedRef` | 37 | | `back` | 后退方法 | `() => void` | 38 | | `forward` | 前进方法 | `() => void` | 39 | | `go` | 跳转方法 | `(step: number) => void` | 40 | 41 | #### Params 42 | | 参数 | 说明 | 类型 | 43 | | --- | --- | --- | 44 | | `initialValue` | 初始值 | `any` | 45 | -------------------------------------------------------------------------------- /docs/.vuepress/components/useBoolean.vue: -------------------------------------------------------------------------------- 1 | 20 | 47 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs-extra') 3 | const execa = require('execa') 4 | const chalk = require('chalk') 5 | // 参数处理 6 | const resolve = p => path.resolve(__dirname, '../', p) 7 | const args = require('minimist')(process.argv.slice(2)) 8 | const formats = args._ 9 | const prod = args.prod || args.p 10 | const sourceMap = args.sourceMap || args.s 11 | const buildTypes = args.types || args.t 12 | 13 | async function run() { 14 | // 如果指定了格式 则清除原dist文件夹 15 | if (!formats) { 16 | await fs.remove(resolve('dist')) 17 | } 18 | // 环境信息 19 | const env = prod ? 'production' : 'development' 20 | console.info(chalk.bold(chalk.yellow(`Rollup for vue-reuse`))) 21 | await execa( 22 | 'rollup', 23 | [ 24 | '-c', 25 | '--environment', 26 | [ 27 | `NODE_ENV:${env}`, 28 | formats ? `FORMATS:${formats.join('-')}` : '', 29 | sourceMap ? `SOURCEMAP:true` : '', 30 | buildTypes ? `BUILDTYPES:true` : '', 31 | ] 32 | .filter(Boolean) 33 | .join(','), 34 | ], 35 | { 36 | stdio: 'inherit', 37 | } 38 | ) 39 | 40 | fs.remove(resolve(`dist/src`)) 41 | } 42 | 43 | run() -------------------------------------------------------------------------------- /src/useSessionStorage/index.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from 'vue-demi' 2 | import { useStorage } from '../useStorage' 3 | import { isBrowser, createStorage } from '../shared/utils' 4 | 5 | export function useSessionStorage( 6 | key: string, 7 | defaultValue?: undefined 8 | ): Ref 9 | export function useSessionStorage(key: string, defaultValue: null): Ref 10 | export function useSessionStorage( 11 | key: string, 12 | defaultValue: string 13 | ): Ref 14 | export function useSessionStorage( 15 | key: string, 16 | defaultValue: number 17 | ): Ref 18 | export function useSessionStorage( 19 | key: string, 20 | defaultValue: boolean 21 | ): Ref 22 | export function useSessionStorage>( 23 | key: string, 24 | defaultValue: T 25 | ): Ref 26 | export function useSessionStorage( 27 | key: string, 28 | defaultValue: T 29 | ): Ref 30 | export function useSessionStorage< 31 | T extends string | number | boolean | Record | [] | null 32 | >(key: string, initialValue: T): Ref { 33 | return useStorage( 34 | isBrowser ? sessionStorage : createStorage(), 35 | key, 36 | initialValue as any 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /__test__/setup/setupTest.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | let state: Record = {} 3 | 4 | const localStorageMock = { 5 | getItem: jest.fn((x) => state[x]), 6 | setItem: jest.fn((x, v) => (state[x] = v)), 7 | removeItem: jest.fn((x) => delete state[x]), 8 | clear: jest.fn(() => (state = {})) 9 | } 10 | const sessionStorageMock = { 11 | getItem: jest.fn((x) => state[x]), 12 | setItem: jest.fn((x, v) => (state[x] = v)), 13 | removeItem: jest.fn((x) => delete state[x]), 14 | clear: jest.fn(() => (state = {})) 15 | } 16 | 17 | Object.defineProperty(window, 'localStorage', { 18 | value: localStorageMock 19 | }) 20 | Object.defineProperty(window, 'sessionStorage', { 21 | value: sessionStorageMock 22 | }) 23 | 24 | // ---- patch fetch 25 | Object.defineProperty(window, 'fetch', { 26 | value: (url: string, { error }: { error: string } = { error: '' }) => 27 | new Promise((resolve) => { 28 | const res = error 29 | ? { 30 | ok: false, 31 | statusText: error 32 | } 33 | : { 34 | ok: true, 35 | json() { 36 | return url 37 | } 38 | } 39 | setTimeout(() => { 40 | resolve(res) 41 | }, 100) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.zh-CN.md: -------------------------------------------------------------------------------- 1 | # vue-reuse 贡献指南 2 | 3 | Hi! 首先感谢你关注并使用 vue-reuse。 4 | 5 | vue-reuse 是一套采用`typescript`实现,为开发者提供基础逻辑到业务逻辑,提供多种组合函数的 Hooks 库 6 | 7 | vue-reuse 的成长离不开大家的支持,如果你愿意为 vue-reuse 贡献代码或提供建议,请阅读以下内容。 8 | 9 | ## Issue 规范 10 | 11 | - issue 仅用于提交 Bug 或 Feature 以及设计相关的内容,其它内容可能会被直接关闭。 12 | 13 | - 在提交 issue 之前,请搜索相关内容是否已被提出。 14 | 15 | - TODO 16 | 17 | ## Pull Request 规范 18 | 19 | - 请先 fork 一份到自己的项目下,不要直接在仓库下建分支。 20 | 21 | - 获取项目后,添加主代码库: 22 | 23 | git remote add upstream https://github.com/xus-code/vue-reuse.git 24 | 25 | - 更新/同步主仓库的代码 26 | 27 | 更新仓库: git fetch upstream 28 | 29 | 同步对应分支的代码,比如`master`:git rebase upstream/master 30 | 31 | - 分支规范/提交信息规范 32 | 33 | 严格按照:[gitflow](https://github.com/xus-code/vue-reuse/blob/master/.github/gitflows.md) 34 | 35 | - **不要提交** `yarn.lock` 文件。 36 | 37 | ## PR 操作流程 38 | 39 | 1. 根据提交需求,在 master/feat 等分支建立本地新分支 `master/doc/xxx`/`feat/xxx` 40 | 41 | 2. 开发完成后进行下列操作 42 | 43 | ``` 44 | git rebase `主分支名称` 对主分支进行线性合并 45 | 46 | git rebase -i 进行commit message的整合,保证提交简洁。 47 | 48 | git push 推送代码到远端 49 | 50 | 发起代码合并请求到对应主分支 51 | ``` 52 | 53 | 3. 如果你是 hooks 开发请遵照 TDD 规则:设计好 hooks 后写对应单测,然后跑测试覆盖率达到 85%代表你的功能就开发符合要求 54 | 55 | ## 开发环境 56 | 57 | 首先你需要 Node.js ,yarn 58 | 59 | yarn docs:dev 60 | -------------------------------------------------------------------------------- /docs/state/useBoolean.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 3 | 一个帮你管理 boolean 值的 Hook。 4 | 5 | ## 代码演示 6 | 7 | #### 基本使用 8 | ::: tip 9 | 1. 可以通过返回参数获取当前值的Boolean状态 10 | 2. 可以通过返回参数动态设置值的Bollean状态 11 | 3. 可以接受一个Boolean类型值来设置当前值状态 12 | ::: 13 | 14 | 15 | #### 代码 16 | ::: details 点击查看代码 17 | <<< @/docs/.vuepress/components/useBoolean.vue 18 | ::: 19 | 20 | 21 | 22 | ## API 23 | ```ts 24 | const [ state, { changeState, setTrue, setFalse }] = useBoolean( 25 | defaultValue?: boolean 26 | ); 27 | ``` 28 | 29 | #### Params 30 | | 参数 | 说明 | 类型 | 31 | | --- | --- | --- | 32 | | `defaultValue` | 可选项,传入默认的状态值 | `boolean` | 33 | | `options` | 可选项,接受一个reactive,动态设置Boolean取值 | `reactive` | 34 | 35 | #### RetrunValue 36 | | 参数 | 说明 | 类型 | 37 | | --- | --- | --- | 38 | | `state` | 状态值 | `boolean` | 39 | | `actions` | 操作集合(见下方表) | `Actions` | 40 | 41 | #### Actions 42 | 43 | | 参数 | 说明 | 类型 | 44 | |----------|---------------------------------------------------|-----------------------------| 45 | | changeState | 触发状态更改的函数,可接受一个参数修改状态值 | `(value?: boolean) => void` | 46 | | setTrue | 设置状态值为 true | `() => void` | 47 | | setFalse | 设置状态值为 false | `() => void` | -------------------------------------------------------------------------------- /__test__/useVirtualList.spec.ts: -------------------------------------------------------------------------------- 1 | import { useVirtualList, Options } from '../src/useVirtualList' 2 | import { nextTick } from './utils' 3 | 4 | describe('test use virtual list', () => { 5 | let mockEle = { scrollTop: 0, clientHeight: 300 } 6 | const setup = (opts: Partial = {}) => { 7 | const res = useVirtualList( 8 | Array.from({ length: 100 }).map((_, i) => i + 1), 9 | { 10 | itemHeight: 30, 11 | ...opts 12 | } 13 | ) 14 | res.containeRef.value = mockEle as HTMLElement 15 | return res 16 | } 17 | 18 | afterEach(() => { 19 | mockEle = { scrollTop: 0, clientHeight: 300 } 20 | }) 21 | 22 | test(`oversacn default 5 `, async () => { 23 | const { list, scrollTo, wrapperProps } = setup() 24 | await nextTick() 25 | expect(list.value.length).toBe(10) 26 | expect(wrapperProps.style.height).toBe(`0px`) 27 | scrollTo(20) 28 | // default overscan 5 * 2 + 10 29 | expect(mockEle.scrollTop).toBe(30 * 20) 30 | expect(list.value.length).toBe(20) 31 | }) 32 | 33 | test('func itemHeight ', () => { 34 | const { list, scrollTo } = setup({ itemHeight: () => 10 }) 35 | expect(list.value.length).toBe(10) 36 | scrollTo(20) 37 | // default overscan 5 * 2 + 30 38 | expect(mockEle.scrollTop).toBe(10 * 20) 39 | expect(list.value.length).toBe(40) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /__test__/useBoolean.spec.ts: -------------------------------------------------------------------------------- 1 | import { useBoolean } from '../src' 2 | import { reactive } from 'vue-demi' 3 | 4 | describe('test useBoolean', () => { 5 | test('test state and actions', async () => { 6 | const [state, { changeState, setTrue, setFalse }] = useBoolean(true) 7 | expect(state.value).toBe(true) 8 | 9 | setFalse() 10 | expect(state.value).toBe(false) 11 | 12 | setTrue() 13 | 14 | expect(state.value).toBe(true) 15 | 16 | changeState() 17 | 18 | expect(state.value).toBe(!state) 19 | }) 20 | 21 | test('test options judge', async () => { 22 | const options: { 23 | [propName: string]: boolean 24 | } = reactive({ 25 | 0: true, 26 | '': true, 27 | usefalse: false 28 | }) 29 | 30 | const [state, { changeState }] = useBoolean(true, options) 31 | 32 | expect(state.value).toBe(true) 33 | 34 | changeState(0) 35 | 36 | expect(state.value).toBe(true) 37 | 38 | changeState('usefalse') 39 | 40 | expect(state.value).toBe(false) 41 | 42 | changeState('') 43 | 44 | expect(state.value).toBe(true) 45 | 46 | options.usefalse = true 47 | 48 | changeState('usefalse') 49 | 50 | expect(state.value).toBe(true) 51 | 52 | options.addOptions = false 53 | 54 | changeState('addOptions') 55 | 56 | expect(state.value).toBe(false) 57 | }) 58 | 59 | }) 60 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | node: true, 5 | }, 6 | extends: [ 7 | 'eslint:recommended', 8 | 'plugin:@typescript-eslint/recommended', 9 | 'prettier', 10 | 'prettier/@typescript-eslint', 11 | ], 12 | parser: '@typescript-eslint/parser', 13 | parserOptions: { 14 | ecmaVersion: 6, 15 | sourceType: 'module', 16 | }, 17 | plugins: ['@typescript-eslint'], 18 | rules: { 19 | '@typescript-eslint/no-explicit-any': 0, 20 | '@typescript-eslint/no-non-null-assertion': 0 21 | }, 22 | overrides: [ 23 | { 24 | files: ['./__test__/*-test.ts', './__test__/*.spec.ts'], 25 | env: { 26 | node: true, 27 | jest: true, 28 | }, 29 | extends: [ 30 | 'eslint:recommended', 31 | 'plugin:@typescript-eslint/recommended', 32 | 'prettier', 33 | 'prettier/@typescript-eslint', 34 | 'plugin:jest/recommended', 35 | ], 36 | rules: { 37 | '@typescript-eslint/no-explicit-any': 0, 38 | '@typescript-eslint/no-empty-function': 0 39 | }, 40 | }, 41 | { 42 | files: ['./scripts/*', './*.js'], 43 | env: { 44 | node: true, 45 | }, 46 | extends: ['eslint:recommended', 'prettier'], 47 | rules: { 48 | '@typescript-eslint/no-var-requires': 0, 49 | }, 50 | }, 51 | ], 52 | } 53 | -------------------------------------------------------------------------------- /src/useBoolean/index.ts: -------------------------------------------------------------------------------- 1 | import { ref, Ref, isReactive, reactive } from 'vue-demi' 2 | 3 | type Primitive = string | number | boolean | symbol 4 | 5 | type BolOptions = { 6 | [propName: string]: boolean 7 | [propNum: number]: boolean 8 | } 9 | 10 | type changeStateType = (value?: Primitive) => void 11 | 12 | type Actions = { 13 | setTrue: () => void 14 | setFalse: () => void 15 | changeState: changeStateType 16 | } 17 | 18 | function toggleBase( 19 | defaultValue: boolean, 20 | options: BolOptions = {} 21 | ): [Ref, changeStateType] { 22 | const state = ref(defaultValue) 23 | 24 | // todo 是否需要抛出Set bolMap 25 | const bolMap: BolOptions = isReactive(options) ? options : reactive(options) 26 | 27 | const changeState = (value?: Primitive) => { 28 | if (bolMap[value as string] !== undefined) { 29 | state.value = bolMap[value as string] 30 | } else if (typeof value !== 'boolean') { 31 | state.value = !state.value 32 | } else { 33 | state.value = value 34 | } 35 | } 36 | 37 | return [state, changeState] 38 | } 39 | 40 | export function useBoolean( 41 | defaultValue: boolean, 42 | options?: BolOptions 43 | ): [Ref, Actions] { 44 | const [state, changeState] = toggleBase(defaultValue, options) 45 | 46 | const actions = () => { 47 | const setTrue = () => changeState(true) 48 | const setFalse = () => changeState(false) 49 | return { changeState, setTrue, setFalse } 50 | } 51 | 52 | return [state, actions()] 53 | } 54 | -------------------------------------------------------------------------------- /docs/UI/useVirtualList.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 一个帮你处理虚拟列表逻辑的`hooks` :tada: 3 | > 1. 你所做的只需要将对应的`props`进行绑定 4 | 5 | ## 代码演示 6 | #### 基本使用 7 | ::: tip Tips 8 | 对于固定高度列表项目的情况,我们可以采用传递固定的`itemHeight`来设定高度 9 | ::: 10 | 11 | #### 代码 12 | ::: details 点击查看代码 13 | <<< @/docs/.vuepress/components/useVirtualList.vue 14 | ::: 15 | 16 | ## API 17 | ```ts 18 | const { 19 | list, 20 | wrapperProps, 21 | containeProps, 22 | containeRef, 23 | scrollTo, 24 | } = useVirtualList(rawList, { 25 | itemHeight, 26 | overscan? 27 | }) 28 | ``` 29 | 30 | #### RetrunValue 31 | | 参数 | 说明 | 类型 | 32 | | --- | --- | --- | 33 | | `list` | 虚拟列表当前显示的内容 | `Ref<{ data: T, index: number }[]>` | 34 | | `wrapperProps` | 包裹列表项元素的容器所需要绑定的`props`(`reactive`对象) | `{ style }` | 35 | | `containeProps` | 包裹滚动部分的容器所需要绑定的`props` | `{ onScroll(evt: Event): void, style: { overflowY: 'auto' } }` | 36 | | `containeRef` | 包裹列表项元素容器`refs`需要绑定的`Ref`对象 | `Ref` | 37 | | `scrollTo` | 滚动到某个列表项的方法 | `(index: number) => void` | 38 | 39 | #### Params 40 | | 参数 | 说明 | 类型 | 41 | | --- | --- | --- | 42 | | `rawList` | 原始列表数据,可以是一个`Ref`的对象 | `T[] | Ref` | 43 | | `itemHeight` | 计算列表项目高度的参数 | `number | (index: number) => number` | 44 | | `overscan` | 在可视范围外,上下各需要显示多少个元素 | `number` | 45 | 46 | #### WrapperProps-style 47 | | 参数 | 说明 | 类型 | 48 | | --- | --- | --- | 49 | | `width` | 宽度 | `'100%'` | 50 | | `boxSizing` | 盒模型 | `'border-box'` | 51 | | `height` | 高度 | `string` | 52 | | `paddingTop` | 上内边距 | `string` | 53 | -------------------------------------------------------------------------------- /docs/.vuepress/components/useSku.vue: -------------------------------------------------------------------------------- 1 | 18 | 33 | 65 | -------------------------------------------------------------------------------- /docs/.vuepress/components/useSkuCustom.vue: -------------------------------------------------------------------------------- 1 | 18 | 33 | 65 | -------------------------------------------------------------------------------- /src/useEventListener/index.ts: -------------------------------------------------------------------------------- 1 | import { Ref, onMounted, isRef, onUnmounted, ref } from 'vue-demi' 2 | 3 | type TargetType = HTMLElement | Ref | (() => HTMLElement) | Window 4 | type Options = { dom?: TargetType } & Partial 5 | type ExtractDomOptions = Pick> 6 | 7 | export function useEventListener( 8 | type: T, 9 | handler: (this: Window, event: WindowEventMap[T]) => any, 10 | options?: ExtractDomOptions 11 | ): void 12 | export function useEventListener( 13 | type: T, 14 | handler: (this: HTMLElement, event: HTMLElementEventMap[T]) => any, 15 | options: Options 16 | ): Ref 17 | export function useEventListener( 18 | type: string, 19 | handler: EventListenerOrEventListenerObject, 20 | options?: Options 21 | ): Ref { 22 | const el = ref(null) 23 | let element: HTMLElement | Window 24 | onMounted(() => { 25 | element = options?.dom 26 | ? typeof options?.dom === 'function' 27 | ? options?.dom() 28 | : isRef(options?.dom) 29 | ? options.dom.value 30 | : options?.dom 31 | : window 32 | el.value && (element = el.value) 33 | element.addEventListener(type, handler, { 34 | capture: options?.capture, 35 | once: options?.once, 36 | passive: options?.passive 37 | }) 38 | }) 39 | onUnmounted(() => { 40 | element.removeEventListener(type, handler, { 41 | capture: options?.capture 42 | }) 43 | }) 44 | 45 | return el 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/deploy-doc.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Doc CI 2 | 3 | on: 4 | push: 5 | tags: 'v**' 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | node-version: [15.x] 14 | 15 | steps: 16 | - uses: actions/checkout@v1 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - name: yarn intall, doc build 22 | run: | 23 | npm install yarn -g 24 | yarn 25 | yarn docs:build 26 | - name: deploy docs 27 | uses: JamesIves/github-pages-deploy-action@3.6.2 28 | with: 29 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} 30 | BRANCH: gh-pages 31 | FOLDER: doc-dist 32 | CLEAN: true 33 | env: 34 | CI: true 35 | sync: 36 | needs: build 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Sync to Gitee 40 | uses: wearerequired/git-mirror-action@master 41 | env: 42 | SSH_PRIVATE_KEY: ${{ secrets.GITEE_RSA_PRIVATE_KEY }} 43 | with: 44 | source-repo: 'git@github.com:xus-code/vue-reuse.git' 45 | destination-repo: 'git@gitee.com:a-sir/vue-reuse.git' 46 | reload-pages: 47 | needs: sync 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: Build Gitee Pages 51 | uses: yanglbme/gitee-pages-action@main 52 | with: 53 | gitee-username: a-sir 54 | gitee-password: ${{ secrets.GITEE_PASSWORD }} 55 | gitee-repo: a-sir/vue-reuse 56 | branch: gh-pages 57 | -------------------------------------------------------------------------------- /.github/gitflows.md: -------------------------------------------------------------------------------- 1 | #### 分支规范 2 | 3 | 目前分支模式会采用类似`antd`的形式,分为`master`和`feat`两个主分支 4 | 5 | - `master`分支 6 | 7 | 1. 一些文档修复、bug 修复、项目底层能力比如打包修复会从`master`检出分支`fix/xxx`出来修复,修复完了通过`pr`合并回`master` 8 | 9 | 2. 同时版本发布也会从`master`来检出 10 | 11 | - `feat`分支 12 | 13 | 开发新的 hooks 从 feat 检出分支`feature/xxx`,开发完成功能并且书写好相应文档和单元测试用例,通过`pr`的形式合并回`feat` 14 | 15 | 最终包含新`hooks`的`feat`也会通过`pr`回归到`master`进行新版本的发布 16 | 17 | - `release`分支 18 | 19 | `release/xxx`通常作为版本发布分支来做版本发布 20 | 21 | #### 提交信息规范 22 | 23 | - `commit`常规规范 24 | 25 | ``` 26 | <类型>(作用域): 标题 27 | 28 | 正文 29 | 30 | 尾部 31 | ``` 32 | 33 | > build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交 34 | > ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle 等)的提交 35 | > docs:文档更新 36 | > feat:新增功能 37 | > merge:分支合并 Merge branch ? of ? 38 | > fix:bug 修复 39 | > perf:性能, 体验优化 40 | > refactor:重构代码(既没有新增功能,也没有修复 bug) 41 | > style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑) 42 | > test:新增测试用例或是更新现有测试 43 | > revert:回滚某个更早之前的提交 44 | > chore:不属于以上类型的其他类型 45 | 46 | - 保持提交信息的整洁 47 | 48 | > 上游分支:通常指你当前分支检出的来源分支,比如`master` 49 | > 50 | > 下游分支:通常指你当前开发功能或者修复`bug`的分支 51 | 52 | 1. 下游分支同步上有分支必须采用`rebase`的形式,避免产生`merge commit` 53 | 2. 下游分支开发中可灵活产生`WIP`开发中的中间`commit`信息,但是在最终申请合并到上游主分支前,需要`rebase -i`主分支并且通过`-i`来`fixup`不必要的`commit`信息,保持汇入主分支时`commit`信息的整洁 54 | 55 | #### PR 相关 56 | 57 | - `pr` 名称 58 | 59 | `pr` 名称应当简介的概括当前 `pr` 所完成的事情,并且以分支名作为开始。 60 | 61 | - `pr` 需要满足的 `CI` 条件 62 | 63 | 1. `Lint` 和 `test` 无报错 64 | 2. `doc build` 和 `bundle build` 无报错 65 | 3. `codecov` 不下降,并且当前功能的测试覆盖率在 `85%` 以上 66 | 67 | - `pr` 包含的提交记录 68 | 69 | 最终的 `pr` 所包含的提交记录尽量不要包含中间状态的 `commit` 记录,比如: 70 | 如果是开发了一个新的 `hook` ,最终记录应该是线性的并且只包含 `feat(xxx): xxxx` 这样一条新增功能的有效 `commit`, 71 | 修复 `bug`也是相似的规则。 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-reuse 2 | 3 | ##### 基于 composition-api 的 hooks 函数库 4 | 5 | ![Lint Test](https://github.com/xus-code/vue-reuse/workflows/Lint%20Test%20CI/badge.svg) 6 | [![Build Status](https://travis-ci.org/xus-code/vue-reuse.svg?branch=master)](https://travis-ci.org/xus-code/vue-reuse) 7 | ![Deploy Docs](https://github.com/xus-code/vue-reuse/workflows/Deploy%20Doc%20CI/badge.svg) 8 | [![license scan](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fxus-code%2Fvue-reuse.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fxus-code%2Fvue-reuse?ref=badge_shield) 9 | [![codecov](https://codecov.io/gh/xus-code/vue-reuse/branch/master/graph/badge.svg?token=PM1K5156D2)](https://codecov.io/gh/xus-code/vue-reuse) 10 | 11 | ## 特性 12 | 13 | - 采用`typescript`实现,提供良好的类型提示 14 | - 从基础逻辑到业务逻辑,提供多种组合函数(WIP) 15 | 16 | ## 参与开发 17 | 18 | - 运行 `yarn` 安装依赖 19 | - 运行 `yarn lint` 校验代码风格 20 | - 运行 `yarn format` 格式化所有 `ts` 代码 21 | - 运行 `yarn cm` 执行交互式 `commit-msg` 生成 22 | - 运行 `yarn test` 执行单元测试,可支持 `jest` 相关参数 23 | - 运行 `yarn docs:dev` 启动开发环境下文档系统 24 | 25 | ## 目录结构 26 | 27 | 当前项目采用独立 `npm` 包的形式来管理,基于 `ts` 开发; 28 | 29 | 1. `docs` 中使用的是 `vuepress` 来构建文档项目,每个不同文件夹放置不同分类中的 `vue-reuse` 文档, 30 | 可参照现有案例编写。 31 | 32 | 2. `__test__` 中存放相应 `hooks` 的测试用例,如果遇到复杂 `hooks` 请在 `__test__` 下建立相应的文件夹。 33 | 34 | 3. `scripts` 中存放 发布、打包两个脚本 35 | 36 | 4. `src` 下存放 `hooks` 实现逻辑 37 | 38 | 5. `src/shared` 下存放共用逻辑和类型文件 39 | 40 | ## 提交规范 41 | 42 | 提交信息请严格遵循 `angular` 团队风格,通过交互式的方式来创建(会在 `git-hooks` 中进行校验拦截) 43 | 44 | #### [`gitflows规范`](https://github.com/xus-code/vue-reuse/blob/master/.github/gitflows.md) 45 | 46 | #### [`API规范`](https://github.com/xus-code/vue-reuse/blob/master/.github/API.md) 47 | 48 | #### [`开发指南`](https://github.com/xus-code/vue-reuse/blob/master/.github/dev.md) 49 | -------------------------------------------------------------------------------- /__test__/useSku.spec.ts: -------------------------------------------------------------------------------- 1 | import { useSku } from '../src' 2 | import { isRef } from 'vue-demi' 3 | 4 | describe('test useSku', () => { 5 | const MockData = [ 6 | { 7 | skuId: '1', 8 | spuSpecValues: [ 9 | { 10 | specId: 'a', 11 | specName: '颜色', 12 | specValueId: '2', 13 | specValueRemark: '黑色', 14 | }, 15 | { 16 | specId: 'b', 17 | specName: '尺寸', 18 | specValueId: '3', 19 | specValueRemark: 'L', 20 | }, 21 | ], 22 | }, 23 | { 24 | skuId: '2', 25 | spuSpecValues: [ 26 | { 27 | specId: 'a', 28 | specName: '颜色', 29 | specValueId: '3', 30 | specValueRemark: '蓝色', 31 | }, 32 | { 33 | specId: 'b', 34 | specName: '尺寸', 35 | specValueId: '4', 36 | specValueRemark: 'S', 37 | }, 38 | ], 39 | }, 40 | ] 41 | 42 | test('should return a skuList and a func', () => { 43 | const { specTap, skuList } = useSku(MockData) 44 | expect(specTap).toBeDefined() 45 | expect(isRef(skuList)).toBeTruthy() 46 | }) 47 | 48 | test('default data', () => { 49 | const { skuList } = useSku(MockData) 50 | 51 | skuList.value.forEach((specLine) => { 52 | specLine.specs.forEach((spec) => { 53 | expect(spec.status).toEqual('pending') 54 | }) 55 | }) 56 | }) 57 | 58 | test('tap spec', () => { 59 | const { specTap, skuList } = useSku(MockData) 60 | const getSpec = (row: number, col: number) => skuList.value[row].specs[col] 61 | specTap(getSpec(0, 0)) 62 | expect(getSpec(0, 0).status).toEqual('selected') 63 | expect(getSpec(0, 1).status).toEqual('pending') 64 | expect(getSpec(1, 0).status).toEqual('pending') 65 | expect(getSpec(1, 1).status).toEqual('disabled') 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /__test__/useHistoryTravel.spec.ts: -------------------------------------------------------------------------------- 1 | import { isRef } from 'vue-demi' 2 | import { useHistoryTravel } from '../src' 3 | 4 | describe('test history travel', () => { 5 | test('should be return a ref', () => { 6 | const { current, backLength, forwardLength } = useHistoryTravel(1) 7 | expect(isRef(current)).toBeTruthy() 8 | expect(current.value).toBe(1) 9 | // expect(isReadonly(backLength)).toBeTruthy() 10 | // expect(isReadonly(forwardLength)).toBeTruthy() 11 | expect(backLength.value).toBe(0) 12 | expect(forwardLength.value).toBe(0) 13 | }) 14 | 15 | test('test history', () => { 16 | const { 17 | current, 18 | backLength, 19 | forwardLength, 20 | back, 21 | forward, 22 | } = useHistoryTravel(1) 23 | expect(isRef(current)).toBeTruthy() 24 | expect(current.value).toBe(1) 25 | current.value = 2 26 | expect(backLength.value).toBe(1) 27 | // back 1 step 28 | back() 29 | expect(current.value).toBe(1) 30 | expect(backLength.value).toBe(0) 31 | expect(forwardLength.value).toBe(1) 32 | // forward 1 step 33 | forward() 34 | expect(current.value).toBe(2) 35 | expect(backLength.value).toBe(1) 36 | expect(forwardLength.value).toBe(0) 37 | }) 38 | 39 | test('method go', () => { 40 | const { current, backLength, forwardLength, go } = useHistoryTravel(1) 41 | 42 | for (const val of [2, 3, 4]) { 43 | current.value = val 44 | expect(current.value).toBe(val) 45 | // expect(backLength.value).toBe(val) 46 | } 47 | 48 | expect(backLength.value).toBe(3) 49 | expect(forwardLength.value).toBe(0) 50 | // back 2 step 51 | go(-2) 52 | expect(current.value).toBe(2) 53 | expect(backLength.value).toBe(1) 54 | expect(forwardLength.value).toBe(2) 55 | // forward 2 step 56 | go(2) 57 | expect(current.value).toBe(4) 58 | expect(backLength.value).toBe(3) 59 | expect(forwardLength.value).toBe(0) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /src/useScroll/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | // shallowReadonly, 3 | reactive, 4 | Ref, 5 | ref, 6 | onMounted, 7 | onUnmounted, 8 | getCurrentInstance, 9 | computed 10 | } from 'vue-demi' 11 | import { useThrottleFn } from '../useThrottleFn' 12 | import { isBrowser } from '../shared/utils' 13 | 14 | interface Pos { 15 | x: number 16 | y: number 17 | } 18 | 19 | type Target = HTMLElement | Document 20 | type Dom = Target | (() => Target) | undefined 21 | 22 | export function useScroll(): [Readonly, Ref] 23 | export function useScroll(dom: Dom): [Readonly] 24 | export function useScroll(dom: Dom = isBrowser ? document : undefined): any { 25 | const position = reactive({ 26 | x: 0, 27 | y: 0 28 | }) 29 | const el = ref(null) 30 | 31 | let element: Target 32 | function updatePosition(target: Target) { 33 | if (typeof target === 'undefined') return 34 | if (target === document) { 35 | if (target.scrollingElement) { 36 | position.x = target.scrollingElement.scrollLeft 37 | position.x = target.scrollingElement.scrollTop 38 | } 39 | } else { 40 | position.x = (target as HTMLElement).scrollLeft 41 | position.y = (target as HTMLElement).scrollTop 42 | } 43 | } 44 | const { run: handler } = useThrottleFn((evt: Event) => { 45 | if (!evt.target) return 46 | updatePosition(evt.target as Target) 47 | }, 100) 48 | 49 | if (getCurrentInstance()) { 50 | onMounted(() => { 51 | const element = (typeof dom === 'function' ? dom() : dom) || el.value 52 | if (element) { 53 | updatePosition(element) 54 | element.addEventListener('scroll', handler) 55 | } 56 | }) 57 | 58 | onUnmounted(() => { 59 | element?.removeEventListener('scroll', handler) 60 | }) 61 | } 62 | 63 | return [computed(() => position), el] 64 | // use shallowReadonly vuepress will build error 65 | // return [shallowReadonly(position), el] 66 | } 67 | -------------------------------------------------------------------------------- /__test__/useLocalStorage.spec.ts: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from '../src/useLocalStorage' 2 | 3 | const KEY = 'local' 4 | describe('test useLocalStorage', () => { 5 | const nextTick = () => Promise.resolve().then() 6 | afterEach(() => { 7 | localStorage.clear() 8 | }) 9 | 10 | test('storage will sync', async () => { 11 | const sv = useLocalStorage(KEY, 1) 12 | expect(sv.value).toBe(1) 13 | sv.value = 2 14 | await nextTick() 15 | expect(localStorage.setItem).toBeCalled() 16 | expect(localStorage.setItem).toBeCalledWith(KEY, JSON.stringify(2)) 17 | }) 18 | 19 | test('test array', async () => { 20 | const sv = useLocalStorage(KEY, [1, 2, 3]) 21 | expect(sv.value).toEqual([1, 2, 3]) 22 | sv.value.push(4) 23 | await nextTick() 24 | expect(localStorage.setItem).toBeCalled() 25 | expect(localStorage.setItem).toBeCalledWith( 26 | KEY, 27 | JSON.stringify([1, 2, 3, 4]) 28 | ) 29 | }) 30 | 31 | test('test object', async () => { 32 | const sv = useLocalStorage(KEY, { a: 1, b: '2', c: { d: 4 } }) 33 | expect(sv.value).toEqual({ a: 1, b: '2', c: { d: 4 } }) 34 | sv.value.c.d = 5 35 | await nextTick() 36 | expect(localStorage.setItem).toBeCalled() 37 | expect(localStorage.setItem).toBeCalledWith( 38 | KEY, 39 | JSON.stringify({ a: 1, b: '2', c: { d: 5 } }) 40 | ) 41 | }) 42 | 43 | test('test undefined', async () => { 44 | const sv = useLocalStorage(KEY) 45 | expect(sv.value).toBeUndefined() 46 | sv.value = 1 47 | await nextTick() 48 | expect(localStorage.setItem).toBeCalled() 49 | expect(localStorage.setItem).toBeCalledWith(KEY, JSON.stringify(1)) 50 | }) 51 | 52 | test('test null', async () => { 53 | const sv = useLocalStorage(KEY, null) 54 | expect(sv.value).toBeUndefined() 55 | sv.value = [1] 56 | await nextTick() 57 | expect(localStorage.setItem).toBeCalled() 58 | expect(localStorage.setItem).toBeCalledWith(KEY, JSON.stringify([1])) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /__test__/useStorage.spec.ts: -------------------------------------------------------------------------------- 1 | import { useStorage } from '../src/useStorage' 2 | 3 | const KEY = 'test' 4 | describe('test useStorage', () => { 5 | const nextTick = () => Promise.resolve().then() 6 | afterEach(() => { 7 | localStorage.clear() 8 | }) 9 | 10 | test('storage will sync', async () => { 11 | const sv = useStorage(localStorage, KEY, 1) 12 | expect(sv.value).toBe(1) 13 | sv.value = 2 14 | await nextTick() 15 | expect(localStorage.setItem).toBeCalled() 16 | expect(localStorage.setItem).toBeCalledWith(KEY, JSON.stringify(2)) 17 | }) 18 | 19 | test('test array', async () => { 20 | const sv = useStorage(localStorage, KEY, [1, 2, 3]) 21 | expect(sv.value).toEqual([1, 2, 3]) 22 | sv.value.push(4) 23 | await nextTick() 24 | expect(localStorage.setItem).toBeCalled() 25 | expect(localStorage.setItem).toBeCalledWith( 26 | KEY, 27 | JSON.stringify([1, 2, 3, 4]) 28 | ) 29 | }) 30 | 31 | test('test object', async () => { 32 | const sv = useStorage(localStorage, KEY, { a: 1, b: '2', c: { d: 4 } }) 33 | expect(sv.value).toEqual({ a: 1, b: '2', c: { d: 4 } }) 34 | sv.value.c.d = 5 35 | await nextTick() 36 | expect(localStorage.setItem).toBeCalled() 37 | expect(localStorage.setItem).toBeCalledWith( 38 | KEY, 39 | JSON.stringify({ a: 1, b: '2', c: { d: 5 } }) 40 | ) 41 | }) 42 | 43 | test('test undefined', async () => { 44 | const sv = useStorage(localStorage, KEY) 45 | expect(sv.value).toBeUndefined() 46 | sv.value = 1 47 | await nextTick() 48 | expect(localStorage.setItem).toBeCalled() 49 | expect(localStorage.setItem).toBeCalledWith(KEY, JSON.stringify(1)) 50 | }) 51 | 52 | test('test null', async () => { 53 | const sv = useStorage(localStorage, KEY, null) 54 | expect(sv.value).toBeUndefined() 55 | sv.value = [1] 56 | await nextTick() 57 | expect(localStorage.setItem).toBeCalled() 58 | expect(localStorage.setItem).toBeCalledWith(KEY, JSON.stringify([1])) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /__test__/useSessionStorage.spec.ts: -------------------------------------------------------------------------------- 1 | import { useSessionStorage } from '../src/useSessionStorage' 2 | 3 | const KEY = 'session' 4 | describe('test useSeesionStorage', () => { 5 | const nextTick = () => Promise.resolve().then() 6 | afterEach(() => { 7 | sessionStorage.clear() 8 | }) 9 | 10 | test('storage will sync', async () => { 11 | const sv = useSessionStorage(KEY, 1) 12 | expect(sv.value).toBe(1) 13 | sv.value = 2 14 | await nextTick() 15 | expect(sessionStorage.setItem).toBeCalled() 16 | expect(sessionStorage.setItem).toBeCalledWith(KEY, JSON.stringify(2)) 17 | }) 18 | 19 | test('test array', async () => { 20 | const sv = useSessionStorage(KEY, [1, 2, 3]) 21 | expect(sv.value).toEqual([1, 2, 3]) 22 | sv.value.push(4) 23 | await nextTick() 24 | expect(sessionStorage.setItem).toBeCalled() 25 | expect(sessionStorage.setItem).toBeCalledWith( 26 | KEY, 27 | JSON.stringify([1, 2, 3, 4]) 28 | ) 29 | }) 30 | 31 | test('test object', async () => { 32 | const sv = useSessionStorage(KEY, { a: 1, b: '2', c: { d: 4 } }) 33 | expect(sv.value).toEqual({ a: 1, b: '2', c: { d: 4 } }) 34 | sv.value.c.d = 5 35 | await nextTick() 36 | expect(sessionStorage.setItem).toBeCalled() 37 | expect(sessionStorage.setItem).toBeCalledWith( 38 | KEY, 39 | JSON.stringify({ a: 1, b: '2', c: { d: 5 } }) 40 | ) 41 | }) 42 | 43 | test('test undefined', async () => { 44 | const sv = useSessionStorage(KEY) 45 | expect(sv.value).toBeUndefined() 46 | sv.value = 1 47 | await nextTick() 48 | expect(sessionStorage.setItem).toBeCalled() 49 | expect(sessionStorage.setItem).toBeCalledWith(KEY, JSON.stringify(1)) 50 | }) 51 | 52 | test('test null', async () => { 53 | const sv = useSessionStorage(KEY, null) 54 | expect(sv.value).toBeUndefined() 55 | sv.value = [1] 56 | await nextTick() 57 | expect(sessionStorage.setItem).toBeCalled() 58 | expect(sessionStorage.setItem).toBeCalledWith(KEY, JSON.stringify([1])) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /src/useStorage/index.ts: -------------------------------------------------------------------------------- 1 | import { Ref, ref, watch } from 'vue-demi' 2 | 3 | type BasicType = boolean | string | number 4 | 5 | export function useStorage( 6 | storage: Storage, 7 | key: string, 8 | defaultValue?: undefined 9 | ): Ref 10 | export function useStorage( 11 | storage: Storage, 12 | key: string, 13 | defaultValue: null 14 | ): Ref 15 | export function useStorage( 16 | storage: Storage, 17 | key: string, 18 | defaultValue: string 19 | ): Ref 20 | export function useStorage( 21 | storage: Storage, 22 | key: string, 23 | defaultValue: number 24 | ): Ref 25 | export function useStorage( 26 | storage: Storage, 27 | key: string, 28 | defaultValue: boolean 29 | ): Ref 30 | export function useStorage>( 31 | storage: Storage, 32 | key: string, 33 | defaultValue: T 34 | ): Ref 35 | export function useStorage( 36 | storage: Storage, 37 | key: string, 38 | defaultValue: T 39 | ): Ref 40 | export function useStorage< 41 | T extends BasicType | Record | unknown[] | null 42 | >(storage: Storage, key: string, defaultValue: T): Ref { 43 | const storageValue = ref() 44 | 45 | function readValue(): T | undefined { 46 | try { 47 | const rawValue = storage.getItem(key) 48 | if (rawValue) { 49 | return JSON.parse(rawValue) 50 | } else { 51 | return defaultValue 52 | } 53 | } catch (error) { 54 | console.warn(error) 55 | } 56 | } 57 | 58 | function writeValue(value: T | undefined) { 59 | if (typeof value === 'undefined') { 60 | storage.removeItem(key) 61 | } else { 62 | storage.setItem(key, JSON.stringify(value)) 63 | } 64 | } 65 | 66 | const _dv = readValue() 67 | _dv && (storageValue.value = _dv) 68 | 69 | watch( 70 | storageValue, 71 | (newValue) => { 72 | writeValue(newValue) 73 | }, 74 | { 75 | flush: 'post', 76 | deep: true 77 | } 78 | ) 79 | 80 | return storageValue 81 | } 82 | -------------------------------------------------------------------------------- /docs/event/useEventlistener.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 3 | 一个帮你处理`eventListener`的`hooks` 4 | 5 | > 1. 可自动在组件卸载时清除事件监听 6 | > 2. 可直接采用`ref`方式监听`Dom`事件 7 | 8 | ## 代码演示 9 | 10 | #### 基本使用 11 | 12 | 13 | #### 代码 14 | ::: details 点击查看代码 15 | <<< @/docs/.vuepress/components/useEventlistener.vue 16 | ::: 17 | 18 | ## API 19 | 20 | ```ts 21 | 22 | 23 | const elRef = useEventListener(eventType, handler, options?) 24 | ``` 25 | 26 | #### RetrunValue 27 | 28 | | 参数 | 说明 | 类型 | 29 | | ------- | ------------------------------- | ------------------------- | 30 | | `elRef` | 一个指定事件绑定元素的`ref`对象 | `Ref` | 31 | 32 | #### Params 33 | 34 | | 参数 | 说明 | 类型 | 35 | | ----------- | ---------------------- | -------------------------------------------------------------- | 36 | | `eventType` | 传递需要监听的事件类型 | `keyof WindowEventMap | keyof HTMLElementEventMap` | 37 | | `handler` | 监听事件的回调函数 | `(evt: WindowEventMap[key] | HTMLElementEventMap[key]) => any` | 38 | | `options` | 监听事件参数 | `{ dom?: TargetType } & Partial` | 39 | 40 | #### Options 41 | 42 | | 参数 | 说明 | 类型 | 43 | | --------- | --------------------------------------------------------------------- | ------------------------------------------------------------- | 44 | | `dom` | 接收绑定监听的元素,优先级高于`elRef` | `HTMLElement | Ref | () => HTMLElement | Window` | 45 | | `once` | 是否只执行一次就自动销毁事件监听 | `Boolean` | 46 | | `capture` | 表示 `listener` 会在该类型的事件捕获阶段传播到该 `EventTarget` 时触发 | `Boolean` | 47 | | `passive` | 如果设置了`true`,表示 `listener` 永远不会调用 `preventDefault()` | `Boolean` | 48 | -------------------------------------------------------------------------------- /__test__/useDrop.spec.ts: -------------------------------------------------------------------------------- 1 | import { useDrop, useDrag } from '../src' 2 | 3 | const mockUriEvent: any = (text: string) => ({ 4 | dataTransfer: { 5 | getData: (key: string) => (key === 'text/uri-list' && text) || null, 6 | setData: () => {} 7 | }, 8 | preventDefault: () => {}, 9 | persist: () => {} 10 | }) 11 | 12 | const mockDomEvent: any = (content: any) => ({ 13 | dataTransfer: { 14 | getData: () => JSON.stringify(content), 15 | setData: () => {} 16 | }, 17 | preventDefault: () => {}, 18 | persist: () => {} 19 | }) 20 | 21 | const mockTextEvent: any = (content: string) => ({ 22 | dataTransfer: { 23 | getData: () => null, 24 | setData: () => {}, 25 | items: [ 26 | { 27 | getAsString: (cb: any) => { 28 | cb(content) 29 | } 30 | } 31 | ] 32 | }, 33 | preventDefault: () => {}, 34 | persist: () => {} 35 | }) 36 | 37 | const mockFileEvent: any = (content: string[]) => ({ 38 | dataTransfer: { 39 | getData: () => null, 40 | setData: () => {}, 41 | files: content 42 | }, 43 | preventDefault: () => {}, 44 | persist: () => {} 45 | }) 46 | 47 | describe('test drop drag ', () => { 48 | const KEY = 'drag' 49 | test('test drag', () => { 50 | const dragProps = useDrag()(KEY) 51 | expect(dragProps.key).toBe(JSON.stringify(KEY)) 52 | const dragProps2 = useDrag()() 53 | expect(dragProps2.key(KEY)).toBe(JSON.stringify(KEY)) 54 | }) 55 | 56 | test('test drop ', async () => { 57 | const onDomFn = jest.fn() 58 | const onFilesFn = jest.fn() 59 | const onTextFn = jest.fn() 60 | const onUriFn = jest.fn() 61 | const [dropProps] = useDrop({ 62 | onDom: onDomFn, 63 | onFiles: onFilesFn, 64 | onText: onTextFn, 65 | onUri: onUriFn 66 | }) 67 | dropProps.onDrop(mockDomEvent('dom')) 68 | expect(onDomFn).toBeCalled() 69 | 70 | dropProps.onDrop(mockUriEvent('uri')) 71 | expect(onUriFn).toBeCalled() 72 | 73 | dropProps.onDrop(mockFileEvent(['file1', 'file2'])) 74 | expect(onFilesFn).toBeCalled() 75 | 76 | dropProps.onDrop(mockTextEvent('text')) 77 | expect(onTextFn).toBeCalled() 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | doc-dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | -------------------------------------------------------------------------------- /docs/.vuepress/components/useVirtualList.vue: -------------------------------------------------------------------------------- 1 | 27 | 67 | 97 | -------------------------------------------------------------------------------- /docs/UI/useDrop.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 一个帮你处理`Dom`元素`drag`/`drop`逻辑的`hooks` :tada: 3 | > 仅需要绑定一些`props`,如果您正在使用`jsx`可以直接通过扩展运算符展开来绑定`props` 4 | > 还能处理文件拖拽及超链接拖拽 5 | 6 | ## 代码演示 7 | #### 基本使用 8 | ::: tip Tips 9 | 1. 拖拽`item`到`drop`区域可将`customKey`值传递到`onDom`事件 10 | 2. 点选`drop`区域,然后进行粘贴操作,可以将粘贴内容传递到`onText`事件 11 | 3. 拖拽`uri`到`drop`区域可将`uri`传递到`onUri`事件 12 | 4. 拖拽文件到`drop`区域可将文件信息传递到`onFiles`事件 13 | ::: 14 | 15 | #### 代码 16 | ::: details 点击查看代码 17 | <<< @/docs/.vuepress/components/useDrop.vue 18 | ::: 19 | 20 | ## API 21 | ```ts 22 | const getDragProps = useDrag() 23 | const dragProps = getDragProps(key?) 24 | const [dropProps, isHovering] = useDrop({ 25 | onDom?: (content, evt?) => void 26 | onText?: (text, evt?) => void 27 | onUri?: (uri, evt?) => void 28 | onFiles?: (files, evt?) => void 29 | }) 30 | ``` 31 | 32 | #### RetrunValue 33 | | 参数 | 说明 | 类型 | 34 | | --- | --- | --- | 35 | | `getDragProps` | 获取`dragProps`的函数 | `(key?: any) => DragProps` | 36 | | `isHovering` | `drop`元素是否有`drag`元素覆盖在上方 | `ComputedRef` | 37 | 38 | #### DragProps 39 | ::: tip 40 | `getDragProps`是否传递`key`值,会直接影响`key`和`onDragStart`的生成 41 | 如果不传递`key`则需要在绑定`key`和`onDragStart`的时候显示的调用并传入`customKey`来生成`key`和`onDragStart`这一般用于可`drag`的元素是多个的情况 42 | ::: 43 | | 参数 | 说明 | 类型 | 44 | | --- | --- | --- | 45 | | `draggable` | 标识元素可拖拽 | `'true' as const` | 46 | | `key` | 可拖拽元素的key值 | `string` | `(key: any) => string` | 47 | | `onDragStart` | `dragstart`时间函数 | `(evt: DragEvent) => void` | `(key: any) => (evt: DragEvent) => void` | 48 | 49 | #### DropProps 50 | | 参数 | 说明 | 类型 | 51 | | --- | --- | --- | 52 | | `onDragOver` | `dragover`事件函数 | `(evt: DragEvent) => any` | 53 | | `onDragEnter` | `dragenter`事件函数 | `(evt: DragEvent) => any` | 54 | | `onDragLeave` | `dragleave`事件函数 | `(evt: DragEvent) => any` | 55 | | `onDrop` | `drop`事件函数 | `(evt: DragEvent) => any` | 56 | | `onPaste` | `paste`事件函数 | `(evt: ClipboardEvent) => any` | 57 | 58 | #### Params 59 | | 参数 | 说明 | 类型 | 60 | | --- | --- | --- | 61 | | `onDom` | 拖拽`Dom`时候的回调 | `(content: any, evt?: DragEvent) => void` | 62 | | `onText` | 粘贴内容时候的回调 | `(text: string, evt?: ClipboardEvent) => void` | 63 | | `onUri` | 拖拽`uri`时候的回调 | `(uri: string, evt?: DragEvent) => void` | 64 | | `onFiles` | 拖拽文件时候的回调 | `(files: File[], evt?: DragEvent) => void` | 65 | 66 | -------------------------------------------------------------------------------- /docs/.vuepress/components/useDrop.vue: -------------------------------------------------------------------------------- 1 | 26 | 62 | 90 | -------------------------------------------------------------------------------- /src/useRequest/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Service, 3 | BaseRequestOptions, 4 | BaseRequestOptionsWithFormat, 5 | RequestService, 6 | RequestOptions, 7 | RequestOptionsWithFormat, 8 | AsyncResult 9 | } from './utils/types' 10 | import { fetchProxy } from './utils/fetchProxy' 11 | import { useAsync } from './useAsync' 12 | 13 | export function useRequest( 14 | service: Service, 15 | config: BaseRequestOptions 16 | ): AsyncResult 17 | export function useRequest( 18 | service: Service, 19 | config: BaseRequestOptionsWithFormat 20 | ): AsyncResult 21 | // custom request 22 | export function useRequest( 23 | service: RequestService

, 24 | config: RequestOptions 25 | ): AsyncResult 26 | export function useRequest( 27 | service: RequestService

, 28 | config: RequestOptionsWithFormat 29 | ): AsyncResult 30 | 31 | export function useRequest(service: any, config: any = {}): any { 32 | const { requestMethod, ...rest } = config 33 | const userService = requestMethod || fetchProxy 34 | let finalRequest: () => Promise 35 | switch (typeof service) { 36 | case 'string': 37 | finalRequest = (...args: any[]) => userService(service, ...args) 38 | break 39 | 40 | case 'object': 41 | { 42 | const { url, ...rest } = service 43 | finalRequest = (...args: any[]) => 44 | requestMethod 45 | ? requestMethod(service, ...args) 46 | : fetchProxy(url, rest) 47 | } 48 | break 49 | 50 | default: 51 | finalRequest = (...args: any[]) => 52 | new Promise((resolve, reject) => { 53 | const s = service(...args) 54 | let fn = s 55 | if (!s?.then) { 56 | switch (typeof s) { 57 | case 'string': 58 | fn = userService(s) 59 | break 60 | 61 | case 'object': 62 | { 63 | const { url, ...rest } = s 64 | fn = requestMethod ? requestMethod(s) : fetchProxy(url, rest) 65 | } 66 | break 67 | } 68 | } 69 | fn.then(resolve).catch(reject) 70 | }) 71 | } 72 | 73 | return useAsync(finalRequest, rest) 74 | } 75 | -------------------------------------------------------------------------------- /docs/.vuepress/components/useHistoryTravel.vue: -------------------------------------------------------------------------------- 1 | 31 | 73 | 103 | -------------------------------------------------------------------------------- /src/useDrop/useDrop.ts: -------------------------------------------------------------------------------- 1 | import { ref, Ref, computed, ComputedRef } from 'vue-demi' 2 | type DropOpt = { 3 | onDom?: (content: any, evt?: DragEvent) => void 4 | onText?: (text: string, evt?: ClipboardEvent) => void 5 | onUri?: (uri: string, evt?: DragEvent) => void 6 | onFiles?: (files: File[], evt?: DragEvent) => void 7 | } 8 | type DragEventHandler = (evt: DragEvent) => any 9 | type DropProps = { 10 | onDragOver: DragEventHandler 11 | onDragEnter: DragEventHandler 12 | onDragLeave: DragEventHandler 13 | onDrop: DragEventHandler 14 | onPaste: (evt: ClipboardEvent) => any 15 | } 16 | 17 | type CallbackFn = ( 18 | dataTransfer: DataTransfer, 19 | evt: DragEvent | ClipboardEvent 20 | ) => any 21 | 22 | function getProps(callback: CallbackFn, isHovering: Ref): DropProps { 23 | return { 24 | onDragOver(evt) { 25 | evt.preventDefault() 26 | }, 27 | onDragEnter(evt) { 28 | evt.preventDefault() 29 | isHovering.value = true 30 | }, 31 | onDragLeave(evt) { 32 | evt.preventDefault() 33 | isHovering.value = false 34 | }, 35 | onDrop(evt) { 36 | evt.preventDefault() 37 | isHovering.value = false 38 | callback(evt.dataTransfer!, evt) 39 | }, 40 | onPaste(evt) { 41 | evt.preventDefault() 42 | callback(evt.clipboardData!, evt) 43 | } 44 | } 45 | } 46 | 47 | export function useDrop( 48 | options: DropOpt = {} 49 | ): [DropProps, ComputedRef] { 50 | const isHovering = ref(false) 51 | const callback: CallbackFn = (dataTransfer, evt) => { 52 | const uri = dataTransfer?.getData('text/uri-list') 53 | const dom = dataTransfer?.getData('custom') 54 | if (dom && options?.onDom) { 55 | options.onDom(dom, evt as DragEvent) 56 | return 57 | } 58 | if (uri && options?.onUri) { 59 | options.onUri(uri, evt as DragEvent) 60 | return 61 | } 62 | if ( 63 | dataTransfer.files && 64 | dataTransfer.files.length > 0 && 65 | options?.onFiles 66 | ) { 67 | options.onFiles([...Array.from(dataTransfer.files)], evt as DragEvent) 68 | return 69 | } 70 | if ( 71 | dataTransfer.items && 72 | dataTransfer.items.length > 0 && 73 | options?.onText 74 | ) { 75 | dataTransfer.items[0].getAsString((text) => { 76 | options.onText!(text, evt as ClipboardEvent) 77 | }) 78 | return 79 | } 80 | } 81 | return [getProps(callback, isHovering), computed(() => isHovering.value)] 82 | } 83 | -------------------------------------------------------------------------------- /__test__/useClickOutside.spec.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { renderComposable } from './utils' 3 | import { useClickOutside } from '../src/useClickOutside' 4 | 5 | describe('test use click outside ', () => { 6 | let div: HTMLDivElement, button: HTMLButtonElement 7 | beforeEach(() => { 8 | div = document.createElement('div') 9 | button = document.createElement('button') 10 | document.body.appendChild(button) 11 | document.body.appendChild(div) 12 | }) 13 | 14 | afterEach(() => { 15 | document.body.removeChild(button) 16 | document.body.removeChild(div) 17 | }) 18 | 19 | test('test listener by dom ', async () => { 20 | const fn = jest.fn() 21 | const wrapper = renderComposable(() => { 22 | const elRef = useClickOutside(fn, button) 23 | return { elRef } 24 | }) 25 | const { vm } = wrapper 26 | vm.$data.elRef = button 27 | await vm.$nextTick() 28 | expect(vm.$data.elRef).toBeDefined() 29 | await button.click() 30 | expect(fn).not.toBeCalled() 31 | await div.click() 32 | expect(fn).toBeCalled() 33 | await div.click() 34 | expect(fn).toBeCalledTimes(2) 35 | 36 | wrapper.destroy() 37 | }) 38 | 39 | test('test listener by dom func ', async () => { 40 | const fn = jest.fn() 41 | const wrapper = renderComposable(() => { 42 | const elRef = useClickOutside(fn, () => button) 43 | return { elRef } 44 | }) 45 | const { vm } = wrapper 46 | vm.$data.elRef = button 47 | await vm.$nextTick() 48 | expect(vm.$data.elRef).toBeDefined() 49 | await button.click() 50 | expect(fn).not.toBeCalled() 51 | await div.click() 52 | expect(fn).toBeCalled() 53 | await div.click() 54 | expect(fn).toBeCalledTimes(2) 55 | 56 | wrapper.destroy() 57 | }) 58 | 59 | test('test listener by ref ', async () => { 60 | const fn = jest.fn() 61 | const root = document.createElement('div') 62 | div.appendChild(root) 63 | const wrapper = mount( 64 | { 65 | setup() { 66 | const elRef = useClickOutside(fn) 67 | return { elRef } 68 | }, 69 | template: ` 70 | 71 | ` 72 | }, 73 | { attachTo: root } 74 | ) 75 | const { vm } = wrapper 76 | await vm.$nextTick() 77 | expect(vm.$data.elRef).toBeDefined() 78 | await wrapper.find('button').trigger('click') 79 | expect(fn).not.toBeCalled() 80 | await div.click() 81 | expect(fn).toBeCalled() 82 | await div.click() 83 | expect(fn).toBeCalledTimes(2) 84 | 85 | wrapper.destroy() 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /docs/work/useSku.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 一个帮你处理`sku`逻辑的`hooks` 3 | > 你仅需提供一个符合结构的`spu`数据 4 | > 您甚至还可以通过`spuOpt`来定制您现有数据结构的特殊键名 5 | 6 | ## 代码演示 7 | #### 基本使用 8 | ::: tip 9 | 通常情况需要`spu`数据满足如下结构: 10 | ```ts 11 | spu: { 12 | [key: stirng]: any 13 | skuListKey: [ 14 | { 15 | [key: stirng]: any 16 | specListKey: [ 17 | { 18 | [key: stirng]: any 19 | specValueKey: any 20 | specValueIdKey: any 21 | specIdKey: any 22 | } 23 | ] 24 | } 25 | ] 26 | } 27 | ``` 28 | ::: 29 | 30 | #### 代码 31 | ::: details 点击查看代码 32 | <<< @/docs/.vuepress/components/useSku.vue 33 | ::: 34 | 35 | #### 定制化使用 36 | ::: tip 37 | 在定制化的情况下,我们任然需要维持`spu`的基本结构,对于`spu`的键值进行定制,比如当前情况的定制内容: 38 | ```ts 39 | spuOpt = { 40 | getSkuList(spu) { 41 | return spu.skuList 42 | }, 43 | getSkuSpecList(sku) { 44 | return sku.specs 45 | }, 46 | } 47 | ``` 48 | ::: 49 | 50 | 51 | #### 代码 52 | ::: details 点击查看代码 53 | <<< @/docs/.vuepress/components/useSkuCustom.vue 54 | ::: 55 | 56 | ## API 57 | ```ts 58 | const { skuList, specTap } = useSku(spu, spuOpt?) 59 | ``` 60 | 61 | #### RetrunValue 62 | | 参数 | 说明 | 类型 | 63 | | --- | --- | --- | 64 | | `skuList` | 包含整个`sku`算法需要显示的规格数据 | `SpecLineInstanceType[]` | 65 | | `specTap` | 点击规格时候调用函数 | `(spec) => void` | 66 | 67 | #### SpecLineInstanceType 68 | | 参数 | 说明 | 类型 | 69 | | --- | --- | --- | 70 | | `specLineTitle` | 当前规格行的规格名称 | `string` | 71 | | `specs` | 当前规格行的所有规格 | `SpecInstanceType[]` | 72 | | `row` | 当前规格行的行号 | `number` | 73 | 74 | #### SpecInstanceType 75 | | 参数 | 说明 | 类型 | 76 | | --- | --- | --- | 77 | | `specValue` | 当前规格的规格值 | `string` | 78 | | `specId` | 当前规格的规格id | `strin | number` | 79 | | `specValueId` | 当前规格的规格值id | `string | number` | 80 | | `status` | 当前规格的状态 | `SpecStatus` | 81 | 82 | #### SpecStatus 83 | | 参数 | 说明 | 类型 | 84 | | --- | --- | --- | 85 | | `PENDING` | 等待选定状态 | `pending` | 86 | | `DISABLED` | 禁用状态 | `disabled` | 87 | | `SELECTED` | 选中状态 | `selected` | 88 | 89 | #### Params 90 | | 参数 | 说明 | 类型 | 91 | | --- | --- | --- | 92 | | `spu` | `spu`数据 | `any` | 93 | | `spuOpt` | `spu`数据相关的定制内容 | `SpuOps` | 94 | 95 | #### SpuOps 96 | | 参数 | 说明 | 类型 | 97 | | --- | --- | --- | 98 | | `skuCodeJoiner` | `sku`中不同规格的连接符 | `string` | 99 | | `specCodeJoiner` | 规格中`id`的连接符 | `string` | 100 | | `getSkuList` | 从`spu`中获取`sku`列表的方法 | `(spu) => skuList` | 101 | | `getSkuSpecList` | 从`sku`中获取规格列表的方法 | `(sku) => specList` | 102 | | `getSkuId` | 从`sku`中获取`skuId`的方法 | `(sku) => skuId` | 103 | | `getSpecId` | 从规格中获取规格id的方法 | `(spec) => specId` | 104 | | `getspecValueId` | 从规格中获取规格值id的方法 | `(spec) => specValueId` | 105 | | `getSpecTitle` | 从规格中获取规格名称的方法 | `(spec) => specTitle` | 106 | | `getspecValue` | 从规格中获取规格值名称的方法 | `(spec) => specValue` | 107 | -------------------------------------------------------------------------------- /docs/.vuepress/mock/sku.js: -------------------------------------------------------------------------------- 1 | export const MockData = [ 2 | { 3 | skuId: '1', 4 | spuSpecValues: [ 5 | { 6 | specId: 'a', 7 | specName: '颜色', 8 | specValueId: '11', 9 | specValueRemark: '黑色', 10 | }, 11 | { 12 | specId: 'b', 13 | specName: '尺寸', 14 | specValueId: '101', 15 | specValueRemark: 'L', 16 | }, 17 | ], 18 | }, 19 | { 20 | skuId: '2', 21 | spuSpecValues: [ 22 | { 23 | specId: 'a', 24 | specName: '颜色', 25 | specValueId: '12', 26 | specValueRemark: '绿色', 27 | }, 28 | { 29 | specId: 'b', 30 | specName: '尺寸', 31 | specValueId: '102', 32 | specValueRemark: 'S', 33 | }, 34 | ], 35 | }, 36 | { 37 | skuId: '2', 38 | spuSpecValues: [ 39 | { 40 | specId: 'a', 41 | specName: '颜色', 42 | specValueId: '12', 43 | specValueRemark: '绿色', 44 | }, 45 | { 46 | specId: 'b', 47 | specName: '尺寸', 48 | specValueId: '103', 49 | specValueRemark: 'XS', 50 | }, 51 | ], 52 | }, 53 | ] 54 | 55 | export function generateSku() { 56 | return MockData.map(sku => { 57 | return sku.spuSpecValues.map(spec => spec.specValueRemark).join('-') 58 | }) 59 | } 60 | 61 | export const MockDataCustomKey = { 62 | skuList: [ 63 | { 64 | skuId: '1', 65 | specs: [ 66 | { 67 | specId: 'a', 68 | specName: '颜色', 69 | specValueId: '11', 70 | specValueRemark: '黑色', 71 | }, 72 | { 73 | specId: 'b', 74 | specName: '尺寸', 75 | specValueId: '101', 76 | specValueRemark: 'L', 77 | }, 78 | ], 79 | }, 80 | { 81 | skuId: '2', 82 | specs: [ 83 | { 84 | specId: 'a', 85 | specName: '颜色', 86 | specValueId: '12', 87 | specValueRemark: '绿色', 88 | }, 89 | { 90 | specId: 'b', 91 | specName: '尺寸', 92 | specValueId: '102', 93 | specValueRemark: 'S', 94 | }, 95 | ], 96 | }, 97 | { 98 | skuId: '2', 99 | specs: [ 100 | { 101 | specId: 'a', 102 | specName: '颜色', 103 | specValueId: '12', 104 | specValueRemark: '绿色', 105 | }, 106 | { 107 | specId: 'b', 108 | specName: '尺寸', 109 | specValueId: '103', 110 | specValueRemark: 'XS', 111 | }, 112 | ], 113 | }, 114 | ] 115 | } 116 | 117 | export const spuOpt = { 118 | getSkuList(spu) { 119 | return spu.skuList 120 | }, 121 | getSkuSpecList(sku) { 122 | return sku.specs 123 | }, 124 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import ts from 'rollup-plugin-typescript2' 3 | import dts from 'rollup-plugin-dts' 4 | 5 | const resolve = (p) => path.resolve(__dirname, p) 6 | let formats = process.env.FORMATS 7 | const needBuildTypes = !!process.env.BUILDTYPES 8 | const pkgJson = require(resolve('./package.json')) 9 | 10 | const outputConfigMap = { 11 | esm: { 12 | file: resolve(`dist/vue.reuse.esm.js`), 13 | format: 'es', 14 | }, 15 | cjs: { 16 | file: resolve(`dist/vue.reuse.cjs.js`), 17 | format: 'cjs', 18 | }, 19 | umd: { 20 | file: resolve(`dist/vue.reuse.js`), 21 | format: 'umd', 22 | name: 'VueReuse' 23 | }, 24 | } 25 | 26 | let RollupConfigs = [] 27 | 28 | formats = formats ? formats.split('-') : Object.keys(outputConfigMap) 29 | 30 | formats.forEach(format => { 31 | RollupConfigs.push(createConfig(format, outputConfigMap[format])) 32 | }) 33 | 34 | needBuildTypes && 35 | RollupConfigs.push({ 36 | input: resolve(`dist/src/index.d.ts`), 37 | output: { 38 | file: resolve(`dist/vue.reuse.d.ts`), 39 | format: 'es', 40 | }, 41 | plugins: [dts()], 42 | }) 43 | 44 | export default RollupConfigs 45 | 46 | function createConfig(format, output) { 47 | if (format === 'umd') { 48 | output.sourcemap = !!process.env.SOURCEMAP 49 | } 50 | // 打包ts 51 | const tsPlugin = ts({ 52 | check: process.env.NODE_ENV === 'production', 53 | tsconfigDefaults: resolve('tsconfig.json'), 54 | cacheRoot: resolve('./node_modules/.ts2_cache'), 55 | tsconfigOverride: { 56 | compilerOptions: { 57 | target: 'es5', 58 | sourceMap: !!process.env.SOURCEMAP, 59 | removeComments: true, 60 | declaration: needBuildTypes, 61 | declarationMap: needBuildTypes, 62 | }, 63 | include: [`src/`], 64 | exclude: ['__test__/**'], 65 | }, 66 | }) 67 | // external 68 | const external = [ 69 | ...Object.keys(pkgJson.peerDependencies || {}), 70 | ...Object.keys(pkgJson.dependencies || {}), 71 | ...(pkgJson.external || []) 72 | ] 73 | // node plugins 74 | const nodePlugins = 75 | format === 'cjs' 76 | ? [] 77 | : [ 78 | require('@rollup/plugin-node-resolve').nodeResolve({ 79 | referBuiltins: true, 80 | }), 81 | require('@rollup/plugin-commonjs')({ 82 | sourceMap: true, 83 | }), 84 | ] 85 | // prod plugins 86 | const prodPlugins = 87 | process.env.NODE_ENV === 'production' 88 | ? [ 89 | require('rollup-plugin-terser').terser({ 90 | module: format === 'esm', 91 | compress: { 92 | ecma: 2015, 93 | }, 94 | }) 95 | ] 96 | : [] 97 | 98 | return { 99 | input: resolve('src/index.ts'), 100 | output, 101 | plugins: [tsPlugin, ...nodePlugins, ...prodPlugins], 102 | external, 103 | treeshake: { 104 | moduleSideEffects: false, 105 | }, 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | module.exports = { 3 | title: 'vue-reuse', 4 | description: 'vue hooks based composition API', 5 | plugins: ['@vuepress/back-to-top'], 6 | dest: 'doc-dist', 7 | base: process.env.DOCS_BASE || '/vue-reuse/', 8 | configureWebpack: { 9 | resolve: { 10 | alias: { 11 | '@xus/vue-reuse': path.resolve(__dirname, '../../src/index.ts') 12 | }, 13 | extensions: ['.ts', '.js', '.styl'] 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.tsx?$/, 19 | use: { 20 | loader: 'babel-loader', 21 | options: { 22 | presets: ['@babel/preset-env', '@babel/preset-typescript'], 23 | plugins: ['@babel/plugin-proposal-class-properties'] 24 | } 25 | }, 26 | exclude: /node_modules/ 27 | } 28 | ] 29 | } 30 | }, 31 | // head: [], 32 | themeConfig: { 33 | repo: 'xus-code/vue-reuse', 34 | docsRepo: 'xus-code/vue-reuse', 35 | logo: '/imgs/logo.png', 36 | docsDir: 'docs', 37 | editLinks: false, 38 | nav: [ 39 | { text: '国内镜像', link: 'https://a-sir.gitee.io/vue-reuse' }, 40 | { 41 | text: '我有疑问', 42 | link: 'https://github.com/xuguo-code/vue-reuse/issues' 43 | }, 44 | { text: 'vue3源码分析', link: 'http://xuguo.xyz/vue3-anaylsis' } 45 | ], 46 | sidebar: { 47 | '/': [ 48 | ['info/', '指南'], 49 | { 50 | title: 'async', 51 | collapsable: false, 52 | children: [['async/useRequest', 'useRequest']] 53 | }, 54 | { 55 | title: 'UI', 56 | collapsable: false, 57 | children: [ 58 | ['UI/useDrop', 'useDrop'], 59 | ['UI/useVirtualList', 'useVirtualList'] 60 | ] 61 | }, 62 | { 63 | title: 'work', 64 | collapsable: false, 65 | children: [['work/useSku', 'useSku']] 66 | }, 67 | { 68 | title: 'Event', 69 | collapsable: false, 70 | children: [ 71 | ['event/useEventlistener', 'useEventlistener'], 72 | ['event/useClickOutside', 'useClickOutside'], 73 | ['event/useScroll', 'useScroll'] 74 | ] 75 | }, 76 | { 77 | title: 'State', 78 | collapsable: false, 79 | children: [ 80 | ['state/useHistoryTravel', 'useHistoryTravel'], 81 | ['state/useLocalStorage', 'useLocalStorage'], 82 | ['state/useSessionStorage', 'useSessionStorage'], 83 | ['state/useBoolean', 'useBoolean'] 84 | ] 85 | }, 86 | { 87 | title: 'sideEffect', 88 | collapsable: false, 89 | children: [ 90 | ['sideEffect/useDebounceRef', 'useDebounceRef'], 91 | ['sideEffect/useDebounceFn', 'useDebounceFn'], 92 | ['sideEffect/useThrottleRef', 'useThrottleRef'], 93 | ['sideEffect/useThrottleFn', 'useThrottleFn'] 94 | ] 95 | } 96 | ] 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /.github/dev.md: -------------------------------------------------------------------------------- 1 | #### 开发指南 2 | 3 | 目前仓库推荐两种开发模式: 文档驱动和测试驱动 4 | 5 | > 本文以 `useDebounceFn` 为示例 6 | 7 | - 文档驱动 8 | 9 | 文档驱动的开发就类似平常的业务开发流程,由于文档系统本就是一个基于 `vue` 的应用, 10 | 11 | 所以首先的第一步就是通过 `npm run docs:dev` / `yarn docs:dev` 指令启动文档应用; 12 | 13 | 接下来将采问答的形式来阐述整个开发环节。 14 | 15 | 1. 如何创建一个 `useDebounceFn` 的文档页面? 16 | 17 | 我们通常会在 `doc` 文件夹下来进行文档工作,具体文件位置如下: 18 | 19 | ``` 20 | |-doc 21 | |-sideEffct 22 | |-useDebounceFn.md 23 | ``` 24 | 25 | `useDebounceFn` 被归纳到 `sideEffect` 类别之中,所以在 `sideEffect` 下创建相应的 `.md` 文档,这是第一步; 26 | 27 | 建立完文档后,我们需要进行菜单配置如下: 28 | 29 | ```js 30 | |-doc 31 | |-.vuepress 32 | |-config 33 | 34 | // config.js 35 | sidebar: { 36 | '/': [ 37 | ... 38 | { 39 | title: 'sideEffect', 40 | collapsable: false, 41 | children: [ 42 | ['sideEffect/useDebounceFn', 'useDebounceFn'], 43 | ], 44 | }, 45 | ], 46 | }, 47 | ``` 48 | 49 | 至此文档页面已经配置完成。 50 | 51 | 2. 如何在文档页面中书写 `hooks` 示例? 52 | 53 | 同样我们将其分为两步,第一步声明用来书写示例的 `vue` 组件: 54 | 55 | ```js 56 | |-doc 57 | |-.vuepress 58 | |-components 59 | |-useDebounceFn.vue 60 | ``` 61 | 62 | 在这个 `.vue` 文件中我们基于 `vue2` + `composition api` 的形式来编写示例。 63 | 64 | 第二步就是在文档页面中引入我们书写的示例: 65 | 66 | ```markdown 67 | #### 基本使用 68 | 69 | 70 | #### 代码 71 | ::: details 点击查看代码 72 | <<< @/docs/.vuepress/components/useDebounceFn.vue 73 | ::: 74 | ``` 75 | 76 | 这就是 `vuepress` 中 一个最常用的在 `md` 中使用组件的示例。 77 | 78 | 3. 如何在示例代码中使用我们在 `src` 下书写的 `hook` 源码? 79 | 80 | ```vue 81 | 87 | 104 | ``` 105 | 106 | 我们仅需要通过 `@xus/vue-reuse` 引入的形式拿到我们实时编写的 `hook` 函数。 107 | 108 | 通过以上三个个问题基本能将基于文档开发如何进行的流程表达清楚,我们通过一个示例代码来驱动 `hooks` 的开发的模式和业务开发较为形似。 109 | 110 | - 测试驱动 111 | 112 | 测试驱动开发是基于我们设计的 `API` 接口进行的,首先我们需要明确的是 当前开发 `hook` 的功能和输入输出; 113 | 114 | 以 `useDebounceFn` 为例,我们的主要功能是生成防抖函数,而输入就是目标函数 `fn` 和防抖时长 `wait`, 115 | 116 | 输出就是取消运行 `cancel` 和运行 `run` 两个函数。 117 | 118 | 1. 如何创建测试文件和启动测试系统? 119 | 120 | 我们在 `__test__` 文件夹下创建如下文件: 121 | 122 | ```js 123 | |-__test__ 124 | |-useDebounceFn.spec.ts 125 | ``` 126 | 127 | 通过 `yarn test -o -w` / `npm run test -o -w` 来启动测试系统。 128 | 129 | 2. 测试驱动的模式是怎么执行的? 130 | 131 | 通过输入输出以及需求的确定我们很容易编写如下测试用例: 132 | 133 | ```js 134 | import { useDebounceFn } from '../src' 135 | 136 | test('fn will be called when run', () => { 137 | const fn = jest.fn() 138 | const { run } = useDebounceFn(fn, 500) 139 | run(1) 140 | run() 141 | nextTask(() => { 142 | expect(fn).toBeCalledTimes(1) 143 | expect(fn).toBeCalledWith() 144 | }) 145 | }) 146 | ``` 147 | 148 | 在 `useDebounceFn` 未实现的情况下,测试运行的结果肯定是 `failed` 我们通过编写 `hook` 代码来解决 `failed` 以达到开发完整功能的目的。 149 | 150 | - 总结 151 | 152 | 至此已经介绍完这两种开发模式了,具体的语法细节还需要参照 `vuepress` 、`jest` 以及 `@vue/test-utils` 的文档来进行具体的熟悉。 153 | -------------------------------------------------------------------------------- /src/useRequest/utils/types.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from 'vue-demi' 2 | type ToRefs = { 3 | [P in keyof T]: Ref 4 | } 5 | export type noop = (...args: any[]) => any 6 | 7 | // ------------- useRequest ------------- // 8 | export type RequestService

= 9 | | string 10 | | { url: string; [key: string]: any } 11 | | ((...args: P) => string) 12 | | ((...args: P) => { url: string; [key: string]: any }) 13 | 14 | export interface BaseRequestOptions { 15 | defaultLoading?: boolean 16 | defaultParams?: P 17 | defaultData?: R 18 | 19 | onSuccess?: (data: R, params: P) => void 20 | onError?: (error: Error, params: P) => void 21 | 22 | manual?: boolean 23 | 24 | loadingDelay?: number 25 | pollingTime?: number 26 | debounceTime?: number 27 | throttleTime?: number 28 | 29 | throwOnError?: boolean 30 | // 缓存 31 | cacheKey?: string 32 | cacheTime?: number 33 | fetchKey?: (...args: P) => string 34 | } 35 | 36 | export type BaseRequestOptionsWithFormat< 37 | R, 38 | P extends any[], 39 | U, 40 | UU extends U 41 | > = { 42 | formatResult: (res: R) => U 43 | } & BaseRequestOptions 44 | 45 | // custom request 46 | export type RequestOptionsWithFormat< 47 | R, 48 | P extends any[], 49 | U, 50 | UU extends U, 51 | S extends RequestService

52 | > = { 53 | requestMethod?: S extends (...args: P) => infer T 54 | ? (param: T) => Promise 55 | : (param: S, ...args: P) => Promise 56 | formatResult: (res: R) => U 57 | } & BaseRequestOptions 58 | 59 | export type RequestOptions> = { 60 | requestMethod?: S extends (...args: any[]) => infer T 61 | ? (param: T) => Promise 62 | : (param: S, ...args: P) => Promise 63 | } & BaseRequestOptions 64 | 65 | // ------------- Fetch ------------- // 66 | export type Service = (...args: P) => Promise 67 | 68 | export type Subscribe = (result: FetchResult) => any 69 | 70 | export interface Fetches { 71 | [key: string]: FetchResult 72 | } 73 | 74 | export interface FetchResult { 75 | loading: boolean 76 | data: R | undefined 77 | error: Error | undefined 78 | params: P | undefined 79 | cancel: noop 80 | refresh: () => Promise 81 | run: (...args: P) => Promise 82 | unmount: () => void 83 | } 84 | 85 | export type CurFetchResult = Pick< 86 | FetchResult, 87 | Exclude, 'run' | 'unmount'> 88 | > 89 | 90 | export interface FetchConfig { 91 | // 请求结果格式化 92 | formatResult?: (res: any) => R 93 | 94 | onSuccess?: (data: any, params: P) => void 95 | onError?: (error: Error, params: P) => void 96 | 97 | pollingTime?: number 98 | 99 | loadingDelay?: number 100 | 101 | debounceTime?: number 102 | throttleTime?: number 103 | 104 | throwOnError?: boolean 105 | } 106 | 107 | // ------------- useAsync ------------- // 108 | export type AsyncOptions = BaseRequestOptions 109 | export type AsyncOptionsWithFormat = { 110 | formatResult: (res: R) => U 111 | } & BaseRequestOptions 112 | 113 | export type AsyncOptionsAll = 114 | | AsyncOptions 115 | | AsyncOptionsWithFormat 116 | 117 | export interface AsyncResult 118 | extends ToRefs> { 119 | reset: () => void 120 | run: (...args: P) => Promise 121 | fetches: { 122 | [key: string]: FetchResult 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/useHistoryTravel/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Ref, 3 | watch, 4 | ref, 5 | UnwrapRef, 6 | getCurrentInstance, 7 | onUnmounted, 8 | computed, 9 | ComputedRef 10 | } from 'vue-demi' 11 | 12 | type OptionalUnwrapRef = UnwrapRef | undefined 13 | 14 | type ReturnValue = { 15 | current: Ref> 16 | go(step: number): void 17 | forward(): void 18 | back(): void 19 | forwardLength: ComputedRef 20 | backLength: ComputedRef 21 | } 22 | 23 | type TravelState = { 24 | past: T[] 25 | future: T[] 26 | } 27 | 28 | enum UpdateStateFrom { 29 | GO, 30 | USER 31 | } 32 | 33 | function dumpIndex(step: number, arr: any[]) { 34 | let index = step > 0 ? step - 1 : arr.length + step 35 | if (index >= arr.length) { 36 | index = arr.length - 1 37 | } 38 | if (index < 0) { 39 | index = 0 40 | } 41 | return index 42 | } 43 | function split(step: number, arr: T[]) { 44 | const index = dumpIndex(step, arr) 45 | 46 | return { 47 | _before: arr.slice(0, index), 48 | _after: arr.slice(index + 1), 49 | _current: arr[index] 50 | } 51 | } 52 | 53 | export function useHistoryTravel(initialValue?: T): ReturnValue { 54 | const current = ref(initialValue) 55 | const _backLength = ref(0) 56 | const _forwardLength = ref(0) 57 | let updateFrom: UpdateStateFrom = UpdateStateFrom.USER 58 | const travelState: TravelState> = { 59 | past: [], 60 | future: [] 61 | } 62 | 63 | function updatePast(oldState: OptionalUnwrapRef[], fullUpdate = false) { 64 | if (fullUpdate) { 65 | travelState.past = oldState 66 | } else { 67 | travelState.past = [...travelState.past, ...oldState] 68 | } 69 | } 70 | 71 | function updateFuture(oldState: OptionalUnwrapRef[], fullUpdate = false) { 72 | if (fullUpdate) { 73 | travelState.future = oldState 74 | } else { 75 | travelState.future = [...oldState, ...travelState.future] 76 | } 77 | } 78 | 79 | function updateLength() { 80 | _backLength.value = travelState.past.length 81 | _forwardLength.value = travelState.future.length 82 | } 83 | 84 | function _back(step = -1) { 85 | if (travelState.past.length <= 0) return 86 | const { _after, _current, _before } = split(step, travelState.past) 87 | updatePast(_before, true) 88 | updateFuture([..._after, current.value]) 89 | current.value = _current 90 | } 91 | 92 | function _forward(step = 1) { 93 | if (travelState.future.length <= 0) return 94 | const { _after, _current, _before } = split(step, travelState.future) 95 | updatePast([current.value, ..._before]) 96 | updateFuture(_after, true) 97 | current.value = _current 98 | } 99 | 100 | function go(step = 0) { 101 | if (step === 0) { 102 | return 103 | } 104 | updateFrom = UpdateStateFrom.GO 105 | if (step > 0) { 106 | return _forward(step) 107 | } 108 | return _back(step) 109 | } 110 | 111 | const stop = watch( 112 | current, 113 | (_, oldState) => { 114 | if (updateFrom === UpdateStateFrom.USER) { 115 | updatePast([oldState]) 116 | } else { 117 | updateFrom = UpdateStateFrom.USER 118 | } 119 | updateLength() 120 | }, 121 | { flush: 'sync' } 122 | ) 123 | 124 | if (getCurrentInstance()) { 125 | onUnmounted(() => stop()) 126 | } 127 | 128 | return { 129 | current, 130 | go, 131 | forward: () => go(1), 132 | back: () => go(-1), 133 | backLength: computed(() => _backLength.value), 134 | forwardLength: computed(() => _forwardLength.value) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xus/vue-reuse", 3 | "description": "composition-api functions reuse your code", 4 | "version": "0.6.0", 5 | "main": "dist/vue.reuse.cjs.js", 6 | "module": "dist/vue.reuse.esm.js", 7 | "types": "dist/vue.reuse.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "scripts": { 12 | "init": "rm -rf node_modules && yarn", 13 | "lint": "eslint --ext .ts src/**/*", 14 | "format": "prettier --write --parser typescript \"src/**/*.ts?(x)\"", 15 | "cm": "git-cz", 16 | "test": "jest", 17 | "docs:dev": "vuepress dev docs", 18 | "docs:build": "vuepress build docs", 19 | "build": "node scripts/build.js", 20 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1", 21 | "release": "node scripts/release.js" 22 | }, 23 | "husky": { 24 | "hooks": { 25 | "pre-commit": "lint-staged", 26 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 27 | } 28 | }, 29 | "lint-staged": { 30 | "*.js": [ 31 | "prettier --write" 32 | ], 33 | "*.ts?(x)": [ 34 | "eslint", 35 | "prettier --parser=typescript --write" 36 | ] 37 | }, 38 | "config": { 39 | "commitizen": { 40 | "path": "cz-conventional-changelog" 41 | } 42 | }, 43 | "repository": { 44 | "type": "git", 45 | "url": "git+https://github.com/xus-code/vue-reuse.git" 46 | }, 47 | "keywords": [ 48 | "vue", 49 | "composition-api", 50 | "hooks", 51 | "vue3", 52 | "composable" 53 | ], 54 | "author": "xuguo", 55 | "license": "MIT", 56 | "bugs": { 57 | "url": "https://github.com/xus-code/vue-reuse/issues" 58 | }, 59 | "homepage": "https://github.com/xus-code/vue-reuse#readme", 60 | "sideEffects": false, 61 | "external": [ 62 | "vue" 63 | ], 64 | "peerDependencies": { 65 | "@xuguo/sku": "^1.1.5", 66 | "vue-demi": "^0.2.0" 67 | }, 68 | "dependencies": { 69 | "@vue/composition-api": "^1.0.0-beta.22", 70 | "@xuguo/sku": "^1.1.5", 71 | "@xuguo/toolbox": "^1.2.0", 72 | "lodash": "^4.17.20" 73 | }, 74 | "devDependencies": { 75 | "@babel/core": "^7.11.1", 76 | "@babel/plugin-proposal-class-properties": "^7.12.1", 77 | "@babel/plugin-proposal-optional-chaining": "^7.12.7", 78 | "@babel/preset-env": "^7.11.0", 79 | "@babel/preset-typescript": "^7.10.4", 80 | "@commitlint/cli": "^9.1.2", 81 | "@commitlint/config-conventional": "^9.1.2", 82 | "@rollup/plugin-commonjs": "^15.0.0", 83 | "@rollup/plugin-node-resolve": "^9.0.0", 84 | "@types/jest": "^26.0.10", 85 | "@types/lodash": "^4.14.165", 86 | "@typescript-eslint/eslint-plugin": "^3.9.1", 87 | "@typescript-eslint/parser": "^3.9.1", 88 | "@vue/test-utils": "^1.0.0-beta.10", 89 | "@vuepress/plugin-back-to-top": "^1.5.3", 90 | "babel-jest": "^26.6.3", 91 | "chalk": "^4.1.0", 92 | "codecov": "^3.8.1", 93 | "commitizen": "^4.1.2", 94 | "conventional-changelog-cli": "^2.1.0", 95 | "cz-conventional-changelog": "^3.2.0", 96 | "enquirer": "^2.3.6", 97 | "eslint": "^7.7.0", 98 | "eslint-config-prettier": "^6.11.0", 99 | "eslint-plugin-jest": "^23.20.0", 100 | "execa": "^4.0.3", 101 | "fs-extra": "^9.0.1", 102 | "jest": "^26.6.3", 103 | "lint-staged": "^10.2.11", 104 | "minimist": "^1.2.5", 105 | "prettier": "^2.0.5", 106 | "rollup": "^2.26.4", 107 | "rollup-plugin-dts": "^1.4.10", 108 | "rollup-plugin-terser": "^7.0.0", 109 | "rollup-plugin-typescript2": "^0.27.2", 110 | "semver": "^7.3.2", 111 | "ts-jest": "^26.4.4", 112 | "ts-loader": "^8.0.12", 113 | "typescript": "^3.9.7", 114 | "vue-demi": "^0.5.3", 115 | "vuepress": "^1.7.1" 116 | } 117 | } -------------------------------------------------------------------------------- /docs/async/useRequest.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 3 | 一个帮你处理处理异步请求的复用函数 :tada: :100: 4 | 5 | ## 代码演示 6 | 7 | ::: tip 默认请求 8 | 在当前例子中,`useReuqest`接收一个`service`函数, 9 | 并且通过`defaultParams`传递了该异步请求函数的参数在组件`setup`后立即执行, 10 | `useReuqest`将接管`data`、`loading`、`error`等相关状态。 11 | ::: 12 | 13 | 14 | 15 | ::: details 点击查看代码 16 | <<< @/docs/.vuepress/components/requestBase.vue 17 | ::: 18 | 19 | ## API 20 | 21 | ```ts 22 | const { 23 | data, 24 | loading, 25 | error, 26 | run, 27 | cancel, 28 | refresh, 29 | reset, 30 | fetches 31 | } = useReuqest(service, options): 32 | ``` 33 | 34 | #### RetrunValue 35 | 36 | | 参数 | 说明 | 类型 | 37 | | --------- | ----------------------------------------------------------------------- | -------------------------------------------------------- | 38 | | `data` | 请求结果 | `Ref` | 39 | | `loading` | 请求状态 | `Ref` | 40 | | `error` | 请求错误状态 | `Ref` | 41 | | `run` | 配置手动执行请求函数时,调用的执行函数 | `function` | 42 | | `cancel` | 配置防抖节流或者轮询时在`wait`时间到达之前,提供一个取消当前`run`的方法 | `Ref` | 43 | | `refresh` | 使用最后一次请求参数重新请求 | `Ref` | 44 | | `reset` | 重置当前`useRequest`声明的请求 | `function` | 45 | | `fetches` | 多个请求并行时对应的请求状态 | `{[fetchKey]: { data, loading, error, cancel, refresh}}` | 46 | 47 | #### Params 48 | 49 | | 参数 | 说明 | 类型 | 50 | | --------- | -------------------- | ---------------- | 51 | | `service` | 请求函数 | `RequestService` | 52 | | `options` | `useRequest`配置文件 | `RequestOptions` | 53 | 54 | #### RequestService 55 | 56 | | 可接受类型 | 说明 | 57 | | ------------------------------------------------------------------ | ------------------------------------------------------------------------------ | 58 | | `(...args: any[]) => Promise` | 一般的`Promise`化请求函数 | 59 | | `string | { url: string, [key: string]: any }` | 需要传递`requestMethod`配置项,会作为第一个参数传递到`requestMethod` | 60 | | `(...args: any[]) => string | { url: string, [key: string]: any }` | 需要传递`requestMethod`配置项,函数返回值会作为第一个参数传递到`requestMethod` | 61 | 62 | #### RequestOptions 63 | 64 | | 配置项 | 说明 | 类型 | 65 | | ---------------- | ---------------------------- | --------------------------------------- | 66 | | `requestMethod` | 请求函数 | `(...args: any[]) => Promise` | 67 | | `formatResult` | 请求结果格式化函数 | `(res: any) => any` | 68 | | `defaultLoading` | 默认加载状态 | `any` | 69 | | `defaultParams` | 默认请求参数 | `any[]` | 70 | | `defaultData` | 默认请求结果 | `any` | 71 | | `onSuccess` | 成功回调 | `(data: any, params: any[]) => void` | 72 | | `onError` | 错误回调 | `(error: Error, params: any[]) => void` | 73 | | `throwOnError` | 请求错误是否对外抛出 | `Boolean` | 74 | | `manual` | 是否手动执行请求 | `Boolean` | 75 | | `loadingDelay` | 加载状态变为`true`的延迟时长 | `number` | 76 | | `pollingTime` | 轮询间隔时长 | `number` | 77 | | `debounceTime` | 防抖时长 | `number` | 78 | | `throttleTime` | 节流时长 | `number` | 79 | | `cacheKey` | 当前请求硬缓存的键值 | `string` | 80 | | `cacheTime` | 当前请求硬缓存的超时时间 | `number` | 81 | | `fetchKey` | 多请求时对应请求的键值 | `(...args: any[]) => string` | 82 | -------------------------------------------------------------------------------- /scripts/release.js: -------------------------------------------------------------------------------- 1 | const semver = require('semver') 2 | const execa = require('execa') 3 | const chalk = require('chalk') 4 | const { prompt } = require('enquirer') 5 | const path = require('path') 6 | const fs = require('fs') 7 | const args = require('minimist')(process.argv.slice(2)) 8 | 9 | const skipTests = args.skipTests || args.st 10 | const skipBuild = args.skipBuild || args.sb 11 | const preId = args.preId || '' 12 | 13 | const versionIncrements = [ 14 | 'patch', 15 | 'minor', 16 | 'major', 17 | 'prepatch', 18 | 'preminor', 19 | 'premajor', 20 | 'prerelease', 21 | ] 22 | 23 | const inc = (curVersion, i) => semver.inc(curVersion, i, preId) 24 | 25 | const run = (bin, args, opts = {}) => 26 | execa(bin, args, { 27 | stdio: 'inherit', 28 | ...opts, 29 | }) 30 | const resolve = p => path.resolve(__dirname, '../', p) 31 | const step = (msg) => console.log(chalk.cyan(msg)) 32 | 33 | const pkgJson = require(resolve('package.json')) 34 | // 主函数 35 | async function main() { 36 | const nextVersion = await ensureVersion() 37 | workForPublish(nextVersion) 38 | } 39 | 40 | async function ensureVersion() { 41 | let targetVersion = '' 42 | const currentVersion = pkgJson.version 43 | if (pkgJson.private) { 44 | return 45 | } 46 | step(`ensure version for ${chalk.yellow(`vue-reuse`)}`) 47 | const { release } = await prompt({ 48 | type: 'select', 49 | name: 'release', 50 | message: 'Select release type', 51 | choices: versionIncrements 52 | .map((i) => `${i} (${inc(currentVersion, i)})`) 53 | .concat(['custom']), 54 | }) 55 | if (release === 'custom') { 56 | targetVersion = ( 57 | await prompt({ 58 | type: 'input', 59 | name: 'version', 60 | message: 'Input custom version', 61 | initial: currentVersion, 62 | }) 63 | ).version 64 | } else { 65 | targetVersion = release.match(/\((.*)\)/)[1] 66 | } 67 | 68 | if (!semver.valid(targetVersion)) { 69 | throw new Error(`version: ${targetVersion} is invalid!`) 70 | } 71 | 72 | const { yes } = await prompt({ 73 | type: 'confirm', 74 | name: 'yes', 75 | message: `Releasing v${targetVersion}. Confirm?`, 76 | }) 77 | 78 | if (!yes) { 79 | return 80 | } 81 | 82 | return targetVersion 83 | } 84 | 85 | async function workForPublish(nextVersion) { 86 | // run test 87 | step(`run test...`) 88 | if (!skipTests) { 89 | await run('yarn', ['test']) 90 | } else { 91 | console.log(`(skipped)`) 92 | } 93 | 94 | // run build 95 | step(`run build...`) 96 | if (!skipBuild) { 97 | await run('yarn', [ 98 | 'build', 99 | '-s', 100 | '-t', 101 | '-p', 102 | ]) 103 | } else { 104 | console.log(`(skipped)`) 105 | } 106 | 107 | // update version 108 | step(`Updating cross dependencies...`) 109 | updateVersion(nextVersion) 110 | 111 | // generate changelog 112 | step(`generate changelog...`) 113 | await run('yarn', ['changelog']) 114 | const { stdout } = await run('git', ['diff'], { 115 | stdio: 'pipe', 116 | }) 117 | if (stdout) { 118 | step(`committing changes...`) 119 | await run('git', ['add', '-A']) 120 | await run('git', [ 121 | 'commit', 122 | '-m', 123 | `release: publish packages: v${nextVersion}`, 124 | '--no-verify', 125 | ]) 126 | } else { 127 | console.info(`nothing to commit...`) 128 | } 129 | 130 | // publish 131 | step(`publish packages...`) 132 | pubilshPackage(nextVersion) 133 | 134 | // push and tag 135 | step(`push to giihub...`) 136 | await run('git', ['tag', `v${nextVersion}`]) 137 | await run('git', ['push', 'origin', `refs/tags/v${nextVersion}`]) 138 | await run('git', ['push', 'origin', 'master']) 139 | 140 | console.log() 141 | } 142 | 143 | function updateVersion(nextVersion) { 144 | const pkgJsonPath = resolve('package.json') 145 | const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')) 146 | pkgJson.version = nextVersion 147 | fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2)) 148 | } 149 | 150 | async function pubilshPackage(nextVersion) { 151 | if (pkgJson.private) { 152 | return 153 | } 154 | 155 | step(`publishing...`) 156 | try { 157 | await run( 158 | 'yarn', 159 | ['publish', '--new-version', nextVersion, '--access', 'public'], 160 | { 161 | cwd: resolve('./'), 162 | stdio: 'pipe', 163 | } 164 | ) 165 | console.log( 166 | chalk.green(`Successfully published @xus/vue-reuse v${nextVersion}`) 167 | ) 168 | } catch (e) { 169 | console.log() 170 | throw e 171 | } 172 | } 173 | 174 | main().catch((err) => { 175 | console.error(err) 176 | }) 177 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [0.6.0](https://github.com/xus-code/vue-reuse/compare/v0.5.2...v0.6.0) (2021-01-09) 2 | 3 | 4 | ### Features 5 | 6 | * **request:** 添加request缓存能力 ([17d4c13](https://github.com/xus-code/vue-reuse/commit/17d4c1300356b8b701f3cb53e27834d189cc7c28)) 7 | * **request:** 新增userequest复用函数 ([27b7cf9](https://github.com/xus-code/vue-reuse/commit/27b7cf97b672db14406e1d9fed77da2f45eba1fa)) 8 | * **src/useBoolean:** 新增boolean功能集合 ([4109e90](https://github.com/xus-code/vue-reuse/commit/4109e907b2ffe2c03e455a6024941bbc692e7ad0)) 9 | * **src/useBoolean:** 修复boolean功能的类型推导 ([214a735](https://github.com/xus-code/vue-reuse/commit/214a735ac01fe2c03ba51be295d051eb008582fa)) 10 | 11 | 12 | 13 | ## [0.5.2](https://github.com/xus-code/vue-reuse/compare/v0.5.1...v0.5.2) (2021-01-01) 14 | 15 | ### Bug Fixes 16 | 17 | - 虚拟列表计算错误修复 ([7a387f8](https://github.com/xus-code/vue-reuse/commit/7a387f8693c5fc13d2ff71e7b68fafbb2403fd79)) 18 | 19 | ## [0.5.1](https://github.com/xus-code/vue-reuse/compare/v0.5.0...v0.5.1) (2020-12-28) 20 | 21 | # [0.5.0](https://github.com/xus-code/vue-reuse/compare/v0.4.0...v0.5.0) (2020-09-14) 22 | 23 | ### Bug Fixes 24 | 25 | - **virtuallist:** 修复虚拟列表 scrollto 计算 padding 错误 ([9ebc8c9](https://github.com/xus-code/vue-reuse/commit/9ebc8c9b014805258d59cd2775d1c4118de5737c)) 26 | - usedrop 类型错误 ([8a6d48f](https://github.com/xus-code/vue-reuse/commit/8a6d48f5224caf41aa521f0e3b6988588a706204)) 27 | 28 | ### Performance Improvements 29 | 30 | - **watch-stop:** 使用组件自动清除 watch 副作用替代手动清除 ([63975c7](https://github.com/xus-code/vue-reuse/commit/63975c75886db4794f3e258333ec204408c0d0ee)) 31 | 32 | # [0.4.0](https://github.com/xus-code/vue-reuse/compare/v0.3.2...v0.4.0) (2020-08-23) 33 | 34 | ### Bug Fixes 35 | 36 | - 去除 usescroll 中的 shallowreadonly,不去除会导致 vuepress vue2.x 打包错误 ([39bf3d4](https://github.com/xus-code/vue-reuse/commit/39bf3d404b092495e3c3aeb1949e362db687b9fd)) 37 | - usehistorytravel 边界问题修复 ([9eaea19](https://github.com/xus-code/vue-reuse/commit/9eaea19aa0cd55a8ef485c836cff4c3a6891c2e8)) 38 | - **dom:** 兼容非 web 平台调用 web api 的情况 ([f8bcdaa](https://github.com/xus-code/vue-reuse/commit/f8bcdaa482fb539f278ce63c3d740e1329d50eba)) 39 | 40 | ### Features 41 | 42 | - 添加 usevirtuallist 处理虚拟列表 ([59ea0b9](https://github.com/xus-code/vue-reuse/commit/59ea0b9e91756fba474bc2bf38043be675af1938)) 43 | 44 | ### Performance Improvements 45 | 46 | - 设置 useClickoutside 的 captrue 为 false ([6a8f6e0](https://github.com/xus-code/vue-reuse/commit/6a8f6e0b463c0300de4267559b4b3997e81df32c)) 47 | - 为 usesku 添加 spuOps 选项 ([0bdd8e5](https://github.com/xus-code/vue-reuse/commit/0bdd8e54778c118dd0804de8bc0478ac50c46337)) 48 | - 修复依赖引用 ([513281a](https://github.com/xus-code/vue-reuse/commit/513281a0d18c651bbd6af56fe710f8dff49c789e)) 49 | - 虚拟列表添加动态数据支持 ([4021bcf](https://github.com/xus-code/vue-reuse/commit/4021bcfdfc254462411fe49d29ea13d50c9261f0)) 50 | - 优化虚拟列表,初始化计算可显示内容 ([980d2b6](https://github.com/xus-code/vue-reuse/commit/980d2b6784ece1f0bfa80e21e2cc9bba93ef21bc)) 51 | - 优化 usedrop files 相关逻辑 ([13e7738](https://github.com/xus-code/vue-reuse/commit/13e77389bef05667948b386c1adec4dedf113f88)) 52 | - 优化 usedrop 相关逻辑 ([1e40607](https://github.com/xus-code/vue-reuse/commit/1e40607f459002b8b7f8d4917f72233a6b4eedeb)) 53 | - 增加 paddingtop 计算缓存 ([ab7127e](https://github.com/xus-code/vue-reuse/commit/ab7127ee742b1c6873928c3eea38e6061c105cfa)) 54 | 55 | ## [0.3.2](https://github.com/xus-code/vue-reuse/compare/v0.3.1...v0.3.2) (2020-08-20) 56 | 57 | ### Bug Fixes 58 | 59 | - 调整 usescroll 节流时长为 100ms ([f555efe](https://github.com/xus-code/vue-reuse/commit/f555efe120534066b1509b707ff4b5be25c16078)) 60 | - 修复 clickoutside 事件绑定错误 ([e13acdf](https://github.com/xus-code/vue-reuse/commit/e13acdfa7d0e2d112b34ea916770e60308c3c7f7)) 61 | 62 | ## [0.3.1](https://github.com/xus-code/vue-reuse/compare/v0.3.0...v0.3.1) (2020-08-20) 63 | 64 | # (2020-08-20) 65 | 66 | ### Features 67 | 68 | - 添加处理虚拟列表需求的 hooks ([152f58e](https://github.com/xus-code/vue-reuse/commit/152f58ea51fd1a0fa4b5ac35812fd55af5e50b6e)) 69 | - 添加能监听值历史记录的 hooks ([4518819](https://github.com/xus-code/vue-reuse/commit/45188192412dc34bd06e7b5941ec2f319d6d5d65)) 70 | - **sku:** 添加 sku 算法 hooks 函数 ([b33801e](https://github.com/xus-code/vue-reuse/commit/b33801ebb844120b79a1f8e380f4e84b97db7d60)) 71 | - 添加监听 scroll 相关 hooks 函数 ([7aa17ad](https://github.com/xus-code/vue-reuse/commit/7aa17addc0af9b9fdf454dea898e104494b6c03b)) 72 | - **storage:** 添加 storage 相关 hooks 函数 ([2c89e7c](https://github.com/xus-code/vue-reuse/commit/2c89e7c80f82f9d86843a3e56423966046be5cda)) 73 | - **ui:** 添加 drop drag 操作的 ui-hooks ([c4363e8](https://github.com/xus-code/vue-reuse/commit/c4363e890959148b5ad0481addc85db5bf20f93a)) 74 | - 添加防抖节流相关 hooks ([5d4f40e](https://github.com/xus-code/vue-reuse/commit/5d4f40ed8cc82311dbaecfc918772b9825c1b38e)) 75 | - **event:** 添加 event 类型的 hooks ([3c3426d](https://github.com/xus-code/vue-reuse/commit/3c3426d2a41ecab61b6cc7bfee2a96d23b299933)) 76 | -------------------------------------------------------------------------------- /src/useRequest/utils/fetch.ts: -------------------------------------------------------------------------------- 1 | import { debounce, throttle } from 'lodash' 2 | import { FetchConfig, FetchResult, noop, Service, Subscribe } from './types' 3 | 4 | export class Fetch { 5 | that: any = this 6 | config: FetchConfig 7 | service: Service 8 | count = 0 9 | unmountedFlag = false 10 | 11 | result: FetchResult = { 12 | loading: false, 13 | data: undefined, 14 | error: undefined, 15 | params: undefined, 16 | cancel: this.cancel.bind(this.that), 17 | refresh: this.refresh.bind(this.that), 18 | run: this.run.bind(this.that), 19 | unmount: this.unmount.bind(this.that) 20 | } 21 | 22 | setResult(s = {}): void { 23 | this.result = { 24 | ...this.result, 25 | ...s 26 | } 27 | this.subscribe(this.result) 28 | } 29 | 30 | subscribe: Subscribe 31 | 32 | unSubscribe: noop[] = [] 33 | 34 | debounceTimer: any 35 | throttleTimer: any 36 | debounceRun: any 37 | throttleRun: any 38 | 39 | loadDelayTimer: any 40 | pollingTimer: any 41 | 42 | constructor( 43 | service: Service, 44 | config: FetchConfig, 45 | subscribe: Subscribe, 46 | initResult?: { loading?: boolean; data?: R; params: P } 47 | ) { 48 | this.service = service 49 | this.config = config 50 | this.subscribe = subscribe 51 | if (initResult) { 52 | this.result = { 53 | ...this.result, 54 | ...initResult 55 | } 56 | } 57 | 58 | this.debounceRun = this.config?.debounceTime 59 | ? debounce(this._run, this.config.debounceTime) 60 | : undefined 61 | 62 | this.throttleRun = this.config?.throttleTime 63 | ? throttle(this._run, this.config.throttleTime) 64 | : undefined 65 | } 66 | 67 | _run(...args: P): Promise { 68 | if (this.pollingTimer) { 69 | clearTimeout(this.pollingTimer) 70 | } 71 | if (this.loadDelayTimer) { 72 | clearTimeout(this.loadDelayTimer) 73 | } 74 | 75 | this.count++ 76 | 77 | const currentCount = this.count 78 | 79 | this.setResult({ 80 | loading: !this.config?.loadingDelay, 81 | params: args 82 | }) 83 | 84 | if (this.config?.loadingDelay) { 85 | this.loadDelayTimer = setTimeout(() => { 86 | this.setResult({ 87 | loading: true 88 | }) 89 | }, this.config.loadingDelay) 90 | } 91 | 92 | return this.service(...args) 93 | .then((data) => { 94 | if (!this.unmountedFlag && currentCount === this.count) { 95 | if (this.loadDelayTimer) { 96 | clearTimeout(this.loadDelayTimer) 97 | } 98 | const result = this.config?.formatResult 99 | ? this.config.formatResult(data) 100 | : data 101 | 102 | this.config?.onSuccess && this.config.onSuccess(result, args) 103 | 104 | this.setResult({ 105 | data: result, 106 | error: null, 107 | loading: false 108 | }) 109 | 110 | return result 111 | } 112 | }) 113 | .catch((error) => { 114 | if (!this.unmountedFlag && currentCount === this.count) { 115 | if (this.loadDelayTimer) { 116 | clearTimeout(this.loadDelayTimer) 117 | } 118 | 119 | this.setResult({ 120 | data: null, 121 | error, 122 | loading: false 123 | }) 124 | 125 | if (this.config?.onError) { 126 | this.config.onError(error, args) 127 | } 128 | 129 | if (this.config?.throwOnError) { 130 | throw error 131 | } 132 | 133 | console.error(error) 134 | 135 | return Promise.reject( 136 | `useRequest has caught the exception, if you need to handle the exception yourself, you can set options.throwOnError to true.` 137 | ) 138 | } 139 | }) 140 | .finally(() => { 141 | // 处理轮询 142 | if (!this.unmountedFlag && currentCount === this.count) { 143 | if (this.config?.pollingTime) { 144 | this.pollingTimer = setTimeout(() => { 145 | this._run(...args) 146 | }, this.config.pollingTime) 147 | } 148 | } 149 | }) 150 | } 151 | 152 | run(...args: P): Promise { 153 | if (this.debounceRun) { 154 | return this.debounceRun(...args) 155 | } 156 | if (this.throttleRun) { 157 | return this.throttleRun(...args) 158 | } 159 | return this._run(...args) 160 | } 161 | 162 | cancel(): void { 163 | this.debounceRun && this.debounceRun.cancel() 164 | this.throttleRun && this.throttleRun.cancel() 165 | this.loadDelayTimer && clearTimeout(this.loadDelayTimer) 166 | this.pollingTimer && clearTimeout(this.pollingTimer) 167 | this.count += 1 168 | this.setResult({ 169 | loading: false 170 | }) 171 | } 172 | 173 | refresh(): Promise { 174 | return this.run(...(this.result.params as P)) 175 | } 176 | 177 | unmount(): void { 178 | this.unmountedFlag = true 179 | this.cancel() 180 | this.unSubscribe.forEach((us) => { 181 | us() 182 | }) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/useVirtualList/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Ref, 3 | ref, 4 | computed, 5 | ComputedRef, 6 | getCurrentInstance, 7 | reactive, 8 | onMounted, 9 | watchEffect, 10 | isRef 11 | } from 'vue-demi' 12 | import { useThrottleFn } from '../useThrottleFn' 13 | 14 | type ReturnValue = { 15 | list: ComputedRef<{ data: T; index: number }[]> 16 | wrapperProps: { 17 | style: { 18 | width: '100%' 19 | boxSizing: 'border-box' 20 | height: string 21 | paddingTop: string 22 | } 23 | } 24 | containeRef: Ref 25 | containeProps: { 26 | onScroll(evt: Event): void 27 | style: { overflowY: 'auto' } 28 | } 29 | scrollTo(index: number): void 30 | } 31 | 32 | export type Options = { 33 | itemHeight: number | ((index: number) => number) 34 | overscan?: number 35 | } 36 | 37 | export function useVirtualList( 38 | rawList: T[] | Ref, 39 | options: Options 40 | ): ReturnValue { 41 | const { itemHeight, overscan = 5 } = options 42 | const el = ref(null) 43 | const paddingTopRef = ref(0) 44 | const totalHeightRef = ref(0) 45 | // list 46 | function list(): T[] { 47 | if (isRef(rawList)) { 48 | return rawList.value 49 | } else { 50 | return rawList 51 | } 52 | } 53 | const listState = reactive({ start: 0, end: 10 }) 54 | const listRef = computed<{ data: T; index: number }[]>(() => 55 | list() 56 | .slice(listState.start, listState.end) 57 | .map((data, index) => { 58 | return { 59 | data, 60 | index: index + listState.start 61 | } 62 | }) 63 | ) 64 | 65 | // totalheight 66 | const heightCache: Record = {} 67 | function getHeight(index: number): number { 68 | if (heightCache[index]) return heightCache[index] 69 | let offset = 0 70 | if (typeof itemHeight === 'number') { 71 | offset = index * itemHeight 72 | } else { 73 | offset = list() 74 | .slice(0, index) 75 | .reduce((sum, _, index) => sum + itemHeight(index), 0) 76 | } 77 | // cache 78 | heightCache[index] = offset 79 | return offset 80 | } 81 | 82 | // scroll to 83 | function scrollTo(index: number): void { 84 | if (el.value) { 85 | // 边界情况 86 | index < 0 && (index = 0) 87 | index > list().length && (index = list().length) 88 | el.value.scrollTop = getHeight(index) 89 | // 计算list 90 | calculateRange() 91 | } 92 | } 93 | 94 | // set state 95 | function calculateRange() { 96 | const element = el.value 97 | if (element) { 98 | // 计算偏移量 99 | const offset = getOffset(element.scrollTop) 100 | // 计算一屏显示个数 101 | const viewCapacity = getViewCapacity(element.clientHeight, offset - 1) 102 | // 设置 103 | const from = offset - overscan 104 | const to = offset + viewCapacity + overscan 105 | const currentLen = list().length 106 | // 到达列表最末端时候不需要再修改 107 | if (to >= currentLen && listState.end === currentLen) return 108 | listState.start = from < 0 ? 0 : from 109 | listState.end = to > currentLen ? currentLen : to 110 | paddingTopRef.value = getHeight(listState.start) 111 | } 112 | } 113 | // throttle 114 | const { run: runCalcelateRange } = useThrottleFn(calculateRange, 80) 115 | 116 | function getOffset(top: number): number { 117 | if (typeof itemHeight === 'number') { 118 | return ((top / itemHeight) | 0) + 1 119 | } else { 120 | let sum = 0 121 | let offset = 0 122 | const len = list().length 123 | for (let i = 0; i < len; i++) { 124 | sum += itemHeight(i) 125 | if (sum >= top) { 126 | offset = i 127 | break 128 | } 129 | } 130 | return offset + 1 131 | } 132 | } 133 | 134 | function getViewCapacity(height: number, offset: number): number { 135 | if (typeof itemHeight === 'number') { 136 | return Math.ceil(height / itemHeight) 137 | } else { 138 | let sum = 0 139 | let capacity = 0 140 | const len = list().length 141 | for (let i = offset; i < len; i++) { 142 | sum += itemHeight(i) 143 | if (sum > height) { 144 | capacity = i 145 | break 146 | } 147 | } 148 | return capacity - offset 149 | } 150 | } 151 | 152 | if (getCurrentInstance()) { 153 | onMounted(() => { 154 | // init totalheight 155 | watchEffect(() => { 156 | const total = getHeight(list().length) 157 | if (total !== totalHeightRef.value) { 158 | totalHeightRef.value = total 159 | } 160 | }) 161 | // init range 162 | calculateRange() 163 | }) 164 | } 165 | 166 | return { 167 | list: listRef, 168 | wrapperProps: reactive({ 169 | style: { 170 | width: '100%' as const, 171 | boxSizing: 'border-box' as const, 172 | height: computed(() => `${totalHeightRef.value}px`), 173 | paddingTop: computed(() => `${paddingTopRef.value}px`) 174 | } 175 | }), 176 | containeRef: el, 177 | containeProps: { 178 | onScroll(e) { 179 | e.preventDefault() 180 | runCalcelateRange() 181 | }, 182 | style: { overflowY: 'auto' as const } 183 | }, 184 | scrollTo 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/useRequest/useAsync.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Service, 3 | AsyncResult, 4 | AsyncOptionsWithFormat, 5 | AsyncOptions, 6 | AsyncOptionsAll, 7 | Fetches, 8 | CurFetchResult, 9 | FetchResult 10 | } from './utils/types' 11 | import { Fetch } from './utils/fetch' 12 | import { getCache, setCache } from './utils/cache' 13 | import { 14 | onMounted, 15 | onUnmounted, 16 | shallowReactive, 17 | shallowRef, 18 | toRefs, 19 | // watch, 20 | watchEffect 21 | } from 'vue-demi' 22 | 23 | const DEFAULT_KEY = 'vue_reuse_request_default_fetch_key' 24 | 25 | export function useAsync( 26 | service: Service, 27 | options: AsyncOptionsWithFormat 28 | ): AsyncResult 29 | export function useAsync( 30 | service: Service, 31 | options?: AsyncOptions 32 | ): AsyncResult 33 | export function useAsync( 34 | service: Service, 35 | options?: AsyncOptionsAll 36 | ): AsyncResult { 37 | const _opts = options || ({} as AsyncOptionsAll) 38 | const { 39 | // hooks 40 | onSuccess, 41 | onError, 42 | throwOnError, 43 | // time 44 | debounceTime, 45 | throttleTime, 46 | pollingTime, 47 | loadingDelay, 48 | // default 49 | defaultData = undefined, 50 | defaultLoading = false, 51 | defaultParams = [], 52 | manual, 53 | // cache and key 54 | fetchKey, 55 | cacheKey, 56 | cacheTime 57 | } = _opts 58 | 59 | // types patch 60 | const serve = service as any 61 | const initialData = defaultData as UU 62 | const initialParams = defaultParams as P 63 | let formatResult: any 64 | if ('formatResult' in _opts) { 65 | formatResult = _opts.formatResult 66 | } 67 | 68 | // fetch config 69 | const fetchConfig = { 70 | formatResult, 71 | onSuccess: onSuccess as (data: UU, params: P) => void, 72 | onError, 73 | throwOnError, 74 | pollingTime, 75 | debounceTime, 76 | throttleTime, 77 | loadingDelay 78 | } 79 | 80 | const fetches = shallowReactive>({}) 81 | const [curFetchResult, setCurFetchResult] = useFetchResult({ 82 | data: initialData, 83 | loading: defaultLoading, 84 | params: initialParams, 85 | error: undefined, 86 | cancel: noopFn('cancel'), 87 | refresh: noopFn('refresh') as () => Promise 88 | }) 89 | const curFetchKey = shallowRef(DEFAULT_KEY) 90 | const subscribe = (key: string, data: FetchResult) => { 91 | setCurFetchResult(data) 92 | fetches[key] = data 93 | } 94 | // get fetches from cache 95 | if (cacheKey) { 96 | const cached = getCache(cacheKey) 97 | if (cached?.data) { 98 | const cacheFetches = (cached.data?.fetches || {}) as Fetches 99 | Object.keys(cacheFetches).forEach((key) => { 100 | const fetch = new Fetch(serve, fetchConfig, subscribe.bind(null, key), { 101 | loading: cacheFetches[key].loading, 102 | data: cacheFetches[key].data, 103 | params: cacheFetches[key].params as P 104 | }) 105 | fetches[key] = fetch.result 106 | }) 107 | curFetchKey.value = cached.data?.fetchKey || DEFAULT_KEY 108 | setCurFetchResult(fetches[curFetchKey.value]) 109 | } 110 | } 111 | 112 | // set fetches to cache 113 | watchEffect(() => { 114 | if (cacheKey && cacheTime) { 115 | setCache( 116 | cacheKey, 117 | { 118 | fetches: fetches, 119 | fetchKey: curFetchKey.value 120 | }, 121 | cacheTime 122 | ) 123 | } 124 | }) 125 | 126 | const run = (...args: P) => { 127 | if (fetchKey) { 128 | curFetchKey.value = fetchKey(...args) || DEFAULT_KEY 129 | } 130 | let curFetch = fetches[curFetchKey.value] 131 | if (!curFetch) { 132 | const fetch = new Fetch( 133 | serve, 134 | fetchConfig, 135 | subscribe.bind(null, curFetchKey.value), 136 | { 137 | loading: defaultLoading, 138 | params: initialParams, 139 | data: initialData 140 | } 141 | ) 142 | curFetch = fetches[curFetchKey.value] = fetch.result 143 | } 144 | 145 | return curFetch.run(...args) 146 | } 147 | const reset = () => { 148 | curFetchKey.value = DEFAULT_KEY 149 | Object.keys(fetches).forEach((key) => { 150 | const fetch = fetches[key] 151 | fetch.unmount() 152 | delete fetches[key] 153 | }) 154 | } 155 | 156 | // setup 157 | onMounted(() => { 158 | if (!manual) { 159 | // 默认执行 160 | run(...initialParams) 161 | } 162 | }) 163 | 164 | onUnmounted(() => { 165 | Object.keys(fetches).forEach((key) => { 166 | const fetch = fetches[key] 167 | fetch.unmount() 168 | }) 169 | }) 170 | 171 | return { 172 | run, 173 | ...toRefs(curFetchResult), 174 | fetches, 175 | reset 176 | } as AsyncResult 177 | } 178 | 179 | function useFetchResult( 180 | initalValue: Partial> = {} 181 | ): [ 182 | Partial>, 183 | (args: Partial>) => void 184 | ] { 185 | const fetchResult = shallowReactive>>( 186 | initalValue 187 | ) 188 | const setFetcResult = ({ 189 | data, 190 | loading, 191 | params, 192 | error, 193 | cancel, 194 | refresh 195 | }: Partial>) => { 196 | fetchResult.data = data 197 | fetchResult.loading = loading 198 | fetchResult.error = error 199 | fetchResult.params = params 200 | fetchResult.cancel = cancel || noopFn('cancel') 201 | fetchResult.refresh = refresh || (noopFn('refresh') as () => Promise) 202 | } 203 | return [fetchResult, setFetcResult] 204 | } 205 | 206 | const noopFn = (name: string) => () => 207 | console.warn(`function ${name} should be call when fetch ready`) 208 | -------------------------------------------------------------------------------- /__test__/useRequest.spec.ts: -------------------------------------------------------------------------------- 1 | import { Wrapper } from '@vue/test-utils' 2 | import { useRequest } from '../src/useRequest' 3 | import { getCache } from '../src/useRequest/utils/cache' 4 | import { waitTime, renderComposable } from './utils' 5 | 6 | const Fetch = (p: number) => 7 | new Promise((resolve) => { 8 | setTimeout(() => { 9 | resolve(p) 10 | }, 100) 11 | }) 12 | 13 | const FetchError = (p: number) => 14 | new Promise((_, reject) => { 15 | setTimeout(() => { 16 | reject(p) 17 | }, 100) 18 | }) 19 | 20 | const parallelFetch = (p: { key: string; res: number }) => 21 | new Promise((resolve) => { 22 | setTimeout(() => { 23 | resolve(p.res) 24 | }, 100) 25 | }) 26 | 27 | const customRequest = (p: string) => 28 | new Promise((resolve) => { 29 | setTimeout(() => { 30 | resolve(p) 31 | }, 100) 32 | }) 33 | 34 | const customRequest2 = (p: string, a: string, b: string) => 35 | new Promise((resolve) => { 36 | setTimeout(() => { 37 | resolve(p + a + b) 38 | }, 100) 39 | }) 40 | 41 | describe('test use async ', () => { 42 | let wrapper: Wrapper 43 | afterEach(() => { 44 | wrapper.destroy() 45 | }) 46 | 47 | test('test request ', async () => { 48 | wrapper = renderComposable(() => 49 | useRequest(Fetch, { 50 | defaultParams: [1] 51 | }) 52 | ) 53 | const { vm } = wrapper 54 | // create 55 | expect(vm.$data.loading).toBeUndefined() 56 | expect(vm.$data.params).toBeUndefined() 57 | expect(vm.$data.data).toBeUndefined() 58 | // after mount begin request 59 | await vm.$nextTick() 60 | expect(vm.$data.loading).toBe(true) 61 | expect(vm.$data.params).toEqual([1]) 62 | expect(vm.$data.data).toBeUndefined() 63 | // after request back 64 | await waitTime(110) 65 | expect(vm.$data.loading).toBe(false) 66 | expect(vm.$data.params).toEqual([1]) 67 | expect(vm.$data.data).toEqual(1) 68 | }) 69 | 70 | test(`test request manaul `, async () => { 71 | wrapper = renderComposable(() => 72 | useRequest(Fetch, { 73 | defaultData: 1, 74 | manual: true 75 | }) 76 | ) 77 | const { vm } = wrapper 78 | // after mount 79 | await vm.$nextTick() 80 | expect(vm.$data.loading).toBe(false) 81 | expect(vm.$data.params).toEqual([]) 82 | expect(vm.$data.data).toBe(1) 83 | // start request 84 | vm.$data.run(3) 85 | expect(vm.$data.loading).toBe(true) 86 | expect(vm.$data.params).toEqual([3]) 87 | expect(vm.$data.data).toBe(1) 88 | await waitTime(110) 89 | expect(vm.$data.loading).toBe(false) 90 | expect(vm.$data.params).toEqual([3]) 91 | expect(vm.$data.data).toBe(3) 92 | }) 93 | 94 | test(`test request parallel `, async () => { 95 | wrapper = renderComposable(() => 96 | useRequest(parallelFetch, { 97 | manual: true, 98 | fetchKey: (p) => p.key 99 | }) 100 | ) 101 | const { vm } = wrapper 102 | // after mount 103 | await vm.$nextTick() 104 | expect(vm.$data.loading).toBe(false) 105 | expect(vm.$data.params).toEqual([]) 106 | expect(vm.$data.data).toBeUndefined() 107 | // start some request 108 | vm.$data.run({ key: '1', res: 1 }) 109 | expect(vm.$data.fetches['1'].loading).toBe(true) 110 | expect(vm.$data.fetches['1'].params).toEqual([{ key: '1', res: 1 }]) 111 | expect(vm.$data.fetches['1'].data).toBeUndefined() 112 | vm.$data.run({ key: '2', res: 2 }) 113 | expect(vm.$data.fetches['2'].loading).toBe(true) 114 | expect(vm.$data.fetches['2'].params).toEqual([{ key: '2', res: 2 }]) 115 | expect(vm.$data.fetches['2'].data).toBeUndefined() 116 | // after request 117 | await waitTime(110) 118 | expect(vm.$data.fetches['1'].loading).toBe(false) 119 | expect(vm.$data.fetches['1'].params).toEqual([{ key: '1', res: 1 }]) 120 | expect(vm.$data.fetches['1'].data).toBe(1) 121 | 122 | expect(vm.$data.fetches['2'].loading).toBe(false) 123 | expect(vm.$data.fetches['2'].params).toEqual([{ key: '2', res: 2 }]) 124 | expect(vm.$data.fetches['2'].data).toBe(2) 125 | // re request 126 | vm.$data.run({ key: '1', res: 10 }) 127 | vm.$data.run({ key: '2', res: 20 }) 128 | await waitTime(110) 129 | expect(vm.$data.fetches['1'].data).toBe(10) 130 | expect(vm.$data.fetches['2'].data).toBe(20) 131 | }) 132 | 133 | test(`test request formatResult `, async () => { 134 | wrapper = renderComposable(() => 135 | useRequest(Fetch, { 136 | defaultParams: [1], 137 | formatResult: (res) => ({ res }), 138 | defaultData: { res: 2 } 139 | }) 140 | ) 141 | const { vm } = wrapper 142 | await vm.$nextTick() 143 | expect(vm.$data.loading).toBe(true) 144 | expect(vm.$data.params).toEqual([1]) 145 | expect(vm.$data.data).toEqual({ res: 2 }) 146 | // after request 147 | await waitTime(110) 148 | expect(vm.$data.loading).toBe(false) 149 | expect(vm.$data.params).toEqual([1]) 150 | expect(vm.$data.data).toEqual({ res: 1 }) 151 | }) 152 | 153 | test(`test request debouce `, async () => { 154 | wrapper = renderComposable(() => 155 | useRequest(Fetch, { 156 | manual: true, 157 | debounceTime: 50 158 | }) 159 | ) 160 | const { vm } = wrapper 161 | await vm.$nextTick() 162 | vm.$data.run(1) 163 | vm.$data.run(2) 164 | vm.$data.run(3) 165 | vm.$data.run(4) 166 | await waitTime(160) 167 | expect(vm.$data.data).toBe(4) 168 | }) 169 | 170 | test(`test request throttle `, async () => { 171 | wrapper = renderComposable(() => 172 | useRequest(Fetch, { 173 | manual: true, 174 | throttleTime: 50 175 | }) 176 | ) 177 | const { vm } = wrapper 178 | await vm.$nextTick() 179 | vm.$data.run(1) 180 | vm.$data.run(2) 181 | vm.$data.run(3) 182 | vm.$data.run(4) 183 | await waitTime(160) 184 | expect(vm.$data.data).toBe(4) 185 | }) 186 | 187 | test(`test request polling `, async () => { 188 | const onSuccess = jest.fn() 189 | wrapper = renderComposable(() => 190 | useRequest(Fetch, { 191 | manual: true, 192 | pollingTime: 50, 193 | onSuccess 194 | }) 195 | ) 196 | const { vm } = wrapper 197 | await vm.$nextTick() 198 | vm.$data.run(1) 199 | // (100 + 50) * 2 200 | await waitTime(310) 201 | expect(onSuccess).toBeCalledTimes(2) 202 | // cancel polling 203 | vm.$data.cancel() 204 | await waitTime(310) 205 | expect(onSuccess).toBeCalledTimes(2) 206 | }) 207 | 208 | test(`test loading delay `, async () => { 209 | wrapper = renderComposable(() => 210 | useRequest(Fetch, { 211 | loadingDelay: 20, 212 | defaultParams: [1] 213 | }) 214 | ) 215 | const { vm } = wrapper 216 | // after mount 217 | await vm.$nextTick() 218 | expect(vm.$data.loading).toBeFalsy() 219 | // before delay 220 | await waitTime(10) 221 | expect(vm.$data.loading).toBeFalsy() 222 | // after delay 223 | await waitTime(30) 224 | expect(vm.$data.loading).toBeTruthy() 225 | // after request 226 | await waitTime(70) 227 | expect(vm.$data.loading).toBeFalsy() 228 | expect(vm.$data.data).toBe(1) 229 | }) 230 | 231 | test(`test request error `, async () => { 232 | const onError = jest.fn() 233 | wrapper = renderComposable(() => 234 | useRequest(FetchError, { 235 | manual: true, 236 | onError 237 | }) 238 | ) 239 | const { vm } = wrapper 240 | await vm.$nextTick() 241 | expect(vm.$data.error).toBeUndefined() 242 | await vm.$data.run(1).catch((err: Error) => { 243 | expect(err).toBe( 244 | `useRequest has caught the exception, if you need to handle the exception yourself, you can set options.throwOnError to true.` 245 | ) 246 | }) 247 | await vm.$nextTick() 248 | expect(onError).toBeCalled() 249 | expect(onError).toBeCalledWith(1, [1]) 250 | expect(vm.$data.error).toBe(1) 251 | }) 252 | 253 | test(`test request error throw `, async () => { 254 | const onError = jest.fn() 255 | wrapper = renderComposable(() => 256 | useRequest(FetchError, { 257 | manual: true, 258 | onError, 259 | throwOnError: true 260 | }) 261 | ) 262 | const { vm } = wrapper 263 | await vm.$nextTick() 264 | expect(vm.$data.error).toBeUndefined() 265 | await vm.$data.run(2).catch((err: Error) => { 266 | expect(err).toBe(2) 267 | }) 268 | await vm.$nextTick() 269 | expect(onError).toBeCalled() 270 | expect(onError).toBeCalledWith(2, [2]) 271 | expect(vm.$data.error).toBe(2) 272 | }) 273 | 274 | test(`test request reset `, async () => { 275 | wrapper = renderComposable(() => 276 | useRequest(Fetch, { 277 | manual: true 278 | }) 279 | ) 280 | const { vm } = wrapper 281 | await vm.$nextTick() 282 | expect(vm.$data.data).toBeUndefined() 283 | await vm.$data.run(2) 284 | expect(vm.$data.data).toBe(2) 285 | vm.$data.reset() 286 | await vm.$nextTick() 287 | expect(vm.$data.data).toBe(2) 288 | expect(vm.$data.fetches).toEqual({}) 289 | }) 290 | 291 | test(`test request cache `, async () => { 292 | const CacheKey = 'test_cache' 293 | const DefaultKey = `vue_reuse_request_default_fetch_key` 294 | wrapper = renderComposable(() => 295 | useRequest(Fetch, { 296 | defaultParams: [1], 297 | cacheKey: CacheKey, 298 | cacheTime: 200 299 | }) 300 | ) 301 | const { vm } = wrapper 302 | // after mount 303 | await vm.$nextTick() 304 | expect(vm.$data.loading).toBeTruthy() 305 | expect(vm.$data.params).toEqual([1]) 306 | expect(vm.$data.data).toBeUndefined() 307 | await waitTime(110) 308 | expect(vm.$data.loading).toBeFalsy() 309 | expect(vm.$data.params).toEqual([1]) 310 | expect(vm.$data.data).toBe(1) 311 | vm.$destroy() 312 | const cached = getCache(CacheKey) 313 | expect(cached.data?.fetchKey).toBe(DefaultKey) 314 | expect(cached.data?.fetches[DefaultKey].data).toBe(1) 315 | // in any where request again cache data will be initial 316 | wrapper = renderComposable(() => 317 | useRequest(Fetch, { 318 | defaultParams: [2], 319 | cacheKey: CacheKey, 320 | cacheTime: 200 321 | }) 322 | ) 323 | const { vm: vm2 } = wrapper 324 | // after mount 325 | await vm2.$nextTick() 326 | expect(vm2.$data.loading).toBeTruthy() 327 | expect(vm2.$data.params).toEqual([2]) 328 | expect(vm2.$data.data).toBe(1) 329 | // after request 330 | await waitTime(110) 331 | expect(vm2.$data.loading).toBeFalsy() 332 | expect(vm2.$data.params).toEqual([2]) 333 | expect(vm2.$data.data).toBe(2) 334 | // after clear cache 335 | await waitTime(100) 336 | const cached2 = getCache(CacheKey) 337 | expect(cached2.data?.fetchKey).toBeUndefined() 338 | expect(cached2.data?.fetches).toBeUndefined() 339 | }) 340 | 341 | test(`test custom request method `, async () => { 342 | wrapper = renderComposable(() => 343 | useRequest('1', { 344 | requestMethod: customRequest2, 345 | defaultParams: ['2', '3'] 346 | }) 347 | ) 348 | const { vm } = wrapper 349 | await vm.$nextTick() 350 | expect(vm.$data.loading).toBe(true) 351 | expect(vm.$data.params).toEqual(['2', '3']) 352 | expect(vm.$data.data).toBeUndefined() 353 | await waitTime(110) 354 | expect(vm.$data.loading).toBe(false) 355 | expect(vm.$data.params).toEqual(['2', '3']) 356 | expect(vm.$data.data).toBe('123') 357 | }) 358 | 359 | test(`test custom request method functional service `, async () => { 360 | const url = (p: string) => p 361 | wrapper = renderComposable(() => 362 | useRequest(url, { 363 | requestMethod: customRequest, 364 | defaultParams: ['1'] 365 | }) 366 | ) 367 | const { vm } = wrapper 368 | await vm.$nextTick() 369 | expect(vm.$data.loading).toBe(true) 370 | expect(vm.$data.params).toEqual(['1']) 371 | expect(vm.$data.data).toBeUndefined() 372 | await waitTime(110) 373 | expect(vm.$data.loading).toBe(false) 374 | expect(vm.$data.params).toEqual(['1']) 375 | expect(vm.$data.data).toBe('1') 376 | }) 377 | 378 | test(`test custom request method object service `, async () => { 379 | const customRequest = ({ url }: { url: string }) => 380 | new Promise((resolve) => { 381 | setTimeout(() => { 382 | resolve(url) 383 | }, 100) 384 | }) 385 | const url = (url: string) => ({ url }) 386 | wrapper = renderComposable(() => 387 | useRequest(url, { 388 | requestMethod: customRequest, 389 | defaultParams: ['1'] 390 | }) 391 | ) 392 | const { vm } = wrapper 393 | await vm.$nextTick() 394 | expect(vm.$data.loading).toBe(true) 395 | expect(vm.$data.params).toEqual(['1']) 396 | expect(vm.$data.data).toBeUndefined() 397 | await waitTime(110) 398 | expect(vm.$data.loading).toBe(false) 399 | expect(vm.$data.params).toEqual(['1']) 400 | expect(vm.$data.data).toBe('1') 401 | vm.$destroy() 402 | // object 403 | wrapper = renderComposable(() => 404 | useRequest( 405 | { url: '1' }, 406 | { 407 | requestMethod: customRequest 408 | } 409 | ) 410 | ) 411 | const { vm: vm1 } = wrapper 412 | await vm1.$nextTick() 413 | expect(vm1.$data.loading).toBe(true) 414 | expect(vm1.$data.params).toEqual([]) 415 | expect(vm1.$data.data).toBeUndefined() 416 | await waitTime(110) 417 | expect(vm1.$data.loading).toBe(false) 418 | expect(vm1.$data.params).toEqual([]) 419 | expect(vm1.$data.data).toBe('1') 420 | }) 421 | 422 | test(`test global fetch `, async () => { 423 | wrapper = renderComposable(() => 424 | useRequest('1', { 425 | manual: true 426 | }) 427 | ) 428 | const { vm } = wrapper 429 | await vm.$nextTick() 430 | expect(vm.$data.data).toBeUndefined() 431 | await vm.$data.run() 432 | expect(vm.$data.data).toBe('1') 433 | await vm.$data.run({ error: 'error' }).catch((err: Error) => { 434 | expect(err).toBe( 435 | `useRequest has caught the exception, if you need to handle the exception yourself, you can set options.throwOnError to true.` 436 | ) 437 | }) 438 | }) 439 | }) 440 | --------------------------------------------------------------------------------