Repository: yanzhandong/v3hooks Branch: master Commit: 71a54b57668c Files: 108 Total size: 155.5 KB Directory structure: gitextract_yd9gghg3/ ├── .gitignore ├── README.md ├── babel.config.js ├── docs/ │ └── question.md ├── example/ │ ├── .eslintignore │ ├── README.md │ ├── babel.config.js │ ├── jsconfig.json │ ├── package.json │ ├── public/ │ │ └── index.html │ ├── src/ │ │ ├── App.vue │ │ ├── components/ │ │ │ └── HelloWorld.vue │ │ ├── main.ts │ │ ├── pages/ │ │ │ ├── home/ │ │ │ │ └── index.vue │ │ │ ├── useBoolean/ │ │ │ │ └── index.vue │ │ │ ├── useCookie/ │ │ │ │ └── index.vue │ │ │ ├── useDate/ │ │ │ │ └── index.vue │ │ │ ├── useDynamicList/ │ │ │ │ └── index.vue │ │ │ ├── useExternal/ │ │ │ │ └── index.vue │ │ │ ├── useFullscreen/ │ │ │ │ └── index.vue │ │ │ ├── useInterval/ │ │ │ │ └── index.vue │ │ │ ├── useLocalStorage/ │ │ │ │ └── index.vue │ │ │ ├── useLockFn/ │ │ │ │ └── index.vue │ │ │ ├── useMediaQuery/ │ │ │ │ └── index.vue │ │ │ ├── useNetwork/ │ │ │ │ └── index.vue │ │ │ ├── useQRCode/ │ │ │ │ └── index.vue │ │ │ ├── useRouteQuery/ │ │ │ │ └── index.vue │ │ │ ├── useSessionStorage/ │ │ │ │ └── index.vue │ │ │ ├── useSetAndUseMap/ │ │ │ │ └── index.vue │ │ │ ├── useTextSelection/ │ │ │ │ └── index.vue │ │ │ ├── useToggle/ │ │ │ │ └── index.vue │ │ │ ├── useVirtualList/ │ │ │ │ └── index.vue │ │ │ └── useWebSocket/ │ │ │ └── index.vue │ │ ├── router.ts │ │ └── shims-vue.d.ts │ ├── tsconfig.json │ └── vue.config.js ├── jest.config.js ├── package.json ├── packages/ │ ├── index.ts │ ├── useBoolean/ │ │ ├── index.md │ │ └── index.ts │ ├── useCookie/ │ │ ├── index.md │ │ └── index.ts │ ├── useDate/ │ │ ├── index.md │ │ └── index.ts │ ├── useDebounce/ │ │ ├── index.md │ │ └── index.ts │ ├── useDebounceFn/ │ │ ├── index.md │ │ └── index.ts │ ├── useDocumentVisibility/ │ │ ├── index.md │ │ └── index.ts │ ├── useDynamicList/ │ │ ├── index.md │ │ └── index.ts │ ├── useExternal/ │ │ ├── index.md │ │ └── index.ts │ ├── useFullscreen/ │ │ ├── index.md │ │ └── index.ts │ ├── useInterval/ │ │ ├── index.md │ │ └── index.ts │ ├── useLocalStorage/ │ │ ├── index.md │ │ └── index.ts │ ├── useLockFn/ │ │ ├── index.md │ │ └── index.ts │ ├── useMap/ │ │ ├── index.md │ │ └── index.ts │ ├── useMediaQuery/ │ │ ├── index.md │ │ └── index.ts │ ├── useNetwork/ │ │ ├── index.md │ │ └── index.ts │ ├── useQRCode/ │ │ ├── index.md │ │ └── index.ts │ ├── useRequest/ │ │ ├── __tests__/ │ │ │ └── index.test.ts │ │ ├── index.md │ │ ├── index.ts │ │ ├── src/ │ │ │ ├── cache.ts │ │ │ ├── fetch.ts │ │ │ ├── loadingDelay.ts │ │ │ ├── polling.ts │ │ │ ├── service.ts │ │ │ └── visibility.ts │ │ └── types.d.ts │ ├── useRouteQuery/ │ │ ├── index.md │ │ └── index.ts │ ├── useSessionStorage/ │ │ ├── index.md │ │ └── index.ts │ ├── useSet/ │ │ ├── index.md │ │ └── index.ts │ ├── useTextSelection/ │ │ ├── index.md │ │ └── index.ts │ ├── useThrottle/ │ │ ├── index.md │ │ └── index.ts │ ├── useThrottleFn/ │ │ ├── index.md │ │ └── index.ts │ ├── useTimeout/ │ │ ├── index.md │ │ └── index.ts │ ├── useToggle/ │ │ ├── index.md │ │ └── index.ts │ ├── useUnmount/ │ │ └── index.ts │ ├── useVirtualList/ │ │ ├── index.md │ │ └── index.ts │ ├── useWebSocket/ │ │ ├── index.md │ │ └── index.ts │ └── utils/ │ ├── index.ts │ ├── memoryCache.ts │ └── testingHelpers.ts ├── rollup.config.js └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # compiled output /node_modules /dist example/node_modules example/dist # Logs logs *.log # OS .DS_Store # Tests packages/*/coverage packages/*/.nyc_output # IDEs and editors .idea .project .classpath .c9 *.launch .settings/ *.sublime-workspace # IDE - VSCode .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json ================================================ FILE: README.md ================================================


V3Hooks

针对 Vue3 的实用Hooks集合

V3Hooks也可以说是ahooks的Vue实现,绝大部分Api是保持一致的


## 🔨安装

npm i v3hooks --save
or
yarn add v3hooks

## 🏃文档

使用文档


## ⚡使用 - **Async** - `useRequest` — 一个完整的管理异步数据请求的Hook,aHook useRequest的Vue3实现,Api完全一致,如果你使用过aHook这将无缝衔接到Vue3. - **Side** - `useDebounce` — 用于处理防抖值的 Hook. - `useDebounceFn` — 用于处理防抖函数的 Hook. - `useThrottle` — 用于处理节流值的 Hook. - `useThrottleFn` — 用于处理节流函数的 Hook. - `useInterval` — 用于处理interval的 Hook. - `useTimeout` — 用于处理timeout的 Hook. - **State** - `useToggle` — 用于在两个状态值间切换的 Hook. - `useBoolean` — 优雅的管理 boolean 值的 Hook. - `useDate` — 用于处理时间格式化 Hook. - `useLocalStorage` — 简单高效管理localStorage的 Hook. - `useSessionStorage` — 简单高效管理SessionStorage的 Hook. - `useCookie` — 用于管理本地Cookie Hook. - `useNetwork` — 用于获取网络状态 Hook. - `useSet` — 用于管理Set的 Hook. - `useMap` — 用于管理Map的 Hook. - `useWebSocket` — 用于处理 WebSocket 的 Hook。 - **UI** - `useVirtualList` — 用于长列表虚拟化列表的 Hook. - `useDynamicList` — 用于管理列表状态 Hook. - `useMediaQuery` — 用于监听 mediaQuery 状态的 Hook。 - `useExternal` — 用于加载异步资源的 Hook. - `useFullscreen` — 一个用于处理 dom 全屏的 Hook. - `useDocumentVisibility` — 可以获取页面可见状态的 Hook. - `useTextSelection` — 实时获取用户当前选取的文本内容及位置Hook. - `useQRCode` — 用来生成二维码的Hook. - **Advanced** - `useLockFn` — 用于增加异步函数增加竞态锁,防并发 Hook. ## 常见问题 常见问题请见 [文档](https://github.com/yanzhandong/v3hooks/blob/master/docs/question.md) ## 🤝 感谢 如果这个项目对您有帮助,欢迎Star ================================================ FILE: babel.config.js ================================================ module.exports = { presets: [ [ '@babel/preset-env', { modules: false, }, ], '@babel/preset-react', ], }; ================================================ FILE: docs/question.md ================================================ ## 常见问题 ### setup中使用data为undefined 因为data是被Ref嵌套的响应式, 直接return到Template中使用没问题,如果想要在Setup中使用需要嵌套一层watchEffect来获取异步数据。 ``` const { data, run, cancel } = useRequest( () => { return axios.get( `https://xxx.com/application/list` ); } ); watchEffect(()=>{ console.log( data?.value ); }) ``` 可以看到下面demo中React中Ahooks也是需要这么使用的 https://codesandbox.io/s/determined-glitter-m77g6?file=/src/App.js ### 在tsx中使用useRequest 在一个tsx项目中使用useRequest,不能直接使用data返回值到html中,因为data是一个被Ref嵌套的响应式数据,在html中使用便利需要使用.value来获取真实数据,可以参考以下例子做法 ``` import { defineComponent, watchEffect, ref } from 'vue'; import { useRequest,useTimeout } from 'v3hooks'; // 模拟列表请求 const mockRequest = ()=>{ return new Promise((resolve,reject)=>{ useTimeout(()=>{ resolve({code:200,data:[{name:'aaa'},{name:'bbbbb'},{name:'ccccc'}]}) },ref(500)) }) }; interface MockDataItem{ name:string } export default defineComponent({ name: 'App', setup() { const content = ref([]); const { data } = useRequest(() => mockRequest() ); watchEffect(()=>{ if(data?.value && data.value.code === 200){ content.value = data.value.data; } }) return () => ( <>

姓名表

