Vue 3 工程化实战:Axios 高阶封装与样式解决方案深度指南

在 Vue 3 的现代开发中,优雅的 API 请求管理和清晰的样式方案是项目可维护性的两大支柱。本文将从工程化角度出发,不仅讲解 Axios 的封装与拦截器原理,还会深入对比 Scoped CSS、CSS Modules 及预处理器的优劣,配合实战代码与图解,助你搭建企业级的 Vue 项目基础架构。

一、API 请求层:Axios 的高阶封装与状态管理

1.1 为什么要封装 Axios?

原生 Axios 虽然强大,但在实际项目中直接使用会面临以下痛点:

  • 代码冗余:每个请求都要写完整的 URL、配置 headers。
  • 维护困难:接口域名更换、Token 逻辑修改需要改动多处。
  • 体验割裂:Loading 状态、错误提示散落在各个组件中。

因此,我们需要一个统一的请求层,将所有与 HTTP 相关的逻辑收敛。

1.1.1 Axios 工作原理图解

1.2 实战:Axios 完整封装步骤

1.2.1 安装依赖
bash 复制代码
npm install axios
# 配合 Element Plus 做消息提示(可选但推荐)
npm install element-plus
1.2.2 创建封装文件 (src/utils/request.js)

这是核心文件,包含实例创建、拦截器逻辑。

javascript 复制代码
import axios from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useUserStore } from '@/store/user' // 假设你使用 Pinia 管理用户状态

// 1. 创建 Axios 实例
const service = axios.create({
  baseURL: import.meta.env.VITE_APP_BASE_API, // 从环境变量读取 API 地址
  timeout: 15000 // 请求超时时间
})

// 2. 请求拦截器
service.interceptors.request.use(
  config => {
    // 这里通常会添加 Token
    const userStore = useUserStore()
    if (userStore.token) {
      // 让每个请求携带自定义 Token,请根据实际情况修改 key
      config.headers['Authorization'] = 'Bearer ' + userStore.token
    }
    return config
  },
  error => {
    // 处理请求错误
    console.log(error)
    return Promise.reject(error)
  }
)

