Vue3异步请求实战:从封装到页面渲染的全流程解析

张开发
2026/4/20 17:54:53 15 分钟阅读

分享文章

Vue3异步请求实战:从封装到页面渲染的全流程解析
1. 为什么需要封装异步请求在Vue3项目开发中几乎每个页面都需要与后端进行数据交互。直接使用axios发起请求虽然简单但随着项目规模扩大会出现以下问题每个请求都需要重复编写基础配置如baseURL、超时时间缺乏统一的错误处理机制难以管理接口地址和参数不方便添加全局loading状态我在实际项目中遇到过这样的场景一个电商后台管理系统有50多个接口最初没有封装axios结果每次接口变更都要修改几十个文件。后来花了两天时间重构把请求逻辑统一封装后维护效率提升了300%。2. 如何优雅地封装axios2.1 基础封装方案先创建一个src/utils/request.js文件import axios from axios // 创建实例时配置默认值 const service axios.create({ baseURL: import.meta.env.VITE_API_URL, // 从环境变量读取 timeout: 10000, headers: { Content-Type: application/json } }) // 添加请求拦截器 service.interceptors.request.use( config { // 在发送请求前可以处理token const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }, error { return Promise.reject(error) } ) // 添加响应拦截器 service.interceptors.response.use( response { // 对响应数据做处理 return response.data }, error { // 对响应错误做处理 if (error.response.status 401) { // token过期处理 } return Promise.reject(error) } ) export default service这个封装方案有以下几个优点统一管理基础配置自动携带token统一处理响应数据全局错误处理2.2 高级封装技巧在实际项目中我还推荐添加以下功能// 请求队列 let requestList [] // 取消重复请求 const removeRepeatRequest (config) { // 实现逻辑... } // 添加loading状态 const showLoading () { // 使用Element Plus等UI库的loading } // 在拦截器中调用 service.interceptors.request.use(config { removeRepeatRequest(config) showLoading() return config })3. 接口管理最佳实践3.1 模块化接口设计建议按业务模块划分接口文件例如src/api/ ├── user.js # 用户相关 ├── product.js # 商品相关 └── order.js # 订单相关以用户模块为例import request from /utils/request export const login (data) { return request({ url: /user/login, method: POST, data }) } export const getUserInfo () { return request({ url: /user/info, method: GET }) }3.2 TypeScript支持如果使用TypeScript可以增强类型提示// src/types/api.d.ts declare interface ResponseDataT any { code: number data: T message: string } // 修改request.ts export function requestT(config: AxiosRequestConfig): PromiseResponseDataT { return service(config) }4. 页面中的异步处理4.1 Composition API写法Vue3的setup语法中使用异步请求import { ref, onMounted } from vue import { getProductList } from /api/product export default { setup() { const list ref([]) const loading ref(false) const fetchData async () { loading.value true try { const res await getProductList() list.value res.data } finally { loading.value false } } onMounted(() { fetchData() }) return { list, loading } } }4.2 使用Suspense处理异步组件对于更复杂的异步场景可以使用Suspense// 异步组件 const AsyncComponent defineAsyncComponent({ loader: async () { const data await fetchData() return { template: div{{ data }}/div, data: () ({ data }) } }, loadingComponent: LoadingSpinner })5. 性能优化技巧5.1 请求缓存方案对于不常变的数据可以添加缓存const cacheMap new Map() export const getProductList () { const cacheKey product_list if (cacheMap.has(cacheKey)) { return Promise.resolve(cacheMap.get(cacheKey)) } return request({ url: /products }).then(res { cacheMap.set(cacheKey, res) return res }) }5.2 取消重复请求在搜索等场景下需要取消上一个请求const controller new AbortController() request({ url: /search, signal: controller.signal }) // 取消请求 controller.abort()6. 常见问题解决方案6.1 文件上传处理封装文件上传方法export const uploadFile (file) { const formData new FormData() formData.append(file, file) return request({ url: /upload, method: POST, data: formData, headers: { Content-Type: multipart/form-data } }) }6.2 大文件分片上传对于大文件上传可以实现分片上传const chunkSize 2 * 1024 * 1024 // 2MB const uploadChunk (chunk, index) { // 上传单个分片 } const uploadBigFile async (file) { const chunks Math.ceil(file.size / chunkSize) for (let i 0; i chunks; i) { const chunk file.slice(i * chunkSize, (i 1) * chunkSize) await uploadChunk(chunk, i) } // 合并分片 }7. 测试与调试技巧7.1 Mock数据方案开发阶段可以使用Mock.jsimport Mock from mockjs Mock.mock(/api/user, { list|10: [{ id|1: 1, name: cname }] })7.2 接口调试工具推荐使用这些工具调试接口PostmanApifoxSwagger UI在项目中集成Swagger// vite.config.js export default defineConfig({ server: { proxy: { /swagger: { target: http://localhost:8080, changeOrigin: true } } } })8. 项目实战案例8.1 电商商品列表实现完整商品列表组件示例template div el-table :dataproducts v-loadingloading el-table-column propname label商品名称 / el-table-column propprice label价格 / /el-table el-pagination current-changehandlePageChange :current-pagepage :page-sizesize :totaltotal / /div /template script setup import { ref, onMounted } from vue import { getProducts } from /api/product const products ref([]) const loading ref(false) const page ref(1) const size ref(10) const total ref(0) const fetchProducts async () { loading.value true try { const res await getProducts({ page: page.value, size: size.value }) products.value res.list total.value res.total } finally { loading.value false } } const handlePageChange (newPage) { page.value newPage fetchProducts() } onMounted(() { fetchProducts() }) /script8.2 用户登录交互实现登录表单最佳实践template el-form :modelform :rulesrules submit.preventhandleSubmit el-form-item propusername el-input v-modelform.username placeholder用户名 / /el-form-item el-form-item proppassword el-input v-modelform.password typepassword placeholder密码 / /el-form-item el-button typeprimary native-typesubmit :loadingloading 登录 /el-button /el-form /template script setup import { ref } from vue import { login } from /api/user import { useRouter } from vue-router const router useRouter() const loading ref(false) const form ref({ username: , password: }) const rules { username: [{ required: true, message: 请输入用户名 }], password: [{ required: true, message: 请输入密码 }] } const handleSubmit async () { loading.value true try { const res await login(form.value) localStorage.setItem(token, res.token) router.push(/) } finally { loading.value false } } /script9. 安全防护措施9.1 防止XSS攻击对接口返回的HTML内容进行处理import DOMPurify from dompurify const sanitizeHTML (html) { return DOMPurify.sanitize(html) }9.2 CSRF防护在axios配置中添加CSRF Token// request.js const token getCookie(csrfToken) service.defaults.headers.common[X-CSRF-TOKEN] token10. 项目架构建议10.1 大型项目结构推荐的项目目录结构src/ ├── api/ # 接口模块 ├── components/ # 公共组件 ├── composables/ # 组合式函数 ├── stores/ # 状态管理 ├── utils/ # 工具函数 └── views/ # 页面组件10.2 组合式函数封装将通用请求逻辑封装成组合式函数// src/composables/useRequest.js import { ref } from vue export function useRequest(apiFn) { const data ref(null) const error ref(null) const loading ref(false) const execute async (...args) { loading.value true try { data.value await apiFn(...args) } catch (err) { error.value err } finally { loading.value false } } return { data, error, loading, execute } }使用时const { data, execute } useRequest(getProductList) onMounted(() { execute() })

更多文章