Vue3 生态工具实战进阶:API 请求封装 + 样式解决方案全攻略(Axios/Sass/CSS Modules)

前言

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 封装思路(企业级标准)
  1. 创建 Axios 实例,统一基础配置(基准 URL、超时时间、请求头);
  2. 配置请求拦截器:添加 token、统一请求参数格式、开启 loading;
  3. 配置响应拦截器:统一数据解构、全局错误处理、token 过期刷新;
  4. 封装取消重复请求逻辑:避免同一接口多次提交;
  5. 封装通用请求方法(get/post/put/delete),简化组件调用;
  6. 暴露请求实例和取消请求方法,满足特殊场景需求。
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 核心总结

  1. API 请求:Axios 封装是企业级项目的标配,核心是拦截器 + 请求状态管理,取消重复请求和统一错误处理能大幅提升用户体验;
  2. 样式解决方案:Scoped CSS 适合新手 / 小型项目,CSS Modules 适合大型 / 多人协作项目,预处理器(Sass)能提升样式开发效率;
  3. 性能优化:Axios 按需引入、样式按需加载、预处理器变量全局复用,能减少代码冗余和打包体积。

4.2 进阶建议

  1. Axios 进阶:结合 Pinia 管理请求缓存,避免重复请求;封装请求重试机制,提升接口稳定性;
  2. 样式进阶:使用 CSS-in-JS(如 Vue3+Styled Components)实现更灵活的样式逻辑;结合 UnoCSS 实现原子化 CSS,减少样式代码量;
  3. 工程化:配置 StyleLint 规范样式代码,结合 husky 实现提交前样式校验;
  4. 性能优化:使用 PurgeCSS 清除未使用的样式,减少打包体积。

最后

本文从实战角度讲解了 Vue3 生态中 API 请求和样式解决方案的核心用法,覆盖封装、配置、优化、选型全流程。如果对你有帮助,欢迎点赞 + 收藏 + 关注,后续会持续更新 Vue3 生态实战内容(如路由管理、状态管理、打包优化)。

如果有任何问题或不同见解,欢迎在评论区交流哦~

相关推荐
有梦想的咸鱼还是咸鱼吗1 小时前
前端必会|防抖与节流从原理到实战,解决90%高频事件卡顿问题
前端
阿诺木1 小时前
Node.js 局域网设备发现:mDNS、UDP 广播和子网扫描
前端
盐焗乳鸽还要砂锅1 小时前
亲手造一只有灵魂的 AI 小龙虾是种什么体验?
前端·llm·agent
YimWu1 小时前
Opencode 核心设计-Session会话机制
前端·agent·ai编程
Mintopia1 小时前
诗词如何影响人:从认知机制到可落地的文本分析技术路线
前端·代码规范
WaywardOne2 小时前
iOS必看!Deepseek给的Runtime实现原理,通俗易懂~
前端·面试
小码哥_常2 小时前
惊!Kotlin集合,你可能只用了40%?
前端
Wect2 小时前
LeetCode 52. N 皇后 II:回溯算法高效求解
前端·算法·typescript
毛骗导演2 小时前
万字解析 OpenClaw 源码架构-跨平台应用之 iOS 应用
前端·ios·架构