
前言
Vue3 作为前端主流框架,其生态工具的灵活运用直接决定开发效率和项目可维护性。在实际开发中,API 请求 是前端与后端交互的核心(也是新手最易写出面条代码的环节),样式解决方案 则是保证组件样式隔离、提升样式复用性的关键。
本文从企业级实战角度,系统讲解 Vue3 生态中 API 请求的核心工具 Axios(封装、拦截器、请求状态管理),以及三大样式解决方案(Scoped CSS、CSS Modules、Sass/Less 预处理器)的配置与最佳实践。内容覆盖基础配置、进阶技巧、踩坑解决方案,既适合 Vue3 新手规范代码写法,也能为中大型项目提供技术选型和性能优化参考。
一、Vue3 API 请求实战:Axios 从基础到企业级封装
1.1 为什么选择 Axios?(对比 Fetch)
在 Vue 项目中,API 请求工具首选 Axios 而非原生 Fetch,核心优势可通过以下对比清晰体现:

个人实战经验:我在早期项目中用过原生 Fetch,光是统一错误处理和请求头配置就写了大量重复代码,后期重构为 Axios 封装后,代码量减少 40%+,且可维护性大幅提升。
1.2 环境准备与 Axios 基础使用
1.2.1 安装 Axios
前置条件:已初始化 Vue3 项目(Vite+TS/JS 均可),Node 版本≥14.18.0。
安装命令(推荐 pnpm,速度更快、体积更小):
bash
# npm
npm install axios --save
# yarn
yarn add axios
# pnpm(推荐)
pnpm add axios
1.2.2 Axios 原生使用(新手入门)
在组件中直接使用 Axios 发送请求(仅作入门演示,生产环境不推荐):
html
<template>
<div class="demo">
<button @click="getUserList">获取用户列表</button>
<div v-if="loading">加载中...</div>
<div v-else-if="error">{{ error }}</div>
<ul v-else>
<li v-for="user in userList" :key="user.id">{{ user.name }}</li>
</ul>
</div>
</template>
<script setup lang="ts">
import axios from 'axios';
import { ref } from 'vue';
const loading = ref(false);
const error = ref('');
const userList = ref([]);
// 原生Axios请求
const getUserList = async () => {
loading.value = true;
error.value = '';
try {
// 基础请求配置
const response = await axios({
url: 'https://api.example.com/users', // 替换为实际接口
method: 'GET',
params: { page: 1, size: 10 }, // GET参数
timeout: 5000, // 超时时间
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token') // 手动加token
}
});
userList.value = response.data.data; // 手动解构数据
} catch (err: any) {
// 手动处理错误
error.value = err.message || '请求失败';
} finally {
loading.value = false;
}
};
</script>
问题暴露:原生使用存在大量重复代码(loading、错误处理、请求头、数据解构),且无法统一管理,这也是我们需要封装 Axios 的核心原因。
1.3 Axios 企业级封装(核心实战)
1.3.1 封装思路(企业级标准)
- 创建 Axios 实例,统一基础配置(基准 URL、超时时间、请求头);
- 配置请求拦截器:添加 token、统一请求参数格式、开启 loading;
- 配置响应拦截器:统一数据解构、全局错误处理、token 过期刷新;
- 封装取消重复请求逻辑:避免同一接口多次提交;
- 封装通用请求方法(get/post/put/delete),简化组件调用;
- 暴露请求实例和取消请求方法,满足特殊场景需求。
1.3.2 第一步:创建 Axios 实例(src/utils/axios.ts)
javascript
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, CancelTokenSource } from 'axios';
// 1. 定义接口返回数据类型(结合后端实际格式)
interface ResponseData<T = any> {
code: number; // 状态码(如200成功,401未授权,500服务器错误)
msg: string; // 提示信息
data: T; // 业务数据
}
// 2. 存储取消请求的Token(用于取消重复请求)
const cancelTokenSourceMap = new Map<string, CancelTokenSource>();
// 3. 创建Axios实例
const service: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // 环境变量(Vite),Vue CLI用process.env
timeout: 10000, // 全局超时时间
headers: {
'Content-Type': 'application/json;charset=utf-8' // 默认请求头
}
});
// 4. 生成请求唯一标识(用于取消重复请求)
const getRequestKey = (config: AxiosRequestConfig) => {
const { method, url, params, data } = config;
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&');
};
export { service, ResponseData, cancelTokenSourceMap, getRequestKey };
1.3.3 第二步:配置请求拦截器
在src/utils/axios.ts中继续添加:
javascript
// 请求拦截器
service.interceptors.request.use(
(config) => {
// 1. 取消重复请求(如果存在相同请求,先取消)
const requestKey = getRequestKey(config);
if (cancelTokenSourceMap.has(requestKey)) {
cancelTokenSourceMap.get(requestKey)?.cancel('重复请求已取消');
cancelTokenSourceMap.delete(requestKey);
}
// 创建新的取消Token
const source = axios.CancelToken.source();
config.cancelToken = source.token;
cancelTokenSourceMap.set(requestKey, source);
// 2. 添加Token(从本地存储/状态管理中获取)
const token = localStorage.getItem('token');
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
// 3. GET请求参数序列化(解决特殊字符问题)
if (config.method?.toUpperCase() === 'GET' && config.params) {
config.params = {
...config.params,
_t: Date.now() // 加时间戳,避免GET请求缓存
};
}
// 4. 开启全局Loading(可结合Element Plus/UI组件库实现)
// 示例:如果项目用Element Plus,可在这里调用ElLoading.service()
// window.$loading = ElLoading.service({ lock: true, text: '加载中...' });
return config;
},
(error) => {
// 请求拦截器错误处理
return Promise.reject(error);
}
);
1.3.4 第三步:配置响应拦截器
在src/utils/axios.ts中继续添加:
javascript
// 响应拦截器
service.interceptors.response.use(
async (response: AxiosResponse<ResponseData>) => {
// 1. 关闭全局Loading
// if (window.$loading) window.$loading.close();
// 2. 移除取消请求的Token
const requestKey = getRequestKey(response.config);
cancelTokenSourceMap.delete(requestKey);
const { code, msg, data } = response.data;
// 3. 统一业务状态码处理
switch (code) {
case 200:
// 成功:直接返回业务数据
return data;
case 401:
// 未授权/Token过期:跳转到登录页
localStorage.removeItem('token');
// 示例:结合Vue Router跳转
// router.push('/login');
return Promise.reject(new Error('登录状态已过期,请重新登录'));
case 403:
// 权限不足
return Promise.reject(new Error('暂无权限访问'));
case 500:
// 服务器错误
return Promise.reject(new Error('服务器内部错误,请稍后重试'));
default:
// 其他错误
return Promise.reject(new Error(msg || '请求失败'));
}
},
(error) => {
// 1. 关闭全局Loading
// if (window.$loading) window.$loading.close();
// 2. 移除取消请求的Token
if (error.config) {
const requestKey = getRequestKey(error.config);
cancelTokenSourceMap.delete(requestKey);
}
// 3. 统一网络错误处理
let errorMsg = '请求失败';
if (axios.isCancel(error)) {
// 取消请求的错误(无需提示)
errorMsg = error.message;
} else if (error.message.includes('timeout')) {
errorMsg = '请求超时,请检查网络';
} else if (error.message.includes('Network Error')) {
errorMsg = '网络异常,请检查网络连接';
} else {
errorMsg = error.message || '请求失败';
}
// 4. 全局错误提示(可结合UI组件库的Message组件)
// 示例:ElMessage.error(errorMsg);
return Promise.reject(new Error(errorMsg));
}
);
1.3.5 第四步:封装通用请求方法
在src/utils/axios.ts末尾添加:
javascript
// 封装GET请求
export const get = <T = any>(url: string, params?: any, config?: AxiosRequestConfig): Promise<T> => {
return service.get(url, { params, ...config });
};
// 封装POST请求
export const post = <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> => {
return service.post(url, data, config);
};
// 封装PUT请求
export const put = <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> => {
return service.put(url, data, config);
};
// 封装DELETE请求
export const del = <T = any>(url: string, params?: any, config?: AxiosRequestConfig): Promise<T> => {
return service.delete(url, { params, ...config });
};
// 取消所有请求(如页面卸载时调用)
export const cancelAllRequest = () => {
cancelTokenSourceMap.forEach((source) => {
source.cancel('页面卸载,取消所有请求');
});
cancelTokenSourceMap.clear();
};
// 默认导出Axios实例(满足特殊场景)
export default service;
1.3.6 配置环境变量(Vite 示例)
在项目根目录创建.env.development(开发环境)和.env.production(生产环境):
bash
# .env.development
VITE_API_BASE_URL = 'https://dev-api.example.com'
# .env.production
VITE_API_BASE_URL = 'https://prod-api.example.com'
1.4 请求状态管理:优雅处理 Loading/Error/Success
封装useRequest组合式 API,统一管理请求状态(src/hooks/useRequest.ts):
javascript
import { ref, unref, watchEffect } from 'vue';
// 定义请求选项
interface RequestOptions {
immediate?: boolean; // 是否立即执行
onSuccess?: (data: any) => void; // 成功回调
onError?: (error: Error) => void; // 错误回调
}
// 默认选项
const defaultOptions: RequestOptions = {
immediate: true
};
// 封装请求状态管理Hook
export const useRequest = <T = any>(
requestFn: (...args: any[]) => Promise<T>,
options: RequestOptions = defaultOptions
) => {
// 请求状态
const loading = ref(false);
const data = ref<T | null>(null);
const error = ref<Error | null>(null);
// 执行请求的方法
const run = async (...args: any[]) => {
loading.value = true;
error.value = null;
try {
const result = await requestFn(...args);
data.value = result;
options.onSuccess?.(result);
return result;
} catch (err) {
error.value = err as Error;
options.onError?.(err as Error);
return Promise.reject(err);
} finally {
loading.value = false;
}
};
// 立即执行请求
if (unref(options.immediate)) {
watchEffect(() => {
run();
});
}
return {
loading,
data,
error,
run // 手动执行请求的方法
};
};
1.5 封装后实战使用(组件中调用)
html
<template>
<div class="user-list">
<h3>用户列表</h3>
<!-- Loading状态 -->
<div v-if="loading" class="loading">加载中...</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error">{{ error.message }}</div>
<!-- 空数据状态 -->
<div v-else-if="!data || data.length === 0" class="empty">暂无用户数据</div>
<!-- 成功状态 -->
<table v-else class="table">
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>手机号</th>
</tr>
</thead>
<tbody>
<tr v-for="user in data" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.phone }}</td>
</tr>
</tbody>
</table>
<!-- 手动触发请求 -->
<button @click="run" :disabled="loading">刷新列表</button>
</div>
</template>
<script setup lang="ts">
import { get } from '@/utils/axios';
import { useRequest } from '@/hooks/useRequest';
// 定义用户类型
interface User {
id: number;
name: string;
phone: string;
}
// 定义请求函数
const fetchUserList = (page = 1) => {
return get<User[]>('/users', { page, size: 10 });
};
// 使用useRequest管理请求状态
const { loading, data, error, run } = useRequest(fetchUserList, {
immediate: true, // 立即执行
onSuccess: (data) => {
console.log('用户列表请求成功:', data);
},
onError: (error) => {
console.error('用户列表请求失败:', error);
}
});
</script>
<style scoped>
.user-list {
width: 800px;
margin: 20px auto;
}
.loading {
color: #409eff;
text-align: center;
padding: 20px;
}
.error {
color: #f56c6c;
text-align: center;
padding: 20px;
}
.empty {
text-align: center;
padding: 20px;
color: #909399;
}
.table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
.table th, .table td {
border: 1px solid #e6e6e6;
padding: 8px;
text-align: center;
}
.table th {
background-color: #f5f7fa;
}
button {
margin-top: 20px;
padding: 8px 16px;
background-color: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background-color: #a0cfff;
cursor: not-allowed;
}
</style>
1.6 实战踩坑与解决方案
| 问题场景 | 原因分析 | 解决方案 |
|---|---|---|
| 重复请求导致数据错乱 | 同一接口短时间内多次调用 | 启用封装中的取消重复请求逻辑,通过请求 Key 标识重复请求 |
| Token 过期后请求失败 | 拦截器中未处理 Token 刷新逻辑 | 在 401 错误中添加 Token 刷新逻辑(先刷新 Token,再重新发起原请求) |
| GET 请求参数含特殊字符导致请求失败 | 参数未序列化 | 在请求拦截器中对 GET 参数进行序列化,或使用 qs 库处理 |
| 跨域请求报错 | 后端未配置 CORS,或请求头不允许 | 1. 后端配置 Access-Control-Allow-Origin;2. Vite 配置代理(见下文) |
补充:Vite 跨域代理配置(解决开发环境跨域)
修改vite.config.ts:
javascript
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src') // 配置@别名
}
},
// 跨域代理配置
server: {
proxy: {
'/api': {
target: 'https://dev-api.example.com', // 后端接口地址
changeOrigin: true, // 开启跨域
rewrite: (path) => path.replace(/^\/api/, '') // 重写路径(如果后端接口无/api前缀)
}
}
}
});
二、Vue3 样式解决方案全解析:从隔离到进阶
2.1 样式污染:前端开发的老大难问题
在 Vue 项目中,若直接写全局 CSS,会出现样式污染(组件 A 的样式影响组件 B),比如:
css
/* 全局样式 */
.button {
background-color: red;
}
所有组件中的.button类都会变成红色,这也是 Vue 提供多种样式隔离方案的核心原因。
2.2 Scoped CSS:基础样式隔离(新手首选)
2.2.1 核心原理
给组件内的所有 DOM 元素添加一个唯一的data-v-xxx属性,CSS 选择器会自动拼接该属性,实现样式隔离。
2.2.2 基础使用
html
<template>
<div class="card">
<h3 class="title">Scoped CSS示例</h3>
<button class="btn">点击</button>
</div>
</template>
<!-- 添加scoped属性,样式仅作用于当前组件 -->
<style scoped>
.card {
border: 1px solid #e6e6e6;
padding: 20px;
border-radius: 4px;
}
.title {
color: #333;
font-size: 16px;
}
.btn {
background-color: #409eff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
}
</style>
2.2.3 局限性与深度选择器
问题:Scoped CSS 无法作用于子组件 / 第三方 UI 组件(如 Element Plus)的内部 DOM。
解决方案 :使用深度选择器::v-deep(Vue3 推荐)//deep/(Vue2)/:deep()(新语法)。
示例:修改 Element Plus 按钮的样式:
html
<template>
<el-button class="my-btn">自定义Element Plus按钮</el-button>
</template>
<style scoped>
/* 深度选择器:作用于子组件内部DOM */
:deep(.my-btn) {
background-color: #67c23a;
border-color: #67c23a;
}
/* 等价写法:::v-deep .my-btn {} */
</style>
2.2.4 Scoped CSS 优缺点
| 优点 | 缺点 |
|---|---|
| 配置简单,无需额外学习 | 无法直接作用于子组件,需深度选择器 |
| 轻量级,无性能损耗 | 深度选择器滥用会破坏样式隔离 |
| 新手友好,上手成本低 | 动态生成的 DOM(如 v-html)无法被作用 |
2.3 CSS Modules:更严谨的样式隔离(企业级首选)
2.3.1 核心原理
将 CSS 类名编译为哈希字符串(如.card → .card_3zyde41),通过对象形式引入类名,彻底避免样式冲突:

