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 小时前
Uniapp H5Builderx 预览Html 显示404问题解决
前端·uni-app
We་ct1 小时前
LeetCode 190. 颠倒二进制位:两种解法详解
前端·算法·leetcode·typescript
踩着两条虫1 小时前
AI驱动的Vue3应用开发平台深入探究(二十五):API与参考之Renderer API 参考
前端·javascript·vue.js·人工智能·低代码·前端框架·ai编程
信创DevOps先锋1 小时前
本土化突围:Gitee如何重新定义企业级项目管理工具价值
前端·gitee·jquery
圣光SG1 小时前
Java类与对象及面向对象基础核心详细笔记
java·前端·数据库
Jinuss2 小时前
源码分析之React中的useImperativeHandle
开发语言·前端·javascript
ZC跨境爬虫2 小时前
CSS核心知识点与定位实战全解析(结合Playwright爬虫案例)
前端·css·爬虫
Jinuss2 小时前
源码分析之React中的forwardRef解读
前端·javascript·react.js
mengsi552 小时前
Antigravity IDE 在浏览器上 verify 成功但本地 IDE 没反应 “开启Tun依然无济于事” —— 解决方案
前端·ide·chrome·antigravity