// 3. 响应拦截器
service.interceptors.response.use(
  response => {
    const res = response.data

    // 如果自定义 code 不是 200,则判定为错误(需与后端约定好规范)
    if (res.code !== 200) {
      ElMessage({
        message: res.message || 'Error',
        type: 'error',
        duration: 5 * 1000
      })

      // 401: 未登录或 Token 过期
      if (res.code === 401) {
        ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          const userStore = useUserStore()
          userStore.logout() // 调用 Pinia 的登出方法清除 Token
          location.reload() // 为了重新实例化 vue-router 对象,避免 bug
        })
      }
      return Promise.reject(new Error(res.message || 'Error'))
    } else {
      return res
    }
  },
  error => {
    console.log('err' + error)
    let { message } = error
    if (message.includes('timeout')) {
      message = '系统接口请求超时'
    } else if (message.includes('Network Error')) {
      message = '系统接口网络异常'
    } else if (message.includes('Request failed with status code')) {
      message = '系统接口' + message.substr(message.length - 3) + '异常'
    }
    
    ElMessage({
      message: message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)

export default service
1.2.3 API 模块化管理 (src/api/user.js)

不要在组件中直接调用 request,而是建立 API 层。

javascript 复制代码
import request from '@/utils/request'

// 用户登录
export function login(data) {
  return request({
    url: '/user/login',
    method: 'post',
    data
  })
}

// 获取用户信息
export function getInfo() {
  return request({
    url: '/user/info',
    method: 'get'
  })
}

// 退出登录
export function logout() {
  return request({
    url: '/user/logout',
    method: 'post'
  })
}

1.3 请求状态管理(配合 Pinia)

除了网络请求,我们通常还需要管理 Loading 状态。这里展示如何用 Pinia 做一个简单的请求状态管理。

javascript 复制代码
// store/loading.js
import { defineStore } from 'pinia'

export const useLoadingStore = defineStore('loading', {
  state: () => ({
    isLoading: false,
    requestCount: 0
  }),
  actions: {
    startLoading() {
      this.requestCount++
      this.isLoading = true
    },
    endLoading() {
      this.requestCount--
      if (this.requestCount <= 0) {
        this.isLoading = false
        this.requestCount = 0
      }
    }
  }
})

然后在 request.js 的拦截器中调用它:

javascript 复制代码
// 引入 store
import { useLoadingStore } from '@/store/loading'

// 请求拦截器中
service.interceptors.request.use(
  config => {
    const loadingStore = useLoadingStore()
    loadingStore.startLoading()
    // ... 其他逻辑
  }
)

// 响应拦截器中 (无论成功失败都要结束)
service.interceptors.response.use(
  response => {
    const loadingStore = useLoadingStore()
    loadingStore.endLoading()
    // ...
  },
  error => {
    const loadingStore = useLoadingStore()
    loadingStore.endLoading()
    // ...
  }
)

二、样式解决方案:Scoped、Modules 与预处理器

Vue 提供了多种样式方案,各有优劣,选择合适的方案能有效避免样式污染。

2.1 方案对比总览

2.2 Scoped CSS:最常用的方案

原理

Vue 通过 PostCSS 给组件内的 DOM 节点添加 data-v-xxx 属性,并在 CSS 选择器末尾加上对应的属性选择器,实现样式隔离。

基础使用
html 复制代码
<template>
  <div class="box">我是带 Scoped 的盒子</div>
</template>

<!-- 加上 scoped 属性 -->
<style scoped>
.box {
  color: blue;
  border: 1px solid blue;
}
</style>
深度选择器 (Deep Selector)

当你使用第三方组件库(如 Element Plus),需要修改其内部样式时,必须使用深度选择器穿透 scoped

html 复制代码
<style scoped>
/* Vue 3 推荐写法 :deep() */
:deep(.el-button) {
  border-radius: 20px;
}

/* 旧版写法 ::v-deep 或 /deep/ (在 Vue 3 中可能会警告) */
</style>

2.3 CSS Modules:更彻底的模块化

CSS Modules 会将类名编译成哈希字符串,从根本上杜绝类名冲突。在 Vite 中开箱即用。

配置 (vite.config.js - 可选)
javascript 复制代码
export default defineConfig({
  css: {
    modules: {
      // 生成哈希类名的格式,开发环境显示文件名便于调试
      localsConvention: 'camelCaseOnly',
      generateScopedName: '[name]__[local]___[hash:base64:5]'
    }
  }
})
实战代码
html 复制代码
<template>
  <!-- 使用 $style 对象访问类名 -->
  <div :class="$style.box">
    <p :class="$style.text">CSS Modules 文本</p>
  </div>
</template>

<!-- 注意这里是 module 而不是 scoped -->
<style module>
.box {
  padding: 20px;
  background-color: #f0f0f0;
}

/* 即使是很常见的类名也不会冲突 */
.text {
  color: #42b883;
  font-weight: bold;
}
</style>

进阶:在 Script 中使用

javascript 复制代码
<script setup>
import { useCssModule } from 'vue'

// 获取 style 对象
const style = useCssModule()

// 可以打印看看编译后的类名
console.log(style.box) // 输出类似 "UserProfile__box___abc123"
</script>

2.4 预处理器:Sass/Less 配置与全局变量注入

虽然原生 CSS 已经很强,但预处理器的变量、循环、混合依然能提升效率。

1. 安装 (以 Sass 为例)

Vite 内置了预处理器支持,只需安装对应的依赖:

bash 复制代码
npm install -D sass
2. 基础使用
css 复制代码
<style lang="scss" scoped>
/* 直接写 SCSS 语法即可 */
$primary-color: #42b883;

.card {
  background: white;
  &:hover {
    border-color: $primary-color;
  }
}
</style>
3. 深度:全局变量注入 (无需每次 import)

如果有一个 src/styles/variables.scss 存放全局变量,每次都在组件里 @import 太麻烦。我们可以在 Vite 中自动注入。

vite.config.js:

javascript 复制代码
import { defineConfig } from 'vite'
import path from 'path'

export default defineConfig({
  css: {
    preprocessorOptions: {
      scss: {
        // 这里的路径要根据你的项目结构调整
        additionalData: `@use "@/styles/variables.scss" as *;`
      }
    }
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  }
})

现在,你在任何组件的 <style lang="scss"> 里都可以直接使用 variables.scss 里定义的变量了!


三、总结与最佳实践

  1. Axios 封装:务必进行二次封装,统一处理 Token、错误和 Loading。
  2. 样式选择
    • 中小型项目Scoped CSS + Sass 足够,灵活且高效。
    • 大型 / 多人协作项目 :推荐 CSS Modules,虽然多敲几个字符,但绝对安全。
    • 追求极致效率 :引入 UnoCSSTailwind CSS (原子化 CSS)。
相关推荐
烈风2 小时前
01_Tauri环境搭建
开发语言·前端·后端
暗不需求2 小时前
深入 JavaScript 核心:用原生 JavaScript 打造就地编辑组件
前端·javascript
一只叁木Meow2 小时前
Vite+:前端开发的"超级管家"来了
前端
不可能的是2 小时前
浏览器端音频转码实战:FFmpeg.wasm 深度定制与踩坑指南
前端
南风知我意9572 小时前
【重构思维】用位运算做权限管理
前端·面试·职场和发展·性能优化·重构
江湖行骗老中医2 小时前
Vue 3 的父子组件传值主要遵循单向数据流的原则:父传子 和 子传父。
前端·javascript·vue.js
RPGMZ2 小时前
RPGMakerMZ 游戏引擎 野外采集点制作
javascript·游戏·游戏引擎·rpgmz·野外采集点
时寒的笔记2 小时前
js基础05_js类、原型对象、原型链&案例(解决无限debugger)
开发语言·javascript·原型模式
Free Tester2 小时前
手动访问网页用的chrome,和使用命令行指定端口启动的chrome,浏览器指纹是否一致
前端·chrome