2.3.2 基础使用(Vue3+Vite)
html
<template>
<!-- 通过$style.类名使用样式 -->
<div :class="$style.card">
<h3 :class="$style.title">CSS Modules示例</h3>
<button :class="$style.btn">点击</button>
<!-- 多类名组合 -->
<button :class="[$style.btn, $style.btnPrimary]">主要按钮</button>
</div>
</template>
<!-- 添加module属性,启用CSS Modules -->
<style module>
.card {
border: 1px solid #e6e6e6;
padding: 20px;
border-radius: 4px;
}
.title {
color: #333;
font-size: 16px;
}
.btn {
border: none;
padding: 8px 16px;
border-radius: 4px;
}
.btnPrimary {
background-color: #409eff;
color: white;
}
</style>
2.3.3 结合 TypeScript(类型提示)
步骤 1:创建src/typings/shims-vue.d.ts,添加类型声明:
javascript
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}
// CSS Modules类型声明
declare module '*.module.css' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.module.scss' {
const classes: { [key: string]: string };
export default classes;
}
步骤 2:自定义模块名称(避免 $style 冲突):
html
<template>
<div :class="styles.card">CSS Modules自定义名称</div>
</template>
<!-- 自定义模块名称为styles -->
<style module="styles">
.card {
color: #409eff;
}
</style>
2.3.4 CSS Modules 优缺点
| 优点 | 缺点 |
|---|---|
| 样式完全隔离,无冲突风险 | 写法稍繁琐,需通过 $style 调用 |
| 支持 TypeScript 类型提示 | 无法直接覆盖第三方组件样式(需结合:global) |
| 适合多人协作的大型项目 | 动态类名需特殊处理 |
2.3.5 全局样式穿透(:global)
css
<style module>
/* 局部样式 */
.card {
padding: 20px;
}
/* 全局样式(不哈希化) */
:global(.global-btn) {
background-color: #f56c6c;
}
</style>
2.4 预处理器配置:Sass/Less(提升样式开发效率)
2.4.1 为什么用预处理器?
预处理器(Sass/Less)提供了 CSS 不具备的特性:变量、嵌套、混合器、继承、函数,能大幅提升样式开发效率,比如:
- 变量:统一主题色、字体大小;
- 嵌套:避免重复写父选择器;
- 混合器:复用常用样式(如清除浮动、圆角)。
2.4.2 Sass 配置(Vite+Vue3)
步骤 1:安装依赖
bash
# 安装Sass(dart-sass,推荐)
pnpm add sass -D
步骤 2:基础使用(SCSS 语法)
html
<template>
<div class="card">
<h3 class="title">Sass示例</h3>
<div class="content">
<p>这是一段内容</p>
</div>
</div>
</template>
<!-- lang="scss"启用Sass -->
<style scoped lang="scss">
// 1. 定义变量
$primary-color: #409eff;
$font-size: 14px;
$border-radius: 4px;
// 2. 嵌套规则
.card {
border: 1px solid #e6e6e6;
padding: 20px;
border-radius: $border-radius;
// 嵌套子选择器
.title {
color: $primary-color;
font-size: $font-size + 2px;
margin-bottom: 10px;
}
.content {
p {
color: #666;
line-height: 1.5;
}
}
}
</style>
步骤 3:全局变量配置(无需手动引入)
修改vite.config.ts,配置全局 Sass 变量:
javascript
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
// Sass全局变量配置
css: {
preprocessorOptions: {
scss: {
// 全局引入变量文件(多个文件用;分隔)
additionalData: `@import "@/styles/variables.scss";`
}
}
}
});
创建src/styles/variables.scss:
css
// 全局变量
$primary-color: #409eff;
$success-color: #67c23a;
$warning-color: #e6a23c;
$danger-color: #f56c6c;
$text-color: #333;
$text-color-secondary: #666;
// 全局混合器
@mixin flex-center {
display: flex;
justify-content: center;
align-items: center;
}
// 全局函数
@function px2rem($px) {
@return $px / 16 + rem;
}
在组件中直接使用全局变量 / 混合器:
css
<style scoped lang="scss">
.card {
@include flex-center; // 使用全局混合器
color: $text-color; // 使用全局变量
font-size: px2rem(16); // 使用全局函数
}
</style>
2.4.3 Less 配置(Vite+Vue3)
步骤 1:安装依赖
bash
pnpm add less -D
步骤 2:基础使用
css
<style scoped lang="less">
// 变量
@primary-color: #409eff;
// 嵌套
.card {
color: @primary-color;
.title {
font-size: 16px;
}
}
</style>
步骤 3:全局变量配置(同 Sass)
修改vite.config.ts:
javascript
css: {
preprocessorOptions: {
less: {
additionalData: `@import "@/styles/variables.less";`,
javascriptEnabled: true // 启用Less JS功能
}
}
}
2.5 样式方案选型指南
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 小型项目 / 新手开发 | Scoped CSS + Sass | 配置简单,兼顾隔离和开发效率 |
| 中大型项目 / 多人协作 | CSS Modules + Sass | 样式完全隔离,支持 TypeScript,适合复杂项目 |
| 需兼容旧项目 | Less | 学习成本低,与 CSS 语法更接近 |
| 第三方 UI 组件样式修改 | Scoped CSS + 深度选择器 | 简单高效,无需额外配置 |
2.6 实战技巧:全局样式与主题切换
2.6.1 全局样式注入
创建src/styles/global.scss:
css
// 全局重置样式
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Microsoft YaHei", sans-serif;
color: #333;
background-color: #f5f7fa;
}
// 全局通用类
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
在main.ts中引入:
javascript
import { createApp } from 'vue';
import App from './App.vue';
// 引入全局样式
import '@/styles/global.scss';
createApp(App).mount('#app');
2.6.2 动态主题切换(结合 CSS 变量)
步骤 1:定义 CSS 变量(src/styles/theme.scss):
css
// 浅色主题
:root {
--theme-color: #409eff;
--bg-color: #ffffff;
--text-color: #333333;
}
// 深色主题
[data-theme="dark"] {
--theme-color: #667eea;
--bg-color: #1e1e1e;
--text-color: #ffffff;
}
步骤 2:在组件中使用 CSS 变量:
css
<style scoped lang="scss">
.card {
background-color: var(--bg-color);
color: var(--text-color);
border: 1px solid var(--theme-color);
}
</style>
步骤 3:封装主题切换 Hook(src/hooks/useTheme.ts):
javascript
import { ref, watchEffect } from 'vue';
// 获取当前主题(默认浅色)
const currentTheme = ref(localStorage.getItem('theme') || 'light');
// 切换主题
const toggleTheme = (theme: 'light' | 'dark') => {
currentTheme.value = theme;
localStorage.setItem('theme', theme);
document.documentElement.setAttribute('data-theme', theme);
};
// 初始化主题
watchEffect(() => {
document.documentElement.setAttribute('data-theme', currentTheme.value);
});
export { currentTheme, toggleTheme };
步骤 4:在组件中使用:
html
<template>
<button @click="toggleTheme(currentTheme === 'light' ? 'dark' : 'light')">
切换{{ currentTheme === 'light' ? '深色' : '浅色' }}主题
</button>
</template>
<script setup lang="ts">
import { currentTheme, toggleTheme } from '@/hooks/useTheme';
</script>
三、综合实战:Axios 封装 + 样式解决方案落地
3.1 需求:搭建带请求状态的商品列表组件
- 技术选型:Axios 封装(请求数据) + CSS Modules + Sass(样式) + 请求状态管理
- 核心需求:请求商品列表、加载状态 / 错误状态处理、样式隔离、主题适配
3.2 完整代码实现
html
<template>
<div :class="styles.container">
<h2 :class="styles.title">商品列表</h2>
<!-- 主题切换按钮 -->
<button :class="styles.themeBtn" @click="toggleTheme(currentTheme === 'light' ? 'dark' : 'light')">
切换{{ currentTheme === 'light' ? '深色' : '浅色' }}主题
</button>
<!-- 加载状态 -->
<div v-if="loading" :class="styles.loading">加载中...</div>
<!-- 错误状态 -->
<div v-else-if="error" :class="styles.error">{{ error.message }}</div>
<!-- 空数据 -->
<div v-else-if="!data || data.length === 0" :class="styles.empty">暂无商品数据</div>
<!-- 商品列表 -->
<div v-else :class="styles.goodsList">
<div v-for="goods in data" :key="goods.id" :class="styles.goodsItem">
<img :src="goods.image" :alt="goods.name" :class="styles.goodsImg" />
<div :class="styles.goodsInfo">
<h3 :class="styles.goodsName">{{ goods.name }}</h3>
<p :class="styles.goodsPrice">¥{{ goods.price }}</p>
<button :class="styles.buyBtn">立即购买</button>
</div>
</div>
</div>
<!-- 刷新按钮 -->
<button :class="styles.refreshBtn" @click="run" :disabled="loading">刷新列表</button>
</div>
</template>
<script setup lang="ts">
import { get } from '@/utils/axios';
import { useRequest } from '@/hooks/useRequest';
import { currentTheme, toggleTheme } from '@/hooks/useTheme';
// 定义商品类型
interface Goods {
id: number;
name: string;
price: number;
image: string;
}
// 请求商品列表
const fetchGoodsList = () => {
return get<Goods[]>('/goods');
};
// 管理请求状态
const { loading, data, error, run } = useRequest(fetchGoodsList, {
immediate: true,
onSuccess: (data) => console.log('商品列表请求成功:', data),
onError: (error) => console.error('商品列表请求失败:', error)
});
</script>
<!-- CSS Modules + Sass + 全局变量 -->
<style module="styles" lang="scss">
.goodsList {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-top: 20px;
}
.goodsItem {
background-color: var(--bg-color);
border: 1px solid var(--theme-color);
border-radius: $border-radius;
padding: 10px;
transition: all 0.3s;
&:hover {
transform: translateY(-5px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
}
.goodsImg {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: $border-radius;
}
.goodsInfo {
padding: 10px 0;
.goodsName {
font-size: 16px;
color: var(--text-color);
margin-bottom: 5px;
}
.goodsPrice {
color: $danger-color;
font-weight: bold;
margin-bottom: 10px;
}
}
.buyBtn {
background-color: var(--theme-color);
color: white;
border: none;
padding: 6px 12px;
border-radius: $border-radius;
cursor: pointer;
}
.container {
width: 1200px;
margin: 20px auto;
color: var(--text-color);
}
.title {
color: var(--theme-color);
font-size: 20px;
margin-bottom: 10px;
}
.themeBtn {
margin-bottom: 10px;
padding: 6px 12px;
background-color: var(--theme-color);
color: white;
border: none;
border-radius: $border-radius;
cursor: pointer;
}
.loading {
color: var(--theme-color);
text-align: center;
padding: 20px;
}
.error {
color: $danger-color;
text-align: center;
padding: 20px;
}
.empty {
text-align: center;
padding: 20px;
color: $text-color-secondary;
}
.refreshBtn {
margin-top: 20px;
padding: 8px 16px;
background-color: var(--theme-color);
color: white;
border: none;
border-radius: $border-radius;
cursor: pointer;
&:disabled {
background-color: #a0cfff;
cursor: not-allowed;
}
}
</style>
四、总结与进阶建议
4.1 核心总结
- API 请求:Axios 封装是企业级项目的标配,核心是拦截器 + 请求状态管理,取消重复请求和统一错误处理能大幅提升用户体验;
- 样式解决方案:Scoped CSS 适合新手 / 小型项目,CSS Modules 适合大型 / 多人协作项目,预处理器(Sass)能提升样式开发效率;
- 性能优化:Axios 按需引入、样式按需加载、预处理器变量全局复用,能减少代码冗余和打包体积。
4.2 进阶建议
- Axios 进阶:结合 Pinia 管理请求缓存,避免重复请求;封装请求重试机制,提升接口稳定性;
- 样式进阶:使用 CSS-in-JS(如 Vue3+Styled Components)实现更灵活的样式逻辑;结合 UnoCSS 实现原子化 CSS,减少样式代码量;
- 工程化:配置 StyleLint 规范样式代码,结合 husky 实现提交前样式校验;
- 性能优化:使用 PurgeCSS 清除未使用的样式,减少打包体积。
最后
本文从实战角度讲解了 Vue3 生态中 API 请求和样式解决方案的核心用法,覆盖封装、配置、优化、选型全流程。如果对你有帮助,欢迎点赞 + 收藏 + 关注,后续会持续更新 Vue3 生态实战内容(如路由管理、状态管理、打包优化)。
如果有任何问题或不同见解,欢迎在评论区交流哦~
