
在 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 里定义的变量了!
三、总结与最佳实践
- Axios 封装:务必进行二次封装,统一处理 Token、错误和 Loading。
- 样式选择 :
- 中小型项目 :
Scoped CSS+Sass足够,灵活且高效。 - 大型 / 多人协作项目 :推荐
CSS Modules,虽然多敲几个字符,但绝对安全。 - 追求极致效率 :引入
UnoCSS或Tailwind CSS(原子化 CSS)。
- 中小型项目 :