ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

v3-admin-vite 内置组合式函数(Composables)完全使用指南:从设备检测到水印防御的 11 个通用工具

v3-admin-vite 内置组合式函数(Composables)完全使用指南:从设备检测到水印防御的 11 个通用工具 前端【免费下载链接】v3-admin-vite☀️ AI-friendly Vue3 admin template | Vue Admin | Vue Template | Vue3 Admin | Vue3 Template | Vue 后台 | Vue 模板 | Vue3 后台 | Vue3 模板项目地址https://gitcode.com/gh_mirrors/v3a/v3-admin-vite点击查看免费下载v3-admin-vite 在src/common/composables目录下内置了一套通用的组合式函数Composables统一通过路径别名/composables/导入。它们覆盖了后台管理系统最常用的场景设备检测、异步下拉、全屏加载、分页、路由监听、主题切换、动态标题、水印、灰度模式与布局切换。阅读完本文你将掌握每个组合式函数的调用签名、参数含义、源码实现原理以及如何将它们与 Element Plus 组件、Pinia Store 组合到自己的页面中从而告别重复造轮子、直接复用项目现成的能力。内置组合式函数总览与导入约定所有内置组合式函数统一存放在 src/common/composables 目录下共有 11 个文件useDevice.ts设备类型检测移动端 / 桌面端useFetchSelect.ts下拉选择器异步数据加载useFullscreenLoading.ts函数执行期间的全屏 LoadingusePagination.ts分页状态与操作封装useRouteListener.ts基于发布订阅的路由变化监听useTheme.ts主题切换支持 View Transition 动画useTitle.ts浏览器标签页动态标题useWatermark.ts页面水印与防删除/隐藏防御useGreyAndColorWeakness.ts灰色模式与色弱模式useLayoutMode.ts布局模式管理与判断usePany.ts项目相关信息如作者、仓库地址等元数据导入方式统一使用路径别名/composables/例如import { useDevice } from /composables/useDevice该别名在 vite.config.ts 与 tsconfig.json 中配置指向src/common目录因此/composables/xxx实际解析到src/common/composables/xxx。使用原则上有四条约定详见 Skill 文档使用原则一节优先使用这些内置组合式函数不要重复造轮子组合式函数内部已处理生命周期如onBeforeUnmount自动清理无需手动管理需要新增通用组合式函数时在src/common/composables目录下创建命名以use开头页面私有的组合式函数应放在对应页面目录的composables子目录下而非src/common/composables。接下来按功能逐一讲解每个组合式函数。设备检测 useDeviceuseDevice用于判断当前设备是移动端还是桌面端内部基于appStore.device提供响应式计算属性源码见 src/common/composables/useDevice.ts。import { useDevice } from /composables/useDevice const { isMobile, isDesktop } useDevice() // 在模板或逻辑中使用 if (isMobile.value) { // 移动端逻辑 }源码实现原理从源码可以看到模块顶层持有一个appStore单例然后定义了两个computedconst isMobile computed(() appStore.device DeviceEnum.Mobile) const isDesktop computed(() appStore.device DeviceEnum.Desktop)DeviceEnum定义在 src/common/constants/app-key.tsexport enum DeviceEnum { Mobile, Desktop }这意味着device的值由 Pinia 的appStore维护任何地方修改appStore.device例如在窗口 resize 监听中切换设备类型isMobile/isDesktop都会自动响应更新。useDevice只返回这两个响应式引用不负责写入设备类型属于纯读取型工具。异步下拉选择器 useFetchSelectuseFetchSelect封装了下拉选择器的异步数据加载逻辑组件挂载时自动调用接口获取选项并对外暴露loading、options、value三个状态源码见 src/common/composables/useFetchSelect.ts。import { useFetchSelect } from /composables/useFetchSelect import { getSelectDataApi } from ./apis/xxx const { loading, options, value } useFetchSelect({ api: getSelectDataApi // 直接传函数引用返回 ApiResponseDataSelectOption[] })在模板中配合 Element Plus 使用el-card v-loadingloading el-select v-modelvalue filterable el-option v-foritem in options v-binditem :keyitem.value placeholder请选择 / /el-select /el-card入参与返回结构接口返回的数据格式即选项对象的结构interface SelectOption { value: string | number label: string disabled?: boolean }入参只需一个api字段类型为返回 Promise 的函数interface FetchSelectProps { api: () PromiseApiData } // ApiData ApiResponseDataSelectOption[]源码行为细节从源码可以看到loadData的具体流程const loadData () { loading.value true options.value [] api().then((res) { options.value res.data }).finally(() { loading.value false }) } onMounted(() { loadData() })需要注意的行为特点在onMounted阶段自动触发首次请求无需手动调用每次加载前会先清空options避免展示上一次的旧数据接口成功后将res.data即SelectOption[]整体赋给options因此在模板中可以直接v-foritem in options配合v-binditem渲染value/label/disabled都会自动透传到el-option上当前实现没有对接口失败做额外处理异常会向上抛出由调用方自行捕获。全屏加载 useFullscreenLoadinguseFullscreenLoading包装一个函数在其执行期间自动显示全屏 Loading函数执行完毕后自动关闭无论成功还是失败都会关闭源码见 src/common/composables/useFullscreenLoading.ts。import { useFullscreenLoading } from /composables/useFullscreenLoading // 方式一行内调用推荐简单场景 const res await useFullscreenLoading(getSuccessApi)([1, 2, 3]) // 方式二自定义 Loading 配置 try { await useFullscreenLoading(getErrorApi, { text: 删除中..., background: #F56C6C20 })() } catch (error) { ElMessage.error((error as Error).message) } // 方式三先赋值再调用适合多次复用 const submitWithLoading useFullscreenLoading(submitApi) await submitWithLoading(formData)源码实现原理核心实现非常精简是一个高阶函数包装器const DEFAULT_OPTIONS { lock: true, text: 加载中... } export const useFullscreenLoading: UseFullscreenLoading (fn, options {}) { let loadingInstance: LoadingInstance return async (...args) { try { loadingInstance ElLoading.service({ ...DEFAULT_OPTIONS, ...options }) return await fn(...args) } finally { loadingInstance.close() } } }理解要点useFullscreenLoading(fn, options)返回一个新函数新函数接收原函数的所有参数并返回一个 Promise内部使用 Element Plus 的ElLoading.service()开启全屏加载默认lock: true锁定滚动、text: 加载中...你传入的options会覆盖默认配置LoadingOptions类型来自element-plus关键在finally块无论fn成功还是抛出异常Loading 都会被关闭因此你不需要担心 Loading 卡死不消失若被包装的函数抛错错误会继续向外传播所以方式二里用try / catch捕获后通过ElMessage.error提示是标准的配套写法。三种调用方式的适用场景行内调用适合一次性操作带自定义配置适合需要提示文案如删除中...的敏感操作先赋值再调用适合表单提交等多次复用的场景。分页 usePaginationusePagination封装分页状态与操作适配 Element Plus 的el-pagination组件源码见 src/common/composables/usePagination.ts。import { usePagination } from /composables/usePagination function getTableData() { // 请求表格数据并更新 paginationData.total } const { paginationData, resetCurrentPage, watchPagination } usePagination({ callback: getTableData, pageSize: 20, pageSizes: [10, 20, 50, 100] }) // 初始化加载并在 currentPage / pageSize 变化时重新请求 watchPagination() function handleSearch() { resetCurrentPage() }在模板中使用el-pagination v-model:current-pagepaginationData.currentPage v-model:page-sizepaginationData.pageSize :page-sizespaginationData.pageSizes :totalpaginationData.total :layoutpaginationData.layout background /默认分页参数参数默认值total0currentPage1pageSizes[10, 20, 50]pageSize10layouttotal, sizes, prev, pager, next, jumper源码行为细节const paginationData reactive({ ...DEFAULT_PAGINATION_DATA, ...initPaginationData }) const resetCurrentPage () { paginationData.currentPage 1 ? callback?.() : (paginationData.currentPage 1) } const watchPagination (options: WatchOptions { immediate: true }) { watch( [() paginationData.currentPage, () paginationData.pageSize], () callback?.(), options ) }几个关键的源码级细节paginationData是一个reactive对象由默认参数与你的传入项浅合并生成模板里所有el-pagination属性都直接绑定它watchPagination()默认immediate: true所以调用一次即完成初始化加载首次请求并会在currentPage或pageSize变化时自动执行callback重新请求数据resetCurrentPage用于搜索场景如果当前已在第 1 页则直接执行回调否则把currentPage重置为 1交给监听触发。这样设计避免了搜索时同时改页码又手动请求造成的重复请求此外还返回了handleCurrentChange与handleSizeChange两个事件处理函数可直接绑定到el-pagination的current-change与size-change事件上不过由于模板中使用了v-model:current-page与v-model:page-size通常不再需要它们callback内部通常需要自行把接口返回的total写入paginationData.total分页数据本身不需要手动改。路由监听 useRouteListeneruseRouteListener基于发布订阅模式内部使用mitt事件总线监听路由变化相比直接watch路由性能更好且组件卸载时自动移除监听源码见 src/common/composables/useRouteListener.ts。import { useRouteListener } from /composables/useRouteListener const { listenerRouteChange, removeRouteListener } useRouteListener() // 监听路由变化 listenerRouteChange((route) { console.log(路由变化了:, route.path) }) // 立即执行一次获取当前路由信息 listenerRouteChange((route) { activeMenu.value route.path }, true) // 第二个参数 immediate true源码实现原理模块顶层创建了一个模块级单例的mitt事件发射器和一个Symbol(ROUTE_CHANGE)作为事件 keyconst emitter mitt() const key Symbol(ROUTE_CHANGE) let latestRoute: RouteLocationNormalizedGeneric同时导出了一个配套函数setRouteChange路由守卫在路由切换时调用它来触发事件并缓存最新路由export function setRouteChange(to: RouteLocationNormalizedGeneric) { emitter.emit(key, to) latestRoute to }useRouteListener内部维护组件自己的回调集合callbackList并做了三件事listenerRouteChange(callback, immediate)把回调加入集合并注册到事件总线若immediate为true且已有latestRoute立即用当前路由调用一次回调方便在初始化时就拿到当前路由信息removeRouteListener(callback)从事件总线移除指定回调onBeforeUnmount钩子遍历callbackList自动移除所有监听避免内存泄漏。为什么要用发布订阅而不是watch源码注释给出了两点理由一是单独用watch监听路由会浪费渲染性能二是发布订阅模式更便于多组件分发管理。注意setRouteChange需要在路由守卫中调用本项目在 src/router/guard.ts 中接入才能保证事件被正确触发。主题切换 useThemeuseTheme管理主题切换支持 View Transition 动画效果从鼠标点击位置以圆形扩散过渡源码见 src/common/composables/useTheme.ts。import { useTheme } from /composables/useTheme const { themeList, activeThemeName, initTheme, setTheme } useTheme() // 初始化主题应用启动时调用一次 initTheme() // 切换主题需要传入鼠标事件以实现过渡动画 function handleThemeChange(event: MouseEvent, themeName: ThemeName) { setTheme(event, themeName) }可用主题nametitlenormal默认dark黑暗dark-blue深蓝ThemeName类型为DefaultThemeName | dark | dark-blue其中normal是必填的默认主题。源码实现原理模块内部维护themeList、activeThemeName初始值从 localStorage 读取见 src/common/utils/local-storage.ts 的getActiveThemeName以及三个核心函数setTheme({ clientX, clientY }, value)以鼠标点击位置为圆心计算到视口四角的最远距离作为扩散半径把--v3-theme-x、--v3-theme-y、--v3-theme-r三个 CSS 变量写入根元素通过setCssVar见 src/common/utils/css.ts然后调用document.startViewTransition存在时包裹主题名切换从而产生圆形扩散的 View Transition 动画不支持startViewTransition的浏览器会直接切换initTheme()通过watchEffect收集副作用——每当activeThemeName变化就移除html根元素上其他主题的 class、添加当前主题的 class并同步写回 localStorage。因此应用启动时调用一次即可之后每次setTheme都会自动触发 class 切换与持久化主题的视觉样式由 src/common/assets/styles/theme 下的normal/dark/dark-blue三套 SCSS 变量与样式驱动注册入口在 src/common/assets/styles/theme/register.scss。注意setTheme要求第一个参数是鼠标事件用于计算动画圆心所以在模板中通常写成handleThemeChange($event, dark)的形式。动态标题 useTitleuseTitle动态设置浏览器标签页标题格式为项目名 | 页面名源码见 src/common/composables/useTitle.ts。import { useTitle } from /composables/useTitle const { setTitle } useTitle() // 设置标题为 V3 Admin Vite | 用户管理 setTitle(用户管理) // 重置为项目默认标题 setTitle()源码实现原理const VITE_APP_TITLE import.meta.env.VITE_APP_TITLE ?? V3 Admin Vite const dynamicTitle refstring() function setTitle(title?: string) { dynamicTitle.value title ? ${VITE_APP_TITLE} | ${title} : VITE_APP_TITLE } watch(dynamicTitle, (value, oldValue) { if (document value ! oldValue) { document.title value } })理解要点项目标题VITE_APP_TITLE来自环境变量import.meta.env.VITE_APP_TITLE未配置时回退为V3 Admin Vite因此可以通过在.env文件中配置VITE_APP_TITLE来改变前缀setTitle内部只是写入响应式ref实际修改document.title由watch统一完成避免了直接操作 DOM 的分散不传参数调用setTitle()会重置为纯项目标题。典型用法是在路由守卫中根据当前路由 meta 的标题调用setTitle(route.meta.title)实现页面标题跟随路由。水印 useWatermarkuseWatermark为页面或指定容器添加水印内置防御机制防止用户通过控制台删除或隐藏水印组件卸载时自动清除源码见 src/common/composables/useWatermark.ts。import { useWatermark } from /composables/useWatermark // 默认挂载到 body const { setWatermark, clearWatermark } useWatermark() // 设置水印 setWatermark(机密文件) // 自定义配置 setWatermark(内部使用, { defense: true, // 开启防御默认 true color: #c0c4cc, // 文本颜色 opacity: 0.5, // 透明度 size: 16, // 字体大小 angle: -20, // 倾斜角度 width: 300, // 单个水印宽度越大密度越低 height: 200 // 单个水印高度越大密度越低 }) // 清除水印 clearWatermark()完整配置项配置项类型默认值说明defensebooleantrue防御模式能防御水印被删除或隐藏但可能有性能损耗colorstring#c0c4cc文本颜色opacitynumber0.5文本透明度sizenumber16文本字体大小familystringserif文本字体SKILL 示例未列出源码默认值为serifanglenumber-20文本倾斜角度widthnumber300单个水印所占宽度数值越大水印密度越低heightnumber200单个水印所占高度数值越大水印密度越低挂载到指定容器useWatermark接受一个RefHTMLElement | null作为可选参数默认挂载到body传入容器元素引用后水印会以该容器为边界script setup langts import { useWatermark } from /composables/useWatermark const localRef useTemplateRef(localRef) const { setWatermark, clearWatermark } useWatermark(localRef) onMounted(() { setWatermark(仅限内部) }) /script template div reflocalRef !-- 内容 -- /div /template注意源码中setWatermark在容器还未挂载parentEl.value为空时会console.warn(请在 DOM 挂载完成后再调用 setWatermark 方法设置水印)因此传容器时务必在onMounted之后调用。源码实现原理Canvas 平铺 MutationObserver 防御useWatermark的实现可以拆成三层来看渲染层用canvas绘制单个水印文本配置color、opacity、size、family、angle通过canvas.toDataURL()生成 base64 背景图铺到watermarkEl上并以left top repeat平铺挂载在body上时水印元素使用position: fixed挂载在普通容器上时使用position: absolute同时会把容器设为position: relative作为定位上下文防御层开启defense后用两个MutationObserver分别观察水印元素属性变动防止被 CSS 隐藏或修改和容器元素子节点变动防止被删除。一旦检测到水印被移除立即用appendChild把水印元素加回容器检测到属性被篡改则触发updateWatermark重新生成水印。这些回调都经过lodash-es的debounce100ms防止频繁触发自适应层用ResizeObserver监听容器大小变化500ms 防抖容器尺寸改变时同步更新水印元素的宽高onBeforeUnmount时自动执行clearWatermark移除所有监听并删除水印元素。防御模式有性能开销源码注释明确说明如果对性能敏感且不需要防篡改可以把defense设为false此时会跳过 mutation 监听。灰色模式与色弱模式 useGreyAndColorWeaknessuseGreyAndColorWeakness初始化灰色模式和色弱模式基于settingsStore的配置自动切换 HTML 根元素的 CSS 类名源码见 src/common/composables/useGreyAndColorWeakness.ts。import { useGreyAndColorWeakness } from /composables/useGreyAndColorWeakness const { initGreyAndColorWeakness } useGreyAndColorWeakness() // 应用启动时调用一次即可后续会自动响应 Store 变化 initGreyAndColorWeakness()源码实现原理const GREY_MODE grey-mode const COLOR_WEAKNESS color-weakness function initGreyAndColorWeakness() { const settingsStore useSettingsStore() watchEffect(() { classList.toggle(GREY_MODE, settingsStore.showGreyMode) classList.toggle(COLOR_WEAKNESS, settingsStore.showColorWeakness) }) }关键点通过watchEffect建立settingsStore.showGreyMode/settingsStore.showColorWeakness与根元素 class 之间的响应式映射两个 Store 配置任一变化html根元素上的grey-mode/color-weakness类都会自动增删因此只需在应用启动时调用一次initGreyAndColorWeakness()之后修改设置面板中的开关即可生效无需在页面里重复调用对应的 CSS 样式在 src/common/assets/styles/index.scss 中定义例如对根元素应用filter: grayscale/filter: invert等调用方无需关心实现细节。布局模式 useLayoutModeuseLayoutMode管理系统布局模式左侧菜单 / 顶部菜单 / 左侧 顶部混合菜单源码见 src/common/composables/useLayoutMode.ts。import { useLayoutMode } from /composables/useLayoutMode import { LayoutModeEnum } from /constants/app-key const { isLeft, isTop, isLeftTop, setLayoutMode } useLayoutMode() // 判断当前布局 if (isLeft.value) { // 左侧菜单布局 } // 切换布局 setLayoutMode(LayoutModeEnum.Top)源码实现原理const isLeft computed(() settingsStore.layoutMode LayoutModeEnum.Left) const isTop computed(() settingsStore.layoutMode LayoutModeEnum.Top) const isLeftTop computed(() settingsStore.layoutMode LayoutModeEnum.LeftTop) function setLayoutMode(mode: LayoutModeEnum) { settingsStore.layoutMode mode }LayoutModeEnum同样定义在 src/common/constants/app-key.tsexport enum LayoutModeEnum { Left left, Top top, LeftTop left-top }与useDevice一样useLayoutMode是对settingsStore.layoutMode的响应式投影三个computed用于判断当前布局setLayoutMode负责写入 Store。项目对应的三种布局组件分别位于 src/layouts/modes/LeftMode.vue、TopMode.vue 与 LeftTopMode.vue布局切换由 src/layouts/index.vue 根据isLeft / isTop / isLeftTop动态渲染。组合使用示例一个完整的表格搜索页将以上函数组合起来可以得到一个典型的后台列表页usePagination负责分页、useFetchSelect负责筛选下拉、useFullscreenLoading负责提交/导出 Loading、useDevice负责移动端适配、useTitle负责页面标题script setup langts import { usePagination } from /composables/usePagination import { useFetchSelect } from /composables/useFetchSelect import { useFullscreenLoading } from /composables/useFullscreenLoading import { useTitle } from /composables/useTitle import { getUserListApi, getUserStatusOptionsApi, exportUserApi } from ./apis const { setTitle } useTitle() setTitle(用户管理) const { loading, options: statusOptions, value: statusValue } useFetchSelect({ api: getUserStatusOptionsApi }) function getUserList() { const res await getUserListApi({ currentPage: paginationData.currentPage, pageSize: paginationData.pageSize }) paginationData.total res.data.total tableData.value res.data.list } const { paginationData, resetCurrentPage, watchPagination } usePagination({ callback: getUserList, pageSize: 20 }) watchPagination() function handleSearch() { resetCurrentPage() // 已在第 1 页时直接请求否则重置页码触发监听 } const exportData useFullscreenLoading(exportUserApi, { text: 导出中... }) async function handleExport() { await exportData({ status: statusValue.value }) } /script这个示例体现了组合式函数的两大优点状态分页、下拉、Loading由各函数独立管理、互不干扰watchPagination、onMounted、onBeforeUnmount等生命周期逻辑全部封装在函数内部页面代码只需关心业务本身。使用原则与扩展规范最后再次强调 Skill 文档中给出的四条使用原则它们是项目约定、也是协作时的规范优先复用能用内置组合式函数解决的场景不要重复造轮子生命周期已托管组合式函数内部已处理生命周期如onBeforeUnmount自动清理无需手动管理通用函数放公共目录新增通用组合式函数时在src/common/composables目录下创建命名以use开头并统一用/composables/别名导入私有函数放页面目录页面私有的组合式函数应放在对应页面目录的composables子目录下而非src/common/composables避免公共目录被无关逻辑污染。遵循这些约定可以让整个项目的状态逻辑保持公共可复用、页面私有着的清晰分层。如果需要在仓库中查看这些函数的最新实现与配套用法可直接前往 src/common/composables 目录并参考 src/pages/demo/composable-demo 下的示例页面其中包含use-fetch-select、use-fullscreen-loading、use-watermark的 Vue 演示与对应 API 定义见 apis/use-fetch-select.ts 与 apis/use-fullscreen-loading.ts。赞分享前端【免费下载链接】v3-admin-vite☀️ AI-friendly Vue3 admin template | Vue Admin | Vue Template | Vue3 Admin | Vue3 Template | Vue 后台 | Vue 模板 | Vue3 后台 | Vue3 模板项目地址https://gitcode.com/gh_mirrors/v3a/v3-admin-vite点击查看免费下载相关推荐V3 Admin Vite 内置组合式函数完全清单useWatermark 防删除水印、usePagination 分页神器 10 实战技巧V3 Admin Vite 内置组合式函数完全清单useWatermark 防删除水印、usePagination 分页神器 10 实战技巧 V3 Admi前端从 io.js 周报到 nodejs.org 博客解读 2015-02-13 期 Weekly Update 的历史与内容管线从 io.js 周报到 nodejs.org 博客解读 2015 02 13 期 Weekly Update 的历史与内容管线 2015 年 2 月 13 日前端sccache 完整入门指南5 分钟装好让编译器缓存从本地跑到团队共享sccache 完整入门指南5 分钟装好让编译器缓存从本地跑到团队共享 改一个文件整条链路重新编译盯着进度条等到烦——写 C/C 和 Rust 的人开发工具构建工具上一篇Recaf中的路径节点PathNode如何表示代码结构层次下一篇Go语言网络编程learning-golang分布式系统开发终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表