欢迎光临
我们一直在努力

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

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

前言

        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,速度更快、体积更小):

# npm
npm install axios –save

# yarn
yarn add axios

# pnpm(推荐)
pnpm add axios

1.2.2 Axios 原生使用(新手入门)

在组件中直接使用 Axios 发送请求(仅作入门演示,生产环境不推荐):

<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)

    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中继续添加:

    // 请求拦截器
    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中继续添加:

    // 响应拦截器
    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末尾添加:

    // 封装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(生产环境):

    # .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):

    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 封装后实战使用(组件中调用)

    <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:

    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),比如:

    /* 全局样式 */
    .button {
    background-color: red;
    }

    所有组件中的.button类都会变成红色,这也是 Vue 提供多种样式隔离方案的核心原因。

    2.2 Scoped CSS:基础样式隔离(新手首选)

    2.2.1 核心原理

    给组件内的所有 DOM 元素添加一个唯一的data-v-xxx属性,CSS 选择器会自动拼接该属性,实现样式隔离。

    2.2.2 基础使用

    <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 按钮的样式:

    <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)

    <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,添加类型声明:

    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 冲突):

    <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)

    <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:安装依赖

    # 安装Sass(dart-sass,推荐)
    pnpm add sass -D

    步骤 2:基础使用(SCSS 语法)

    <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 变量:

    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:

    // 全局变量
    $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;
    }

    在组件中直接使用全局变量 / 混合器:

    <style scoped lang="scss">
    .card {
    @include flex-center; // 使用全局混合器
    color: $text-color; // 使用全局变量
    font-size: px2rem(16); // 使用全局函数
    }
    </style>

    2.4.3 Less 配置(Vite+Vue3)
    步骤 1:安装依赖

    pnpm add less -D

    步骤 2:基础使用

    <style scoped lang="less">
    // 变量
    @primary-color: #409eff;

    // 嵌套
    .card {
    color: @primary-color;
    .title {
    font-size: 16px;
    }
    }
    </style>

    步骤 3:全局变量配置(同 Sass)

    修改vite.config.ts:

    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:

    // 全局重置样式
    * {
    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中引入:

    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):

    // 浅色主题
    :root {
    –theme-color: #409eff;
    –bg-color: #ffffff;
    –text-color: #333333;
    }

    // 深色主题
    [data-theme="dark"] {
    –theme-color: #667eea;
    –bg-color: #1e1e1e;
    –text-color: #ffffff;
    }

    步骤 2:在组件中使用 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):

    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:在组件中使用:

    <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 完整代码实现

    <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 生态实战内容(如路由管理、状态管理、打包优化)。

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

    赞(0)
    未经允许不得转载:171主机测评 » Vue3 生态工具实战进阶:API 请求封装 + 样式解决方案全攻略(Axios/Sass/CSS Modules)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址