ARTICLE DETAIL

资讯详情

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

Vue3公共方法封装:四层架构设计提升代码复用与维护性

Vue3公共方法封装:四层架构设计提升代码复用与维护性 1. 从“能用”到“好用”为什么我们需要封装公共方法在Vue3项目里我们经常遇到一些场景比如需要把用户选择的日期从“2024-05-08”格式化成“2024年5月8日”显示在页面上或者在用户点击提交按钮后需要弹出一个统一的成功提示又或者需要从URL里解析出特定的查询参数。最开始我们可能会图省事直接在组件的script setup里写一个formatDate函数或者从某个教程里复制一段showToast的代码粘贴到当前需要用的组件里。一次两次还好但当项目逐渐变大这种“散装”的公共方法就会带来一系列头疼的问题。你会在A页面、B页面、甚至C页面的utils文件夹里发现三个功能相似但命名和实现略有不同的formatTime函数。当产品经理说“所有成功提示的显示时间延长到3秒”时你就得像个侦探一样全局搜索ElMessage.success然后小心翼翼地修改每一个地方生怕漏掉一个。更麻烦的是当你发现某个日期格式化函数在处理“2023-02-30”这种非法日期时会崩溃你需要修复的副本可能遍布整个项目。这就是公共方法封装要解决的核心问题提升代码的复用性、维护性和一致性。封装不是简单的把函数扔进一个叫utils.js的文件里而是基于Vue3的组合式API思想构建一套清晰、健壮、易于测试和管理的共享逻辑体系。它让我们的代码从“能用”变得“好用”从“写一次跑通就行”进化到“写一次处处用好维护”。2. 公共方法封装的四层架构设计很多开发者对封装的理解停留在“工具函数集合”这远远不够。一个健壮的Vue3项目其公共方法应该是有层次、分类型的。我根据多年项目经验总结出一个四层架构模型这能帮你清晰地规划项目中的共享逻辑。2.1 基础工具层纯函数的世界这一层是封装的基石包含所有无副作用、不依赖Vue运行时、不依赖浏览器DOM/BOM的纯函数。它们只进行计算和数据处理输入确定输出就确定。核心特征与设计原则零依赖除了JavaScript语言本身和你的业务数据模型不导入任何第三方库如Vue, axios, dayjs。这保证了其极致的可移植性和可测试性。函数式鼓励使用纯函数避免修改输入参数总是返回一个新值。单一职责一个函数只做好一件事。例如formatDate负责格式化parseQueryString负责解析不要混在一起。实战案例一个健壮的日期格式化函数直接使用new Date()和toLocaleString往往无法满足复杂多变的业务需求且兼容性处理繁琐。更常见的做法是引入dayjs或date-fns但即使如此封装也大有可为。// utils/date.js import dayjs from dayjs; import isSameOrBefore from dayjs/plugin/isSameOrBefore; import isSameOrAfter from dayjs/plugin/isSameOrAfter; import dayjs/locale/zh-cn; // 按需加载插件和语言包 dayjs.extend(isSameOrBefore); dayjs.extend(isSameOrAfter); dayjs.locale(zh-cn); /** * 格式化日期时间 * param {string|number|Date|dayjs.Dayjs} date - 日期输入 * param {string} formatStr - 格式字符串默认YYYY-MM-DD HH:mm:ss * param {string} fallback - 解析失败时的默认返回值 * returns {string} 格式化后的字符串 */ export function formatDate(date, formatStr YYYY-MM-DD HH:mm:ss, fallback --) { if (!date) return fallback; const dayjsObj dayjs(date); // 使用dayjs的isValid方法比直接try-catch更精准 return dayjsObj.isValid() ? dayjsObj.format(formatStr) : fallback; } /** * 计算相对时间如“3天前” * param {string|number|Date} date - 过去的时间 * returns {string} 相对时间描述 */ export function timeAgo(date) { const now dayjs(); const target dayjs(date); if (!target.isValid()) return 无效日期; const diffInSeconds now.diff(target, second); const diffInMinutes now.diff(target, minute); const diffInHours now.diff(target, hour); const diffInDays now.diff(target, day); if (diffInSeconds 60) return 刚刚; if (diffInMinutes 60) return ${diffInMinutes}分钟前; if (diffInHours 24) return ${diffInHours}小时前; if (diffInDays 30) return ${diffInDays}天前; // 超过一个月显示具体日期 return formatDate(date, YYYY-MM-DD); } /** * 判断日期是否在某个范围内常用于活动时间校验 * param {string} date - 待判断日期 * param {string} start - 开始日期 * param {string} end - 结束日期 * returns {boolean} */ export function isDateInRange(date, start, end) { const dayjsDate dayjs(date); const dayjsStart dayjs(start); const dayjsEnd dayjs(end); // 使用插件提供的方法语义更清晰 return dayjsDate.isSameOrAfter(dayjsStart) dayjsDate.isSameOrBefore(dayjsEnd); }封装要点解析集中管理依赖在date.js中统一导入、配置dayjs及其插件、语言包。这样项目中所有用到日期处理的地方都共享同一份配置避免散落各处导致的不一致。防御性编程formatDate函数中对输入进行判空和有效性校验并提供fallback参数。这避免了因为一个无效日期导致整个页面渲染崩溃提升了组件健壮性。清晰的JSDoc注释注释不仅说明了参数和返回值还明确了参数类型和默认值。这在团队协作和后期维护时价值巨大许多IDE能基于此提供智能提示。功能细分将“格式化”、“相对时间”、“范围判断”拆分成不同函数而不是做一个“万能”函数符合单一职责原则。2.2 UI交互层基于组件库的二次封装这一层封装与用户界面直接交互的逻辑通常围绕UI组件库如Element Plus, Ant Design Vue, Naive UI进行。目的是统一项目中的交互风格、简化复杂组件的调用、并集中处理通用行为。核心价值统一视觉与行为确保全站的提示框、对话框、加载状态等看起来和用起来都一样。简化复杂API将组件库需要多个属性、多个事件才能实现的常用功能封装成开箱即用的函数或组件。注入全局配置例如统一设置消息提示的显示时长、弹出位置或对话框的确认按钮文字。实战案例封装一个支持Promise的确认对话框Element Plus的ElMessageBox.confirm本身返回Promise但我们可以封装得更符合业务习惯。// utils/message.js import { ElMessage, ElMessageBox, ElLoading } from element-plus; // 统一消息配置 const messageDefaults { duration: 3000, showClose: false, grouping: true, // 合并相同内容消息 }; export const $message { success: (msg, options {}) ElMessage.success({ ...messageDefaults, ...options, message: msg }), warning: (msg, options {}) ElMessage.warning({ ...messageDefaults, ...options, message: msg }), error: (msg, options {}) ElMessage.error({ ...messageDefaults, ...options, message: msg }), info: (msg, options {}) ElMessage.info({ ...messageDefaults, ...options, message: msg }), }; /** * 确认对话框支持自定义标题、内容、按钮文字 * param {string} content - 对话框内容 * param {string} title - 标题默认“提示” * param {Object} options - 扩展选项如confirmButtonText, cancelButtonText, type * returns {Promiseboolean} 用户点击确认返回true取消或关闭返回false */ export function $confirm(content, title 提示, options {}) { const { confirmButtonText 确认, cancelButtonText 取消, type warning, ...restOptions } options; return ElMessageBox.confirm(content, title, { confirmButtonText, cancelButtonText, type, ...restOptions, }) .then(() true) .catch(() false); // 统一捕获取消和关闭操作返回false } /** * 全局加载层单例模式避免重复叠加 */ let loadingInstance null; export const $loading { show: (text 加载中...) { if (!loadingInstance) { loadingInstance ElLoading.service({ lock: true, text, background: rgba(0, 0, 0, 0.7), }); } else { // 如果已存在则更新文本 loadingInstance.setText(text); } }, hide: () { if (loadingInstance) { loadingInstance.close(); loadingInstance null; } }, };封装要点解析配置统一与覆盖$message对象预先注入了项目的默认配置messageDefaults同时允许调用时通过options覆盖。这保证了风格统一又保留了灵活性。简化调用与增强语义$confirm函数将ElMessageBox.confirm的多个参数进行了合理化默认和结构简化并统一了返回值Promiseboolean使业务代码逻辑更清晰if (await $confirm(确定删除)) { // 删除操作 }。实现单例模式$loading通过闭包保存loadingInstance实例确保同一时间只有一个全屏加载层这是处理加载状态的最佳实践能有效避免因多次快速调用show而产生的多个叠加层。2.3 网络请求层Axios的深度封装与拦截器管理这是前端架构中的关键一层。对Axios的封装质量直接影响到项目的可维护性、错误处理能力和开发体验。封装的目标是让业务组件无需关心令牌、加载状态、错误提示等底层细节。核心封装内容实例创建与全局配置Base URL, 超时时间请求头。请求/响应拦截器注入认证令牌、统一处理错误、转换响应数据格式。统一的API函数将RESTful风格的接口封装成语义清晰的函数。实战案例创建一个健壮的请求实例// utils/request.js import axios from axios; import { $message, $loading } from ./message; import router from /router; // 假设你有路由实例 // 1. 创建axios实例 const request axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, // 从环境变量读取 timeout: 15000, headers: { Content-Type: application/json;charsetutf-8 }, }); // 2. 请求拦截器 request.interceptors.request.use( (config) { // 显示全局加载可根据config中自定义属性决定是否显示 if (!config.hideLoading) { $loading.show(); } // 注入认证令牌 const token localStorage.getItem(access_token); // 或从Pinia store中获取 if (token) { config.headers.Authorization Bearer ${token}; } // 针对POST请求处理常见的表单数据序列化根据后端要求调整 if (config.method post config.headers[Content-Type]?.includes(x-www-form-urlencoded)) { config.data qs.stringify(config.data); } return config; }, (error) { $loading.hide(); return Promise.reject(error); } ); // 3. 响应拦截器 request.interceptors.response.use( (response) { $loading.hide(); // 根据后端统一响应结构处理例如 { code: 200, data: {}, message: success } const res response.data; if (res.code 200 || res.code 0) { // 成功码需与后端约定 return res.data; // 直接返回业务数据剥离code/message } else { // 业务逻辑错误如参数错误、权限不足 $message.error(res.message || 请求错误 [${res.code}]); // 可以针对特定code做特殊处理如token过期跳转登录 if (res.code 401) { localStorage.removeItem(access_token); router.push(/login); } // 返回一个reject的Promise阻止后续的.then()执行 return Promise.reject(new Error(res.message || Error)); } }, (error) { $loading.hide(); // HTTP状态码错误如404, 500, 网络超时 if (error.response) { // 请求已发出服务器有响应 const { status, data } error.response; switch (status) { case 401: $message.error(未授权请重新登录); router.push(/login); break; case 403: $message.error(拒绝访问); break; case 404: $message.error(请求的资源不存在); break; case 500: $message.error(服务器内部错误); break; default: $message.error(data?.message || 网络错误 [${status}]); } } else if (error.request) { // 请求已发出但无响应网络断开、超时 $message.error(网络连接异常请检查后重试); } else { // 请求配置出错 $message.error(请求配置错误 error.message); } return Promise.reject(error); } ); // 4. 封装常用的请求方法提供更清晰的API export const http { get: (url, params, config {}) request.get(url, { params, ...config }), post: (url, data, config {}) request.post(url, data, config), put: (url, data, config {}) request.put(url, data, config), delete: (url, config {}) request.delete(url, config), // 可以继续封装upload, download等方法 }; export default request; // 也可以导出原实例以备特殊需求封装要点解析环境变量集成baseURL从import.meta.env读取便于区分开发、测试、生产环境。精细化加载控制通过config.hideLoading这样的自定义属性允许在个别请求如静默轮询中禁用全局加载动画提升用户体验。响应数据标准化在响应拦截器中将后端返回的{code, data, message}结构拆解成功时直接返回data业务组件无需再res.data.data这样层层解构。失败时统一提示并reject。全面的错误处理区分网络错误无响应、HTTP错误状态码4xx/5xx和业务逻辑错误code非成功码并给予用户恰当的反馈。特别是401处理自动跳转登录页。提供简洁的http对象相比直接使用request.get()http.get()的语义更清晰且为未来统一添加更多逻辑如请求重试留出空间。2.4 业务组合层使用Composables封装复杂交互逻辑这是Vue3组合式API带来的革命性特性。我们可以将分散在组件数据(data)、方法(methods)、生命周期(mounted)中的有状态的、与组件生命周期相关的逻辑抽离成可复用的组合函数 (composables)。与工具层的区别工具函数是无状态、无副作用的纯函数。而组合函数 (composables) 内部可以使用ref,reactive,computed,onMounted等Vue响应式API和生命周期钩子它封装的是“逻辑”而不仅仅是“计算”。实战案例封装一个支持搜索、分页的表格逻辑几乎每个后台管理系统都有大量的表格页面它们通常包含查询条件、分页参数、加载状态、表格数据获取。我们可以将其封装。// composables/useTable.js import { ref, reactive, computed, watch } from vue; import { http } from /utils/request; /** * 通用表格逻辑Hook * param {Function} fetchApi - 获取表格数据的API函数需接收params对象 * param {Object} initialQuery - 初始查询条件 * param {Object} options - 配置项如 immediate是否立即加载 * returns {Object} 返回状态和方法 */ export function useTable(fetchApi, initialQuery {}, options {}) { const { immediate true } options; // 状态定义 const loading ref(false); const tableData ref([]); const total ref(0); // 查询条件与分页参数使用reactive便于整体重置 const queryParams reactive({ page: 1, size: 10, ...initialQuery, // 合并初始查询条件 }); // 获取数据的方法 const fetchData async () { if (!fetchApi || typeof fetchApi ! function) { console.error(useTable: fetchApi必须是一个函数); return; } loading.value true; try { // 调用传入的API函数 const res await fetchApi(queryParams); // 假设API返回结构为 { list: [], total: 100 } tableData.value res.list || []; total.value res.total || 0; } catch (error) { console.error(获取表格数据失败:, error); tableData.value []; total.value 0; } finally { loading.value false; } }; // 监听分页参数变化自动重新获取数据可根据需求决定是否启用 watch( () [queryParams.page, queryParams.size], () { fetchData(); } ); // 搜索重置页码为1 const handleSearch () { queryParams.page 1; fetchData(); }; // 重置搜索条件 const handleReset () { Object.keys(queryParams).forEach((key) { // 保留分页参数只重置查询条件需要根据initialQuery判断哪些是查询条件 // 这里简化处理如果initialQuery里有定义则重置为其初始值否则重置为空分页参数除外 if (![page, size].includes(key)) { if (initialQuery[key] ! undefined) { queryParams[key] initialQuery[key]; } else { queryParams[key] ; // 或 null取决于后端接受什么 } } }); queryParams.page 1; fetchData(); }; // 立即执行一次 if (immediate) { fetchData(); } // 暴露给组件的所有状态和方法 return { // 状态 loading, tableData, total, queryParams, // 计算属性例如总页数 totalPage: computed(() Math.ceil(total.value / queryParams.size)), // 方法 fetchData, handleSearch, handleReset, }; }在组件中的使用!-- UserManage.vue -- template div el-form :modelqueryParams inline el-form-item label用户名 el-input v-modelqueryParams.username placeholder请输入 keyup.enterhandleSearch / /el-form-item el-form-item el-button typeprimary clickhandleSearch :loadingloading搜索/el-button el-button clickhandleReset重置/el-button /el-form-item /el-form el-table :datatableData v-loadingloading !-- 表格列定义 -- /el-table el-pagination v-model:current-pagequeryParams.page v-model:page-sizequeryParams.size :totaltotal current-changefetchData size-changefetchData layouttotal, sizes, prev, pager, next, jumper / /div /template script setup import { useTable } from /composables/useTable; import { getUserList } from /api/user; // 你的业务API // 一行代码引入所有表格逻辑 const { loading, tableData, total, queryParams, handleSearch, handleReset, fetchData, } useTable(getUserList, { username: , status: }); // 传入API和初始查询条件 /script封装要点解析高内聚低耦合将表格相关的所有状态loading,tableData,queryParams和操作fetchData,handleSearch聚合在一个函数内组件只需关心视图渲染和传递一个fetchApi。极大的代码复用任何需要表格搜索分页的页面都可以通过useTable快速搭建代码量减少70%以上。灵活性通过参数initialQuery和options可以定制初始状态和行为。fetchApi的传入使得它可以适配任何后端接口。清晰的职责分离组件只负责视图和用户交互数据获取和状态管理逻辑全部委托给composable使组件代码非常简洁和专注。3. 封装的高级技巧与最佳实践掌握了基础封装后我们还需要关注一些提升封装质量和开发体验的高级技巧。3.1 类型安全为封装函数添加TypeScript支持在大型项目中TypeScript能极大提升代码的可靠性和开发效率。为你的封装函数添加类型定义让它们变得“智能”。为工具函数添加类型// utils/date.ts import dayjs, { Dayjs } from dayjs; export function formatDate( date: string | number | Date | Dayjs | null | undefined, formatStr: string YYYY-MM-DD HH:mm:ss, fallback: string -- ): string { // ... 实现 } export function timeAgo(date: string | number | Date | Dayjs): string { // ... 实现 }为组合函数定义返回类型// composables/useTable.ts import { Ref, ComputedRef } from vue; interface UseTableOptions { immediate?: boolean; } interface UseTableReturnT any, Q Recordstring, any { loading: Refboolean; tableData: RefT[]; total: Refnumber; queryParams: Q { page: number; size: number }; // 合并分页参数 totalPage: ComputedRefnumber; fetchData: () Promisevoid; handleSearch: () void; handleReset: () void; } export function useTableT, Q extends Recordstring, any( fetchApi: (params: Q { page: number; size: number }) Promise{ list: T[]; total: number }, initialQuery: Q {} as Q, options: UseTableOptions {} ): UseTableReturnT, Q { // ... 实现 }添加类型后在VSCode等编辑器中你会获得完美的参数提示、类型检查和自动补全几乎可以避免所有因参数类型错误导致的问题。3.2 性能优化避免重复计算与内存泄漏封装虽好但也要注意性能。工具函数对于计算密集型的纯函数如复杂的数据转换可以考虑使用memoization缓存技术避免相同输入下的重复计算。例如使用lodash.memoize。组合函数要特别注意清除副作用。如果你的composable中设置了事件监听器、定时器或订阅了外部数据源必须在onUnmounted生命周期钩子中清理它们。// composables/useEventListener.js import { onMounted, onUnmounted } from vue; export function useEventListener(target, event, callback) { onMounted(() target.addEventListener(event, callback)); onUnmounted(() target.removeEventListener(event, callback)); // 关键清理 }3.3 统一的导出与维护建立清晰的索引文件当utils和composables文件夹下的文件越来越多时一个清晰的index.js文件至关重要。它有两个好处一是简化导入路径二是作为模块的“目录”方便管理。// utils/index.js export * from ./date; export * from ./message; export * from ./request; // 可以按需导出具体函数避免命名冲突 // export { formatDate, timeAgo } from ./date; // export { $message, $confirm } from ./message; // composables/index.js export { useTable } from ./useTable; export { useEventListener } from ./useEventListener; export { useLocalStorage } from ./useLocalStorage;在组件中你就可以这样使用import { formatDate, $message, http, useTable } from /utils; // 简洁明了4. 封装实战从零构建一个Vue3项目的公共方法体系让我们以一个假设的“任务管理系统”为例串联以上所有知识看看一个完整的封装体系如何在项目中落地。第一步搭建目录结构src/ ├── api/ # 所有后端接口定义按模块划分 │ ├── task.js │ ├── user.js │ └── index.js ├── composables/ # 组合函数 │ ├── useTable.js │ ├── useForm.js (可封装表单逻辑) │ └── index.js ├── utils/ # 工具函数 │ ├── date.js │ ├── message.js │ ├── request.js │ ├── validate.js (表单校验规则) │ └── index.js └── main.js第二步按顺序封装先写utils/request.js因为它是数据通信的基础。接着写api/task.js使用封装好的http对象定义具体的业务接口如getTaskList,createTask,updateTask。然后写utils/date.js和utils/message.js它们是通用的辅助工具。根据业务需求编写composables/useTable.js并在任务列表页面引入。随着项目进行你可能会发现很多表单有类似的校验、提交、重置逻辑于是可以抽象出composables/useForm.js。第三步在组件中享受封装带来的便利在TaskList.vue组件中你的代码会变得异常简洁和清晰script setup import { ref } from vue; import { useTable } from /composables; import { getTaskList } from /api/task; import { $confirm, $message } from /utils; import { formatDate } from /utils/date; // 1. 使用useTable快速构建列表逻辑 const { loading, tableData, queryParams, handleSearch, handleReset, } useTable(getTaskList, { keyword: , status: }); // 2. 业务方法删除任务 const handleDelete async (id) { const confirmed await $confirm(确定删除此任务, 警告, { type: error }); if (!confirmed) return; try { await deleteTask(id); $message.success(删除成功); fetchData(); // 刷新列表 } catch (error) { // 错误已在request拦截器中统一处理 } }; // 3. 在模板中使用工具函数 const formatDeadline (date) { return formatDate(date, MM-DD HH:mm); }; /script通过这样一套从底层工具到高层业务组合的完整封装你的Vue3项目将拥有坚实的架构基础。新功能的开发会像搭积木一样快速代码维护成本显著降低团队协作也更加顺畅。记住封装的终极目标不是创造更多代码而是通过精心的设计让代码变得更少、更清晰、更强大。
返回列表