{ content.value.map((item:MockDataItem)=>{ return (

{item.name}

) }) } ); } }); ``` ================================================ FILE: example/.eslintignore ================================================ dist ================================================ FILE: example/README.md ================================================ # example ## Project setup ``` npm install ``` ### Compiles and hot-reloads for development ``` npm run serve ``` ### Compiles and minifies for production ``` npm run build ``` ### Lints and fixes files ``` npm run lint ``` ### Customize configuration See [Configuration Reference](https://cli.vuejs.org/config/). ================================================ FILE: example/babel.config.js ================================================ module.exports = { presets: [ '@vue/cli-plugin-babel/preset' ] } ================================================ FILE: example/jsconfig.json ================================================ { "compilerOptions": { "target": "es5", "module": "esnext", "baseUrl": "./", "moduleResolution": "node", "paths": { "@/*": [ "src/*" ] }, "lib": [ "esnext", "dom", "dom.iterable", "scripthost" ] } } ================================================ FILE: example/package.json ================================================ { "name": "example", "version": "0.1.0", "private": true, "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint" }, "dependencies": { "@vueuse/core": "^5.0.3", "axios": "^0.21.1", "core-js": "^3.8.3", "v3hooks": "^1.5.0", "vue": "^3.0.4", "vue-router": "4.0.3" }, "devDependencies": { "@babel/core": "^7.12.16", "@babel/eslint-parser": "^7.12.16", "@typescript-eslint/eslint-plugin": "^4.15.1", "@typescript-eslint/parser": "^4.15.1", "@vue/cli-plugin-babel": "~5.0.0-beta.2", "@vue/cli-plugin-eslint": "~5.0.0-beta.2", "@vue/cli-plugin-typescript": "~4.5.0", "@vue/cli-service": "~5.0.0-beta.2", "@vue/compiler-sfc": "^3.0.4", "@vue/eslint-config-typescript": "^7.0.0", "eslint": "^7.20.0", "eslint-plugin-vue": "^7.2.0", "typescript": "~4.1.5" }, "eslintConfig": { "root": true, "env": { "node": true }, "extends": [ "plugin:vue/vue3-essential", "eslint:recommended", "@vue/typescript" ], "parserOptions": { "parser": "@typescript-eslint/parser" }, "rules": {} }, "browserslist": [ "> 1%", "last 2 versions", "not dead", "not ie 11" ] } ================================================ FILE: example/public/index.html ================================================ <%= htmlWebpackPlugin.options.title %>
================================================ FILE: example/src/App.vue ================================================ ================================================ FILE: example/src/components/HelloWorld.vue ================================================ ================================================ FILE: example/src/main.ts ================================================ import { createApp } from 'vue' import App from './App.vue' import { createRouter } from './router' const app = createApp(App); const router = createRouter(); app.use(router); app.mount('#app') ================================================ FILE: example/src/pages/home/index.vue ================================================ ================================================ FILE: example/src/pages/useBoolean/index.vue ================================================ ================================================ FILE: example/src/pages/useCookie/index.vue ================================================ ================================================ FILE: example/src/pages/useDate/index.vue ================================================ ================================================ FILE: example/src/pages/useDynamicList/index.vue ================================================ ================================================ FILE: example/src/pages/useExternal/index.vue ================================================ ================================================ FILE: example/src/pages/useFullscreen/index.vue ================================================ ================================================ FILE: example/src/pages/useInterval/index.vue ================================================ ================================================ FILE: example/src/pages/useLocalStorage/index.vue ================================================ ================================================ FILE: example/src/pages/useLockFn/index.vue ================================================ ================================================ FILE: example/src/pages/useMediaQuery/index.vue ================================================ ================================================ FILE: example/src/pages/useNetwork/index.vue ================================================ ================================================ FILE: example/src/pages/useQRCode/index.vue ================================================ ================================================ FILE: example/src/pages/useRouteQuery/index.vue ================================================ ================================================ FILE: example/src/pages/useSessionStorage/index.vue ================================================ ================================================ FILE: example/src/pages/useSetAndUseMap/index.vue ================================================ ================================================ FILE: example/src/pages/useTextSelection/index.vue ================================================ ================================================ FILE: example/src/pages/useToggle/index.vue ================================================ ================================================ FILE: example/src/pages/useVirtualList/index.vue ================================================ ================================================ FILE: example/src/pages/useWebSocket/index.vue ================================================ ================================================ FILE: example/src/router.ts ================================================ import { createRouter as _createRouter, createWebHashHistory } from 'vue-router' // Auto generates routes from vue files under ./pages // https://vitejs.dev/guide/features.html#glob-import // @ts-ignore // const pages = import.meta.glob('./pages/*/index.vue') // const routes = Object.keys(pages).map((path) => { // // @ts-ignore // const name = path.match(/\.\/pages(.*)\/index\.vue$/)[1].toLowerCase(); // console.log(name,'name'); // return { // path: name === '/home' ? '/' : name, // component: pages[path] // () => import('./pages/*.vue') // } // }) export function createRouter() { return _createRouter({ // use appropriate history implementation for server/client // import.meta.env.SSR is injected by Vite. history: createWebHashHistory(), routes:[ { path:'/', component: () => import('./pages/home/index.vue') }, { path:'/useVirtualList', component: () => import('./pages/useVirtualList/index.vue') }, { path:'/useDynamicList', component: () => import('./pages/useDynamicList/index.vue') }, { path:'/useLocalStorage', component: () => import('./pages/useLocalStorage/index.vue') }, { path:'/useSessionStorage', component: () => import('./pages/useSessionStorage/index.vue') }, // { // path:'/useRouteQuery', // component: () => import('./pages/useRouteQuery/index.vue') // }, { path:'/useCookie', component: () => import('./pages/useCookie/index.vue') }, { path:'/useDate', component: () => import('./pages/useDate/index.vue') }, { path:'/useNetwork', component: () => import('./pages/useNetwork/index.vue') }, { path:'/useLockFn', component: () => import('./pages/useLockFn/index.vue') }, { path:'/useSetAndUseMap', component: () => import('./pages/useSetAndUseMap/index.vue') }, { path:'/useMediaQuery', component: () => import('./pages/useMediaQuery/index.vue') }, { path:'/useExternal', component: () => import('./pages/useExternal/index.vue') }, { path:'/useFullscreen', component: () => import('./pages/useFullscreen/index.vue') }, { path:'/useTextSelection', component: () => import('./pages/useTextSelection/index.vue') }, { path:'/useInterval', component: () => import('./pages/useInterval/index.vue') }, { path:'/useQRCode', component: () => import('./pages/useQRCode/index.vue') }, { path:'/useToggle', component: () => import('./pages/useToggle/index.vue') }, { path:'/useBoolean', component: () => import('./pages/useBoolean/index.vue') }, { path:'/useWebSocket', component: () => import('./pages/useWebSocket/index.vue') } ] }) } ================================================ FILE: example/src/shims-vue.d.ts ================================================ /* eslint-disable */ declare module '*.vue' { import type { DefineComponent } from 'vue' const component: DefineComponent<{}, {}, any> export default component } ================================================ FILE: example/tsconfig.json ================================================ { "compilerOptions": { "target": "esnext", "module": "esnext", "strict": true, "jsx": "preserve", "importHelpers": true, "moduleResolution": "node", "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "sourceMap": true, "baseUrl": ".", "types": [ "webpack-env" ], "paths": { "@/*": [ "src/*" ] }, "lib": [ "esnext", "dom", "dom.iterable", "scripthost" ] }, "include": [ "src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx" ], "exclude": [ "node_modules" ] } ================================================ FILE: example/vue.config.js ================================================ module.exports = { devServer: { port: 8081 } } ================================================ FILE: jest.config.js ================================================ module.exports = { moduleFileExtensions: ['js', 'ts', 'jsx', 'tsx'], transform: { '^.+\\.(js|jsx)$': 'babel-jest', '^.+\\.(ts|tsx)$': 'ts-jest', }, testEnvironment: 'jsdom', collectCoverageFrom: [ '/packages/**/*.{ts,tsx}', '!**/node_modules/**', '!/packages/__tests__/**/*', '!/dist/**/*', ], coveragePathIgnorePatterns: ['/node_modules/', '/__fixtures__/', '/fixtures/', '/__tests__/helpers/', '/__tests__/utils/', '__mocks__'], testMatch: ['**/__tests__/**/*.test.[jt]s?(x)'], globals: { 'ts-jest': { babelConfig: './babel.config.js', }, } }; ================================================ FILE: package.json ================================================ { "name": "v3hooks", "version": "1.5.0", "author": "yanzhandong", "description": "针对 Vue3 的实用Hooks集合", "keywords": [ "vuehook", "hook", "vue3", "vue3hook", "v3hooks", "vueHook" ], "homepage": "https://yanzhandong868.gitbook.io/v3hooks/", "main": "dist/index.cjs.js", "module": "dist/index.es.js", "unpkg": "dist/index.js", "types": "dist/index.d.ts", "repository": { "type": "git", "url": "git+https://github.com/yanzhandong/v3hooks.git" }, "files": [ "dist" ], "license": "MIT", "scripts": { "test": "jest", "watch": "rollup -c --watch", "build": "cross-env NODE_ENV=production rollup -c" }, "devDependencies": { "@babel/core": "^7.16.12", "@babel/plugin-transform-runtime": "^7.16.10", "@babel/preset-env": "^7.16.11", "@babel/preset-react": "^7.16.7", "@rollup/plugin-babel": "^5.3.0", "@rollup/plugin-commonjs": "^21.0.1", "@rollup/plugin-node-resolve": "^13.1.3", "@types/jest": "^27.4.0", "@types/js-cookie": "^2.2.7", "@vue/test-utils": "^2.0.0-rc.17", "babel-jest": "^27.5.1", "cross-env": "^7.0.3", "jest": "^27.5.1", "mockdate": "^3.0.5", "rollup": "^2.52.6", "rollup-plugin-dts": "^3.0.2", "rollup-plugin-terser": "^7.0.2", "rollup-plugin-typescript2": "^0.30.0", "rollup-plugin-vue": "^6.0.0", "ts-jest": "^27.1.3", "tslib": "^2.3.0", "typescript": "^4.3.5", "vue": "^3.0.11", "vue-router": "4.0.3" }, "dependencies": { "@babel/runtime": "^7.16.7", "dayjs": "^1.10.5", "easyqrcodejs": "^4.4.5", "js-cookie": "^2.2.1" } } ================================================ FILE: packages/index.ts ================================================ import useRequest from './useRequest/index'; import useDate from './useDate/index'; import useDebounce from './useDebounce/index'; import useDebounceFn from './useDebounceFn/index'; import useThrottle from './useThrottle/index'; import useThrottleFn from './useThrottleFn/index'; import useBoolean from './useBoolean/index'; import useToggle from './useToggle/index'; import useVirtualList from './useVirtualList/index'; import useDynamicList from './useDynamicList/index'; import useLocalStorage from './useLocalStorage/index'; import useSessionStorage from './useSessionStorage/index'; import useNetwork from './useNetwork/index'; import useCookie from './useCookie/index'; import useLockFn from './useLockFn/index'; import useSet from './useSet/index'; import useMap from './useMap/index'; import useMediaQuery from './useMediaQuery/index'; import useExternal from './useExternal/index'; import useFullscreen from './useFullscreen/index'; import useDocumentVisibility from './useDocumentVisibility/index' import useTextSelection from './useTextSelection/index' import useInterval from './useInterval/index' import useTimeout from './useTimeout/index' import useQRCode from './useQRCode/index' import useWebSocket from './useWebSocket/index' import useUnmount from './useUnmount/index' // 暂时无法@/分包 未引入vue-router会导致都不能用 // import useRouteQuery from './useRouteQuery/index'; export { // Async useRequest, // Side useDebounce, useDebounceFn, useThrottle, useThrottleFn, useInterval, useTimeout, // State useBoolean, useToggle, useDate, useCookie, useLocalStorage, useSessionStorage, // useRouteQuery, useNetwork, useSet, useMap, useWebSocket, // UI useVirtualList, useDynamicList, useMediaQuery, useExternal, useFullscreen, useDocumentVisibility, useTextSelection, useQRCode, //Advanced useLockFn, useUnmount } ================================================ FILE: packages/useBoolean/index.md ================================================ # useBoolean 优雅的管理 boolean 值的 Hook。 ## 使用 ``` ``` useBoolean默认切换布尔值状态,也可以接收一个参数作为新的值。 ## Api ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | defaultValue | 可选项,传入默认的状态值 | Ref | false | ### Result | 参数 | 说明 | 类型 | | :----| :---- | :---- | | state | 状态值 | - | | actions | 操作集合 | Actions | ### Actions | 参数 | 说明 | 类型 | | :----| :---- | :---- | | toggle | 触发状态更改的函数,可以接受一个可选参数修改状态值 | (value?: boolean) => void | | setTrue | 设置状态值为 true | () => void | | setFalse | 设置状态值为 false | () => void | ================================================ FILE: packages/useBoolean/index.ts ================================================ import { Ref } from 'vue'; import useToggle from '../useToggle'; // 默认值 const defaultValue = false; interface Actions{ toggle: ()=> void; setTrue: ()=> void; setFalse: ()=> void; } function useBoolean( value?: boolean ): [Ref,Actions] /** * * @param defaultValue * @returns */ function useBoolean (value?: boolean){ value = value || defaultValue; const [ state, [ toggle ]] = useToggle(value,!value); const setTrue = () => toggle(true); const setFalse = () => toggle(false); const actions: Actions = { toggle, setTrue, setFalse } return [state, actions ] } export default useBoolean ================================================ FILE: packages/useCookie/index.md ================================================ # useCookie 一个用来操作Cookie的 Hook 。 ## 使用Demo ```vue ``` useCookie接受一个key是cookie中的键名。 修改返回的state可直接修改cookie. ## 注意点 * state等于undefined或者null可用于删除本地Cookie 例:`state.value = undefined;` ## Api ``` interface Options{ watch?: boolean, defaultValue?: string | undefined, expires?: number | Date, path?: string, domain?: string, secure?: boolean, sameSite?: 'strict' | 'lax' | 'none' } const state = useCookieState( key: string, options?: Options, ) ``` ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | key | 存储在本地 cookie 的 key 值 | string | - | | options |可选项,配置 cookie 属性,详见 Options | Options | - | ### Result | 参数 | 说明 | 类型 | | :----| :---- | :---- | | state | 本地 cookie 值 | Ref | ### Options | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | defaultValue | 可选,定义 cookie 默认值,但不同步到本地 cookie | string | undefined | | expires |可选,定义 cookie 存储有效时间 | number | Date | - | | path | 可选,定义 cookie 可用的路径 | string | / | | domain | 可选,定义 cookie 可用的域,默认为 cookie 创建的域名 | string | - | | secure | 可选,cookie 传输是否需要 https 安全协议 | boolean | - | | sameSite | 可选,cookie 不能与跨域请求一起发送 | `strict` | `lax` | `none` | - | ================================================ FILE: packages/useCookie/index.ts ================================================ import { ref, watch as vueWatch } from 'vue'; import Cookies from 'js-cookie'; interface Options{ watch?: boolean, defaultValue?: string | undefined, expires?: number | Date, path?: string, domain?: string, secure?: boolean, sameSite?: 'strict' | 'lax' | 'none' } const defaultOptions = { watch: false, defaultValue: undefined }; const useCookie = (key: string, options?:Options)=>{ const { watch, defaultValue } = { ...defaultOptions, ...options }; const state = ref( Cookies.get(key) || defaultValue ); const setCookie = ( value: any )=>{ Cookies.set(key, value, { ...options }); state.value = value; }; if( watch ){ vueWatch( state, (value)=>{ if( value === null || value === undefined ){ Cookies.remove(key); return } setCookie(value); }, { deep: true } ) } return state }; export default useCookie ================================================ FILE: packages/useDate/index.md ================================================ # useDate 一个用来操作时间的 Hook 。 内部使用了 dayjs 作为format工具 ## 使用Demo ```vue ``` useDate接受一个时间并根据options来返回格式化后的数据。 可根据返回的refresh来进行更新调用. 其中method的参数具体使用可以参考dayjs的 [取值/赋值](https://dayjs.gitee.io/docs/zh-CN/get-set/millisecond) ## Api ``` interface Options{ format?: string method?: 'format' | 'millisecond' | 'second' | 'minute' | 'hour' | 'date' |'day' | 'month' | 'year', methodParam?: number } function useDate( value?: Value | undefined, options?: Options ): { readonly data: any, refresh: () => void; } ``` ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | initialValue | 初始化的时间值 | string - number - Date | Date | | options | 可选项,配置时间属性,详见 Options | Options | - | ### Options | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | format | 针对日期格式化 | string | `YYYY-MM-DD HH:mm:ss` | | method | 获取时间的操作方法 | 见Api Options.method | `format` | | methodParam | 针对日期格式化的操作方法的参数 | number | - | ### Result | 参数 | 说明 | 类型 | | :----| :---- | :---- | | data | 格式化后的时间值 | Ref | | refresh | 格式化后的时间值 | (refreshValue)=> void | ================================================ FILE: packages/useDate/index.ts ================================================ import { ref, readonly } from 'vue'; import dayjs from 'dayjs'; const defaultOptions ={ format: 'YYYY-MM-DD HH:mm:ss', method: 'format' }; type Value = string | number | Date; interface Options{ format?: string method?: 'format' | 'millisecond' | 'second' | 'minute' | 'hour' | 'date' |'day' | 'month' | 'year', methodParam?: number } function useDate( value?: Value | undefined, options?: Options ): { readonly data: any, refresh: (refreshValue?: Value) => void; } function useDate( initialValue?:Value, options?: Options ){ const state = ref(); let value = initialValue || +new Date(); const { format, method, methodParam } = { ...defaultOptions, ...options } const refresh = ( refreshValue?:Value )=>{ console.log(refreshValue); value = refreshValue || +new Date(); switch( method ){ case 'format': state.value = dayjs(value).format(format); break; case undefined : break; default: let data: any = dayjs(value); if( methodParam ){ data = data[method](methodParam); if( options && options.format ){ data = data.format(format); } } state.value = data } }; refresh(); const data = readonly( state); return { data, refresh } }; export default useDate ================================================ FILE: packages/useDebounce/index.md ================================================ # useDebounce 用来处理防抖值的 Hook。 ## 使用 ``` ``` 使用useDebounce后,频繁设置debounceCurrValue不会立刻改变debounceValue, debounceValue只会在输入结束 500ms 后变化。 ## Api ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | value | 需要防抖的值 | any | - | | wait | 超时时间,单位为毫秒 | number | 1000 | ================================================ FILE: packages/useDebounce/index.ts ================================================ // import { debounce } from '../utils' import useDebounceFn from '../useDebounceFn' import { ref,Ref, watch } from 'vue'; // 默认值 const defaultDelay = 1000; /** * 处理防抖值 * @param value * @param delay * @returns */ const useDebounce = ( value: Ref , delay?:number )=>{ delay = delay || defaultDelay; const res = ref(value.value) as Ref; // 利用useDebounceFn来简化处理值 const { run } = useDebounceFn(()=> res.value = value.value ,delay); watch(value,()=> run(),{ deep: true }); return res; }; export default useDebounce ================================================ FILE: packages/useDebounceFn/index.md ================================================ # useDebounceFn 用来处理防抖函数的 Hook。 ## 使用 ``` ``` 频繁调用 debounceFnRun,但只会在所有点击完成 500ms 后执行一次相关函数 ## Api ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | fn | 需要防抖执行的函数 | () => void | - | | wait | 超时时间,单位为毫秒 | number | 1000 | ================================================ FILE: packages/useDebounceFn/index.ts ================================================ import { Fn, debounce } from '../utils' const defaultDelay = 1000; /** * 处理防抖函数 * @param fn * @param delay * @returns */ const useDebounceFn = ( fn:Fn, delay?: number )=>{ const run = debounce( fn, typeof( delay ) === 'number'? delay : defaultDelay ); return { run }; } export default useDebounceFn ================================================ FILE: packages/useDocumentVisibility/index.md ================================================ # useDocumentVisibility 可以获取页面可见状态的 Hook。 ## 使用Demo ### 基础用法 ```vue ``` 监听 document 的可见状态 ## Api ``` const useDocumentVisibility: () => Ref; ``` ### Result | 参数 | 说明 | 类型 | | :----| :---- | :---- | | documentVisibility | 判断 document 是否在是否处于可见状态 | boolean | ================================================ FILE: packages/useDocumentVisibility/index.ts ================================================ import { ref, Ref } from 'vue'; const useDocumentVisibility = (): Ref =>{ const documentVisibility = ref( document.hidden ); let handler = ()=> { documentVisibility.value = document.hidden; }; document.addEventListener('visibilitychange',handler ); return documentVisibility } export default useDocumentVisibility ================================================ FILE: packages/useDynamicList/index.md ================================================ # useDynamicList 一个帮助你管理列表状态,并能生成唯一 key 的 Hook。 ## 使用Demo ```vue ``` useDynamicList接受一个数组,导出一个list及一系列操作数组的方法。 ## Api ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | initialValue | 列表的初始值 | T[] | [] | ### Result | 参数 | 说明 | 类型 | | :----| :---- | :---- | | list | 当前的列表 | T[] | | resetList | 重新设置 list 的值 | (list: T[]) => void | | insert | 在指定位置插入元素 | (index: number, obj: T) => void | | merge | 在指定位置插入多个元素 | (index: number, obj: T[]) => void | | replace | 替换指定元素 | (index: number, obj: T) => void | | remove | 删除指定元素 | (index: number) => void | | move | 移动元素 | (oldIndex: number, newIndex: number) => void | | getKey | 获得某个元素的 uuid | (index: number) => number | | getIndex | 获得某个key的 index | (key: number) => number | | push | 在列表末尾添加元素 | (obj: T) => void | | pop | 移除末尾元素 | () => void | | unshift | 在列表起始位置添加元素 | (obj: T) => void | | shift | 移除起始位置元素 | () => void | ================================================ FILE: packages/useDynamicList/index.ts ================================================ import { ref, Ref, isRef } from 'vue'; const useDynamicList = (initialValue: Ref)=>{ let uuid = (0); const uuidKeys = ref([]); const setUUID = (index?:number)=>{ index = index === undefined ? uuidKeys.value.length : index; uuidKeys.value.splice( index, 0 ,uuid++ ); }; (()=>{ initialValue.value.forEach(()=> setUUID() ); })() const resetList = (resetList: Ref | T[])=>{ uuidKeys.value = []; if( isRef(resetList) ){ resetList.value.forEach(()=> setUUID() ); initialValue = resetList; return } resetList.forEach(()=> setUUID() ); initialValue.value = resetList; }; const insert = (index: number, obj: T)=>{ initialValue.value.splice( index, 0, obj); setUUID(index); }; const merge = (index: number, obj: T[])=>{ obj.forEach((active,i) => setUUID(index + i) ); initialValue.value.splice( index, 0, ...obj); }; const replace = (index: number, obj: T)=>{ initialValue.value.splice( index, 1, obj); }; const remove = (index: number)=>{ uuidKeys.value.splice( index, 1); initialValue.value.splice( index, 1); }; const move = (oldIndex: number, newIndex: number)=>{ if (oldIndex === newIndex) return; [ initialValue.value[oldIndex], initialValue.value[newIndex] ] = [ initialValue.value[newIndex], initialValue.value[oldIndex] ]; [ uuidKeys.value[oldIndex], uuidKeys.value[newIndex] ] = [ uuidKeys.value[newIndex], uuidKeys.value[oldIndex] ]; }; const getKey = (index: number)=> uuidKeys.value[index]; const getIndex = (key: number)=> uuidKeys.value.indexOf(key); const push = (obj: T)=>{ initialValue.value.push( obj ); setUUID(); }; const pop = ()=>{ initialValue.value.pop(); uuidKeys.value.pop(); }; const unshift = (obj: T)=>{ initialValue.value.unshift( obj ); setUUID(0); }; const shift = ()=>{ initialValue.value.shift(); uuidKeys.value.shift(); }; return { list: initialValue, resetList, insert, merge, replace, remove, move, getKey, getIndex, push, pop, unshift, shift } } export default useDynamicList ================================================ FILE: packages/useExternal/index.md ================================================ # useExternal 一个用于动态地向页面加载或卸载外部资源的 Hook。 ## 使用Demo ### 基本使用 ```vue ``` 页面上加载外部 javascript 文件,例如引入 449c2e5dd6a948e3af67b69c4ece907a.js ### 引入css ```vue ``` 页面上加载外部 css 文件,例如引入 bootstrap-badge.css ### 引入图片 ```vue ``` 加载一个静态图片作为外部资源引入页面 useExternal接受一个src路径, 导出资源本身和操作方法. ## Api ``` interface Options { manual?: boolean; async?: boolean; crossOrigin?: 'anonymous' | 'use-credentials'; referrerPolicy?: 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url'; noModule?: boolean; defer?: boolean; media?: string; target?: HTMLElement | Ref; } const useExternal: (src: string, onLoaded?: ((el: Elements) => void) | undefined, options?: Options) => { resources: Ref; load: () => Promise; unload: () => void; }; ``` ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | src | 外部资源 url 地址 | string | - | | onLoaded | 资源加载成功回调 | (el: Elements) => void) | - | | options | 参数 | Options | - | ### Options | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | manual | 是否手动执行 | boolean | false | | async | script标签是否增加async属性 | boolean | - | | crossOrigin | script标签是否增加crossOrigin属性 | `'anonymous' - 'use-credentials'` | - | | referrerPolicy | script标签是否增加referrerPolicy属性 | 原生referrerPolicy | | - | | noModule | script标签是否增加noModule属性 | boolean | - | | defer | script标签是否增加defer属性 | boolean | - | | media | 引入外链样式表 的 media 属性, 如 all/screen/print/handheld | string | all | | target | 需插入外部图片资源 的父容器 DOM 节点或者 Ref | HTMLElement | Ref | - | ### Result | 参数 | 说明 | 类型 | | :----| :---- | :---- | | resources | 加载后的资源 | any | | load | 开始加载外部资源 | () => void | | unload | 卸载外部资源 | () => void | ================================================ FILE: packages/useExternal/index.ts ================================================ import { ref, Ref, isRef } from 'vue'; type Elements = HTMLScriptElement| HTMLLinkElement | HTMLImageElement; interface Options{ manual?: boolean, async?: boolean, crossOrigin?: 'anonymous' | 'use-credentials' referrerPolicy?: 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url' noModule?: boolean defer?: boolean, media?: string, target?: HTMLElement | Ref } const useExternal = ( src: string, onLoaded?: (el: Elements) => void, options: Options = {}, )=>{ const resources = ref( null ); const { manual = false, async, crossOrigin, referrerPolicy, noModule, defer, media = 'all', target = document.body } = options; let el: Elements = document.createElement('script'); let parentEl: Element | HTMLElement = document.head; const loadScript = ()=> new Promise((resolve)=>{ const isExist = document.querySelector(`script[src="${src}"]`); if( isExist ) return el = document.createElement('script'); el.src = src; el.type = 'text/javascript'; if( async ) el.async = async; if( defer ) el.defer = defer; if( noModule ) el.noModule = noModule; if( crossOrigin ) el.crossOrigin = crossOrigin; if( referrerPolicy ) el.referrerPolicy = referrerPolicy; el = parentEl.appendChild(el); resolve(el); }); const loadCss = ()=> new Promise((resolve)=>{ const isExist = document.querySelector(`link[href="${src}"]`); if( isExist ) return el = document.createElement('link'); el.href = src; el.rel = 'stylesheet' el.type='text/css'; el.media = media; el = parentEl.appendChild(el); resolve(el); }); const loadImage = ()=> new Promise((resolve)=>{ const isExist = document.querySelector(`img[src="${src}"]`); if( isExist ) return el = document.createElement('img'); el.src = src; parentEl = isRef( target) ? target.value : target; parentEl.appendChild(el); resolve(el); }); const load = ()=> new Promise(async (resolve, reject)=>{ if( /\.js$/.test(src) ){ await loadScript(); } if( /\.css$/.test(src)){ await loadCss(); } if( /\.(gif|jpg|jpeg|png|svg|GIF|JPG|PNG|)$/.test(src)){ await loadImage() } resources.value = el; el.addEventListener('error', (event: any) => reject(event)); el.addEventListener('abort', (event: any) => reject(event)); el.addEventListener('load', () => { onLoaded && onLoaded(el) }); resolve(el) }); const unload = ()=>{ if( resources.value ){ parentEl.removeChild(resources.value); } } if( !manual ) load() return { resources, load, unload } } export default useExternal ================================================ FILE: packages/useFullscreen/index.md ================================================ # useFullscreen 一个用于处理 dom 全屏的 Hook。 ## 使用Demo ### 基础用法 ```vue ``` 没有传值默认为document.body ### 局部全屏 ```vue ``` Ref传值为fullScreen的div标签,则只会此区域全屏 ## 介绍 useFullscreen接受一个HTMLElement, 导出操作方法. ## Api ``` interface Actions { setFull: () => void; exitFull: () => void; toggle: () => void; } const useFullscreen: ( target?: Target$1 | undefined, options?: Options | undefined ) => [ isFullscreen: Ref, actions: Actions ]; ``` ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | target | 原生Dom或者被Ref嵌套的Dom | `HTMLElement or Ref or (() => HTMLElement)` | document.body | | options | 设置(可选) | Options | - | ### Options | 参数 | 说明 | 类型 | | :----| :---- | :---- | | onExitFull | 监听退出全屏 | ()=>void | | onFull | 监听全屏 | ()=>void | ### Result | 参数 | 说明 | 类型 | | :----| :---- | :---- | | isFullscreen | 是否全屏 | boolean | | setFull | 设置全屏 | ()=>void | | exitFull | 退出全屏 | ()=>void | | toggleFull | 切换全屏 | ()=>void | ================================================ FILE: packages/useFullscreen/index.ts ================================================ import { ref, Ref, isRef, onMounted, onUnmounted } from 'vue'; interface Options{ onFull?: ()=> void, onExitFull?: ()=> void } interface Actions{ setFull: ()=> void, exitFull: ()=> void, toggle: ()=> void } type Target = HTMLElement | ( () => HTMLElement ) | Ref; const defaultOptions = { onFull: function(){}, onExitFull: function(){}, }; const useFullscreen = ( target?: Target, options?: Options ):[ isFullscreen: Ref, actions: Actions ]=>{ const fullScreenElement = !!document.fullscreenElement; const isFullscreen = ref(fullScreenElement); const { onFull, onExitFull } = { ...defaultOptions, ...options}; let el:HTMLElement = document.body; const getEl = ()=>{ if( typeof target === 'function'){ return target() } return isRef( target ) ? target.value : target; }; const handler = ()=>{ if(isFullscreen.value){ onFull() }else{ onExitFull() } }; onMounted(()=>{ el = getEl() || el; el.addEventListener('fullscreenchange',handler) }); onUnmounted(()=>{ el.removeEventListener('fullscreenchange',handler) }); const actions:Actions = { setFull: ()=>{ if( isFullscreen.value ) return el.requestFullscreen(); isFullscreen.value = true; }, exitFull: ()=>{ if( !isFullscreen.value ) return document.exitFullscreen(); isFullscreen.value = false; }, toggle: ()=>{ isFullscreen.value ? actions.exitFull(): actions.setFull() } }; return [ isFullscreen, actions ] }; export default useFullscreen ================================================ FILE: packages/useInterval/index.md ================================================ # useInterval 一个可以处理 setInterval 的 Hook。 ## 基础使用 ```vue ``` 每1000ms,执行一次,设置delay为null则立即中断 ## 非中断式调用 ```vue ``` useInterval可以接受一个普通number参数,这样的useInterval可以接受一个普通number参数,不会被中断会一直被执行,谨慎使用!! ## Api ``` interface UseIntervalOptions { immediate?: boolean; } const useInterval: ( fn: Fn, delay: number | Ref, options?: UseIntervalOptions | undefined ) => void; ``` ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | fn | 要重复调用的函数 | (...args: any[]) => void | - | | delay | 间隔时间,当取值为 null 或 undefined 时会停止计时器 | number - Ref - Ref - Ref | - | | options | 配置计时器的行为,详见下面的 Options | Options | - | ### Options | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | immediate | 参数可以用来控制是否在首次渲染时立即执行 | boolean| false | ================================================ FILE: packages/useInterval/index.ts ================================================ import { ref, Ref, onMounted, onUnmounted, isRef } from 'vue'; import { Fn } from '../utils'; interface UseIntervalOptions{ immediate?: boolean } const defaultOptions = { immediate: false, }; const useInterval = ( fn: Fn, delay: number | Ref, options?: UseIntervalOptions )=>{ const{ immediate } = {...defaultOptions,...options}; const state = isRef(delay) ? delay : ref(delay); if( immediate ) fn() let timer: null | NodeJS.Timeout = null; const clear = ()=> timer && clearTimeout(timer) const handler = ()=>{ if( state.value === undefined || state.value === null ) return fn(); run(); }; const run = ()=>{ if( state.value === undefined || state.value === null ){ clear(); return } setTimeout(handler,state.value) } run(); onUnmounted(()=> clear() ) }; export default useInterval ================================================ FILE: packages/useLocalStorage/index.md ================================================ # useLocalStorage 一个可以将状态持久化存储在 localStorage 中的 Hook 。 ## 使用Demo ```vue ``` useLocalStorage接受一个key和一个value,导出一个响应式的state, 用户直接赋值state.value可自动修改本地localStorage。 ## 注意点 * 不设置value可用于获取本地LocalStorage 例:`useLocalStorage('useLocalStorage')` * value等于undefined或者null可用于删除本地Storage 例:`state.value = undefined;` ## Api ``` const state = useLocalStorage( key: string, initialValue?: any, options?: Options ); ``` ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | key | LocalStorage存储键名 | any | - | | initialValue | 初始值 | any | {} | | options | 配置 | Options | - | ### Options | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | watch | 是否实时修改LocalStorage | boolean | true | ### Result | 参数 | 说明 | 类型 | | :----| :---- | :---- | | state | 可以被修改的数据源 | Ref | ================================================ FILE: packages/useLocalStorage/index.ts ================================================ import { ref, Ref, isRef, watch as vueWatch } from 'vue'; import { TypeSerializers,getValueType } from '../utils' const storage = localStorage; interface Options{ watch: boolean } const defaultOptions = { watch: true } const useLocalStorage = ( key: string, initialValue?: T | Ref, options?:Options)=>{ const { watch } = { ...defaultOptions, ...options }; const data = ref() as Ref; try{ if( initialValue !== undefined ){ data.value = isRef( initialValue ) ? initialValue.value : initialValue; }else{ data.value = JSON.parse( storage.getItem(key) || '{}' ); } }catch(error){ console.log(error,'useLocalStorage初始化失败') } const type = getValueType(data.value); // 判断类型取格式化方法 let serializer = TypeSerializers[type]; const setStorage = ()=> storage.setItem( key, serializer.write(data.value) );; // 状态监听 if( watch ){ vueWatch( data, (newValue)=>{ if( newValue === undefined || newValue === null ){ storage.removeItem(key); return } setStorage(); }, { deep:true } ) } setStorage() return data }; export default useLocalStorage ================================================ FILE: packages/useLockFn/index.md ================================================ # useLockFn 用于给一个异步函数增加竞态锁,防止并发执行。 ## 使用Demo ```vue ``` useLockFn接受一个异步的函数, 并返回一个具有执行锁的函数 ## Api ``` const useLockFn: (fn: Fn) => (...args: ArgsAny) => Promise; ``` ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | fn | 需要增加竞态锁的函数 | (...args: any[]) => any | - | ### Result | 参数 | 说明 | 类型 | | :----| :---- | :---- | | fn | 增加了竞态锁的函数 | (...args: any[]) => any | ================================================ FILE: packages/useLockFn/index.ts ================================================ import { ref } from 'vue'; type ArgsAny = any[]; type Fn = (...args: ArgsAny)=> Promise; const useLockFn = (fn: Fn)=>{ const lock = ref(false); return async( ...args: ArgsAny )=>{ if( lock.value ) return lock.value = true; try{ const ret = await fn(...args); lock.value = false; return ret; }catch ( error ){ lock.value = false; throw error; } } }; export default useLockFn ================================================ FILE: packages/useMap/index.md ================================================ # useMap 一个可以管理 Map 类型状态的 Hook。 ## 使用Demo ```vue ``` useMap接受一个 Map 可接受的参数, 并导出以下方法. ## Api ``` type MapValue = readonly (readonly [any, any])[] interface Actions{ set: ( key: string, value: T)=> void, get: ( key: string )=> T, remove: ( key: string )=> void, has: ( key: string )=> boolean, clear: ()=> void, setAll: (newMap: MapValue)=> void; reset: ()=> void, } function useMap (initialValue?:MapValue) : [ state: Ref>, actions: Actions ] ``` ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | initialValue | 可选项,传入默认的 Map 参数 | readonly[any,any] | - | ### Result | 参数 | 说明 | 类型 | | :----| :---- | :---- | | state | Map 对象 | Map | | set | 添加元素 | ( key: string, value: T)=> void | | get | 移除元素 | ( key: string )=> T | | remove | 移除元素 | ( key: string )=> void | | has | 判断是否存在元素 |( key: string )=> boolean | | clear | 清空Map | ()=> void | | setAll | 添加并生成一个新的 Map 对象 | (newMap: (readonly [any, any])[])=> void | | reset | 重置为默认值 | ()=> void | ================================================ FILE: packages/useMap/index.ts ================================================ import { ref, Ref, markRaw } from 'vue'; type MapValue = readonly (readonly [any, any])[] interface Actions{ set: ( key: string, value: T)=> void, get: ( key: string )=> T, remove: ( key: string )=> void, has: ( key: string )=> boolean, clear: ()=> void, setAll: (newMap: MapValue)=> void; reset: ()=> void, } function useMap (initialValue?:MapValue) : [ state: Ref>, actions: Actions ] function useMap(initialValue?:MapValue){ const initialMap = initialValue ? new Map(initialValue) : new Map(); const state = ref(initialMap) as Ref>; const actions:Actions = { set:( key: any, value: T )=>{ state.value.set(key,value); }, get:( key: any )=>{ return state.value.get(key); }, remove: ( key: any )=>{ state.value.delete( key ); }, has: ( key: any )=> state.value.has(key), clear: ()=> state.value.clear(), setAll: ( newMap: MapValue )=>{ state.value = new Map(newMap); }, reset: ()=> state.value = initialMap }; return [ state, markRaw(actions) ] }; export default useMap ================================================ FILE: packages/useMediaQuery/index.md ================================================ # useMediaQuery 一个监听 mediaQuery 状态的 Hook。 ## 使用Demo ```vue ``` useMediaQuery接受一个MediaQuery条件, 导出一个state boolean值来判断是否满足MediaQuery条件 ## Api ``` const useMediaQuery: (query: string) => vue.Ref; ``` ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | query | MediaQuery条件 | string | - | ### Result | 参数 | 说明 | 类型 | | :----| :---- | :---- | | state | 否满足MediaQuery条件 | boolean | ================================================ FILE: packages/useMediaQuery/index.ts ================================================ import { ref, onUnmounted } from 'vue'; const useMediaQuery = (query:string)=>{ const mediaQuery = window.matchMedia(query); const stata = ref( mediaQuery.matches ); const handleChange = ( event: MediaQueryListEvent )=> stata.value = event.matches; mediaQuery.addEventListener('change',handleChange) onUnmounted(()=>{ mediaQuery.removeEventListener('change',handleChange) }); return stata }; export default useMediaQuery ================================================ FILE: packages/useNetwork/index.md ================================================ # useNetwork 一个用来获取网络状态的 Hook 。 ## 使用Demo ```vue ``` useNetwork返回网络状态信息 ## Api ``` const useNetwork: () => { since?: number | Date, online?: boolean, rtt?: number, type?: string, downlink?: number, saveData?: boolean, downlinkMax?: number, effectiveType?: string, }; ``` ### Result | 属性 | 描述 | 类型 | |----------|--------------------------------------|----------------------| | online | 网络是否为在线 | `boolean` | | since | 在线与不在线最后改变时间 | `Date` | | rtt | 当前连接下评估的往返时延 | `number` | | type | 设备使用与所述网络进行通信的连接的类型 | `bluetooth` \| `cellular` \| `ethernet` \| `none` \| `wifi` \| `wimax` \| `other` \| `unknown` | | downlink | 有效带宽估算(单位:兆比特/秒) | `number` | | downlinkMax | 最大下行速度(单位:兆比特/秒) | `number` | | saveData | 用户代理是否设置了减少数据使用的选项 | `boolean` | | effectiveType | 网络连接的类型 | `slow-2g` \| `2g` \| `3g` \| `4g` | ================================================ FILE: packages/useNetwork/index.ts ================================================ import { reactive, onMounted, onUnmounted } from "vue"; const getConnection = ()=> { const nav = navigator as any; if (typeof nav !== 'object') return null; return nav.connection || nav.mozConnection || nav.webkitConnection; } export interface NetworkState { since?: number | Date; online?: boolean; rtt?: number; type?: string; downlink?: number; saveData?: boolean; downlinkMax?: number; effectiveType?: string; } const handlerSetConnection = ()=>{ const connection = getConnection(); return { rtt: connection.rtt, type: connection.type, saveData: connection.saveData, downlink: connection.downlink, downlinkMax: connection.downlinkMax, effectiveType: connection.effectiveType, } as NetworkState; }; const useNetwork = ()=>{ const state = reactive({ online: navigator.onLine, since: +new Date(), ...handlerSetConnection() }); const onOnline = ()=>{ state.online = true; state.since = +new Date(); }; const onOffline = ()=>{ state.online = false; state.since = +new Date(); }; const onConnectionChange = () => { const connectionData = handlerSetConnection(); Object.keys(connectionData).forEach((key)=>{ let propertyKey = key as T; state[propertyKey] = connectionData[propertyKey]; }) }; onMounted(()=>{ window.addEventListener('online', onOnline); window.addEventListener('offline', onOffline); getConnection()?.addEventListener('change', onConnectionChange); }) onUnmounted(()=>{ window.removeEventListener('online',onOnline); window.removeEventListener('offline',onOffline); getConnection()?.removeEventListener('change', onConnectionChange); }) return state } export default useNetwork ================================================ FILE: packages/useQRCode/index.md ================================================ # useQRCode 一个用来生成二维码的 Hook 。 ## 使用Demo ```vue ``` usQRCide接受一个静态url,也可以是一个被Ref包裹的url,当Ref值发生变化时,二维码会跟随内容进行变化。 ## Api ``` type Text = Ref | string; interface useQRCodeOptions { onRenderingStart?: (qrCodeOptions: any) => void; onRenderingEnd?: (qrCodeOptions: any, dataURL: string) => void; [key: string]: any; } const useQRCode: (text: Text, options?: useQRCodeOptions | undefined) => Ref; ``` ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | text | 需要生成二维码的url或text | `string` \| `Ref` | - | | options | 二维码配置项 | Options | - | ### Options Options配置项可以参考EasyQRCodeJSuseQRCode的底层是使用了EasyQRCodeJS来作为二维码的实现。 ### Result | 参数 | 说明 | 类型 | | :----| :---- | :---- | | state | base64格式的二维码图片 | string | ================================================ FILE: packages/useQRCode/index.ts ================================================ import qrcode from "easyqrcodejs"; import { ref, Ref, watch, isRef } from "vue"; type Text = Ref | string; interface useQRCodeOptions{ onRenderingStart?: (qrCodeOptions:any)=> void; onRenderingEnd?: (qrCodeOptions:any,dataURL:string)=>void; [key:string]: any; } const defaultUseQRCodeOptions = { onRenderingEnd:()=>{} }; const useQRCode = ( text:Text, options?: useQRCodeOptions )=>{ const state = ref(); const { onRenderingEnd, ...otherOptions } = {...defaultUseQRCodeOptions,...options}; const Qrcode = new qrcode(document.createElement('div'),{ text: isRef(text) ? text.value : text, ...otherOptions, onRenderingEnd(qrCodeOptions:any, dataURL:string){ state.value = dataURL; onRenderingEnd(qrCodeOptions, dataURL) } }) if(isRef(text)){ watch(text,()=>{ Qrcode.makeCode(text.value) }) } return state } export default useQRCode ================================================ FILE: packages/useRequest/__tests__/index.test.ts ================================================ import { shallowMount } from '@vue/test-utils'; import { defineComponent, ref } from 'vue'; import MockDate from 'mockdate'; import { sleep } from '../../utils/testingHelpers'; import useRequest from '../index'; import { Service, BaseOptions, Result } from '../types' describe('useRequest', () => { const originalError = console.error; const originalWarn = console.warn; const requestDelayTime = 500; beforeAll(() => { console.error = (...args) => { if (/Vue warn/.test(args[0])) { return; } originalError.call(console, ...args); }; console.warn = (...args) => { if (/Vue warn/.test(args[0])) { return; } originalWarn.call(console, ...args); }; }); afterAll(() => { console.error = originalError; console.warn = originalWarn; }); const request: Service = (req:any) => new Promise((resolve, reject) => { setTimeout(() => { if (req === 0) { reject(new Error('fail')); } else { resolve('success'); } }, requestDelayTime); }); it('should be defined', () => { expect(useRequest).toBeDefined(); }); const setUp = (service:Service, options?:BaseOptions) => useRequest(service, options); let hook:Result; it('useRequest should auto run', async () => { let successValue:string = ''; const successCallback = (text:string) => { successValue = text; }; const errorCallback = jest.fn(); const wrapper = shallowMount( defineComponent({ setup() { hook = setUp(request, { onSuccess: successCallback, onError: errorCallback, }); }, }), ); expect(hook.loading.value).toEqual(true); await sleep(requestDelayTime); expect(hook.loading.value).toEqual(false); expect(hook.data.value).toEqual('success'); expect(successValue).toEqual('success'); expect(errorCallback).not.toHaveBeenCalled(); hook.run(0); expect(hook.loading.value).toEqual(true); await sleep(requestDelayTime); expect(hook.error.value).toEqual(new Error('fail')); expect(hook.loading.value).toEqual(false); expect(errorCallback).toHaveBeenCalled(); hook.run(1); await sleep(requestDelayTime); expect(hook.data.value).toEqual('success'); expect(hook.loading.value).toEqual(false); expect(errorCallback).toHaveBeenCalled(); wrapper.unmount(); }); it('useRequest should be manually triggered', async () => { const wrapper = shallowMount( defineComponent({ setup() { hook = setUp(request, { manual: true, }); }, }), ); expect(hook.loading.value).toEqual(false); hook.run(1); expect(hook.loading.value).toEqual(true); await sleep(requestDelayTime); expect(hook.loading.value).toEqual(false); expect(hook.data.value).toEqual('success'); wrapper.unmount(); }); it('useRequest polling should work', async () => { const callback = jest.fn(); const wrapper = shallowMount( defineComponent({ setup() { hook = setUp( () => { callback(); return request(); }, { pollingInterval: 100, pollingWhenHidden: true, }, ); }, }), ); expect(hook.loading.value).toEqual(true); expect(callback).toHaveBeenCalled(); expect(callback).toHaveBeenCalledTimes(1); await sleep(100); expect(callback).toHaveBeenCalledTimes(2); await sleep(100); expect(callback).toHaveBeenCalledTimes(3); hook.cancel(); expect(callback).toHaveBeenCalledTimes(3); hook.run(); expect(callback).toHaveBeenCalledTimes(4); wrapper.unmount(); }); it('useRequest debounceInterval should work', async () => { const callback = jest.fn(); const wrapper = shallowMount( defineComponent({ setup() { hook = setUp( () => { callback(); return request(); }, { manual: true, debounceInterval: 100, }, ); }, }), ); hook.run(1); hook.run(2); hook.run(3); hook.run(4); await sleep(100); expect(callback).toHaveBeenCalled(); expect(callback).toHaveBeenCalledTimes(1); hook.run(1); hook.run(2); hook.run(3); hook.run(4); await sleep(100); expect(callback).toHaveBeenCalled(); expect(callback).toHaveBeenCalledTimes(2); wrapper.unmount(); }); it('useRequest throttleInterval should work', async () => { const callback = jest.fn(); const wrapper = shallowMount( defineComponent({ setup() { hook = setUp( () => { callback(); return request(); }, { manual: true, throttleInterval: 100, }, ); }, }), ); await sleep(100); hook.run(1); hook.run(2); await sleep(100); hook.run(3); hook.run(4); expect(callback).toHaveBeenCalledTimes(2); wrapper.unmount(); }); it('useRequest mutate should work', async () => { const wrapper = shallowMount( defineComponent({ setup() { hook = setUp(request); }, }), ); await sleep(requestDelayTime); expect(hook.data.value).toEqual('success'); hook.mutate('hello'); expect(hook.data.value).toEqual('hello'); wrapper.unmount(); }); it('useRequest loadingDelay should work', async () => { const wrapper = shallowMount( defineComponent({ setup() { hook = setUp(request, { loadingDelay: 2000, }); }, }), ); expect(hook.loading.value).toEqual(false); await sleep(1000); expect(hook.loading.value).toEqual(false); wrapper.unmount(); }); it('useRequest loadingDelay should delay', async () => { const wrapper = shallowMount( defineComponent({ setup() { hook = setUp(request, { loadingDelay: 300, }); }, }), ); expect(hook.loading.value).toEqual(false); await sleep(requestDelayTime); expect(hook.loading.value).toEqual(false); wrapper.unmount(); }); it('useRequest refreshDeps should work', async () => { const refreshValue = ref(1); const callback = jest.fn(); const wrapper = shallowMount( defineComponent({ setup() { hook = setUp( () => { callback(); return request(); }, { refreshDeps: [refreshValue], }, ); }, }), ); expect(hook.loading.value).toEqual(true); expect(callback).toHaveBeenCalledTimes(1); await sleep(requestDelayTime); expect(hook.loading.value).toEqual(false); refreshValue.value = 2; await sleep(0); expect(hook.loading.value).toEqual(true); expect(callback).toHaveBeenCalledTimes(2); await sleep(requestDelayTime); expect(hook.loading.value).toEqual(false); wrapper.unmount(); }); it('useRequest ready should work', async () => { const ready = ref(false); const wrapper = shallowMount( defineComponent({ setup() { hook = setUp(request, { ready, }); }, }), ); expect(hook.loading.value).toEqual(false); ready.value = true; await sleep(0); expect(hook.loading.value).toEqual(true); wrapper.unmount(); }); it('useRequest initialData should work', async () => { const wrapper = shallowMount( defineComponent({ setup() { hook = setUp(request, { initialData: 'hello', }); }, }), ); expect(hook.loading.value).toEqual(true); expect(hook.data.value).toEqual('hello'); await sleep(requestDelayTime); expect(hook.data.value).toEqual('success'); wrapper.unmount(); }); it('useRequest formatResult should work', async () => { let formarParams = ''; const wrapper = shallowMount( defineComponent({ setup() { hook = setUp(request, { formatResult: p => { formarParams = p; return 'hello'; }, }); }, }), ); expect(hook.loading.value).toEqual(true); await sleep(requestDelayTime); expect(hook.loading.value).toEqual(false); expect(formarParams).toEqual('success'); expect(hook.data.value).toEqual('hello'); wrapper.unmount(); }); it('useRequest defaultParams should work', async () => { const defaultParamsRequest = (...req:any[]) => new Promise(resolve => { setTimeout(() => { resolve(req); }, 500); }); const wrapper = shallowMount( defineComponent({ setup() { hook = setUp(defaultParamsRequest, { defaultParams: [1, 2, 3], }); }, }), ); expect(hook.loading.value).toEqual(true); await sleep(requestDelayTime); expect(hook.data.value).toEqual([1, 2, 3]); wrapper.unmount(); }); it('useRequest throwOnError to be false should work', async () => { const wrapper = shallowMount( defineComponent({ setup() { hook = setUp(request, { manual: true, }); }, }), ); hook.run(0); await sleep(requestDelayTime); expect(hook.data.value).toEqual(undefined); expect(hook.error.value).toEqual(new Error('fail')); wrapper.unmount(); }); it('useRequest cacheKey should work', async () => { const wrapper = shallowMount( defineComponent({ setup() { hook = setUp(request, { cacheKey: 'testCacheKey', }); }, }), ); await sleep(requestDelayTime); expect(hook.loading.value).toEqual(false); expect(hook.data.value).toEqual('success'); wrapper.unmount(); let hook2:any; const wrapper2 = shallowMount( defineComponent({ setup() { hook2 = setUp(request, { cacheKey: 'testCacheKey', }); }, }), ); expect(hook2.data.value).toEqual('success'); await sleep(requestDelayTime); expect(hook2.loading.value).toEqual(false); wrapper2.unmount(); }); it('useRequest staleTime should work', async () => { MockDate.set(0); const wrapper = shallowMount( defineComponent({ setup() { hook = setUp(request, { cacheKey: 'testStaleTime', staleTime: 3000, }); }, }), ); expect(hook.loading.value).toEqual(true); await sleep(requestDelayTime); expect(hook.loading.value).toEqual(false); expect(hook.data.value).toEqual('success'); wrapper.unmount(); MockDate.set(1000); let hook2:any; const wrapper2 = shallowMount( defineComponent({ setup() { hook2 = setUp(request, { cacheKey: 'testStaleTime', staleTime: 3000, }); }, }), ); expect(hook.loading.value).toEqual(false); expect(hook2.data.value).toEqual('success'); wrapper2.unmount(); MockDate.set(3001); let hook3:any; const wrapper3 = shallowMount( defineComponent({ setup() { hook3 = setUp(request, { cacheKey: 'testStaleTime', staleTime: 3000, }); }, }), ); expect(hook3.data.value).toEqual('success'); await sleep(requestDelayTime); expect(hook3.loading.value).toEqual(false); wrapper3.unmount(); }); it('useRequest cacheTime should work', async () => { MockDate.set(0); const wrapper = shallowMount( defineComponent({ setup() { hook = setUp(request, { cacheKey: 'testCacheTime', cacheTime: 2000, }); }, }), ); expect(hook.loading.value).toEqual(true); await sleep(requestDelayTime); expect(hook.loading.value).toEqual(false); expect(hook.data.value).toEqual('success'); wrapper.unmount(); MockDate.set(500); await sleep(500); let hook2:any; const wrapper2 = shallowMount( defineComponent({ setup() { hook2 = setUp(request, { cacheKey: 'testCacheTime', cacheTime: 2000, }); }, }), ); expect(hook2.data.value).toEqual('success'); wrapper2.unmount(); MockDate.set(3001); await sleep(3001); let hook3:any; const wrapper3 = shallowMount( defineComponent({ setup() { hook3 = setUp(request, { cacheKey: 'testCacheTime', cacheTime: 2000, }); }, }), ); expect(hook3.data.value).toEqual(undefined); await sleep(requestDelayTime); expect(hook3.loading.value).toEqual(false); expect(hook3.data.value).toEqual('success'); wrapper3.unmount(); }); }); ================================================ FILE: packages/useRequest/index.md ================================================ # useRequest 专注于管理异步请求的Hook,加速你的日常开发 * 自动请求/手动请求 * SWR(stale-while-revalidate) * 缓存/预加载 * 屏幕聚焦重新请求 * 轮询 * 防抖 * 节流 * 依赖请求 * 突变 * loading delay ## 请求方式 如果service是object,useRequest会使用 Fetch 来发送网络请求 ``` const { data } = useRequest( { url: 'http://xxx.xx.com/api/userInfo', method: 'POST' } ); ``` 如果service是async函数,useRequest会调用此函数来发送网络请求 ``` const { data } = useRequest( () => { return axios.post( `http://xxx.xx.com/api/userInfo` ); }, ); ``` ## 使用 ### 默认请求 ``` const { data, loading } = useRequest( () => { return axios.post( `http://xxx.xx.com/api/userInfo` ); }, ); watchEffect(()=>{ console.log( data?.value ); }) ``` 在这个例子中, useRequest 接收了一个异步函数 ,在组件初次加载时, 自动触发该函数执行。同时 useRequest 会自动管理异步请求的 loading , data 状态。你可以通过data和loading来实现你的需求 因为返回的data为响应式,js中获取data.value需要在watchEffect中使用,而在template中使用是不需要的。 ### 手动触发 ``` const { data, run, loading } = useRequest( () => { return axios.post( `http://xxx.xx.com/api/userInfo` ); }, { manual: true } ); ``` 通过设置 options.manual = true , 则需要手动调用 run 时才会触发执行异步函数。 ### 轮询 ``` const { data, loading } = useRequest( () => { return axios.post( `http://xxx.xx.com/api/userInfo` ); }, { pollingInterval: 1000 } ); ``` 通过设置 options.pollingInterval ,进入轮询模式,定时触发函数执行。 ### 依赖请求 ``` import { onMounted, ref } from 'vue' const isReady = ref(false); const { data, loading } = useRequest( () => { return axios.post( `http://xxx.xx.com/api/userInfo` ); }, { ready: isReady } ); onMounted(() => { isReady.value = true; }) ``` 只有当 options.ready 变为 true 时, 才会发起请求,基于该特性可以实现串行请求,依赖请求等。 ### 防抖 ``` const { data, loading } = useRequest( () => { return axios.post( `http://xxx.xx.com/api/userInfo` ); }, { debounceInterval: 1000 } ); ``` 通过设置 options.debounceInterval ,则进入防抖模式。此时如果频繁触发 run ,则会以防抖策略进行请求。 ### 节流 ``` const { data, loading } = useRequest( () => { return axios.post( `http://xxx.xx.com/api/userInfo` ); }, { throttleInterval: 1000 } ); ``` 通过设置 options.throttleInterval ,则进入节流模式。此时如果频繁触发 run ,则会以节流策略进行请求。 ### 缓存 & SWR ``` const { data, loading } = useRequest( () => { return axios.post( `http://xxx.xx.com/api/userInfo` ); }, { caccacheKey: 'mock1' } ); ``` 如果设置了 options.cacheKey , useRequest 会将当前请求结束数据缓存起来。下次组件初始化时,如果有缓存数据,我们会优先返回缓存数据,然后在背后发送新请求,也就是 SWR 的能力。你可以通过 cacheTime 设置缓存数据回收时间,也可以通过 staleTime 设置数据保持新鲜时间。 ### 预加载 ``` const { data, loading } = useRequest( () => { return axios.post( `http://xxx.xx.com/api/userInfo` ); }, { caccacheKey: 'mock1' } ); ``` 同一个 cacheKey 的请求,是全局共享的,也就是你可以提前请求数据。后续请求会提前返回之前预加载的数据,利用该特性,可以很方便的实现预加载。 ### 屏幕聚焦重新请求 ``` const { data, loading } = useRequest( () => { return axios.post( `http://xxx.xx.com/api/userInfo` ); }, { refreshOnWindowFocus: true, focusTimespan: 2000 } ); ``` 如果你设置了 options.refreshOnWindowFocus = true ,则在浏览器窗口 refocus 和 revisible 时,会重新发起请求。你可以通过设置 options.focusTimespan 来设置请求间隔,默认无 。 ### 突变 ``` const { data, mutate } = useRequest( () => { return axios.post( `http://xxx.xx.com/api/userInfo` ); } ); ``` 你可以通过 mutate ,直接修改 data. ### Loading Delay ``` const { data, loading } = useRequest( () => { return axios.post( `http://xxx.xx.com/api/userInfo` ); }, { loadingDelay: 300 } ); ``` 通过设置 options.loadingDelay ,可以延迟 loading 变成 true 的时间,有效防止闪烁。 ### refreshDeps 开发中会经常碰到这个需求,当某些 state 变化时,我们需要重新执行异步请求,使用useRequest将很方便的实现此功能。 ``` import { onMounted, ref } from 'vue' const random = ref(1); const { data, loading } = useRequest( () => { return axios.post( `http://xxx.xx.com/api/userInfo` ); }, { refreshDeps: [ random ] } ); setInterval(()=>{ random.value = Math.random() },1000) ``` 当例子中 random 发生变化时,会重新执行 service。 ## Api ### Result | 参数 | 说明 | 类型 | | :----| :---- | :---- | | data | service 返回的数据,默认为 undefined | undefined / any | | error | service 抛出的异常,默认为 undefined | undefined / Error | | loading | service 是否正在执行 | boolean | | run | 手动触发 service 执行,参数会传递给 service | (...args: any[]) => void | | refresh | 使用上一次的 params,重新执行 service| () => void | | cancel | 如果有轮询,停止 | () => void | | mutate | 突变直接修改 data | (newData) => void | | loading | service 是否正在执行 | boolean | ### Params 所有的 Options 均是可选的。 | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | manual | 是否需要手动执行 | boolean | false | | defaultParams | 如果 manual=false ,自动执行 run 的时候,默认带上的参数 | any[] | - | | refreshDeps | 在 manual = false 时,refreshDeps 变化,会触发 service 重新执行 | any[] | [] | | loadingDelay | 设置显示 loading 的延迟时间,避免闪烁 | number | - | | pollingInterval | 轮询间隔,单位为毫秒。设置后,将进入轮询模式,定时触发 run | number | - | | pollingWhenHidden | 在页面隐藏时,是否继续轮询。默认为 true,即不会停止轮询 | boolean | true | | refreshOnWindowFocus | 屏幕重新聚焦时间间隔,在当前时间间隔内,不会重新发起请求 | number | - | | focusTimespan | 在页面隐藏时,是否继续轮询。默认为 true,即不会停止轮询 | boolean | true | | debounceInterval | 防抖间隔, 单位为毫秒,设置后,请求进入防抖模式。 | number | - | | throttleInterval | 节流间隔, 单位为毫秒,设置后,请求进入节流模式。 | number | - | | ready | 只有当 ready 为 true 时,才会发起请求 | boolean | - | | cacheKey | * 请求唯一标识。如果设置了 cacheKey,我们会启用缓存机制 * 我们会缓存每次请求的 data , error , params , loading在缓存机制下 * 同样的请求我们会先返回缓存中的数据,同时会在背后发送新的请求,待新数据返回后,重新触发数据更新 | string | - | | cacheTime | 设置缓存数据回收时间。默认缓存数据 5 分钟后回收,需要配和 cacheKey 使用 | number | 300000 | | staleTime | 缓存数据保持新鲜时间。在该时间间隔内,认为数据是新鲜的,不会重新发请求,需要配和 cacheKey 使用 | number | 0 | ## 致敬 因为本人之前React中一直使用 umi ,在 umi 中使用了useRequest, Api使用非常的顺手。到了vue3中没有此hook,所以实现一份Vue的简版 useRequest来供Vue3项目使用。 React Ahooks [原版链接](https://ahooks.js.org/zh-CN/hooks/async) ================================================ FILE: packages/useRequest/index.ts ================================================ import { BaseOptions, Result, Run, Service, FetchService, Cancel } from './types'; import { shallowRef, ref, watch } from 'vue'; import { debounce, throttle, throttleAndDeBounce } from '../utils'; import { loadingDelayAsync, clearLoadingDelayTimer } from './src/loadingDelay'; import { serveiceProxy, argsSymbolKey } from './src/service'; import Polling from './src/polling'; import { visibility } from './src/visibility'; import { handleResCache } from './src/cache' import memoryCache from '../utils/memoryCache' // service请求参数缓存 const reqCache = new memoryCache(); // 请求参数缓存 const resCache = new memoryCache(); // 默认参数 const defaultOptions = { manual: false, initialData: undefined, onSuccess: ()=>{}, onError: ()=>{}, formatResult: (data:any)=> data, defaultParams: [], pollingInterval: 0, pollingWhenHidden: true, ready: undefined, debounceInterval: undefined, throttleInterval: undefined, refreshOnWindowFocus: false, focusTimespan: undefined, loadingDelay: 0, refreshDeps: [], cacheTime: 300000, staleTime: 0, }; const useRequest = (service: Service | FetchService,options?:BaseOptions )=>{ const { manual, initialData, onSuccess, onError, formatResult, defaultParams, pollingInterval, pollingWhenHidden, ready, debounceInterval, throttleInterval, refreshOnWindowFocus, focusTimespan, loadingDelay, refreshDeps, cacheKey, cacheTime, staleTime } = { ...defaultOptions, ...options }; const data = shallowRef(initialData); const error = ref(undefined); const loading = ref(false); const latestTime = ref(0); // 取消轮询 const cancel:Cancel = () => Polling.cancel(); // 执行轮询 const pollingRun = ()=>{ if( pollingInterval < 4 || Polling.isActive){ return } Polling.run( run, pollingInterval, pollingWhenHidden ); }; // 执行网络请求 let run:Run = (...args: any[])=>{ // 请求开始时间 const reqTime = +new Date(); // 判断开启缓存 && 有缓存,先返回缓存 // 缓存修改并不会阻止顺序执行,service请求会继续发出 // 也就是所谓SWR能力 if( cacheKey && resCache.has(cacheKey)){ data.value = resCache.get(cacheKey) if( latestTime.value + staleTime > reqTime ){ return } }else if(loadingDelay > 0){ loadingDelayAsync( loadingDelay ).then(()=> loading.value = true) }else { loading.value = true; } // 更新最新一次请求开始时间 latestTime.value = reqTime; serveiceProxy( service, args, reqCache ).then(( responseData )=>{ clearLoadingDelayTimer(); responseData = formatResult(responseData) data.value = responseData as any; loading.value = false; onSuccess(responseData,args) // 处理缓存 handleResCache( responseData, resCache, cacheKey, cacheTime ) }).catch(( e:Error )=>{ loading.value = false; error.value = e; onError(error.value,args) }); // 非激活状态执行轮询 pollingRun() }; const refresh = ()=>{ const args = reqCache.get(argsSymbolKey) || []; run(...args) }; // 是否自动执行 if( !manual && ready === undefined){ // 是否携带默认参数 defaultParams.length > 0 ? run(...defaultParams) : run() // 是否执行轮询 pollingRun() } //监听依赖请求是否执行 watch(()=> ready,(curr)=>{ if( curr?.value === true){ refresh() } },{ deep: true }) //多个监听依赖请求是否执行 watch(refreshDeps,()=> refresh() , { deep: true }); // 防抖+节流 if( debounceInterval !== undefined && throttleInterval !== undefined && typeof(debounceInterval) === 'number' && typeof(throttleInterval) === 'number' ){ run = throttleAndDeBounce(run,debounceInterval,throttleInterval); }else{ // 防抖 if( debounceInterval !== undefined && typeof(debounceInterval) === 'number' ){ run = debounce(run,debounceInterval) } // 节流 if( throttleInterval !== undefined && typeof(throttleInterval) === 'number' ){ run = throttle(run,throttleInterval) } } // 屏幕聚焦重新请求 if(refreshOnWindowFocus === true){ visibility( refresh, focusTimespan ); } // 突变改变data值 const mutate = (state:any)=>{ data.value = state; }; // 返回值 const res: Result = { data, error, run, refresh, loading, cancel, mutate }; return res }; export default useRequest ================================================ FILE: packages/useRequest/src/cache.ts ================================================ import memoryCache from '../../utils/memoryCache' const handleResCache = ( data: any, resCache: memoryCache, cacheKey?: string, cacheTime?: number, )=>{ if( cacheKey ){ if( resCache.has(cacheKey)){ resCache.put(cacheKey,data) return } resCache.put(cacheKey,data,cacheTime) } } export { handleResCache } ================================================ FILE: packages/useRequest/src/fetch.ts ================================================ export interface FetchParams extends RequestInit{ url: RequestInfo, } const defaultParams = { method: 'POST', // *GET, POST, PUT, DELETE, etc. mode: 'cors', // no-cors, *cors, same-origin cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached credentials: 'same-origin', // include, *same-origin, omit headers: { 'Content-Type': 'application/json' }, redirect: 'follow', // manual, *follow, error referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url }; /** * 使用fetch发起请求 * @param Params * @returns FetchData */ function fetchData( p:FetchParams ){ const params = { ...defaultParams, ...p } as FetchParams; return fetch( params.url, params); } export default fetchData ================================================ FILE: packages/useRequest/src/loadingDelay.ts ================================================ /** * loading的延迟计算 * @param startTime * @param loadingDelay * @param args * @returns */ let loadingDelayTimer:NodeJS.Timeout | null = null; export const loadingDelayAsync = ( loadingDelay:number)=>{ clearLoadingDelayTimer(); return new Promise((resolve)=>{ loadingDelayTimer = setTimeout(()=> resolve(loadingDelayTimer),Math.max( loadingDelay, 0 ) ); }) }; /** * 取消loading延迟计算Timer */ export const clearLoadingDelayTimer = ()=>{ if( loadingDelayTimer ){ clearTimeout(loadingDelayTimer) } } ================================================ FILE: packages/useRequest/src/polling.ts ================================================ import { Run } from '../types' /** * 执行轮询 */ class Polling{ isActive: boolean; // 是否是激活状态 private pollingInterval: number; private pollingWhenHidden: boolean; constructor() { this.isActive = false; this.pollingInterval = 0; this.pollingWhenHidden = true; } run( run:Run, pollingInterval: number, pollingWhenHidden: boolean ){ this.isActive = true; this.pollingInterval = pollingInterval; this.pollingWhenHidden = pollingWhenHidden; this.task(run); } cancel(){ this.isActive = false } private task( run:Run ){ setTimeout(()=>{ if( !this.isActive ) return if( this.pollingWhenHidden ){ run(); }else{ if(!document.hidden){ run(); } } this.task(run); },this.pollingInterval) } } export default new Polling() ================================================ FILE: packages/useRequest/src/service.ts ================================================ import fetchData from './fetch' import MemoryCache from '../../utils/memoryCache' const argsSymbolKey = 'argsKey'; const serveiceProxy = async (service: any, args: any[], reqParamsCache: MemoryCache)=>{ try{ if( args.length > 0 ){ reqParamsCache.put(argsSymbolKey,args); } if( Object.prototype.toString.call(service) === "[object Function]"){ return await service(...args); } if( Object.prototype.toString.call(service) === "[object Object]" ){ const response = await fetchData(service); return response.json() } }catch(error){ return Promise.reject(error) } } export { serveiceProxy, argsSymbolKey } ================================================ FILE: packages/useRequest/src/visibility.ts ================================================ import { Run } from '../types' import { throttle } from '../../utils' /** * 监听屏幕是否聚焦 * @param run * @param focusTimespan */ const visibility = (run:Run ,focusTimespan?: undefined | number)=>{ let handler = ()=> { if (!document.hidden) { run() } }; if(focusTimespan !== undefined && typeof(focusTimespan) === 'number'){ handler = throttle(handler,focusTimespan); } document.addEventListener('visibilitychange',handler ); } export { visibility } ================================================ FILE: packages/useRequest/types.d.ts ================================================ import { Ref } from 'vue'; import { FetchParams } from './src/fetch' // Service请求实例 export type Service = (...args: any[]) => Promise; export interface FetchService extends FetchParams{}; // 请求参数 export interface BaseOptions { manual?: boolean; // 是否需要手动触发 initialData?: any; //默认的 data formatResult?: (response: any) => any; // 格式化请求结果 onSuccess?: (data: any, params: any[]) => void; // 成功回调 onError?: (error: Error, params: any[]) => void; // 失败回调 defaultParams?: any[]; // 如果 manual=false ,自动执行 run 的时候,默认带上的参数 pollingInterval?: number; // 轮询请求时间 pollingWhenHidden?: boolean; // 在屏幕不可见时,暂时暂停定时任务。 ready?: undefined | Ref; // 依赖请求 debounceInterval?: undefined | number; // 防抖 throttleInterval?: undefined | number; // 节流 refreshOnWindowFocus?: boolean, // 屏幕聚焦重新请求 focusTimespan?: undefined | number, // 屏幕聚焦重新间隔 loadingDelay?: number, //loading延迟时间 refreshDeps?: any[], //依赖刷新监听 cacheKey?: string, //缓存key cacheTime?: number, //缓存数据回收时间 staleTime?: number //缓存数据新鲜时间 }; // 执行请求 export type Run = (...args: any[])=> void; // 重新执行 export type Refresh = ()=> void; //取消请求 export type Cancel = (()=> void); // 突变 export type Mutate = (state:any)=> void; // hook返回值 export interface Result { data: Ref; // 是否需要手动触发 loading: Ref; run: Run; refresh: Refresh; cancel: Cancel; mutate: Mutate; error: Ref; }; ================================================ FILE: packages/useRouteQuery/index.md ================================================ # useRouteQuery 一个获取vueRouter query的 Hook 。 请确保项目已安装Vue Router v4.x版本及以上,否则将不能使用此Hook. ## 使用Demo ```vue ``` useRouteQuery接受一个key是query中的参数key。 修改返回的state可直接修改url中的query。 ## Api ``` const state = useRouteQuery( key: string, ); ``` ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | key | query中的键名 | string | - | ### Result | 参数 | 说明 | 类型 | | :----| :---- | :---- | | state | query值也可以直接被修改,这样将同步修改query | Ref | ================================================ FILE: packages/useRouteQuery/index.ts ================================================ import { computed } from 'vue'; import { useRoute, useRouter } from 'vue-router'; const useRouteQuery = ( key: string)=>{ const route = useRoute(); const router = useRouter(); return computed({ get:()=>{ return route.query[key]; }, set: val => { router.replace({ query: { ...route.query ,[key]: val } }); } }) }; export default useRouteQuery ================================================ FILE: packages/useSessionStorage/index.md ================================================ # useSessionStorage 一个可以将状态持久化存储在 sessionStorage 中的 Hook 。 ## 使用Demo ```vue ``` useSessionStorage接受一个key和一个value,导出一个响应式的state, 用户直接赋值state.value可自动修改本地sessionStorage。 ## 注意点 * 不设置value可用于获取本地sessionStorage 例:`useSessionStorage('useSessionStorage')` * value等于undefined或者null可用于删除本地Storage 例:`state.value = undefined;` ## Api ``` const state = useSessionStorage( key: string, initialValue?: any, options?: Options ); ``` ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | key | sessionStorage存储键名 | any | - | | initialValue | 初始值 | any | {} | | options | 配置 | Options | - | ### Options | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | watch | 是否实时修改sessionStorage | boolean | true | ### Result | 参数 | 说明 | 类型 | | :----| :---- | :---- | | state | 可以被修改的数据源 | Ref | ================================================ FILE: packages/useSessionStorage/index.ts ================================================ import { ref, Ref, isRef, watch as vueWatch } from 'vue'; import { TypeSerializers, getValueType } from '../utils' const storage = sessionStorage; interface Options{ watch: boolean } const defaultOptions = { watch: true } const useSessionStorage = ( key: string, initialValue?: T | Ref, options?:Options)=>{ const { watch } = { ...defaultOptions, ...options }; const data = ref() as Ref; try{ if( initialValue !== undefined ){ data.value = isRef( initialValue ) ? initialValue.value : initialValue; }else{ data.value = JSON.parse( storage.getItem(key) || '{}' ); } }catch(error){ console.log(error,'useLocalStorage初始化失败') } const type = getValueType(data.value); // 判断类型取格式化方法 let serializer = TypeSerializers[type]; const setStorage = ()=> storage.setItem( key, serializer.write(data.value) ); // 状态监听 if( watch ){ vueWatch( data, (newValue)=>{ if( newValue === undefined || newValue === null ){ storage.removeItem(key); return } setStorage(); }, { deep:true } ) } setStorage() return data }; export default useSessionStorage ================================================ FILE: packages/useSet/index.md ================================================ # useSet 一个可以管理 Set 类型状态的 Hook。 ## 使用Demo ```vue ``` useSet接受一个 Set 可接受的参数, 并导出以下方法. ## Api ``` interface Actions{ add: (value: T)=> void, remove: (value: T)=> void, has: (value: T)=> boolean, clear: ()=> void, reset: ()=> void, } function useSet (initialValue?:T[]) : [ state: Ref>, actions: Actions ] ``` ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | initialValue | 可选项,传入默认的 Set 参数 | T[] | - | ### Result | 参数 | 说明 | 类型 | | :----| :---- | :---- | | state | Set 对象 | Set | | add | 添加元素 | (value: T)=> void | | remove | 移除元素 | (value: T)=> void | | has | 判断是否存在元素 | (value: T)=> void | | clear | 清空set | ()=> void | | reset | 重置为默认值 | ()=> void | ================================================ FILE: packages/useSet/index.ts ================================================ import { ref, Ref, markRaw } from 'vue'; interface Actions{ add: (value: T)=> void, remove: (value: T)=> void, has: (value: T)=> boolean, clear: ()=> void, reset: ()=> void, } function useSet (initialValue?:T[]) : [ state: Ref>, actions: Actions ] function useSet(initialValue?:T[]){ const initialSet = initialValue ? new Set(initialValue) : new Set(); const state = ref(initialSet); const actions:Actions = { add: ( value: T )=>{ state.value.add(value); }, remove: ( value: T )=>{ state.value.delete(value); }, has: ( value: T )=> state.value.has(value), clear: ()=> state.value.clear(), reset: ()=>{ state.value = new Set(); } }; return [ state, markRaw(actions) ] }; export default useSet ================================================ FILE: packages/useTextSelection/index.md ================================================ # useTextSelection 实时获取用户当前选取的文本内容及位置的hook。 ## 使用Demo ### 基础用法 ```vue ``` 没有传值默认为document, 页面上可选择的文本都会被计算 ### 监听特定区域文本选择 ```vue ``` 传值为Ref的P标签,只会监听特定区域。 ## 介绍 useTextSelection接受一个HTMLElement, 导出已选择的文本和位置信息。 ## Api ``` const useTextSelection: ( target?: HTMLElement | Ref | (() => HTMLElement) | Document ) => vue.ToRefs<{ text: string; rect: { left: number; right: number; top: number; bottom: number; height: number; width: number; }; }>; ``` ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | target | 原生Dom或者被Ref嵌套的Dom | `HTMLElement or Ref or (() => HTMLElement) or Document` | document | ### Result | 参数 | 说明 | 类型 | | :----| :---- | :---- | | text | 用户选取的文本值 | string | | rect | 位置信息 | Rect | ### Rect | 参数 | 说明 | 类型 | | :----| :---- | :---- | | left | 文本的左坐标 | number | | right | 文本的右坐标 | number | | top | 文本的顶坐标 | number | | bottom | 文本的底坐标 | number | | height | 文本的高度 | number | | width | 文本的宽度 | number | ================================================ FILE: packages/useTextSelection/index.ts ================================================ import { Ref, isRef, reactive, onMounted, onUnmounted, toRefs } from 'vue'; type Target = HTMLElement | Ref | (() => HTMLElement ) | Document; const defaultReact = { left: NaN, right: NaN, top: NaN, bottom: NaN, height: NaN, width: NaN }; const useTextSelection = ( target:Target = document )=>{ let state = reactive({ text: '', rect: defaultReact }) let el:HTMLElement | Document = document; const getEl = ()=>{ if( typeof target === 'function'){ return target() } return isRef( target ) ? target.value : target; }; const getRect = (selection: Selection | null)=>{ if( !selection ) return defaultReact const range = selection.getRangeAt(0); const { height, width, top, left, right, bottom } = range.getBoundingClientRect(); return { height, width, top, left, right, bottom, }; }; const handleMouseup = ()=>{ let currSelection: Selection | null = window.getSelection(); let text = currSelection ? currSelection.toString() : ''; let rect = getRect( currSelection ); state.text = text; state.rect = rect; }; const handleMousedown = ()=>{ if( state.text ){ state.text = '' state.rect = defaultReact; } const currSelection:Selection | null = window.getSelection(); if (!currSelection) return; currSelection.removeAllRanges(); }; onMounted(()=>{ el = getEl(); el.addEventListener('mouseup',handleMouseup); document.addEventListener('mousedown', handleMousedown); }) onUnmounted(()=>{ el.removeEventListener('mouseup',handleMouseup); document.removeEventListener('mousedown', handleMousedown); }) return toRefs( state ) }; export default useTextSelection ================================================ FILE: packages/useThrottle/index.md ================================================ # useThrottle 用来处理节流值的 Hook。 ## 使用 ``` ``` 使用useThrottle后,频繁设置throttleCurrValue, throttleValue每隔 500ms 变化一次。。 ## Api ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | value | 需要防抖的值 | any | - | | wait | 超时时间,单位为毫秒 | number | 1000 | ================================================ FILE: packages/useThrottle/index.ts ================================================ // import { debounce } from '../utils' import useThrottleFn from '../useThrottleFn' import { ref,Ref, watch } from 'vue'; // 默认值 const defaultDelay = 1000; /** * 处理防抖值 * @param value * @param delay * @returns */ const useThrottle = ( value: Ref , delay?:number )=>{ delay = delay || defaultDelay; const res = ref(value.value) as Ref; // 利用useDebounceFn来简化处理值 const { run } = useThrottleFn(()=> res.value = value.value ,delay); watch(value,()=> run(),{ deep: true }); return res; }; export default useThrottle ================================================ FILE: packages/useThrottleFn/index.md ================================================ # useThrottleFn 用来处理节流函数的 Hook。 ## 使用 ``` ``` 频繁调用 run,但只会每隔 500ms 执行一次相关函数。 ## Api ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | fn | 需要节流执行的函数 | () => void | - | | wait | 超时时间,单位为毫秒 | number | 1000 | ================================================ FILE: packages/useThrottleFn/index.ts ================================================ import { Fn, throttle } from '../utils' const defaultDelay = 1000; /** * 处理节流函数 * @param fn * @param delay * @returns */ const useThrottleFn = ( fn:Fn, delay?: number )=>{ const run = throttle( fn, typeof( delay ) === 'number'? delay : defaultDelay ); return { run }; } export default useThrottleFn ================================================ FILE: packages/useTimeout/index.md ================================================ # useTimeout 一个可以处理 setTimeout 计时器函数的 Hook。 ## 基础使用 ```vue ``` 1000ms执行一次,设置delay为null则立即中断 ## 非中断式调用 ```vue ``` useTimeout可以接受一个普通number参数,这样的timeout不能被中断 ## Api ``` const useTimeout: ( fn: Fn, delay: number | Ref ) => void; ``` ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | fn | 要重复调用的函数 | (...args: any[]) => void | - | | delay | 间隔时间,当取值为 null 或 undefined 时会停止计时器 | Ref | Ref | Ref | - | ================================================ FILE: packages/useTimeout/index.ts ================================================ import { ref, isRef, Ref, onUnmounted } from 'vue'; import { Fn } from '../utils'; const useTimeout = ( fn: Fn, delay: number | Ref )=>{ const state = isRef(delay) ? delay : ref(delay); let timer: null | NodeJS.Timeout = null; const clear = ()=> timer && clearTimeout(timer) const handler = ()=>{ if(state.value === undefined || state.value === null ) return fn(); }; const run = ()=>{ if( state.value === undefined || state.value === null ){ clear(); return } setTimeout(handler,state.value) } run() onUnmounted(()=> clear()) }; export default useTimeout ================================================ FILE: packages/useToggle/index.md ================================================ # useToggle 用于在多个状态值间切换的 Hook。 (此处与 ahooks 略有不同,ahooks只能两个状态切换,本hook支持N个状态切换) ## 基础使用 ``` ``` useToggle接受多个参数,且在actions中进行同等数量导出。Actions中第一个为toggle切换,其余为设置对应参数。 ## 异步值Toggle ``` ``` useToggle可以接受ref值的切换,内部支持了响应式,如果ref值发生变化,state会监听其变化同步修改。 ## Api ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | value | 需要切换的值 | string - number - boolean - undefined | - | | ... | 同上 | 同上 | - | ### Result | 参数 | 说明 | 类型 | | :----| :---- | :---- | | state | 状态值 | - | | actions | 操作集合 | Actions | ### Actions | 参数 | 说明 | 类型 | | :----| :---- | :---- | | toggle | 触发状态更改的函数,可以接受可选参数修改状态值 | (state?: any) => void | | action | 按照value顺序设置state为vulue | () => void | | ... | 同上 | 同上 | ================================================ FILE: packages/useToggle/index.ts ================================================ import { ref, Ref, isRef, watch } from 'vue'; type State = string | number | boolean | undefined; type RefState = Ref type Fn = (v?:any)=> void; type Actions = Fn[]; function useToggle( ...args: (T | U)[] ): [U,Actions] /** * 用于在N个状态值间切换。 * @param args * @returns */ function useToggle(...args: (T | U)[]){ const argsStateArr = args.map((variable)=> isRef(variable) ? variable : ref(variable)); const initialValue = argsStateArr[0].value; const state = ref(initialValue); let activeState = argsStateArr[0]; // 1: 监听当前被激活的state异步 // 2: 如果当前的异步发生改变则修改state watch([activeState],()=>{ state.value = activeState.value },{ deep:true }) let currIndex = 0; const len = args.length; const toggle = (param?:T | U)=>{ // 判定是否在参数里 if( param !== undefined && args.includes(param) ){ state.value = isRef(param) ? param.value : param; activeState = isRef(param) ? param : ref(param); return } // 顺序变化 currIndex = currIndex + 1 > len - 1 ? 0 : currIndex + 1; state.value = argsStateArr[currIndex].value; activeState = argsStateArr[currIndex]; }; const createHandle = ()=>{ return argsStateArr.map((active,index)=>{ return ()=>{ state.value = active.value; activeState = active; currIndex = index; } }) }; const actions: Actions = [ toggle, ...createHandle() ]; return [ state, actions ] } export default useToggle ================================================ FILE: packages/useUnmount/index.ts ================================================ import { onScopeDispose, onUnmounted, version } from 'vue'; const useUnmount = (fn:any) => { const unmounted = onScopeDispose ?? onUnmounted; unmounted(() => { fn(); }); }; export default useUnmount; ================================================ FILE: packages/useVirtualList/index.md ================================================ # useVirtualList 长列表虚拟化列表的 Hook,用于解决展示海量数据渲染时首屏渲染缓慢和滚动卡顿问题。 ## 使用 ``` ``` useVirtualList接受一个数组,导出一个虚拟化的list以元素配置,具体配置看Api ## Api ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | arr | 包含大量数据的列表 | T[] | [] | | options | 可选配置项,见 Options | Options | - | ### Options | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | itemHeight | 行高度,静态高度可以直接写入像素值,动态高度可传入函数| number | ((index: number) => number) | - | | overscan | 视区上、下额外展示的 dom 节点数量 | number | 5 | ### Result | 参数 | 说明 | 类型 | | :----| :---- | :---- | | list | 当前需要展示的列表内容 | T[] | | containerProps | 滚动容器的 props | object | | wrapperStyle | children 外层包裹器 Style | object | | scrollTo | 快速滚动到指定 index | (index: number) => void | ================================================ FILE: packages/useVirtualList/index.ts ================================================ import { reactive, ref, Ref } from 'vue'; export interface OptionType { itemHeight: number | ((index: number) => number); overscan?: number; } const useVirtualList = (state: T[], options: OptionType) => { let start = 0; let end = 10; const list = ref(state.slice(start, end)) as Ref; const { itemHeight, overscan = 5 } = options; const containerRef = ref(); const totalHeight: number = (() => { if (typeof itemHeight === 'number') { return state.length * itemHeight; } return state.reduce((sum, _, index) => sum + itemHeight(index), 0); })(); // 计算当前视图展示数量 const getViewCapacity = (containerHeight: number) => { if (typeof itemHeight === 'number') { return Math.ceil(containerHeight / itemHeight); } let sum = 0; let capacity = 0; for (let i = start; i < state.length; i++) { const height = (itemHeight as (index: number) => number)(i); sum += height; if (sum >= containerHeight) { capacity = i; break; } } return capacity - start; }; // 获取当前索引 const getOffset = (scrollTop: number) => { if (typeof itemHeight === 'number') { return Math.floor(scrollTop / itemHeight) + 1; } let sum = 0; let offset = 0; for (let i = 0; i < state.length; i++) { const height = (itemHeight as (index: number) => number)(i); sum += height; if (sum >= scrollTop) { offset = i; break; } } return offset + 1; }; // 获取当前索引向上高度 const getDistanceTop = (index: number) => { if (typeof itemHeight === 'number') { const height = index * itemHeight; return height; } const height = state.slice(0, index).reduce((sum, _, i) => sum + itemHeight(i), 0); return height; }; let offsetTop = getDistanceTop(start); // 计算展示指定位置 const calculateRange = () => { const element = containerRef.value; if (element) { const offset = getOffset(element.scrollTop); const viewCapacity = getViewCapacity(element.clientHeight); const from = offset - overscan; const to = offset + viewCapacity + overscan; start = from < 0 ? 0 : from; end = to > state.length ? state.length : to; list.value = state.slice(start, end); // 实时计算 offsetTop = getDistanceTop(start); wrapperStyle.marginTop = offsetTop + 'px'; wrapperStyle.height = totalHeight - offsetTop + 'px'; } }; // 滚动容器的外层监听 const containerProps = reactive({ ref: (ele: any) => { containerRef.value = ele; }, onScroll: (e: any) => { e.preventDefault(); calculateRange() }, style: { overflowY: 'auto' as const }, }); // children 外层包裹器 style const wrapperStyle = reactive({ width: '100%', height: totalHeight - offsetTop + 'px', marginTop: offsetTop + 'px' }); // 快速滚动到指定 index const scrollTo = (index: number) => { if (containerRef.value) { containerRef.value.scrollTop = getDistanceTop(index); calculateRange(); } }; return { list, wrapperStyle, containerProps, scrollTo } }; export default useVirtualList ================================================ FILE: packages/useWebSocket/index.md ================================================ # useWebSocket 用于处理 WebSocket 的 Hook。 ## 基础使用 ```vue ``` useWebSocket接受一个string的socket地址,返回一个集合对象。 上面例子中接受参数使用了latestMessage来获取最新一次通讯的event对象。 也可以使用onMessage来获取event对象,例子如下 ``` useWebSocket('ws://82.157.123.54:9010/ajaxchattest',{ onMessage(event){ console.log(event) } }) ``` ## Api ``` enum ReadyState { Connecting = 0, Open = 1, Closing = 2, Closed = 3 } interface UseWebSocketOptions { manual?: boolean; reconnectLimit?: number; reconnectInterval?: number; onOpen?: (event: WebSocketEventMap['open']) => void; onClose?: (event: WebSocketEventMap['close']) => void; onMessage?: (event: WebSocketEventMap['message']) => void; onError?: (event: WebSocketEventMap['error']) => void; } interface Result { latestMessage: Ref; sendMessage: WebSocket['send']; disconnect: () => void; connect: () => void; readyState: Ref; webSocketIns: Ref; } function useWebSocket(socketUrl: string, options?: UseWebSocketOptions): Result; ``` ### Params | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | socketUrl | 必填,webSocket 地址 | string | - | | options | 选填,连接配置项 | options | - | ### Options | 参数 | 说明 | 类型 | 默认值 | | :----| :---- | :---- | :---- | | onOpen | 选填,webSocket 连接成功回调 | (event: WebSocketEventMap['open']) => void | - | | onClose | 选填,webSocket 连接成功回调 | (event: WebSocketEventMap['close']) => void | - | | onMessage | 选填,webSocket 收到消息回调 | (message: WebSocketEventMap['message']) => void | - | | onError | 选填,webSocket 错误回调 | (event: WebSocketEventMap['error']) => void | - | | reconnectLimit | 选填,重试次数 | number | 3 | | reconnectInterval | 选填,重试时间间隔(ms) | number | 3000 | | manual | 选填,手动启动连接 | boolean | false | ### Result | 参数 | 说明 | 类型 | | :----| :---- | :---- | | latestMessage | 最新消息 | WebSocketEventMap['message'] | | sendMessage | 发送消息函数 | WebSocket['send'] | | disconnect | 手动断开 webSocket 连接 | () => void | | connect | 手动连接 webSocket,如果当前已有连接,则关闭后重新连接 | () => void | | readyState | 当前 webSocket 连接状态 | ReadyState | | webSocketIns | webSocket 实例 | WebSocket | ================================================ FILE: packages/useWebSocket/index.ts ================================================ import { Ref, ref } from "vue"; import useTimeout from '../useTimeout' interface UseWebSocketOptions { manual?: boolean, reconnectLimit?: number, reconnectInterval?: number, onOpen?: (event:WebSocketEventMap['open'])=> void, onClose?: (event:WebSocketEventMap['close'])=> void, onMessage?: (event:WebSocketEventMap['message'])=> void, onError?: (event:WebSocketEventMap['error'])=> void, } enum ReadyState { Connecting = 0, Open = 1, Closing = 2, Closed = 3, } interface Result{ latestMessage: Ref; sendMessage: WebSocket['send']; disconnect: () => void; connect: () => void; readyState: Ref; webSocketIns: Ref; } const defaultOptions = { manual: false, reconnectLimit: 3, reconnectInterval: 3000, onOpen:()=>{}, onClose:()=>{}, onMessage:()=>{}, onError:()=>{}, }; function useWebSocket ( socketUrl: string, options?: UseWebSocketOptions ):Result; function useWebSocket( socketUrl: string, options?: UseWebSocketOptions ){ const { manual, reconnectLimit, reconnectInterval, onOpen, onClose, onMessage, onError, } = {...defaultOptions,...options}; if(!socketUrl || typeof(socketUrl)!== 'string'){ throw new Error('useWebSocket require string socketUrl') } let readyState = ref(ReadyState.Connecting); const reconnectCount = ref(0); const socket = ref(); const latestMessage = ref(); const run = ()=>{ socket.value = new WebSocket(socketUrl); socket.value.addEventListener('open', function (event) { readyState.value = ReadyState.Open onOpen(event) }); socket.value.addEventListener('message', function (event) { latestMessage.value = event; onMessage(event) }); socket.value.addEventListener('error', function (event) { console.log('error ', event); reconnect(); onError(event) }); socket.value.addEventListener('close', function (event) { readyState.value = ReadyState.Closed onClose(event) }); }; const connect = ()=>{ if( readyState.value !== ReadyState.Open){ reconnectCount.value = 0; run() } }; const reconnect = ()=>{ if(reconnectCount.value >= reconnectLimit ) return useTimeout(()=>{ reconnectCount.value++ run() },ref(reconnectInterval)) } const disconnect = ()=>{ if( ( readyState.value === ReadyState.Connecting || readyState.value === ReadyState.Open ) && socket.value ){ readyState.value = ReadyState.Closing socket.value.close() } }; const sendMessage = (data: string | ArrayBufferLike | Blob | ArrayBufferView)=>{ if( data && socket.value && readyState.value === ReadyState.Open ) socket.value.send(data) }; if( !manual ) connect() return { latestMessage, readyState, connect, disconnect, sendMessage, webSocketIns: socket } }; export default useWebSocket ================================================ FILE: packages/utils/index.ts ================================================ type Fn = (...[]: any[])=> any; /** * 防抖 * @param fn * @param delay * @returns */ const debounce = (fn:Fn,delay:number)=>{ let timer: NodeJS.Timeout| null = null; return function(...args:[]){ if(timer) clearTimeout(timer) timer = setTimeout(()=>{ // @ts-ignore fn.call(this,...args) },delay) } } /** * 节流 * @param fn * @param delay * @returns */ const throttle = (fn:Fn,delay:number)=>{ let oldNow = Date.now(); return function(...args:[]){ const currNow = Date.now(); if( currNow - oldNow < delay) return oldNow = currNow; // @ts-ignore fn.call(this,...args) } } /** * 防抖+节流 * @param fn * @param DBdelay * @param TRdelay * @returns */ const throttleAndDeBounce = (fn:Fn,DBdelay:number,TRdelay:number)=>{ let oldNow = Date.now(); let timer: NodeJS.Timeout| null = null; return function(...args:[]){ const currNow = Date.now(); if( currNow - oldNow < TRdelay){ if(timer) clearTimeout(timer); timer= setTimeout(()=>{ oldNow = currNow; // @ts-ignore fn.call(this,...args) },DBdelay) return } oldNow = currNow; // @ts-ignore fn.call(this,...args) } }; type Serializer = { read(raw: string): T write(value: T): string } /** * 按照类型格式数据的常量Map */ const TypeSerializers: Record<'boolean' | 'object' | 'number' | 'any' | 'string', Serializer> = { boolean: { read: (v: any) => v != null ? v === 'true' : null, write: (v: any) => String(v), }, object: { read: (v: any) => v ? JSON.parse(v) : null, write: (v: any) => JSON.stringify(v), }, number: { read: (v: any) => v != null ? Number.parseFloat(v) : null, write: (v: any) => String(v), }, any: { read: (v: any) => (v != null && v !== 'null') ? v : null, write: (v: any) => String(v), }, string: { read: (v: any) => v != null ? v : null, write: (v: any) => String(v), }, } /** * 获取数据类型 * @param defaultValue * @returns */ const getValueType = (defaultValue:unknown)=>{ return defaultValue == null ? 'any' : typeof defaultValue === 'boolean' ? 'boolean' : typeof defaultValue === 'string' ? 'string' : typeof defaultValue === 'object' ? 'object' : Array.isArray(defaultValue) ? 'object' : !Number.isNaN(defaultValue) ? 'number' : 'any'; }; export { Fn, debounce, throttle, throttleAndDeBounce, TypeSerializers, getValueType } ================================================ FILE: packages/utils/memoryCache.ts ================================================ interface Timer { [key:string]: NodeJS.Timeout } interface Options{ maxCache?: number } class MemoryCache{ memoryCache: Map timer: Timer maxCache: number constructor(options?:Options){ this.memoryCache = new Map(); this.timer = {}; this.maxCache = options?.maxCache || 1000; } /** * 增加缓存 * @param key * @param value * @param time * @param timeoutCallback */ put( key: string, value: any, time?: number, timeoutCallback?: ()=>{} ){ if( !key || !value){ throw new Error('key & value is required') } if( this.size() >= this.maxCache){ this.del( [...this.memoryCache][0][0] ) } this.memoryCache.set(key,value); if( time && typeof time === 'number' && time > 0 ){ this.timer[key] = setTimeout(()=>{ this.del(key) delete this.timer[key] timeoutCallback && timeoutCallback() },time) } }; /** * 获取缓存 * @param key * @returns */ get( key: string ){ if( !this.has(key) ) return null return this.memoryCache.get(key); }; /** * 判断是否有缓存 * @param key * @returns */ has( key: string ){ return this.memoryCache.has(key); } /** * 删除缓存 * @param key * @returns */ del( key: string ){ if( !this.has(key ) ) return if( this.timer[key] ){ clearTimeout( this.timer[key] ) delete this.timer[key] } this.memoryCache.delete(key) } /** * 清除缓存 * @returns */ clear(){ if( this.size() <= 0) return this.memoryCache.clear() for( let i in this.timer){ clearTimeout( this.timer[i] ) delete this.timer[i] } } /** * 获取缓存条数 * @returns */ size(){ return this.memoryCache.size } } export default MemoryCache ================================================ FILE: packages/utils/testingHelpers.ts ================================================ export function sleep(time:number) { return new Promise(resolve => { setTimeout(() => { resolve(true); }, time); }); } ================================================ FILE: rollup.config.js ================================================ import { nodeResolve } from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import { terser } from 'rollup-plugin-terser'; import { babel } from '@rollup/plugin-babel'; import typescript from 'rollup-plugin-typescript2'; import dts from 'rollup-plugin-dts'; const input = 'packages/index.ts'; const isProd = process.env.NODE_ENV; const fn = 'index'; const outputDir = isProd? 'dist' : 'example/dist'; const basePlugins = [ // 打包插件 nodeResolve(), // 查找和打包node_modules中的第三方模块 commonjs(), // 将 CommonJS 转换成 ES2015 模块供 Rollup 处理 typescript({ // 解析TypeScript tsconfigOverride: { compilerOptions: { declaration: false, }, }, }), ] const devPlugins = []; const prodPlugins = [ babel({ babelHelpers: 'bundled', }) ]; const plugins = [...basePlugins].concat(isProd ? prodPlugins : devPlugins); const configs = [ { input, output: [ { file: `${outputDir}/${fn}.es.js`, format: 'esm', globals: { 'vue': 'Vue' } }, { file: `${outputDir}/${fn}.cjs.js`, format: 'cjs', globals: { 'vue': 'Vue' } }, { name: 'v3hooks', file: `${outputDir}/${fn}.js`, format: 'umd', globals: { 'vue': 'Vue' }, plugins: [terser()], }, ], plugins: plugins, external: [ 'vue', 'vue-router', ] }, { input, output: { file: `${outputDir}/${fn}.d.ts`, format: 'esm', }, plugins: [ dts(), ], external: [ 'vue', ] } ] // const configs = ['esm','cjs'].map((format)=>({ // input, // output: { // file: `${outputDir}/${format}.js`, // format, // globals: { // 'vue': 'Vue' // } // }, // plugins: plugins, // external: [ // 'vue', // 'vue-router', // ] // }) // ) // configs.push({ // input, // output: { // file: `${outputDir}/index.d.ts`, // format: 'esm', // }, // plugins: [ // dts(), // ], // external: [ // 'vue', // ] // }) export default configs ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { /* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ "module": "ESNext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ "declaration": true, /* Generates corresponding '.d.ts' file. */ "declarationDir": "./types", // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ // "outDir": "./", /* Redirect output structure to the directory. */ // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "composite": true, /* Enable project compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ // "removeComments": true, /* Do not emit comments to output. */ // "noEmit": true, /* Do not emit outputs. */ // "importHelpers": true, /* Import emit helpers from 'tslib'. */ "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ /* Strict Type-Checking Options */ "strict": true, /* Enable all strict type-checking options. */ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* Enable strict null checks. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */ // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ /* Additional Checks */ // "noUnusedLocals": true, /* Report errors on unused locals. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ /* Module Resolution Options */ "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ /* Source Map Options */ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ /* Experimental Options */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ /* Advanced Options */ "skipLibCheck": true, /* Skip type checking of declaration files. */ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ }, "include": [ "packages" ], "exclude": [ "node_modules", "**/**/*.test.ts", "**/**/*.stories.tsx", "**/**/*.md", "**/dist", "packages/.test", "packages/_docs" ] }