Bombax's Knowledge Document Notes Bombax's Knowledge Document Notes
首页
  • 前置

    • 尚硅谷Java学习
    • 基础软件安装与配置
  • 核心

    • Java从入门到精通(JDK17版)
    • MySQL从入门到高级-基础篇
    • MySQL从入门到高级-高级篇
    • JDBC 核心技术(JDK21版)
    • JavaWeb 技术
  • 学习笔记

    • POJO 概念
  • Spring Cloud

    • SpringCloud
    • SpringCloud-Alibaba
  • 持久层框架

    • MyBatis
    • MyBatis-Plus
  • 相关知识

    • Mybatis 代码生成工具比较
  • 安全框架

    • 安全框架之 Spring Security
    • 安全框架之 Shiro
  • 定时任务框架

    • 定时任务框架之 Quartz
    • 定时任务框架之 XXL-JOB
  • Java 日志热门框架
  • Git 常用命令
  • Swagger API 文档生成工具
  • Motan RPC (opens new window)
  • Lombok Tutorial (opens new window)
  • Lombok Features (opens new window)
  • FastJSON2 (opens new window)
  • Spring Framework 5 中文文档 (opens new window)
  • XStream (opens new window)
  • fluent-validator 业务逻辑验证框架 (opens new window)
  • ehcache java 缓存框架 (opens new window)
  • jetcache java 缓存框架 (opens new window)
  • caffeine 缓存框架 (opens new window)
  • Spring Cache (opens new window)
  • 主流缓存框架调研 (opens new window)
  • redisson 官方中文文档 (opens new window)
  • LiquiBase 中文学习指南 (opens new window)
  • LiquiBase 官方文档 (opens new window)
  • 分类
  • 归档
GitHub (opens new window)

bombax

小小程序猿
首页
  • 前置

    • 尚硅谷Java学习
    • 基础软件安装与配置
  • 核心

    • Java从入门到精通(JDK17版)
    • MySQL从入门到高级-基础篇
    • MySQL从入门到高级-高级篇
    • JDBC 核心技术(JDK21版)
    • JavaWeb 技术
  • 学习笔记

    • POJO 概念
  • Spring Cloud

    • SpringCloud
    • SpringCloud-Alibaba
  • 持久层框架

    • MyBatis
    • MyBatis-Plus
  • 相关知识

    • Mybatis 代码生成工具比较
  • 安全框架

    • 安全框架之 Spring Security
    • 安全框架之 Shiro
  • 定时任务框架

    • 定时任务框架之 Quartz
    • 定时任务框架之 XXL-JOB
  • Java 日志热门框架
  • Git 常用命令
  • Swagger API 文档生成工具
  • Motan RPC (opens new window)
  • Lombok Tutorial (opens new window)
  • Lombok Features (opens new window)
  • FastJSON2 (opens new window)
  • Spring Framework 5 中文文档 (opens new window)
  • XStream (opens new window)
  • fluent-validator 业务逻辑验证框架 (opens new window)
  • ehcache java 缓存框架 (opens new window)
  • jetcache java 缓存框架 (opens new window)
  • caffeine 缓存框架 (opens new window)
  • Spring Cache (opens new window)
  • 主流缓存框架调研 (opens new window)
  • redisson 官方中文文档 (opens new window)
  • LiquiBase 中文学习指南 (opens new window)
  • LiquiBase 官方文档 (opens new window)
  • 分类
  • 归档
GitHub (opens new window)
  • 前置

  • 核心

    • Java从入门到精通(JDK17版)

    • MySQL从入门到高级-基础篇

    • MySQL从入门到高级-高级篇

    • JDBC 核心技术(JDK21版)
    • JavaWeb技术

      • 第一章 概述
      • 第二章 HTML&CSS
      • 第三章 JavaScript
      • 第四章 XML、Tomcat、HTTP
      • 第五章 Servlet
      • 第六章 会话、过滤器、监听器
      • 第七章 前端工程化-上
      • 第八章 前端工程化-中
      • 第九章 前端工程化-下
        • 八、Vue3 数据交互 axios
          • 8.0 预讲知识-promise
          • 8.0.1 同步与异步函数
          • 8.0.2 Promise 简介
          • 8.0.3 Promise 基本用法
          • 8.0.4 Promise catch() 错误处理
          • 8.0.5 async/await 使用
          • 8.1 Axios 介绍
          • 8.1.1 AJAX 技术回顾
          • 8.1.2 Axios 简介
          • 8.2 Axios 入门案例
          • 8.2.1 案例需求
          • 8.2.2 项目准备
          • 8.2.3 基础实现(Promise 方式)
          • 8.2.4 响应数据结构
          • 8.2.5 使用 async/await 优化代码
          • 8.2.6 Axios 请求配置参数
          • 8.3 Axios GET 和 POST 方法
          • 8.3.1 方法签名
          • 8.3.2 GET 请求详解
          • 8.3.3 POST 请求详解
          • 8.3.4 GET 与 POST 对比
          • 8.4 Axios 拦截器
          • 8.4.1 拦截器概念
          • 8.4.2 拦截器基本用法
          • 8.4.3 创建 Axios 实例
          • 8.4.4 使用自定义实例
          • 8.4.5 拦截器应用场景
        • 九、Vue3 状态管理 Pinia
          • 9.1 Pinia 介绍
          • 9.1.1 什么是 Pinia
          • 9.1.2 为什么需要状态管理
          • 9.1.3 Pinia vs Vuex
          • 9.1.4 Pinia 核心概念
          • 9.1.5 Pinia 适用场景
          • 9.2 Pinia 基本用法
          • 9.2.1 项目环境准备
          • 9.2.2 定义 Pinia Store
          • 9.2.3 在 Vue 应用中注册 Pinia
          • 9.2.4 在组件中操作 Pinia 数据
          • 9.2.5 在组件中展示 Pinia 数据
          • 9.2.6 配置路由
          • 9.2.7 根组件中切换路由
          • 9.2.8 启动项目并测试
          • 9.2.9 完整工作流程总结
          • 9.3 Pinia 进阶特性
          • 9.3.1 State 的多种定义方式
          • 9.3.2 Getters 进阶用法
          • 9.3.3 Actions 进阶用法
          • 9.3.4 $subscribe 状态监听
          • 9.3.5 数据持久化方案
          • 9.3.6 Pinia 最佳实践总结
        • 十、Element-plus 组件库
          • 10.1 Element-plus 介绍
          • 10.1.1 什么是 Element Plus
          • 10.1.2 Element Plus vs Element UI
          • 10.1.3 浏览器兼容性
          • 10.1.4 适用场景
          • 10.1.5 与其他 UI 框架对比
          • 10.1.6 版本与生态
          • 10.2 Element-plus 入门案例
          • 10.2.1 项目初始化
          • 10.2.2 引入方式对比
          • 方式一:完整引入(全量导入)
          • 方式二:手动按需引入
          • 方式三:自动按需引入(推荐)⭐
          • 10.2.3 完整入门案例(使用完整引入)
          • 10.3 Element-plus 常用组件
        • 十一、前端技术栈扩展
          • 11.1 React 框架
          • 11.1.1 React 简介
          • 11.1.2 React 快速入门
          • 11.2 Webpack 构建工具
          • 11.2.1 Webpack 简介
          • 11.2.2 Webpack 配置示例
          • 11.2.3 Webpack vs Vite 对比
          • 11.3 其他前端技术栈
          • 11.3.1 TypeScript - 类型安全的 JavaScript
          • 11.3.2 Sass/Less - CSS 预处理器
          • 11.3.3 Tailwind CSS - 实用优先的 CSS 框架
          • 11.3.4 技术选型建议
          • 11.4 前端工程化最佳实践
          • 11.4.1 项目结构规范
          • 11.4.2 代码规范与工具
          • 11.4.3 性能优化策略
          • 11.4.4 前端监控与调试
        • 十二、案例开发-日程管理
          • 12.1 本章概述
          • 12.2 用户认证功能重构
          • 12.2.1 创建 src/utils/request.js 文件
          • 12.2.2 前端 Pinia 状态管理配置
          • 12.2.3 在 Vue 应用中注册 Pinia
          • 12.2.4 定义用户信息 Store
          • 12.2.5 定义日程数据 Store
          • 12.2.6 Regist 组件实现(异步请求 + 注册逻辑)
          • 12.2.7 Login 组件实现(表单校验 + 登录逻辑)
          • 12.2.8 添加跨域处理器
          • 12.2.8.1 什么是跨域
          • 12.2.8.2 为什么会产生跨域
          • 12.2.8.3 如何解决跨域
          • 12.2.9 重构 UserController
          • 12.2.10 删除登录校验过滤器
          • 12.2.11 Header 组件实现(根据登录状态动态显示)
          • 12.2.12 路由守卫配置(权限控制)
          • 12.3 日程管理增删改查功能
          • 12.3.1 功能概述
          • 12.3.2 前端组件实现
          • 12.3.3 后端 Controller 层实现
          • 12.3.4 后端 Service 层实现
          • 12.3.5 后端 DAO 层实现
          • 12.3.6 删除功能的安全性考虑
          • 12.4 日程管理项目总结
          • 12.4.1 完整功能清单
          • 12.4.2 关键技术点回顾
          • 12.4.3 项目优化建议
        • 13. 微头条项目开发
  • 学习笔记

  • Java基础
  • 核心
  • JavaWeb技术
bombax
2025-12-11
目录

第九章 前端工程化-下

# 第九章 前端工程化-下

# 八、Vue3 数据交互 axios

# 8.0 预讲知识-promise

# 8.0.1 同步与异步函数

同步函数(Synchronous):函数执行时会阻塞后续代码,必须等待当前函数执行完毕才能继续。

<script>
    // 同步函数示例
    let fun1 = () => {
        console.log("fun1 invoked")
    }
    
    fun1()  // 1. 执行函数
    console.log("other code processing")  // 2. 等待 fun1 执行完毕后执行
    
    // 输出顺序:
    // fun1 invoked
    // other code processing
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13

异步函数(Asynchronous):函数不会阻塞后续代码执行,会在未来某个时间点完成。常见场景包括定时器、网络请求、文件读取等。

<script>
    // 异步函数示例:定时器
    setTimeout(function() {
        console.log("setTimeout invoked")  // 3. 2秒后执行
    }, 2000)
    
    console.log("other code processing")  // 1. 立即执行,不等待 setTimeout
    
    // 输出顺序:
    // other code processing
    // setTimeout invoked (2秒后)
</script>
1
2
3
4
5
6
7
8
9
10
11
12

为什么需要异步编程?

  • 避免阻塞:网络请求可能需要几秒,如果同步等待会导致页面卡死
  • 提升性能:多个任务可以并发执行,不必串行等待
  • 改善用户体验:页面保持响应,用户可以继续操作

# 8.0.2 Promise 简介

前端中的异步编程技术,类似 Java 中的多线程+线程结果回调!

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

  • 本质:Promise 是一个容器对象,保存着某个未来才会完成的异步操作结果
  • 优势:提供统一的 API,使异步代码更易读、易维护
  • 标准化:ES6 将 Promise 纳入标准,所有现代浏览器原生支持

Promise 的三种状态

  1. Pending(进行中):初始状态,异步操作尚未完成
  2. Fulfilled(已成功):异步操作成功完成,有返回值
  3. Rejected(已失败):异步操作失败,有错误信息

状态转换规则

Pending  ──成功──>  Fulfilled (resolve)
   │
   └─失败──>  Rejected (reject)
1
2
3

重要特性

  • 状态不可逆:一旦从 Pending 变为 Fulfilled 或 Rejected,状态就固定了,无法再改变
  • 结果唯一:Promise 的结果(成功值或失败原因)一旦确定就不会改变
  • 异步执行:Promise 的 then/catch 回调会在当前同步代码执行完后才执行

解决的问题:回调地狱

// 传统回调方式(回调地狱)
getUserInfo(userId, function(user) {
    getOrders(user.id, function(orders) {
        getOrderDetail(orders[0].id, function(detail) {
            // 嵌套层级过深,难以维护
        })
    })
})

// Promise 链式调用(扁平化)
getUserInfo(userId)
    .then(user => getOrders(user.id))
    .then(orders => getOrderDetail(orders[0].id))
    .then(detail => console.log(detail))
    .catch(error => console.error(error))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 8.0.3 Promise 基本用法

创建 Promise 实例

Promise 是一个构造函数,接收一个执行器函数 executor,该函数会立即执行。

<script>
    // 创建 Promise 实例
    let promise = new Promise(function(resolve, reject) {
        // 执行器函数立即执行(同步)
        console.log("promise executor running...")
        
        // 模拟异步操作
        let success = Math.random() > 0.5
        
        if (success) {
            // 操作成功,调用 resolve 并传递结果
            resolve("操作成功的数据")
        } else {
            // 操作失败,调用 reject 并传递错误信息
            reject("操作失败的原因")
        }
    })
    
    console.log('同步代码继续执行')  // Promise 不阻塞后续代码
    
    // 通过 then 方法处理 Promise 的结果
    promise.then(
        function(value) {
            // 处理成功情况(Promise 状态变为 Fulfilled)
            console.log(`成功回调:${value}`)
        },
        function(error) {
            // 处理失败情况(Promise 状态变为 Rejected)
            console.log(`失败回调:${error}`)
        }
    )
    
    console.log('then 方法注册完毕')
    
    /* 输出顺序分析:
     * 1. "promise executor running..."  (立即执行)
     * 2. "同步代码继续执行"              (同步代码)
     * 3. "then 方法注册完毕"            (同步代码)
     * 4. "成功回调" 或 "失败回调"       (微任务,在同步代码后执行)
     */
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

关键知识点

  1. 执行器函数立即执行:new Promise() 中的函数会立即同步执行
  2. resolve 和 reject:这两个参数是函数,用于改变 Promise 状态
    • resolve(value):将状态从 Pending 改为 Fulfilled
    • reject(reason):将状态从 Pending 改为 Rejected
  3. then 方法异步执行:then 中的回调函数会在微任务队列中执行,不会阻塞主线程
  4. 链式调用:then 方法返回新的 Promise,支持链式调用

实际应用示例:模拟网络请求

// 模拟异步请求用户数据
function fetchUserData(userId) {
    return new Promise((resolve, reject) => {
        console.log(`开始请求用户 ${userId} 的数据...`)
        
        // 模拟网络延迟
        setTimeout(() => {
            if (userId > 0) {
                resolve({ id: userId, name: '张三', age: 25 })
            } else {
                reject('无效的用户ID')
            }
        }, 1000)
    })
}

// 使用 Promise
fetchUserData(1)
    .then(
        user => console.log('获取用户成功:', user),
        error => console.error('获取用户失败:', error)
    )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 8.0.4 Promise catch() 错误处理

catch 方法说明

catch() 是 .then(null, rejection) 的语法糖,专门用于捕获 Promise 链中的错误。

<script>
    let promise = new Promise(function(resolve, reject) {
        console.log("执行异步操作...")
        // 抛出异常(会被 catch 捕获)
        throw new Error("操作过程中发生错误")
    })
    
    console.log('注册错误处理器')
    
    // 方式1:使用 then 的第二个参数处理错误
    promise.then(
        value => console.log('成功:', value),
        error => console.error('失败:', error.message)
    )
    
    // 方式2:使用 catch 处理错误(推荐)
    promise
        .then(value => console.log('成功:', value))
        .catch(error => console.error('失败:', error.message))
    
    console.log('错误处理器注册完毕')
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

catch 的三种捕获场景

// 场景1:捕获 reject
new Promise((resolve, reject) => {
    reject('操作失败')
})
.catch(error => console.error('捕获 reject:', error))

// 场景2:捕获执行器中的异常
new Promise((resolve, reject) => {
    throw new Error('执行器异常')
})
.catch(error => console.error('捕获异常:', error.message))

// 场景3:捕获 then 回调中的异常
Promise.resolve('成功')
    .then(value => {
        throw new Error('then 回调中的错误')
    })
    .catch(error => console.error('捕获 then 错误:', error.message))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

错误处理最佳实践

// ✅ 推荐:使用 catch 统一处理错误
fetchUserData()
    .then(user => processUser(user))
    .then(result => saveResult(result))
    .catch(error => {
        // 统一错误处理
        console.error('操作失败:', error)
        showErrorMessage(error.message)
    })

// ❌ 不推荐:每个 then 都处理错误(代码冗余)
fetchUserData()
    .then(
        user => processUser(user),
        error => console.error(error)  // 重复代码
    )
    .then(
        result => saveResult(result),
        error => console.error(error)  // 重复代码
    )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

catch 后可以继续链式调用

Promise.reject('第一次失败')
    .catch(error => {
        console.error('捕获错误:', error)
        return '错误已处理'  // 返回新值
    })
    .then(value => {
        console.log('继续执行:', value)  // 输出:继续执行:错误已处理
    })
1
2
3
4
5
6
7
8

注意事项

  • Promise 链末尾应始终添加 catch,避免未捕获的错误
  • catch 返回的也是 Promise,可以继续链式调用
  • 如果 catch 中再次抛出异常,需要后续的 catch 捕获

# 8.0.5 async/await 使用

async/await 简介

async/await 是 ES2017 (ES8) 引入的语法糖,基于 Promise 实现,让异步代码看起来像同步代码,提高可读性。

async 函数特性

<script>
    // async 函数总是返回 Promise
    
    // 情况1:返回普通值 → 自动包装为 resolved Promise
    async function fun1() {
        return 10
    }
    fun1().then(value => console.log(value))  // 输出:10
    
    // 情况2:返回 Promise → 直接返回该 Promise
    async function fun2() {
        return Promise.resolve('成功')
    }
    fun2().then(value => console.log(value))  // 输出:成功
    
    // 情况3:抛出异常 → 返回 rejected Promise
    async function fun3() {
        throw new Error('出错了')
    }
    fun3().catch(error => console.error(error.message))  // 输出:出错了
    
    // 情况4:返回 rejected Promise
    async function fun4() {
        return Promise.reject('失败')
    }
    fun4().catch(error => console.error(error))  // 输出:失败
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

await 关键字特性

<script>
    // await 只能在 async 函数中使用
    
    async function demo() {
        // 1. await 后跟 Promise:等待 Promise 完成,返回其结果
        let result1 = await Promise.resolve('Promise 结果')
        console.log(result1)  // 输出:Promise 结果
        
        // 2. await 后跟普通值:直接返回该值
        let result2 = await 100
        console.log(result2)  // 输出:100
        
        // 3. await 会暂停 async 函数执行,等待 Promise 完成
        console.log('开始等待')
        let result3 = await new Promise(resolve => {
            setTimeout(() => resolve('延迟结果'), 1000)
        })
        console.log('等待结束:', result3)  // 1秒后输出
    }
    
    demo()
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

错误处理最佳实践

// 方式1:try-catch 捕获错误(推荐)
async function fetchData() {
    try {
        let user = await fetchUser(1)
        let orders = await fetchOrders(user.id)
        let detail = await fetchOrderDetail(orders[0].id)
        return detail
    } catch (error) {
        // 统一错误处理
        console.error('数据获取失败:', error)
        throw error  // 可以选择重新抛出
    }
}

// 方式2:在调用处使用 catch
fetchData()
    .then(data => console.log('成功:', data))
    .catch(error => console.error('失败:', error))

// 方式3:使用 Promise.allSettled 处理多个请求
async function fetchMultiple() {
    let results = await Promise.allSettled([
        fetchUser(1),
        fetchUser(2),
        fetchUser(3)
    ])
    
    results.forEach((result, index) => {
        if (result.status === 'fulfilled') {
            console.log(`用户${index + 1}:`, result.value)
        } else {
            console.error(`用户${index + 1} 失败:`, result.reason)
        }
    })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

async/await 与 Promise 对比

// Promise 链式调用
function getUserOrders() {
    return fetchUser(1)
        .then(user => fetchOrders(user.id))
        .then(orders => fetchOrderDetail(orders[0].id))
        .catch(error => console.error(error))
}

// async/await 方式(更易读)
async function getUserOrders() {
    try {
        let user = await fetchUser(1)
        let orders = await fetchOrders(user.id)
        let detail = await fetchOrderDetail(orders[0].id)
        return detail
    } catch (error) {
        console.error(error)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

并发执行优化

// ❌ 串行执行(慢)
async function sequential() {
    let user1 = await fetchUser(1)  // 等待1秒
    let user2 = await fetchUser(2)  // 再等待1秒
    let user3 = await fetchUser(3)  // 再等待1秒
    // 总耗时:3秒
}

// ✅ 并行执行(快)
async function parallel() {
    let [user1, user2, user3] = await Promise.all([
        fetchUser(1),
        fetchUser(2),
        fetchUser(3)
    ])
    // 总耗时:1秒
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

注意事项

  1. await 会阻塞当前 async 函数:后续代码必须等待 await 完成
  2. 不影响外部代码:async 函数外的代码不会被 await 阻塞
  3. 错误处理必不可少:未捕获的错误会导致 Promise 变为 rejected
  4. 合理使用并发:独立的异步操作应使用 Promise.all 并行执行

# 8.1 Axios 介绍

# 8.1.1 AJAX 技术回顾

什么是 AJAX?

AJAX(Asynchronous JavaScript and XML)是一种在无需重新加载整个页面的情况下,与服务器交换数据并更新部分网页内容的技术。

AJAX 核心特点

  • 异步通信:不阻塞页面,提升用户体验
  • 局部刷新:只更新需要改变的部分,减少数据传输
  • 无需插件:基于浏览器原生 API,无需额外安装
  • 多种实现方式:XMLHttpRequest、Fetch API、第三方库等

AJAX 工作原理

  1. 用户触发事件(如点击按钮)
  2. JavaScript 创建 XMLHttpRequest 对象
  3. 向服务器发送 HTTP 请求
  4. 服务器处理请求并返回数据
  5. JavaScript 接收响应数据
  6. 更新页面 DOM,无需刷新整个页面

原生 XMLHttpRequest 示例(了解)

// 原生 AJAX 代码(较为繁琐)
function loadData() {
    // 1. 创建 XMLHttpRequest 对象(兼容性处理)
    let xhr = window.XMLHttpRequest 
        ? new XMLHttpRequest()  //  IE7+, Firefox, Chrome, Opera, Safari 浏览器执行代码
        : new ActiveXObject("Microsoft.XMLHTTP") // IE6, IE5 浏览器执行代码
    
    // 2. 设置回调函数
    xhr.onreadystatechange = function() {
        // readyState: 0未初始化 1已连接 2已发送 3接收中 4完成
        if (xhr.readyState === 4 && xhr.status === 200) {
            console.log('响应数据:', xhr.responseText)
            document.getElementById("result").innerHTML = xhr.responseText
        }
    }
    
    // 3. 配置请求
    xhr.open("GET", "/api/data", true)
    
    // 4. 发送请求
    xhr.send()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

原生 AJAX 的问题

  • 代码冗长:需要处理浏览器兼容性
  • 回调地狱:多个请求嵌套难以维护
  • 错误处理复杂:需要手动判断状态码
  • 缺少拦截器:无法统一处理请求和响应
  • 无法取消请求:不支持请求中断

# 8.1.2 Axios 简介

什么是 Axios?

Axios 是一个基于 Promise 的现代化 HTTP 客户端库,可在浏览器和 Node.js 中使用。

  • 官网:https://axios-http.com/zh/docs/intro (opens new window)
  • 特点:同构(Isomorphic),同一套代码可在浏览器和服务端运行
  • 底层实现:
    • 浏览器端:基于 XMLHttpRequest
    • Node.js 端:基于原生 http 模块

Axios 核心特性

特性 说明 应用场景
Promise API 基于 Promise,支持 async/await 简化异步代码编写
请求/响应拦截 统一处理请求和响应 添加 Token、日志记录、错误处理
自动转换 JSON 自动序列化和反序列化 JSON 无需手动转换数据格式
请求取消 支持取消未完成的请求 避免重复请求、路由切换时取消请求
超时控制 设置请求超时时间 避免长时间等待
CSRF 防护 客户端支持 XSRF 防御 提升安全性
进度监控 上传/下载进度监控 文件上传下载进度条
并发请求 支持同时发送多个请求 提升加载效率

Axios 与原生 AJAX 对比

// 原生 AJAX(繁琐)
let xhr = new XMLHttpRequest()
xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
        let data = JSON.parse(xhr.responseText)
        console.log(data)
    }
}
xhr.open('GET', '/api/user/1')
xhr.send()

// Axios(简洁)
axios.get('/api/user/1')
    .then(response => console.log(response.data))
    .catch(error => console.error(error))

// Axios + async/await(更简洁)
async function getUser() {
    try {
        let response = await axios.get('/api/user/1')
        console.log(response.data)
    } catch (error) {
        console.error(error)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

为什么选择 Axios?

  1. 简洁易用:API 设计简洁,学习成本低
  2. 功能强大:拦截器、取消请求、超时等高级功能
  3. Promise 原生支持:完美配合 async/await
  4. 广泛应用:Vue、React 项目的首选 HTTP 库
  5. 活跃维护:社区活跃,文档完善,问题响应快

# 8.2 Axios 入门案例

# 8.2.1 案例需求

通过 Axios 请求后台 API,获取并展示随机土味情话。

接口信息

  • 请求地址:https://api.uomg.com/api/rand.qinghua?format=json
  • 请求方式:GET 或 POST
  • 响应格式:
{
  "code": 1,
  "content": "我努力不是为了你而是因为你。"
}
1
2
3
4

# 8.2.2 项目准备

1. 创建 Vite 项目

# 创建项目
npm create vite@latest axios-demo -- --template vue

# 进入项目目录
cd axios-demo

# 安装依赖
npm install
1
2
3
4
5
6
7
8

2. 安装 Axios

npm install axios
1

3. 项目结构

axios-demo/
├── src/
│   ├── App.vue          # 主组件
│   └── main.js          # 入口文件
├── package.json
└── vite.config.js
1
2
3
4
5
6

# 8.2.3 基础实现(Promise 方式)

App.vue 代码

<script setup>
import axios from 'axios'
import { onMounted, reactive } from 'vue'

// 响应式数据:存储情话内容
let jsonData = reactive({
  code: 1,
  content: ''
})

// 获取情话的函数
let getLoveMessage = () => {
  // 使用 axios 发送请求
  axios({
    method: 'post',                  // 请求方式:GET、POST、PUT、DELETE 等
    url: 'https://api.uomg.com/api/rand.qinghua?format=json',  // 请求地址
    data: {                          // POST 请求体数据(自动转为 JSON)
      username: '123456'             // 示例参数
    }
  })
  .then(function(response) {         // 请求成功的回调
    console.log('响应数据:', response)
    // 将服务器返回的数据合并到 jsonData
    Object.assign(jsonData, response.data)
  })
  .catch(function(error) {           // 请求失败的回调
    console.error('请求失败:', error)
    jsonData.content = '获取失败,请稍后重试'
  })
}

// 组件挂载时自动加载一次
onMounted(() => {
  getLoveMessage()
})
</script>

<template>
  <div>
    <h1>今日土味情话</h1>
    <p>{{ jsonData.content }}</p>
    <button @click="getLoveMessage">换一句</button>
  </div>
</template>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

启动测试

npm run dev
1

打开浏览器访问 http://localhost:5173,页面会自动显示一条土味情话,点击按钮可以刷新获取新的内容。

# 8.2.4 响应数据结构

Axios 会对服务器响应进行包装,返回的 response 对象包含以下属性:

{
  data: {},          // 服务器返回的数据(最常用)
  status: 200,       // HTTP 状态码
  statusText: 'OK',  // HTTP 状态文本
  headers: {},       // 响应头(小写格式,如 response.headers['content-type'])
  config: {},        // 本次请求的配置信息
  request: {}        // 原始请求对象(浏览器中是 XMLHttpRequest 实例)
}
1
2
3
4
5
6
7
8

获取响应数据

axios.get('/api/user/1')
  .then(function(response) {
    console.log(response.data)        // 服务器数据
    console.log(response.status)      // 200
    console.log(response.statusText)  // 'OK'
    console.log(response.headers)     // 响应头对象
    console.log(response.config)      // 请求配置
  })

// 简化写法:直接解构 data
axios.get('/api/user/1')
  .then(({ data }) => {
    console.log(data)  // 直接获取服务器数据
  })
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 8.2.5 使用 async/await 优化代码

使用 async/await 可以让异步代码更简洁、易读,避免回调嵌套。

<script setup>
import axios from 'axios'
import { onMounted, reactive } from 'vue'

// 响应式数据
let jsonData = reactive({
  code: 1,
  content: ''
})

// 封装请求函数(返回 Promise)
let getLoveWords = async () => {
  return await axios({
    method: 'post',
    url: 'https://api.uomg.com/api/rand.qinghua?format=json',
    data: {
      username: '123456'
    }
  })
}

// 处理数据的函数
let getLoveMessage = async () => {
  try {
    // 等待请求完成,直接解构获取 data
    let { data } = await getLoveWords()
    // 更新响应式数据
    Object.assign(jsonData, data)
  } catch (error) {
    // 错误处理
    console.error('请求失败:', error)
    jsonData.content = '获取失败,请稍后重试'
  }
}

// 组件挂载时自动加载
onMounted(() => {
  getLoveMessage()
})
</script>

<template>
  <div>
    <h1>今日土味情话</h1>
    <p>{{ jsonData.content }}</p>
    <button @click="getLoveMessage">换一句</button>
  </div>
</template>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

async/await 优势对比

// Promise 方式(较繁琐)
function getData() {
  axios.get('/api/user')
    .then(userRes => {
      return axios.get(`/api/orders/${userRes.data.id}`)
    })
    .then(orderRes => {
      return axios.get(`/api/order-detail/${orderRes.data[0].id}`)
    })
    .then(detailRes => {
      console.log(detailRes.data)
    })
    .catch(error => {
      console.error(error)
    })
}

// async/await 方式(更清晰)
async function getData() {
  try {
    let userRes = await axios.get('/api/user')
    let orderRes = await axios.get(`/api/orders/${userRes.data.id}`)
    let detailRes = await axios.get(`/api/order-detail/${orderRes.data[0].id}`)
    console.log(detailRes.data)
  } catch (error) {
    console.error(error)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 8.2.6 Axios 请求配置参数

Axios 支持丰富的配置选项,以下是常用配置参数说明:

基础配置

{
  // 请求地址
  url: '/user',
  
  // 请求方式(默认 get)
  method: 'get',  // 'get', 'post', 'put', 'delete', 'patch' 等
  
  // 基础 URL,会自动拼接到 url 前面(除非 url 是绝对路径)
  baseURL: 'https://api.example.com',
  
  // 自定义请求头
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123'
  },
  
  // URL 查询参数(GET 请求常用)
  // 会自动拼接到 URL 后面:/user?id=123&name=张三
  params: {
    id: 123,
    name: '张三'
  },
  
  // 请求体数据(POST/PUT/PATCH 请求使用)
  data: {
    username: 'zhangsan',
    password: '123456'
  },
  
  // 请求超时时间(毫秒)
  timeout: 5000,  // 5秒后超时
  
  // 跨域请求时是否携带 Cookie
  withCredentials: false,
  
  // 响应数据类型
  responseType: 'json',  // 'json', 'text', 'blob', 'arraybuffer' 等
  
  // HTTP 基本认证
  auth: {
    username: 'admin',
    password: 'admin123'
  },
  
  // 上传进度回调
  onUploadProgress: function(progressEvent) {
    let percent = Math.round((progressEvent.loaded * 100) / progressEvent.total)
    console.log(`上传进度:${percent}%`)
  },
  
  // 下载进度回调
  onDownloadProgress: function(progressEvent) {
    let percent = Math.round((progressEvent.loaded * 100) / progressEvent.total)
    console.log(`下载进度:${percent}%`)
  },
  
  // 状态码验证(返回 true 则 resolve,false 则 reject)
  validateStatus: function(status) {
    return status >= 200 && status < 300  // 默认值
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

常用配置示例

// 完整配置示例
axios({
  method: 'post',
  url: '/api/user/login',
  baseURL: 'https://api.example.com',
  headers: {
    'Content-Type': 'application/json'
  },
  data: {
    username: 'zhangsan',
    password: '123456'
  },
  timeout: 5000,
  withCredentials: true
})
.then(response => {
  console.log(response.data)
})
.catch(error => {
  console.error(error)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 8.3 Axios GET 和 POST 方法

Axios 提供了便捷的 GET 和 POST 方法,相比通用的 axios() 方法更加简洁。

# 8.3.1 方法签名

GET 方法

// 语法:axios.get(url[, config])
axios.get(url, {
  params: {},      // URL 查询参数
  headers: {},     // 请求头
  timeout: 5000    // 其他配置
})
1
2
3
4
5
6

POST 方法

// 语法:axios.post(url[, data[, config]])
axios.post(url, {
  // 请求体数据(自动转为 JSON)
  username: 'zhangsan',
  password: '123456'
}, {
  params: {},      // URL 查询参数
  headers: {},     // 请求头
  timeout: 5000    // 其他配置
})
1
2
3
4
5
6
7
8
9
10

# 8.3.2 GET 请求详解

参数传递方式

GET 请求的参数通过 URL 查询字符串传递,使用 params 配置。

// 方式1:直接在 URL 中拼接参数
axios.get('/api/user?id=123&name=张三')

// 方式2:使用 params 配置(推荐)
axios.get('/api/user', {
  params: {
    id: 123,
    name: '张三'
  }
})
// 实际请求:/api/user?id=123&name=%E5%BC%A0%E4%B8%89
1
2
3
4
5
6
7
8
9
10
11

GET 请求完整示例

<script setup>
import axios from 'axios'
import { onMounted, reactive } from 'vue'

let jsonData = reactive({
  code: 1,
  content: ''
})

// GET 请求函数
let getLoveWords = async () => {
  try {
    return await axios.get(
      'https://api.uomg.com/api/rand.qinghua',
      {
        params: {              // URL 查询参数
          format: 'json',
          username: 'zhangsan',
          password: '123456'
        },
        headers: {             // 自定义请求头
          'Accept': 'application/json, text/plain, */*'
        },
        timeout: 5000          // 5秒超时
      }
    )
  } catch (error) {
    console.error('请求失败:', error)
    throw error
  }
}

// 处理数据
let getLoveMessage = async () => {
  try {
    let { data } = await getLoveWords()
    Object.assign(jsonData, data)
  } catch (error) {
    jsonData.content = '获取失败,请稍后重试'
  }
}

// 组件挂载时自动加载
onMounted(() => {
  getLoveMessage()
})
</script>

<template>
  <div>
    <h1>今日土味情话</h1>
    <p>{{ jsonData.content }}</p>
    <button @click="getLoveMessage">换一句</button>
  </div>
</template>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

GET 请求应用场景

  • 查询数据:获取用户列表、文章列表等
  • 搜索功能:根据关键词搜索
  • 数据过滤:按条件筛选数据
  • 分页查询:传递页码和每页数量参数

# 8.3.3 POST 请求详解

参数传递方式

POST 请求的数据通过请求体(Request Body)传递,支持多种格式。

// 方式1:JSON 格式(默认,推荐)
axios.post('/api/user/login', {
  username: 'zhangsan',
  password: '123456'
})
// Content-Type: application/json
// 请求体:{"username":"zhangsan","password":"123456"}

// 方式2:FormData 格式(文件上传)
let formData = new FormData()
formData.append('username', 'zhangsan')
formData.append('file', fileObject)
axios.post('/api/upload', formData)
// Content-Type: multipart/form-data

// 方式3:URL 编码格式
let params = new URLSearchParams()
params.append('username', 'zhangsan')
params.append('password', '123456')
axios.post('/api/login', params)
// Content-Type: application/x-www-form-urlencoded
// 请求体:username=zhangsan&password=123456
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

POST 请求完整示例

<script setup>
import axios from 'axios'
import { onMounted, reactive } from 'vue'

let jsonData = reactive({
  code: 1,
  content: ''
})

// POST 请求函数
let getLoveWords = async () => {
  try {
    return await axios.post(
      'https://api.uomg.com/api/rand.qinghua',
      {
        // 请求体数据(自动转为 JSON)
        username: 'zhangsan',
        password: '123456'
      },
      {
        // 第三个参数:配置对象
        params: {              // URL 查询参数
          format: 'json'
        },
        headers: {             // 自定义请求头
          'Accept': 'application/json, text/plain, */*',
          'X-Requested-With': 'XMLHttpRequest'
        },
        timeout: 5000          // 超时时间
      }
    )
  } catch (error) {
    console.error('请求失败:', error)
    throw error
  }
}

// 处理数据
let getLoveMessage = async () => {
  try {
    let { data } = await getLoveWords()
    Object.assign(jsonData, data)
  } catch (error) {
    jsonData.content = '获取失败,请稍后重试'
  }
}

// 组件挂载时自动加载
onMounted(() => {
  getLoveMessage()
})
</script>

<template>
  <div>
    <h1>今日土味情话</h1>
    <p>{{ jsonData.content }}</p>
    <button @click="getLoveMessage">换一句</button>
  </div>
</template>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

POST 请求应用场景

  • 用户登录:提交用户名和密码
  • 表单提交:创建或更新数据
  • 文件上传:上传图片、文档等
  • 数据创建:新增用户、发布文章等

# 8.3.4 GET 与 POST 对比

对比项 GET POST
数据位置 URL 查询字符串 请求体
数据大小 受 URL 长度限制(约 2KB) 无限制
数据类型 只支持 ASCII 字符 支持任意类型(JSON、文件等)
安全性 参数暴露在 URL 中 参数在请求体中,相对安全
缓存 可被缓存 不会被缓存
浏览器历史 参数保留在历史记录中 不保留
幂等性 幂等(多次请求结果相同) 非幂等(可能产生副作用)
适用场景 查询、搜索、获取数据 提交表单、创建/更新数据

Content-Type 说明

// 1. application/json(默认,推荐)
axios.post('/api/user', { name: '张三' })
// 请求体:{"name":"张三"}

// 2. application/x-www-form-urlencoded
axios.post('/api/user', 'name=张三&age=25', {
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
})
// 请求体:name=张三&age=25

// 3. multipart/form-data(文件上传)
let formData = new FormData()
formData.append('file', fileObject)
axios.post('/api/upload', formData)
// 请求体:二进制数据流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 8.4 Axios 拦截器

# 8.4.1 拦截器概念

拦截器(Interceptors)允许在请求发送前或响应返回后执行自定义逻辑,实现统一处理。

拦截器执行流程

请求拦截器 → 发送请求 → 服务器处理 → 响应拦截器 → then/catch
    ↓                                        ↓
  添加 Token                              统一错误处理
  日志记录                                数据格式转换
  参数处理                                状态码判断
1
2
3
4
5

两种拦截器类型

  1. 请求拦截器:在请求发送前执行

    • 添加认证 Token
    • 修改请求头
    • 显示 Loading 动画
    • 记录请求日志
  2. 响应拦截器:在响应返回后执行

    • 统一错误处理
    • 数据格式转换
    • 隐藏 Loading 动画
    • Token 过期处理

# 8.4.2 拦截器基本用法

请求拦截器

// 添加请求拦截器
axios.interceptors.request.use(
  function (config) {
    // 请求发送前的处理
    console.log('请求配置:', config)
    
    // 可以修改 config 对象
    config.headers.Authorization = 'Bearer token123'
    
    // 必须返回 config
    return config
  },
  function (error) {
    // 请求错误时的处理
    console.error('请求错误:', error)
    return Promise.reject(error)
  }
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

响应拦截器

// 添加响应拦截器
axios.interceptors.response.use(
  function (response) {
    // 状态码 2xx 时触发
    console.log('响应数据:', response)
    
    // 可以只返回 data 部分,简化后续处理
    return response.data
  },
  function (error) {
    // 状态码超出 2xx 时触发
    console.error('响应错误:', error)
    
    // 统一错误处理
    if (error.response) {
      switch (error.response.status) {
        case 401:
          console.log('未授权,请登录')
          break
        case 404:
          console.log('请求的资源不存在')
          break
        case 500:
          console.log('服务器错误')
          break
      }
    }
    
    return Promise.reject(error)
  }
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 8.4.3 创建 Axios 实例

在实际项目中,通常会创建自定义 Axios 实例,配置拦截器和默认参数。

创建 src/utils/request.js

import axios from 'axios'

// 创建 Axios 实例
const instance = axios.create({
  baseURL: 'https://api.example.com',  // 基础 URL
  timeout: 10000,                       // 请求超时时间
  headers: {
    'Content-Type': 'application/json'
  }
})

// 请求拦截器
instance.interceptors.request.use(
  config => {
    console.log('发送请求:', config.url)
    
    // 1. 添加认证 Token
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    
    // 2. 添加时间戳(防止缓存)
    if (config.method === 'get') {
      config.params = {
        ...config.params,
        _t: Date.now()
      }
    }
    
    // 3. 显示 Loading
    // showLoading()
    
    return config
  },
  error => {
    console.error('请求错误:', error)
    return Promise.reject(error)
  }
)

// 响应拦截器
instance.interceptors.response.use(
  response => {
    console.log('响应成功:', response.data)
    
    // 隐藏 Loading
    // hideLoading()
    
    // 只返回 data 部分,简化业务代码
    return response.data
  },
  error => {
    // 隐藏 Loading
    // hideLoading()
    
    // 统一错误处理
    if (error.response) {
      const { status, data } = error.response
      
      switch (status) {
        case 400:
          console.error('请求参数错误')
          break
        case 401:
          console.error('未授权,请登录')
          // 跳转到登录页
          // router.push('/login')
          break
        case 403:
          console.error('拒绝访问')
          break
        case 404:
          console.error('请求的资源不存在')
          break
        case 500:
          console.error('服务器内部错误')
          break
        case 503:
          console.error('服务不可用')
          break
        default:
          console.error(`请求失败:${data.message || '未知错误'}`)
      }
    } else if (error.request) {
      console.error('网络错误,请检查网络连接')
    } else {
      console.error('请求配置错误:', error.message)
    }
    
    return Promise.reject(error)
  }
)

// 导出实例
export default instance
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

# 8.4.4 使用自定义实例

在组件中使用

<script setup>
// 导入自定义的 axios 实例
import request from './utils/request.js'
import { onMounted, reactive } from 'vue'

let jsonData = reactive({
  code: 1,
  content: ''
})

// 使用自定义实例发送请求
let getLoveWords = async () => {
  try {
    // 注意:响应拦截器已经返回 response.data
    // 所以这里直接得到的就是业务数据
    return await request.post('/api/rand.qinghua', {
      username: 'zhangsan',
      password: '123456'
    }, {
      params: {
        format: 'json'
      }
    })
  } catch (error) {
    console.error('请求失败:', error)
    throw error
  }
}

let getLoveMessage = async () => {
  try {
    // 由于拦截器已返回 data,这里直接获取业务数据
    let data = await getLoveWords()
    Object.assign(jsonData, data)
  } catch (error) {
    jsonData.content = '获取失败,请稍后重试'
  }
}

onMounted(() => {
  getLoveMessage()
})
</script>

<template>
  <div>
    <h1>今日土味情话</h1>
    <p>{{ jsonData.content }}</p>
    <button @click="getLoveMessage">换一句</button>
  </div>
</template>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

# 8.4.5 拦截器应用场景

1. 认证 Token 管理

// 请求拦截器:添加 Token
instance.interceptors.request.use(config => {
  const token = localStorage.getItem('token')
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

// 响应拦截器:Token 过期处理
instance.interceptors.response.use(
  response => response.data,
  error => {
    if (error.response && error.response.status === 401) {
      // Token 过期,清除本地存储
      localStorage.removeItem('token')
      // 跳转到登录页
      window.location.href = '/login'
    }
    return Promise.reject(error)
  }
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

2. Loading 状态管理

let loadingCount = 0

// 请求拦截器:显示 Loading
instance.interceptors.request.use(config => {
  loadingCount++
  showLoading()
  return config
})

// 响应拦截器:隐藏 Loading
instance.interceptors.response.use(
  response => {
    loadingCount--
    if (loadingCount === 0) {
      hideLoading()
    }
    return response.data
  },
  error => {
    loadingCount--
    if (loadingCount === 0) {
      hideLoading()
    }
    return Promise.reject(error)
  }
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

3. 请求日志记录

// 请求拦截器:记录请求日志
instance.interceptors.request.use(config => {
  console.log(`[${new Date().toLocaleTimeString()}] 请求:${config.method.toUpperCase()} ${config.url}`)
  console.log('请求参数:', config.params || config.data)
  return config
})

// 响应拦截器:记录响应日志
instance.interceptors.response.use(
  response => {
    console.log(`[${new Date().toLocaleTimeString()}] 响应成功:${response.config.url}`)
    console.log('响应数据:', response.data)
    return response.data
  },
  error => {
    console.error(`[${new Date().toLocaleTimeString()}] 响应失败:${error.config?.url}`)
    console.error('错误信息:', error.message)
    return Promise.reject(error)
  }
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

4. 统一错误提示

// 响应拦截器:统一错误提示
instance.interceptors.response.use(
  response => response.data,
  error => {
    let message = '请求失败'
    
    if (error.response) {
      switch (error.response.status) {
        case 400:
          message = '请求参数错误'
          break
        case 401:
          message = '未授权,请登录'
          break
        case 403:
          message = '拒绝访问'
          break
        case 404:
          message = '请求的资源不存在'
          break
        case 500:
          message = '服务器内部错误'
          break
        default:
          message = error.response.data.message || '未知错误'
      }
    } else if (error.request) {
      message = '网络错误,请检查网络连接'
    }
    
    // 显示错误提示(使用 Element Plus 或其他 UI 库)
    // ElMessage.error(message)
    alert(message)
    
    return Promise.reject(error)
  }
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 九、Vue3 状态管理 Pinia

# 9.1 Pinia 介绍

# 9.1.1 什么是 Pinia

Pinia (opens new window) 是 Vue 官方推荐的新一代状态管理库,由 Vue 核心团队成员 Eduardo San Martin Morote 创建并维护。它是 Vuex 的继任者,专为 Vue 3 设计,同时也支持 Vue 2。

# 9.1.2 为什么需要状态管理

组件间数据传递方式对比:

方式 适用场景 局限性
Props / Emit 父子组件通信 多层嵌套时需要层层传递(Props Drilling)
路由传参 页面级组件间传递数据 数据不持久,刷新页面会丢失
Pinia 状态管理 多组件共享状态 需要引入额外依赖

状态管理解决的核心问题:

  • 多组件共享数据:购物车数量、用户登录信息等需要在多个页面展示
  • 数据同步更新:一处修改,所有使用该数据的组件自动响应
  • 集中式管理:统一的数据源,便于维护和调试

企业级应用的额外需求:

  • ✅ 强大的团队协作约定和规范
  • ✅ 与 Vue DevTools 深度集成(时间轴、状态快照、时间旅行调试)
  • ✅ 模块热更新(HMR)支持
  • ✅ 服务端渲染(SSR)支持
  • ✅ TypeScript 完整类型推导

# 9.1.3 Pinia vs Vuex

Pinia 相比 Vuex 的核心优势:

特性 Vuex 4 Pinia
API 风格 Options API Composition API 风格,更简洁
Mutations 必须通过 mutations 修改状态 ❌ 无需 mutations,直接修改 state
模块化 需要手动配置模块嵌套 ✅ 自动模块化,每个 store 独立
TypeScript 类型推导复杂 ✅ 完美的类型推导和自动补全
体积 较大 ✅ 约 1KB(gzip 后)
DevTools 支持 ✅ 更好的调试体验

Pinia 的核心优势总结:

  1. 更简洁:去除 mutations,代码量减少约 30%
  2. 更安全:完整的 TypeScript 支持,编译时发现错误
  3. 更灵活:支持多个 store,无需模块嵌套
  4. 更轻量:体积更小,性能更优

# 9.1.4 Pinia 核心概念

Pinia 使用 Store(仓库)来存储和管理状态,每个 Store 包含三部分:

┌─────────────────────────────────┐
│         Pinia Store             │
├─────────────────────────────────┤
│  id: 'user'          (唯一标识) │
├─────────────────────────────────┤
│  State   (状态数据)              │
│  ├─ username: '张三'            │
│  └─ age: 18                     │
├─────────────────────────────────┤
│  Getters (计算属性)              │
│  └─ fullInfo: () => ...         │
├─────────────────────────────────┤
│  Actions (修改状态的方法)        │
│  └─ updateUser() { ... }        │
└─────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

类比 Java 实体类理解:

  • Store ≈ Java 实体类(Entity Class)
  • id ≈ 类名
  • state ≈ 类的属性(字段)
  • getters ≈ get 方法(只读,不修改数据)
  • actions ≈ 业务方法(可修改数据)

# 9.1.5 Pinia 适用场景

适合使用 Pinia 的场景:

  • ✅ 用户登录信息(多页面展示用户名、头像)
  • ✅ 购物车数据(商品列表、购物车页面同步更新)
  • ✅ 全局配置(主题切换、语言切换)
  • ✅ 权限控制(角色权限、菜单权限)

不适合使用 Pinia 的场景:

  • ❌ 组件内部私有状态(如表单临时数据)
  • ❌ 简单的父子组件通信(直接用 props/emit 更高效)
  • ❌ 一次性的临时数据(如弹窗显示状态)

选择建议:如果数据只在 2 个以下组件使用,优先使用 props/emit;如果 3 个及以上组件共享,考虑使用 Pinia。

# 9.2 Pinia 基本用法

本节通过一个完整示例演示 Pinia 的基本使用流程,涉及项目初始化、Store 定义、数据读取和修改等核心操作。

# 9.2.1 项目环境准备

1. 创建 Vite 项目并安装依赖

# 创建 Vite 项目(选择 Vue 模板)
npm create vite
# 进入项目目录并安装基础依赖
npm install 
# 安装 Vue Router(用于演示多页面数据共享)
npm install vue-router@4 --save
1
2
3
4
5
6

2. 安装 Pinia

npm install pinia
1

项目目录结构(推荐):

src/
├── store/              # Pinia 状态管理目录
│   └── store.js        # Store 定义文件
├── components/         # 组件目录
│   ├── Operate.vue     # 数据操作组件
│   └── List.vue        # 数据展示组件
├── routers/
│   └── router.js       # 路由配置
├── App.vue
└── main.js             # 入口文件
1
2
3
4
5
6
7
8
9
10

# 9.2.2 定义 Pinia Store

文件路径:src/store/store.js

import { defineStore } from 'pinia'

/**
 * 定义 Person Store
 * @param {string} id - Store 的唯一标识符(必填)
 * @param {Object} options - Store 配置对象
 */
export const definedPerson = defineStore(
    // 1. id: Store 的唯一标识,类似类名,不能重复
    'personPinia',
    {
        // 2. state: 定义状态数据(必须使用箭头函数返回对象)
        state: () => {
            return {
                username: '张三',       // 用户名
                age: 0,                 // 年龄
                hobbies: ['唱歌', '跳舞']  // 爱好列表
            }
        },

        // 3. getters: 计算属性(类似 Vue 的 computed)
        //    - 用于派生状态,不直接修改数据
        //    - 可通过 this 访问 state
        //    - 调用时作为属性使用(不加括号)
        getters: {
            // 获取爱好数量
            getHobbiesCount() {
                return this.hobbies.length
            },
            // 获取年龄(示例:可添加格式化逻辑)
            getAge() {
                return this.age
            }
        },

        // 4. actions: 定义修改状态的方法(类似 Vue 的 methods)
        //    - 可以是同步或异步方法
        //    - 可通过 this 直接修改 state
        actions: {
            // 年龄加倍
            doubleAge() {
                this.age = this.age * 2
            }
        }
    }
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

核心要点说明:

配置项 作用 类比理解 注意事项
id Store 的唯一标识 Java 类名 必须唯一,建议用驼峰命名
state 存储状态数据 Java 类的属性字段 必须用箭头函数返回对象
getters 计算派生状态 Java 的 get 方法 不应修改 state,只读取和计算
actions 修改状态的方法 Java 的业务方法 可包含异步逻辑(如 API 请求)

# 9.2.3 在 Vue 应用中注册 Pinia

文件路径:src/main.js

import { createApp } from 'vue'
import App from './App.vue'
import router from './routers/router.js'

// 1. 导入 Pinia 的创建函数
import { createPinia } from 'pinia'
// 2. 创建 Pinia 实例
const pinia = createPinia()
// 3. 创建 Vue 应用实例
const app = createApp(App)
// 4. 注册插件(注意顺序:先注册插件,再挂载应用)
app.use(router)   // 注册路由
app.use(pinia)    // 注册 Pinia(全局可用)
// 5. 挂载应用
app.mount('#app')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

关键注意事项:

  • ✅ 必须在 app.mount() 之前调用 app.use(pinia)
  • ✅ Pinia 需要在任何使用 Store 的组件之前注册
  • ✅ 一个应用只需创建一个 Pinia 实例

# 9.2.4 在组件中操作 Pinia 数据

文件路径:src/components/Operate.vue

<script setup>
    import { ref } from 'vue'
    import { definedPerson } from '../store/store.js'
    // 1. 调用 Store 定义函数,获取 Store 实例(响应式对象)
    const person = definedPerson()
    // 2. 定义本地响应式数据(用于表单输入)
    const hobby = ref('')
</script>

<template>
    <div>
        <h1>Operate 视图 - 操作 Pinia 数据</h1>
        <!-- 方式1: 直接修改 state(推荐用于简单场景) -->
        请输入姓名: <input type="text" v-model="person.username"> <br>
        请输入年龄: <input type="text" v-model="person.age"> <br>
        <!-- 方式2: 通过 v-model 批量修改数组 -->
        请增加爱好:
        <input type="checkbox" value="吃饭" v-model="person.hobbies"> 吃饭
        <input type="checkbox" value="睡觉" v-model="person.hobbies"> 睡觉
        <input type="checkbox" value="打豆豆" v-model="person.hobbies"> 打豆豆 <br>
        <!-- 方式3: 调用 actions 中的方法 -->
        <button @click="person.doubleAge()">年龄加倍</button> <br>
        <!-- 方式4: 使用 $reset() 恢复 state 默认值 -->
        <button @click="person.$reset()">恢复默认值</button> <br>
        <!-- 方式5: 使用 $patch() 批量修改多个属性 -->
        <button @click="person.$patch({username:'奥特曼', age:100, hobbies:['晒太阳','打怪兽']})">
            变身奥特曼
        </button> <br>
        <!-- 调试输出:显示 Store 完整数据 -->
        <pre>{{ person }}</pre>
    </div>
</template>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

数据修改方式对比:

方式 语法 适用场景 性能
直接修改 person.age++ 单个属性修改 ⭐⭐⭐
$patch 对象 person.$patch({ age: 20 }) 批量修改多个属性 ⭐⭐⭐⭐
$patch 函数 person.$patch(state => state.age++) 复杂逻辑修改 ⭐⭐⭐
actions person.doubleAge() 业务逻辑封装、异步操作 ⭐⭐⭐⭐
$reset person.$reset() 恢复初始值 ⭐⭐⭐

# 9.2.5 在组件中展示 Pinia 数据

文件路径:src/components/List.vue

<script setup>
    import { definedPerson } from '../store/store.js'
    // 获取 Store 实例(与 Operate.vue 中是同一个实例)
    const person = definedPerson()
</script>

<template>
    <div>
        <h1>List 视图 - 展示 Pinia 数据</h1>
        <!-- 直接读取 state 属性 -->
        <p>读取姓名: <strong>{{ person.username }}</strong></p>
        <p>读取年龄: <strong>{{ person.age }}</strong></p>
        <!-- 使用 getters(作为属性访问,不加括号) -->
        <p>通过 getter 获取年龄: <strong>{{ person.getAge }}</strong></p>
        <p>爱好数量: <strong>{{ person.getHobbiesCount }}</strong></p>
        <!-- 遍历数组数据 -->
        <p>所有的爱好:</p>
        <ul>
            <li v-for="(hobby, index) in person.hobbies" :key="index">
                {{ hobby }}
            </li>
        </ul>
    </div>
</template>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

关键知识点:

  1. Store 实例共享:Operate.vue 和 List.vue 调用 definedPerson() 返回的是 同一个实例,数据实时同步
  2. Getters 使用方式:在模板中作为 属性访问(person.getAge),不需要加括号
  3. 响应式更新:当 Operate.vue 修改数据时,List.vue 会自动更新(Vue 响应式系统)

# 9.2.6 配置路由

文件路径:src/routers/router.js

// 1. 导入路由创建函数
import { createRouter, createWebHashHistory } from 'vue-router'

// 2. 导入组件
import List from '../components/List.vue'
import Operate from '../components/Operate.vue'

// 3. 创建路由实例并配置路由规则
const router = createRouter({
    // 使用 Hash 模式(URL 带 # 号)
    history: createWebHashHistory(),
    routes: [
        {
            path: '/operate',
            component: Operate
        },
        {
            path: '/list',
            component: List
        }
    ]
})

// 4. 导出路由实例
export default router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 9.2.7 根组件中切换路由

文件路径:src/App.vue

<script setup>
// 无需导入任何内容,路由已在 main.js 中全局注册
</script>

<template>
    <div class="app-container">
        <h2>Pinia 状态管理示例</h2>
        <hr>
        <!-- 路由导航链接 -->
        <nav>
            <router-link to="/operate">📝 数据操作页</router-link>
            <router-link to="/list">📋 数据展示页</router-link>
        </nav>
        <hr>
        <!-- 路由出口:匹配的组件将渲染在这里 -->
        <router-view></router-view>
    </div>
</template>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 9.2.8 启动项目并测试

1. 启动开发服务器

npm run dev
1

2. 测试步骤

访问 http://localhost:5173(或终端显示的地址):

步骤 操作 预期结果
1 点击"数据操作页"按钮 进入 Operate.vue 组件
2 修改姓名为"李四" 输入框显示"李四"
3 点击"年龄加倍"按钮 年龄从 0 变为 0(可手动输入 18 测试)
4 点击"数据展示页"按钮 进入 List.vue,显示修改后的数据
5 在 Operate 页面点击"变身奥特曼" List 页面数据实时更新

3. 核心验证点

  • ✅ 两个组件共享同一份数据(修改一处,另一处自动更新)
  • ✅ Getters 正常工作(显示爱好数量)
  • ✅ Actions 正确执行(年龄加倍功能)
  • ✅ $patch 批量修改成功
  • ✅ $reset 恢复默认值

# 9.2.9 完整工作流程总结

┌─────────────────────────────────────────────────────────┐
│                   Pinia 数据流                           │
├─────────────────────────────────────────────────────────┤
│  1. main.js 创建并注册 Pinia 实例                        │
│           ↓                                              │
│  2. store.js 定义 Store(state, getters, actions)      │
│           ↓                                              │
│  3. Operate.vue 调用 definedPerson() 获取 Store 实例    │
│           ↓                                              │
│  4. 用户操作触发数据修改(直接修改/actions/$patch)       │
│           ↓                                              │
│  5. Pinia 自动触发响应式更新                             │
│           ↓                                              │
│  6. List.vue 中的数据自动更新(同一 Store 实例)         │
└─────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

最佳实践建议:

  1. ✅ Store 文件统一放在 src/store/ 目录
  2. ✅ Store ID 使用驼峰命名(如 userStore、cartStore)
  3. ✅ 复杂业务逻辑封装在 actions 中,不在组件中直接修改 state
  4. ✅ 使用 $patch 批量修改多个属性,性能更优
  5. ✅ Getters 仅用于派生状态,不修改数据

# 9.3 Pinia 进阶特性

# 9.3.1 State 的多种定义方式

方式一:标准写法(推荐)

import { defineStore } from 'pinia'

export const usePersonStore = defineStore('personStore', {
    state: () => {
        return {
            username: '',
            age: 0,
            hobbies: ['唱歌', '跳舞']
        }
    }
})
1
2
3
4
5
6
7
8
9
10
11

方式二:简写形式

export const usePersonStore = defineStore('personStore', {
    state: () => ({
        username: '',
        age: 0,
        hobbies: ['唱歌', '跳舞']
    })
})
1
2
3
4
5
6
7

为什么 state 必须是函数?

  • ✅ 确保每个 Store 实例拥有独立的数据(避免数据污染)
  • ✅ 支持服务端渲染(SSR)时的数据隔离
  • ✅ 便于实现 $reset() 功能(重置为初始状态)

# 9.3.2 Getters 进阶用法

1. 基础用法:箭头函数接收 state

export const useStore = defineStore('main', {
    state: () => ({
        count: 0,
        user: { firstName: '张', lastName: '三' }
    }),
    getters: {
        // 推荐:箭头函数,接收 state 作为参数
        doubleCount: (state) => state.count * 2,
        
        // 计算属性可以访问其他 getter
        quadrupleCount: (state) => state.doubleCount * 2  // ❌ 错误示例
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13

2. 访问其他 Getters(使用 this)

export const useStore = defineStore('main', {
    state: () => ({
        count: 10
    }),
    getters: {
        doubleCount: (state) => state.count * 2,
        
        // 使用普通函数才能访问 this
        quadrupleCount() {
            return this.doubleCount * 2  // ✅ 正确:通过 this 访问其他 getter
        }
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13

3. 传递参数给 Getters

export const useStore = defineStore('main', {
    state: () => ({
        users: [
            { id: 1, name: '张三' },
            { id: 2, name: '李四' }
        ]
    }),
    getters: {
        // 返回一个函数,实现参数传递
        getUserById: (state) => {
            return (userId) => state.users.find(user => user.id === userId)
        }
    }
})

// 组件中使用
const store = useStore()
const user = store.getUserById(1)  // ✅ 传递参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

4. Getters 跨 Store 访问

// userStore.js
export const useUserStore = defineStore('user', {
    state: () => ({ username: 'admin' })
})

// cartStore.js
import { useUserStore } from './userStore.js'

export const useCartStore = defineStore('cart', {
    getters: {
        summary() {
            const userStore = useUserStore()  // ✅ 访问其他 Store
            return `${userStore.username}的购物车`
        }
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 9.3.3 Actions 进阶用法

1. 异步 Actions(处理 API 请求)

export const useUserStore = defineStore('user', {
    state: () => ({
        userInfo: null,
        loading: false,
        error: null
    }),
    actions: {
        // 异步获取用户信息
        async fetchUser(userId) {
            this.loading = true
            this.error = null
            try {
                const response = await fetch(`/api/users/${userId}`)
                this.userInfo = await response.json()
            } catch (err) {
                this.error = err.message
            } finally {
                this.loading = false
            }
        }
    }
})

// 组件中使用
const userStore = useUserStore()
await userStore.fetchUser(123)  // ✅ 等待异步完成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

2. Actions 调用其他 Actions

export const useCounterStore = defineStore('counter', {
    state: () => ({ count: 0 }),
    actions: {
        increment() {
            this.count++
        },
        // 调用其他 action
        incrementBy(amount) {
            for (let i = 0; i < amount; i++) {
                this.increment()  // ✅ 通过 this 调用
            }
        }
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

3. Actions 跨 Store 调用

// logStore.js
export const useLogStore = defineStore('log', {
    actions: {
        addLog(message) {
            console.log(`[LOG]: ${message}`)
        }
    }
})

// userStore.js
import { useLogStore } from './logStore.js'

export const useUserStore = defineStore('user', {
    actions: {
        login(username) {
            const logStore = useLogStore()
            logStore.addLog(`用户 ${username} 登录`)  // ✅ 调用其他 Store 的 action
        }
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 9.3.4 $subscribe 状态监听

基础用法:监听 State 变化

<script setup>
import { ref } from 'vue'
import { definedPerson } from '../store/store.js'

const person = definedPerson()

// 监听 Store 的所有变化
person.$subscribe((mutation, state) => {
    console.log('--- Store 状态变化 ---')
    
    // mutation.type: 变化类型
    // - 'direct': 直接修改(person.age++)
    // - 'patch object': 使用 $patch({ age: 20 })
    // - 'patch function': 使用 $patch(state => state.age++)
    console.log('变化类型:', mutation.type)
    
    // mutation.storeId: Store 的 id
    console.log('Store ID:', mutation.storeId)
    
    // mutation.payload: $patch 传递的数据(仅 patch 类型有)
    console.log('修改的数据:', mutation.payload)
    
    // state: 修改后的最新状态(Proxy 对象)
    console.log('最新状态:', state)
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

进阶用法:detached 选项(组件卸载后继续监听)

person.$subscribe((mutation, state) => {
    // 数据变化时保存到 localStorage
    localStorage.setItem('person', JSON.stringify(state))
}, { detached: true })  // ✅ 组件销毁后仍然监听
1
2
3
4

实际应用场景:自动保存到本地存储

<script setup>
import { onMounted } from 'vue'
import { useUserStore } from '../store/userStore.js'

const userStore = useUserStore()

// 从 localStorage 恢复数据
onMounted(() => {
    const savedData = localStorage.getItem('userStore')
    if (savedData) {
        userStore.$patch(JSON.parse(savedData))
    }
})

// 监听变化并自动保存
userStore.$subscribe((mutation, state) => {
    localStorage.setItem('userStore', JSON.stringify(state))
}, { detached: true })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 9.3.5 数据持久化方案

方案一:手动实现(结合 $subscribe)

// store/userStore.js
import { defineStore } from 'pinia'

const STORAGE_KEY = 'user_store'

export const useUserStore = defineStore('user', {
    state: () => ({
        username: '',
        token: ''
    }),
    actions: {
        // 初始化:从 localStorage 加载数据
        initFromStorage() {
            const data = localStorage.getItem(STORAGE_KEY)
            if (data) {
                this.$patch(JSON.parse(data))
            }
        },
        // 保存到 localStorage
        saveToStorage() {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(this.$state))
        }
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

方案二:使用 Pinia 插件(推荐)

npm install pinia-plugin-persistedstate
1
// main.js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)  // ✅ 注册插件

// store/userStore.js
export const useUserStore = defineStore('user', {
    state: () => ({
        username: '',
        token: ''
    }),
    persist: true  // ✅ 开启持久化(自动保存到 localStorage)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

高级配置:自定义持久化规则

export const useUserStore = defineStore('user', {
    state: () => ({
        username: '',
        token: '',
        tempData: ''  // 不需要持久化
    }),
    persist: {
        key: 'my-user-store',  // 自定义 localStorage key
        storage: sessionStorage,  // 使用 sessionStorage
        paths: ['username', 'token']  // 只持久化指定字段
    }
})
1
2
3
4
5
6
7
8
9
10
11
12

# 9.3.6 Pinia 最佳实践总结

实践 说明 示例
命名规范 Store ID 用驼峰,导出函数用 use 开头 useUserStore
单一职责 每个 Store 只管理一类数据 用户信息、购物车分别独立
避免直接修改 复杂逻辑封装在 actions 中 ❌ store.count++ → ✅ store.increment()
Getters 纯函数 不在 getters 中修改 state ✅ 只读取和计算
异步操作 统一在 actions 中处理 API 请求、定时器等
类型安全 配合 TypeScript 使用 编译时发现错误

# 十、Element-plus 组件库

# 10.1 Element-plus 介绍

# 10.1.1 什么是 Element Plus

Element Plus (opens new window) 是一套基于 Vue 3 的开源企业级 UI 组件库,是 Element UI 的 Vue 3 升级版本,由饿了么前端团队维护。它提供了 80+ 个高质量组件,覆盖了企业级应用开发的绝大多数场景。

核心特性:

  • ✅ 完整的 TypeScript 支持:所有组件均提供类型定义
  • 🎨 主题定制:基于 CSS 变量,支持深度定制和暗黑模式
  • 🚀 按需加载:支持 Tree Shaking,有效减小打包体积
  • ♿ 无障碍访问:遵循 WAI-ARIA 标准
  • 🌏 国际化:内置 40+ 种语言包
  • 📦 零 CSS 依赖:无需引入第三方 CSS 库

# 10.1.2 Element Plus vs Element UI

对比项 Element UI Element Plus
支持框架 Vue 2 Vue 3
TypeScript 类型定义不完整 原生 TypeScript 编写
组合式 API ❌ 不支持 ✅ 完全支持
打包体积 较大(约 700KB) 更小(约 500KB,按需加载更优)
浏览器支持 IE 10+ 现代浏览器(不支持 IE)
维护状态 维护中 活跃开发
主题定制 SCSS 变量 CSS 变量(更灵活)

# 10.1.3 浏览器兼容性

最低要求:

  • 支持 ES2018 (opens new window) 特性(Promise、async/await、解构赋值等)
  • 支持 ResizeObserver (opens new window) API

推荐浏览器版本:

  • Chrome / Edge ≥ 64
  • Firefox ≥ 67
  • Safari ≥ 12
  • ❌ 不支持 IE 浏览器(IE 11 及以下)

注意

兼容性说明:如需支持旧版浏览器,请使用 Babel (opens new window) 进行转译,并添加相应的 Polyfill(如 @babel/polyfill)。

# 10.1.4 适用场景

✅ 推荐使用:

  1. 中后台管理系统(CRM、ERP、数据看板)
  2. 企业内部工具平台
  3. 需要快速搭建原型的项目
  4. 对 UI 一致性要求较高的项目

⚠️ 不太适合:

  1. 高度定制化的 C 端产品(设计风格固定)
  2. 对打包体积极其敏感的移动端应用(建议用 Vant)
  3. 需要支持 IE 浏览器的项目

# 10.1.5 与其他 UI 框架对比

UI 框架 技术栈 组件数量 适用场景 打包体积(Gzip)
Element Plus Vue 3 80+ 中后台系统 ~150KB(按需加载)
Ant Design Vue Vue 3 60+ 企业级应用 ~200KB
Naive UI Vue 3 90+ 中后台系统 ~180KB
Arco Design Vue 3 60+ 企业级应用 ~160KB
Vant Vue 3 70+ 移动端应用 ~100KB

选择建议:

  • 🎯 Element Plus:社区生态最完善,文档最友好,适合快速开发
  • 🏢 Ant Design Vue:设计规范最严谨,适合大型企业项目
  • 🚀 Naive UI:性能最优,TypeScript 支持最好
  • 📱 Vant:移动端首选

# 10.1.6 版本与生态

当前稳定版本:v2.x(2024 年最新)

配套工具:

  • 图标库:@element-plus/icons-vue(提供 300+ 图标)
  • 主题编辑器:在线主题配置工具
  • Vite 插件:unplugin-vue-components(自动按需导入)
  • CLI 工具:@element-plus/unplugin-element-plus

学习资源:

  • 官方文档:https://element-plus.org/zh-CN/ (opens new window)
  • GitHub 仓库:https://github.com/element-plus/element-plus (opens new window)
  • 在线示例:https://element-plus.run/ (opens new window)

# 10.2 Element-plus 入门案例

# 10.2.1 项目初始化

步骤 1:创建 Vite + Vue 3 项目

# 创建项目(选择 Vue + TypeScript)
npm create vite@latest my-element-app
# 进入项目目录
cd my-element-app
# 安装基础依赖
npm install
1
2
3
4
5
6

创建时的选择:

? Project name: › my-element-app
? Select a framework: › Vue
? Select a variant: › TypeScript
1
2
3

步骤 2:安装 Element Plus

# 安装 Element Plus
npm install element-plus
# 安装图标库(可选,但推荐)
npm install @element-plus/icons-vue
# 安装其他常用依赖
npm install vue-router@4 pinia axios
1
2
3
4
5
6

# 10.2.2 引入方式对比

Element Plus 提供 三种引入方式,各有优缺点:

# 方式一:完整引入(全量导入)

优点:使用简单,无需配置
缺点:打包体积大(约 700KB),首次加载慢
适用场景:原型开发、小型项目、使用了大量组件的项目

配置代码(main.ts):

import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'

const app = createApp(App)

// 注册 Element Plus
app.use(ElementPlus)

// 注册所有图标组件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

app.mount('#app')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 方式二:手动按需引入

优点:打包体积最小,可控性强
缺点:每个组件需手动导入,开发效率低
适用场景:组件使用较少的项目

示例代码:

<script setup lang="ts">
import { ElButton, ElSwitch } from 'element-plus'
import 'element-plus/es/components/button/style/css'
import 'element-plus/es/components/switch/style/css'
import { ref } from 'vue'

const value = ref(true)
</script>

<template>
  <el-button type="primary">按钮</el-button>
  <el-switch v-model="value" />
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
# 方式三:自动按需引入(推荐)⭐

优点:开发便捷 + 体积优化,按需加载样式
缺点:需要配置 Vite 插件
适用场景:大多数生产项目

步骤 1:安装插件

npm install -D unplugin-vue-components unplugin-auto-import
1

步骤 2:配置 vite.config.ts

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
      // 自动导入 Vue 相关函数(如 ref, computed)
      imports: ['vue', 'vue-router', 'pinia'],
      dts: 'src/auto-imports.d.ts' // 生成类型定义文件
    }),
    Components({
      resolvers: [ElementPlusResolver()],
      dts: 'src/components.d.ts' // 生成组件类型定义
    })
  ]
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

步骤 3:直接在组件中使用(无需导入)

<script setup lang="ts">
// 无需导入,插件会自动识别并按需加载
const value = ref(true)
</script>

<template>
  <el-button type="primary">按钮</el-button>
  <el-switch v-model="value" />
</template>
1
2
3
4
5
6
7
8
9

提示

推荐方案:生产项目使用 方式三(自动按需引入),开发效率高且体积优化好。

# 10.2.3 完整入门案例(使用完整引入)

步骤 1:配置 main.ts

import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'

const app = createApp(App)

// 注册 Element Plus
app.use(ElementPlus)

// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

app.mount('#app')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

步骤 2:编写组件 App.vue

<script setup lang="ts">
import { ref } from 'vue'

const switchValue = ref(true)
const message = () => {
  console.log('按钮被点击了!')
}
</script>

<template>
  <div class="demo-container">
    <h2>Element Plus 入门案例</h2>
    
    <!-- 基础按钮 -->
    <div class="demo-section">
      <h3>按钮组件</h3>
      <el-button>默认按钮</el-button>
      <el-button type="primary">主要按钮</el-button>
      <el-button type="success">成功按钮</el-button>
      <el-button type="warning">警告按钮</el-button>
      <el-button type="danger">危险按钮</el-button>
    </div>

    <!-- 图标按钮 -->
    <div class="demo-section">
      <h3>图标按钮</h3>
      <el-button type="primary" :icon="Search">搜索</el-button>
      <el-button type="success" :icon="Check" circle />
      <el-button type="danger" :icon="Delete" circle />
    </div>

    <!-- 开关组件 -->
    <div class="demo-section">
      <h3>开关组件</h3>
      <el-switch
        v-model="switchValue"
        size="large"
        active-text="开启"
        inactive-text="关闭"
      />
      <br />
      <el-switch v-model="switchValue" active-text="ON" inactive-text="OFF" />
      <br />
      <el-switch
        v-model="switchValue"
        size="small"
        active-text="开"
        inactive-text="关"
      />
    </div>

    <!-- 状态展示 -->
    <div class="demo-section">
      <p>当前开关状态:{{ switchValue ? '开启' : '关闭' }}</p>
    </div>
  </div>
</template>

<style scoped>
.demo-container {
  padding: 20px;
  max-width: 800px;
  margin: 0 auto;
}

.demo-section {
  margin-bottom: 30px;
  padding: 20px;
  border: 1px solid #eee;
  border-radius: 8px;
}

.demo-section h3 {
  margin-top: 0;
  color: #409eff;
}

.demo-section .el-button {
  margin-right: 10px;
  margin-bottom: 10px;
}

.demo-section .el-switch {
  margin-bottom: 15px;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

步骤 3:启动项目

npm run dev
1

# 10.3 Element-plus 常用组件

提示

官方文档:https://element-plus.org/zh-CN/component/overview.html (opens new window)

# 十一、前端技术栈扩展

# 11.1 React 框架

# 11.1.1 React 简介

什么是 React?

React 是由 Facebook(Meta)开发的用于构建用户界面的 JavaScript 库,专注于视图层(MVC 中的 V)。

  • 官网:https://react.dev/ (opens new window)
  • 中文文档:https://zh-hans.react.dev/ (opens new window)
  • 特点:组件化、声明式编程、虚拟 DOM、单向数据流
  • 适用场景:单页应用(SPA)、复杂交互界面、移动端(React Native)

React 核心特性

特性 说明 优势
虚拟 DOM 内存中的轻量级 DOM 表示 高效更新、减少真实 DOM 操作
组件化 可复用的独立 UI 单元 提高代码复用性和可维护性
单向数据流 数据从父组件流向子组件 数据流清晰、易于调试
JSX 语法 JavaScript + XML 混合语法 直观描述 UI 结构
生态丰富 React Router、Redux、Ant Design 等 完整的技术解决方案

React vs Vue 对比

对比项 React Vue
学习曲线 较陡峭(需掌握 JSX、Hooks) 平缓(模板语法类似 HTML)
数据绑定 单向数据流 双向数据绑定(v-model)
状态管理 Redux、MobX、Zustand Vuex、Pinia
生态系统 更庞大、社区更活跃 相对较小但完善
适用场景 大型复杂应用、跨平台(RN) 快速开发、渐进式集成

# 11.1.2 React 快速入门

1. 创建 React 项目

# 使用 Vite 创建(推荐,快速)
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev

# 使用 Create React App(官方脚手架)
npx create-react-app my-react-app
cd my-react-app
npm start
1
2
3
4
5
6
7
8
9
10

2. JSX 语法基础

JSX 是 JavaScript 的语法扩展,允许在 JS 中编写类似 HTML 的代码。

import React from 'react'

function Welcome() {
  const name = '张三'
  const isLoggedIn = true
  const users = ['Alice', 'Bob', 'Charlie']

  return (
    <div className="container">
      {/* 变量插值 */}
      <h1>Hello, {name}!</h1>

      {/* 条件渲染 */}
      {isLoggedIn ? <p>欢迎回来</p> : <p>请先登录</p>}

      {/* 列表渲染 */}
      <ul>
        {users.map((user, index) => (
          <li key={index}>{user}</li>
        ))}
      </ul>

      {/* 绑定事件 */}
      <button onClick={() => alert('点击了按钮')}>
        点击我
      </button>
    </div>
  )
}

export default Welcome
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

JSX 关键语法

  • 变量插值:使用 {} 包裹 JavaScript 表达式
  • 属性名:className(代替 class)、htmlFor(代替 for)
  • 条件渲染:使用三元运算符或 && 逻辑运算符
  • 列表渲染:使用 map 方法,需提供唯一 key 属性
  • 事件绑定:使用驼峰命名(如 onClick、onChange)

# 11.2 Webpack 构建工具

# 11.2.1 Webpack 简介

什么是 Webpack?

Webpack 是一个现代 JavaScript 应用程序的静态模块打包工具,将项目中的各种资源(JS、CSS、图片等)打包成浏览器可识别的格式。

  • 官网:https://webpack.js.org/ (opens new window)
  • 中文文档:https://webpack.docschina.org/ (opens new window)
  • 核心概念:入口(Entry)、输出(Output)、加载器(Loader)、插件(Plugin)、模式(Mode)
  • 竞品对比:Vite(更快)、Rollup(库打包)、Parcel(零配置)

Webpack 核心能力

功能 说明 应用场景
模块打包 将多个模块合并为一个或多个 bundle 减少 HTTP 请求
代码转换 通过 Loader 转换文件(TS、Less、图片等) 兼容旧浏览器、使用新语法
代码分割 按需加载、动态导入 优化首屏加载速度
资源优化 压缩代码、Tree Shaking 减小包体积
开发服务器 热更新(HMR)、代理 提升开发效率

# 11.2.2 Webpack 配置示例

基础配置文件

// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  // 模式:development(开发)、production(生产)
  mode: 'development',

  // 入口文件
  entry: './src/index.js',

  // 输出配置
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
    clean: true
  },

  // 开发服务器
  devServer: {
    static: './dist',
    port: 3000,
    hot: true,           // 热更新
    open: true           // 自动打开浏览器
  },

  // 模块规则
  module: {
    rules: [
      // 处理 JavaScript(Babel)
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },

      // 处理 CSS
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      },

      // 处理图片
      {
        test: /\.(png|jpg|jpeg|gif|svg)$/,
        type: 'asset/resource',
        generator: {
          filename: 'images/[name].[hash][ext]'
        }
      }
    ]
  },

  // 插件
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html'
    }),
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css'
    })
  ],

  // 代码分割
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

package.json 脚本配置

{
  "scripts": {
    "dev": "webpack serve --mode development",
    "build": "webpack --mode production"
  },
  "devDependencies": {
    "webpack": "^5.88.0",
    "webpack-cli": "^5.1.0",
    "webpack-dev-server": "^4.15.0",
    "babel-loader": "^9.1.0",
    "@babel/core": "^7.22.0",
    "@babel/preset-env": "^7.22.0",
    "css-loader": "^6.8.0",
    "style-loader": "^3.3.0",
    "mini-css-extract-plugin": "^2.7.0",
    "html-webpack-plugin": "^5.5.0"
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 11.2.3 Webpack vs Vite 对比

对比项 Webpack Vite
启动速度 较慢(需预打包) 极快(按需编译)
热更新速度 较慢 极快(基于 ESM)
配置复杂度 较高 低(约定优于配置)
生态成熟度 非常成熟 快速成长
适用场景 大型项目、需要精细控制 快速开发、现代浏览器
浏览器兼容 支持旧浏览器 仅支持现代浏览器

# 11.3 其他前端技术栈

# 11.3.1 TypeScript - 类型安全的 JavaScript

什么是 TypeScript?

TypeScript 是 JavaScript 的超集,添加了静态类型系统,由 Microsoft 开发维护。

  • 官网:https://www.typescriptlang.org/ (opens new window)
  • 中文文档:https://www.tslang.cn/ (opens new window)
  • 核心特性:静态类型检查、接口、泛型、装饰器、命名空间
  • 编译目标:编译为纯 JavaScript,可运行在任何 JS 环境

TypeScript 核心语法

// 基本类型
let age: number = 25
let name: string = '张三'
let isStudent: boolean = true
let hobbies: string[] = ['读书', '运动']

// 接口(定义对象结构)
interface User {
  id: number
  name: string
  email?: string  // 可选属性
  readonly createdAt: Date  // 只读属性
}

const user: User = {
  id: 1,
  name: '张三',
  createdAt: new Date()
}

// 函数类型
function add(a: number, b: number): number {
  return a + b
}

// 泛型
function getFirstElement<T>(arr: T[]): T | undefined {
  return arr[0]
}

const firstNum = getFirstElement([1, 2, 3])  // 类型:number | undefined
const firstName = getFirstElement(['a', 'b'])  // 类型:string | undefined

// 联合类型
type Status = 'success' | 'error' | 'loading'
let currentStatus: Status = 'loading'

// 类型别名
type Point = { x: number; y: number }
const point: Point = { x: 10, y: 20 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

TypeScript 在 Vue/React 中的使用

// Vue 3 + TypeScript
import { defineComponent, ref } from 'vue'

export default defineComponent({
  setup() {
    const count = ref<number>(0)
    const increment = (): void => {
      count.value++
    }
    return { count, increment }
  }
})

// React + TypeScript
import React, { useState } from 'react'

interface Props {
  title: string
  count?: number
}

const Counter: React.FC<Props> = ({ title, count = 0 }) => {
  const [value, setValue] = useState<number>(count)

  return (
    <div>
      <h2>{title}</h2>
      <p>Count: {value}</p>
      <button onClick={() => setValue(value + 1)}>+1</button>
    </div>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 11.3.2 Sass/Less - CSS 预处理器

Sass 示例

// 变量
$primary-color: #42b983;
$font-size: 16px;

// 嵌套
.container {
  padding: 20px;

  .header {
    font-size: $font-size * 1.5;
    color: $primary-color;

    &:hover {
      color: darken($primary-color, 10%);
    }
  }

  .content {
    margin-top: 20px;
  }
}

// Mixin(混入)
@mixin flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

.box {
  @include flex-center;
  width: 200px;
  height: 200px;
}

// 函数
@function calculate-rem($px) {
  @return $px / 16 * 1rem;
}

.title {
  font-size: calculate-rem(24); // 1.5rem
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

Less 示例

// 变量
@primary-color: #42b983;
@font-size: 16px;

// 嵌套
.container {
  padding: 20px;

  .header {
    font-size: @font-size * 1.5;
    color: @primary-color;

    &:hover {
      color: darken(@primary-color, 10%);
    }
  }
}

// Mixin
.flex-center() {
  display: flex;
  justify-content: center;
  align-items: center;
}

.box {
  .flex-center();
  width: 200px;
  height: 200px;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 11.3.3 Tailwind CSS - 实用优先的 CSS 框架

什么是 Tailwind CSS?

Tailwind CSS 是一个实用优先的 CSS 框架,通过组合原子类快速构建 UI。

  • 官网:https://tailwindcss.com/ (opens new window)
  • 特点:原子类、高度可定制、JIT 模式(按需生成)
  • 优势:减少自定义 CSS、设计一致性高、响应式简单
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init
1
2

Tailwind CSS 使用示例

<!-- 传统 CSS 方式 -->
<style>
.button {
  padding: 0.5rem 1rem;
  background-color: #3b82f6;
  color: white;
  border-radius: 0.375rem;
  font-weight: 600;
}
.button:hover {
  background-color: #2563eb;
}
</style>

<button class="button">点击我</button>

<!-- Tailwind CSS 方式 -->
<button class="px-4 py-2 bg-blue-500 text-white rounded-md font-semibold hover:bg-blue-600">
  点击我
</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 11.3.4 技术选型建议

前端框架选择

选择 Vue:
✓ 团队熟悉 HTML/CSS,快速上手
✓ 中小型项目、后台管理系统
✓ 需要渐进式集成到现有项目

选择 React:
✓ 大型复杂应用、需要高度定制
✓ 跨平台开发(React Native)
✓ 团队熟悉 JSX 和函数式编程
1
2
3
4
5
6
7
8
9

构建工具选择

选择 Vite:
✓ 新项目、追求开发体验
✓ 现代浏览器为主要目标
✓ Vue/React 快速开发

选择 Webpack:
✓ 需要兼容旧浏览器
✓ 复杂的构建需求和定制化配置
✓ 已有 Webpack 项目维护
1
2
3
4
5
6
7
8
9

CSS 方案选择

选择 Sass/Less:
✓ 需要复杂的样式逻辑
✓ 变量、函数、嵌套等预处理功能

选择 Tailwind CSS:
✓ 快速开发、设计系统一致性
✓ 减少自定义 CSS 代码量
✓ 响应式设计需求多

选择 CSS Modules:
✓ 样式隔离、避免全局污染
✓ 组件化开发
1
2
3
4
5
6
7
8
9
10
11
12

# 11.4 前端工程化最佳实践

# 11.4.1 项目结构规范

Vue 3 项目结构

my-vue-app/
├── public/                 # 静态资源(不会被打包)
│   └── favicon.ico
├── src/
│   ├── assets/             # 打包资源(图片、字体等)
│   ├── components/         # 公共组件
│   │   ├── common/         # 通用组件(按钮、输入框等)
│   │   └── business/       # 业务组件
│   ├── views/              # 页面组件
│   ├── router/             # 路由配置
│   │   └── index.js
│   ├── stores/             # Pinia 状态管理
│   │   └── user.js
│   ├── api/                # API 接口封装
│   │   └── user.js
│   ├── utils/              # 工具函数
│   │   ├── request.js      # Axios 封装
│   │   └── validate.js     # 表单验证
│   ├── styles/             # 全局样式
│   │   ├── reset.css
│   │   └── variables.scss
│   ├── App.vue             # 根组件
│   └── main.js             # 入口文件
├── .env                    # 环境变量
├── .env.development        # 开发环境变量
├── .env.production         # 生产环境变量
├── vite.config.js          # Vite 配置
└── package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

React 项目结构

my-react-app/
├── public/
├── src/
│   ├── components/         # 公共组件
│   ├── pages/              # 页面组件
│   ├── hooks/              # 自定义 Hooks
│   ├── store/              # Redux 状态管理
│   │   ├── slices/         # Redux Slices
│   │   └── index.js
│   ├── services/           # API 服务
│   ├── utils/              # 工具函数
│   ├── styles/             # 样式文件
│   ├── App.jsx
│   └── index.jsx
├── .env
└── package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 11.4.2 代码规范与工具

1. ESLint - 代码质量检查

npm install -D eslint
npx eslint --init
1
2
// .eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true
  },
  extends: [
    'eslint:recommended',
    'plugin:vue/vue3-recommended', // Vue 3
    'plugin:react/recommended'     // React
  ],
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-unused-vars': 'warn',
    'vue/multi-word-component-names': 'off'
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

2. Prettier - 代码格式化

npm install -D prettier
1
// .prettierrc
{
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "none",
  "printWidth": 100
}
1
2
3
4
5
6
7
8

3. Husky + lint-staged - Git 提交钩子

npm install -D husky lint-staged
npx husky install
1
2
// package.json
{
  "lint-staged": {
    "*.{js,jsx,vue,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ]
  },
  "scripts": {
    "prepare": "husky install"
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 11.4.3 性能优化策略

1. 路由懒加载

// Vue Router
const routes = [
  {
    path: '/about',
    component: () => import('./views/About.vue') // 懒加载
  }
]

// React Router
import { lazy, Suspense } from 'react'
const About = lazy(() => import('./pages/About'))

<Suspense fallback={<div>Loading...</div>}>
  <About />
</Suspense>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

2. 图片优化

<!-- 使用 WebP 格式 -->
<picture>
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg" alt="描述">
</picture>

<!-- 懒加载图片 -->
<img src="placeholder.jpg" data-src="large-image.jpg" loading="lazy">
1
2
3
4
5
6
7
8

3. 代码分割

// Webpack 动态导入
import(/* webpackChunkName: "lodash" */ 'lodash').then(({ default: _ }) => {
  console.log(_.join(['Hello', 'webpack'], ' '))
})
1
2
3
4

4. Tree Shaking(摇树优化)

// ❌ 导入整个库
import _ from 'lodash'

// ✅ 按需导入
import debounce from 'lodash/debounce'
1
2
3
4
5

# 11.4.4 前端监控与调试

1. Vue Devtools / React Developer Tools

浏览器扩展,用于调试组件状态、路由、性能等。

2. 错误监控(Sentry)

npm install @sentry/vue @sentry/react
1
// Vue 3
import * as Sentry from '@sentry/vue'

Sentry.init({
  app,
  dsn: 'YOUR_DSN',
  integrations: [new Sentry.BrowserTracing()],
  tracesSampleRate: 1.0
})

// React
import * as Sentry from '@sentry/react'

Sentry.init({
  dsn: 'YOUR_DSN',
  integrations: [new Sentry.BrowserTracing()],
  tracesSampleRate: 1.0
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

提示

学习建议

  1. Vue 开发者:熟练掌握 Vue 3 + Vite + Pinia + TypeScript
  2. React 开发者:掌握 React Hooks + Redux Toolkit + TypeScript
  3. 通用技能:TypeScript、Webpack/Vite、Sass/Tailwind CSS
  4. 进阶方向:性能优化、工程化工具链、微前端架构

# 十二、案例开发-日程管理

# 12.1 本章概述

本章基于前面学习的 Vue3、Axios、Vue Router 和 Pinia 技术,实现一个完整的前后端分离的日程管理系统。通过本章学习,您将掌握:

  • ✅ Pinia 在实际项目中的应用(用户状态管理、数据持久化)
  • ✅ 前后端数据交互流程(RESTful API 设计)
  • ✅ 路由守卫实现权限控制
  • ✅ CRUD 操作的完整实现

技术栈总览:

前端技术栈               后端技术栈
├─ Vue 3                ├─ Servlet(Jakarta EE)
├─ Pinia(状态管理)     ├─ JDBC(数据库操作)
├─ Vue Router(路由)    ├─ MD5(密码加密)
├─ Axios(HTTP 请求)    └─ JSON(数据格式)
└─ Vite(构建工具)
1
2
3
4
5
6

核心功能模块:

  1. 用户认证模块:登录/注册、状态保持、路由守卫
  2. 日程管理模块:查询、新增、修改、删除日程
  3. 数据持久化:前端使用 Pinia,后端使用 MySQL

日程管理源码 (opens new window)

# 12.2 用户认证功能重构

# 12.2.1 创建 src/utils/request.js 文件

功能说明:封装 axios 实例,统一配置请求基础路径和拦截器逻辑,便于全局请求管理。

关键配置:

  • baseURL:配置后端服务器地址,后续所有请求自动拼接该前缀
  • 请求拦截器:可在请求发送前添加 Token、Loading 效果等
  • 响应拦截器:统一处理响应数据或错误信息
import axios from 'axios'

// ==================== 1. 创建 axios 实例 ====================
const instance = axios.create({
    baseURL: 'http://localhost:8080/',  // 后端服务器地址(自动拼接到所有请求前)
    timeout: 10000                       // 请求超时时间(10秒)
})

// ==================== 2. 请求拦截器 ====================
instance.interceptors.request.use(
    config => {
        // ✅ 在请求发送前执行的逻辑
        // 示例:添加 Token 认证头
        // const token = localStorage.getItem('token')
        // if (token) {
        //     config.headers['Authorization'] = token
        // }
        return config  // 必须返回配置对象,否则请求无法发送
    },
    error => {
        // ❌ 请求配置错误时执行(如网络断开)
        console.error('请求配置错误:', error)
        return Promise.reject(error)
    }
)

// ==================== 3. 响应拦截器 ====================
instance.interceptors.response.use(
    response => {
        // ✅ 响应状态码为 2xx 时执行
        // 可直接返回 response.data,简化后续使用
        return response
    },
    error => {
        // ❌ 响应状态码超出 2xx 范围时执行(如 404、500)
        console.error('响应错误:', error.response?.status)
        // 示例:统一处理常见错误
        // if (error.response?.status === 401) {
        //     alert('登录过期,请重新登录')
        //     router.push('/login')
        // }
        return Promise.reject(error)
    }
)

// ==================== 4. 导出实例 ====================
export default instance
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 12.2.2 前端 Pinia 状态管理配置

安装 pinia 依赖

npm install pinia
1

创建 src/pinia.js 文件

import { createPinia } from 'pinia'
// 创建 Pinia 实例(全局唯一)
const pinia = createPinia()
// 导出供 main.js 注册使用
export default pinia
1
2
3
4
5

为什么单独创建文件?

  • ✅ 便于在路由守卫中访问 Store(避免循环依赖)
  • ✅ 统一管理 Pinia 实例,便于后续添加插件(如持久化插件)

# 12.2.3 在 Vue 应用中注册 Pinia

文件路径:src/main.js

import { createApp } from 'vue'
import App from './App.vue'
import router from './router/router.js'  // 导入路由
import pinia from './pinia.js'          // 导入 Pinia 实例

// 注册插件(顺序:先 router,再 pinia✅ 注册后全局可用)
createApp(App).use(router).use(pinia).mount('#app')
1
2
3
4
5
6
7

注册顺序说明:

  • Vue Router 和 Pinia 的注册顺序一般不影响功能
  • 但建议先注册 router,因为路由守卫可能需要访问 Store

# 12.2.4 定义用户信息 Store

文件路径:src/store/userStore.js

import { defineStore } from 'pinia'

/**
 * 用户信息 Store
 * 用于存储登录用户的基本信息(跨组件共享)
 */
export const defineUser = defineStore('loginUser', {
    state: () => {
        return {
            uid: 0,          // 用户 ID(0 表示未登录)
            username: ''     // 用户名(空字符串表示未登录)
        }
    },
    getters: {
        // 判断用户是否已登录
        isLoggedIn: (state) => state.username !== '' && state.uid > 0
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

核心设计思路:

  • uid 和 username 初始值表示未登录状态
  • 通过 isLoggedIn getter 判断登录状态(便于路由守卫使用)
  • 登录成功后,后端返回用户信息并更新到 Store

# 12.2.5 定义日程数据 Store

文件路径:src/store/scheduleStore.js

import { defineStore } from 'pinia'

/**
 * 日程数据 Store
 * 用于存储当前用户的所有日程列表
 */
export const defineSchedule = defineStore('scheduleList', {
    state: () => {
        return {
            itemList: []  // 日程列表(数组元素结构:{ sid, uid, title, completed })
        }
    },
    getters: {
        // 获取未完成的日程数量
        unfinishedCount: (state) => {
            return state.itemList.filter(item => item.completed === 0).length
        },
        // 获取已完成的日程数量
        finishedCount: (state) => {
            return state.itemList.filter(item => item.completed === 1).length
        }
    },
    actions: {
        // 更新日程列表(从后端获取数据后调用)
        updateList(newList) {
            this.itemList = newList
        }
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

数据结构说明:

// itemList 数组元素示例
{
    sid: 1,           // 日程 ID(主键)
    uid: 1,           // 用户 ID(外键)
    title: '学习Vue', // 日程标题
    completed: 0      // 完成状态(0 未完成 / 1 已完成)
}
1
2
3
4
5
6
7

# 12.2.6 Regist 组件实现(异步请求 + 注册逻辑)

功能说明:实现用户注册表单,包含实时校验、用户名占用检查、密码确认等功能。

校验规则:

  • 用户名:5-10 位字母或数字,且不能被占用
  • 密码:6 位纯数字
  • 确认密码:需与密码一致

前后端交互流程:

  1. 用户名失焦时调用后端检查是否被占用
  2. 所有校验通过后提交注册请求
  3. 根据后端响应码执行不同逻辑(200 成功 / 其他失败)
<script setup>
// ==================== 导入依赖 ====================
import { ref, reactive } from 'vue'
import request from '../utils/request'       // 导入封装的 axios 实例
import { useRouter } from 'vue-router'
const router = useRouter()

// ==================== 响应式数据 ====================
let registUser = reactive({
    username: "",  // 用户名
    userPwd: ""    // 密码
})
let usernameMsg = ref('')    // 用户名校验提示
let userPwdMsg = ref('')     // 密码校验提示
let reUserPwdMsg = ref('')   // 确认密码校验提示
let reUserPwd = ref('')      // 确认密码输入值

// ==================== 校验函数 ====================
// 1️⃣ 校验用户名(格式 + 占用检查)
async function checkUsername() {
    let usernameReg = /^[a-zA-Z0-9]{5,10}$/
    
    // 第一步:输入框为空不进行信息提示
    if (registUser.username === '') {
        usernameMsg.value = ''
        return false
    }
    // 第二步:校验格式
    if (!usernameReg.test(registUser.username)) {
        usernameMsg.value = "格式有误"
        return false
    }
    // 第三步:向后端请求校验是否被占用
    try {
        let { data } = await request.post(`user/checkUsernameUsed?username=${registUser.username}`)
        if (data.code !== 200) {
            usernameMsg.value = "用户名占用"
            return false
        }
        usernameMsg.value = "可用"
        return true
    } catch (error) {
        usernameMsg.value = "网络错误"
        return false
    }
}

// 2️⃣ 校验密码格式
function checkUserPwd() {
    let userPwdReg = /^[0-9]{6}$/
    if (registUser.userPwd === '') {
        userPwdMsg.value = ''
        return false
    } 
    if (!userPwdReg.test(registUser.userPwd)) {
        userPwdMsg.value = "格式有误"
        return false
    }
    userPwdMsg.value = "OK"
    return true
}

// 3️⃣ 校验确认密码(格式 + 一致性)
function checkReUserPwd() {
    let userPwdReg = /^[0-9]{6}$/
    if (reUserPwd.value === '') {
        reUserPwdMsg.value = ''
        return false
    }
    // 第一步:校验格式
    if (!userPwdReg.test(reUserPwd.value)) {
        reUserPwdMsg.value = "格式有误"
        return false
    }
    // 第二步:校验两次密码是否一致
    if (registUser.userPwd !== reUserPwd.value) {
        reUserPwdMsg.value = "两次密码不一致"
        return false
    }
    reUserPwdMsg.value = "OK"
    return true
}

// ==================== 业务函数 ====================
// 📌 注册提交函数
async function regist() {
    // 第一步:校验所有输入框
    let flag1 = await checkUsername()
    let flag2 = await checkUserPwd()
    let flag3 = await checkReUserPwd()
    if (!(flag1 && flag2 && flag3)) {
        alert("校验不通过,请再次检查数据")
        return
    }
    // 第二步:发送注册请求
    try {
        let { data } = await request.post("user/regist", registUser)
        if (data.code === 200) {
            alert("注册成功,快去登录吧")
            router.push("/login")  // 跳转到登录页
        } else {
            alert("抱歉,用户名被抢注了")
        }
    } catch (error) {
        alert("网络错误,请稍后重试")
    }
}

// 📌 清空表单
function clearForm() {
    registUser.username = ""
    registUser.userPwd = ""
    usernameMsg.value = ""
    userPwdMsg.value = ""
    reUserPwd.value = ""
    reUserPwdMsg.value = ""
}
</script>

<template>
  <div>
    <h3 class="ht">请注册</h3>

    <table class="tab" cellspacing="0px">
        <tbody>
        <tr class="ltr">
            <td>请输入账号</td>
            <td>
                <input class="ipt" 
                    id="usernameInput" 
                    type="text" 
                    name="username" 
                    v-model="registUser.username"
                    @blur="checkUsername()">
                <span id="usernameMsg" class="msg" v-text="usernameMsg"></span>
            </td>
        </tr>
        <tr class="ltr">
            <td>请输入密码</td>
            <td>
                <input class="ipt" 
                    id="userPwdInput" 
                    type="password" 
                    name="userPwd" 
                    v-model="registUser.userPwd"
                    @blur="checkUserPwd()">
                <span id="userPwdMsg" class="msg" v-text="userPwdMsg"></span>
            </td>
        </tr>
        <tr class="ltr">
            <td>确认密码</td>
            <td>
                <input class="ipt" 
                    id="reUserPwdInput" 
                    type="password" 
                    v-model="reUserPwd"
                    @blur="checkReUserPwd()">
                <span id="reUserPwdMsg" class="msg" v-text="reUserPwdMsg"></span>
            </td>
        </tr>
        <tr class="ltr">
            <td colspan="2" class="buttonContainer">
                <input class="btn1" type="button" @click="regist()" value="注册">
                <input class="btn1" type="button" @click="clearForm()" value="重置">
                <router-link to="/login">
                  <button class="btn1">去登录</button>
                </router-link>
            </td>
        </tr>
        </tbody>
    </table>
  </div>
</template>
<style scoped>
.ht {
    text-align: center;
    color: cadetblue;
    font-family: 幼圆;
}
.tab {
    width: 500px;
    border: 5px solid cadetblue;
    margin: 0px auto;
    border-radius: 5px;
    font-family: 幼圆;
}
.ltr td {
    border: 1px solid  powderblue;
}
.ipt {
    border: 0px;
    width: 50%;
}
.btn1 {
    border: 2px solid powderblue;
    border-radius: 4px;
    width:60px;
    background-color: antiquewhite;
}
.msg {
    color: gold;
}
.buttonContainer {
    text-align: center;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206

# 12.2.7 Login 组件实现(表单校验 + 登录逻辑)

功能说明:实现用户登录表单,校验格式后向后端提交登录请求,根据响应码处理不同情况。

响应码说明:

状态码 含义 前端处理
200 登录成功 跳转到日程管理页面
501 用户名不存在 提示用户名错误
503 密码错误 提示密码错误
其他 未知错误 提示未知错误

前后端交互流程:

用户输入 → 前端格式校验 → 提交登录请求 → 后端验证 → 返回状态码 → 存储登录状态 → 前端路由跳转
1
<script setup>
// ==================== 导入依赖 ====================
import { ref, reactive } from 'vue'
import request from "../utils/request.js"
import { useRouter } from 'vue-router'
import { defineUser } from '../store/userStore'

let sysUser = defineUser()
const router = useRouter()

// ==================== 响应式数据 ====================
let loginUser = reactive({
  username: "",  // 用户名
  userPwd: ""    // 密码
})
let usernameMsg = ref("")  // 用户名校验提示
let userPwdMsg = ref("")   // 密码校验提示

// ==================== 校验函数 ====================
// 1️⃣ 校验用户名格式
function checkUsername() {
  let usernameReg = /^[a-zA-Z0-9]{5,10}$/
  if (loginUser.username === '') {
    usernameMsg.value = ''
    return false
  } else if (!usernameReg.test(loginUser.username)) {
    usernameMsg.value = "格式有误"
    return false
  }
  usernameMsg.value = "OK"
  return true
}

// 2️⃣ 校验密码格式
function checkUserPwd() {
  let userPwdReg = /^[0-9]{6}$/
  if (loginUser.userPwd === '') {
    userPwdMsg.value = ''
    return false
  } else if (!userPwdReg.test(loginUser.userPwd)) {
    userPwdMsg.value = "格式有误"
    return false
  }
  userPwdMsg.value = "OK"
  return true
}

// ==================== 业务函数 ====================
// 📌 登录提交函数
async function login() {
  // 第一步:前端校验格式
  let flag1 = checkUsername()
  let flag2 = checkUserPwd()
  if (!(flag1 && flag2)) {
    alert("请检查输入格式")
    return
  }
  // 第二步:发送登录请求
  try {
    let { data } = await request.post("user/login", loginUser)
    // 第三步:根据响应码处理不同情况
    if (data.code === 200) {
      alert("登录成功")
      // 获取登录的用户信息,更新到 pinia 中
      Object.assign(sysUser, data.data.loginUser)
      router.push("/showSchedule")  // 跳转到日程管理页面
    } else if (data.code === 503) {
      alert("密码有误")
    } else if (data.code === 501) {
      alert("用户名有误")
    } else {
      alert("未知错误")
    }
  } catch (error) {
    alert("网络错误,请稍后重试")
    console.error('登录请求失败:', error)
  }
}

// 📌 清空表单
function clearForm() {
  loginUser.username=''
  loginUser.userPwd=''
  usernameMsg.value=''
  userPwdMsg.value=''
}
</script>

<template>
  <div>
    <h3 class="ht">请登录</h3>
    <table class="tab" cellspacing="0px">
      <tbody>
      <tr class="ltr">
        <td>请输入账号</td>
        <td>
          <input class="ipt"
                 type="text"
                 v-model="loginUser.username"
                 @blur="checkUsername()">
          <span id="usernameMsg" v-text="usernameMsg"></span>
        </td>
      </tr>
      <tr class="ltr">
        <td>请输入密码</td>
        <td>
          <input class="ipt"
                 type="password"
                 v-model="loginUser.userPwd"
                 @blur="checkUserPwd()">
          <span id="userPwdMsg" v-text="userPwdMsg"></span>
        </td>
      </tr>
      <tr class="ltr">
        <td colspan="2" class="buttonContainer">
          <input class="btn1" type="button" @click="login()" value="登录">
          <input class="btn1" type="button" value="重置">
          <router-link to="/regist">
            <button class="btn1">去注册</button>
          </router-link>
        </td>
      </tr>
      </tbody>
    </table>
  </div>
</template>

<style scoped>
.ht {
  text-align: center;
  color: cadetblue;
  font-family: 幼圆;
}
.tab {
  width: 500px;
  border: 5px solid cadetblue;
  margin: 0px auto;
  border-radius: 5px;
  font-family: 幼圆;
}
.ltr td {
  border: 1px solid  powderblue;
}
.ipt {
  border: 0px;
  width: 50%;
}
.btn1 {
  border: 2px solid powderblue;
  border-radius: 4px;
  width: 60px;
  background-color: antiquewhite;
}
#usernameMsg , #userPwdMsg {
  color: gold;
}
.buttonContainer {
  text-align: center;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160

# 12.2.8 添加跨域处理器

# 12.2.8.1 什么是跨域

核心概念: 跨域(Cross-Origin)是由浏览器的同源策略(Same-Origin Policy)引起的安全限制。同源策略是浏览器最核心的安全机制,用于防止恶意网站窃取其他网站的数据。

同源的定义: 两个 URL 必须满足以下三个条件才算同源:

  1. 协议(Protocol) 相同:http vs https 不同源
  2. 主机(Host) 相同:localhost vs 127.0.0.1 不同源
  3. 端口(Port) 相同::8080 vs :3000 不同源

示例对比:

当前页面 请求目标 是否跨域 原因
http://localhost:5173/ http://localhost:8080/user/login ❌ 跨域 端口不同 (5173 ≠ 8080)
http://localhost:8080/ https://localhost:8080/api/data ❌ 跨域 协议不同 (http ≠ https)
http://localhost:8080/ http://127.0.0.1:8080/api/data ❌ 跨域 主机不同 (localhost ≠ 127.0.0.1)
http://localhost:8080/page1 http://localhost:8080/page2 ✅ 同源 协议、主机、端口完全相同
# 12.2.8.2 为什么会产生跨域

前后端分离架构的典型场景:

┌─────────────────────────────────────────────────────────────┐
│                          浏览器                              │
│  ┌───────────────────────────────────────────────────────┐  │
│  │  http://localhost:5173 (前端 Vite 服务)               │  │
│  │  ↓ 请求 HTML/CSS/JS 静态资源                          │  │
│  └───────────────────────────────────────────────────────┘  │
│         ↓ AJAX 请求数据                                     │
│  ┌───────────────────────────────────────────────────────┐  │
│  │  http://localhost:8080 (后端 Tomcat 服务)             │  │
│  │  ↓ 返回 JSON 数据                                     │  │
│  └───────────────────────────────────────────────────────┘  │
│         ⚠️ 端口不同,触发同源策略限制!                     │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13

跨域产生的根本原因:

  • 前端开发服务器(如 Vite 的 http://localhost:5173)和后端服务器(如 Tomcat 的 http://localhost:8080)端口不同
  • 浏览器检测到跨域请求时,会阻止读取响应数据,即使后端已成功返回数据

浏览器报错示例:

Access to XMLHttpRequest at 'http://localhost:8080/user/login' 
from origin 'http://localhost:5173' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
1
2
3
# 12.2.8.3 如何解决跨域

方案一:前端代理模式(开发阶段推荐)

在前端项目的 vite.config.js 中配置代理:

export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',  // 后端真实地址
        changeOrigin: true,                // 修改请求头中的 Origin
        rewrite: path => path.replace(/^\/api/, '')
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11

工作原理:

浏览器 → 请求 http://localhost:5173/api/user/login (前端服务器,同源)
       ↓
前端服务器 → 转发到 http://localhost:8080/user/login (后端服务器)
       ↓
浏览器 ← 接收数据(浏览器认为是同源请求,无跨域问题)
1
2
3
4
5

方案二:后端 CORS 过滤器(生产环境推荐)

在后端添加 CORS(Cross-Origin Resource Sharing)响应头,告诉浏览器允许跨域请求。

CorsFilter 过滤器代码:

package com.bombax.schedule.filter;

import com.bombax.schedule.common.Result;
import com.bombax.schedule.util.WebUtil;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;

/**
 * 跨域资源共享(CORS)过滤器,拦截所有请求
 * 功能:为所有 HTTP 响应添加 CORS 相关响应头,允许跨域请求
 */
@WebFilter("/*")
public class CrosFilter implements Filter {
    protected static final Logger logger = LoggerFactory.getLogger(CrosFilter.class);

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        logger.info("请求方式: {}", request.getMethod());
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        // ==================== 设置 CORS 响应头 ====================
        // 1️⃣ 允许所有来源访问(生产环境建议指定具体域名,如 "http://localhost:5173")
        response.setHeader("Access-Control-Allow-Origin", "*");
        // 2️⃣ 允许的 HTTP 方法
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE, HEAD");
        // 3️⃣ 预检请求缓存时间(单位:秒),避免频繁预检
        response.setHeader("Access-Control-Max-Age", "3600");
        // 4️⃣ 允许客户端发送的自定义请求头
        response.setHeader("Access-Control-Allow-Headers", "access-control-allow-origin, authority, content-type, version-info, X-Requested-With");

        // ==================== 处理预检请求 ====================
        // 浏览器在发送跨域 POST/PUT/DELETE 请求前,会先发送 OPTIONS 预检请求
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            // 预检请求直接返回 200 成功,不需要执行业务逻辑
            WebUtil.writeJson(response, Result.ok(null));
        } else {
            // 非预检请求,放行到后续过滤器或 Servlet
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

关键知识点:

CORS 响应头 作用 示例值
Access-Control-Allow-Origin 允许访问的源 *(所有)或 http://localhost:5173
Access-Control-Allow-Methods 允许的 HTTP 方法 GET, POST, PUT, DELETE
Access-Control-Allow-Headers 允许的自定义请求头 Content-Type, Authorization
Access-Control-Max-Age 预检请求缓存时间(秒) 3600(1 小时)

预检请求(Preflight Request):

  • 触发条件:跨域 + 非简单请求(如 POST 带 JSON 数据)
  • 请求方法:OPTIONS
  • 作用:浏览器先询问服务器是否允许跨域,服务器返回 200 后才发送真正的请求

简单请求 vs 预检请求:

简单请求(直接发送):
  - GET 查询用户列表
  - POST 表单提交(Content-Type: application/x-www-form-urlencoded)

需要预检的请求(先 OPTIONS,再发送):
  - POST JSON 数据(Content-Type: application/json)
  - PUT/DELETE 请求
  - 带自定义请求头(如 Authorization)
1
2
3
4
5
6
7
8

框架中的简化写法(Spring Boot):

@RestController
@CrossOrigin(origins = "*")  // 一个注解搞定跨域
public class UserController {
    // ...
}
1
2
3
4
5

# 12.2.9 重构 UserController

功能说明:提供用户注册、登录、用户名校验三个 RESTful 接口,使用统一结果对象 Result 返回数据。

接口列表:

接口路径 请求方法 功能说明 响应码
/user/checkUsernameUsed?username=xxx POST 校验用户名是否被占用 200 成功 / 505 占用
/user/regist POST 用户注册 200 成功 / 505 占用
/user/login POST 用户登录 200 成功 / 501 用户名错误 / 503 密码错误
package com.bombax.schedule.controller;

import com.bombax.schedule.common.Result;
import com.bombax.schedule.common.ResultCodeEnum;
import com.bombax.schedule.pojo.SysUser;
import com.bombax.schedule.service.SysUserService;
import com.bombax.schedule.service.impl.SysUserServiceImpl;
import com.bombax.schedule.util.MD5Util;
import com.bombax.schedule.util.WebUtil;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.util.Map;

/**
 * 用户模块控制器
 * 功能:处理用户注册、登录、用户名校验等请求
 */
@WebServlet("/user/*")
public class SysUserController extends BaseController {

    private SysUserService userService = new SysUserServiceImpl();

    // ==================== 1️⃣ 校验用户名是否被占用 ====================
    /**
     * 接口:POST /user/checkUsernameUsed?username=xxx
     * 功能:注册时实时校验用户名是否已被占用
     * 响应:{ "code": 200, "message": "success", "data": null }  // 可用
     *      { "code": 505, "message": "用户名被占用", "data": null } // 占用
     */
    protected void checkUsernameUsed(HttpServletRequest req, HttpServletResponse resp) {
        // 获取前端传递的用户名参数
        String username = req.getParameter("username");

        // 查询数据库中是否存在该用户名
        SysUser sysUser = userService.findByUsername(username);

        Result<Object> result = Result.ok(null);

        // 如果有响应已占用,否则响应未占用
        if (null != sysUser) {
            result = Result.build(null, ResultCodeEnum.USERNAME_USED);
        }
        // 将结果对象转换为 JSON 并响应给前端
        WebUtil.writeJson(resp, result);
    }

    // ==================== 2️⃣ 用户注册 ====================
    /**
     * 接口:POST /user/regist
     * 请求体:{ "username": "admin", "userPwd": "123456" }
     * 功能:将用户信息写入数据库,密码自动 MD5 加密
     * 响应:{ "code": 200, "message": "success", "data": null }  // 成功
     *      { "code": 505, "message": "用户名被占用", "data": null } // 失败
     */
    protected void regist(HttpServletRequest req, HttpServletResponse resp) {
        // 1. 接收客户端提交的 json 参数,并转换为 User 对象,获取信息
        SysUser registUser = WebUtil.readJson(req, SysUser.class);
        // 2. 调用服务层方法,完成注册功能
        // 2.1 将参数放入一个 SysUser 对象中,在调用 regist 方法时传入
        int rows = userService.regist(registUser);
        // 3. 根据注册结果(成功 or 失败)做页面跳转
        Result<Object> result = Result.ok(null);
        if (rows < 1) {
            result = Result.build(null, ResultCodeEnum.USERNAME_USED);
        }
        WebUtil.writeJson(resp, result);
    }

    // ==================== 3️⃣ 用户登录 ====================
    /**
     * 接口:POST /user/login
     * 请求体:{ "username": "admin", "userPwd": "123456" }
     * 功能:验证用户名和密码,返回不同响应码
     * 响应:{ "code": 200 }  // 登录成功
     *      { "code": 501 }  // 用户名不存在
     *      { "code": 503 }  // 密码错误
     */
    protected void login(HttpServletRequest req, HttpServletResponse resp) {
        // 1. 接受前端传递的用户名和密码
        SysUser sysUser = WebUtil.readJson(req, SysUser.class);

        // 2. 根据用户名查询数据库
        SysUser loginUser = userService.findByUsername(sysUser.getUsername());

        Result<Object> result;
        if (null == loginUser) {
            result = Result.build(null, ResultCodeEnum.USERNAME_ERROR);
        } else if (!MD5Util.encrypt(sysUser.getUserPwd()).equals(loginUser.getUserPwd())) {
            // 3. 判断密码是否匹配
            result = Result.build(null, ResultCodeEnum.PASSWORD_ERROR);
        } else {
            // 登录成功后,将登录的用户信息 响应给客户端
            loginUser.setUserPwd(null);
            Map<String, Object> map = Map.of("loginUser", loginUser);
            result = Result.ok(map);
        }
        WebUtil.writeJson(resp, result);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101

关键实现细节:

  1. 统一响应格式:
    public class Result {
        private Integer code;      // 响应码(200成功 / 501用户名错误 / 503密码错误 / 505用户名占用)
        private String message;    // 响应消息
        private Object data;       // 响应数据
    }
    
    1
    2
    3
    4
    5
  2. 密码安全处理:
    • 存储:注册时使用 MD5Util.encrypt() 对密码加密后存入数据库
    • 验证:登录时将前端传来的明文密码加密后与数据库密文比对
  3. 前后端数据交换:
    • 前端 → 后端:WebUtil.readJson(req, SysUser.class) 将 JSON 转换为对象
    • 后端 → 前端:WebUtil.writeJson(resp, result) 将对象转换为 JSON

# 12.2.10 删除登录校验过滤器

原因说明:

  • 传统模式使用 Cookie + Session 记录用户登录状态,需要过滤器校验 Session
  • 前后端分离模式使用 Token(如 JWT)记录状态,无需 Session
  • 因此删除登录校验过滤器,未来通过 Axios 请求拦截器 自动携带 Token 实现身份认证

Token vs Session 对比:

特性 Session Token (JWT)
存储位置 服务器内存 客户端(LocalStorage/Cookie)
可扩展性 差(多台服务器需共享 Session) 好(无状态,任意服务器可验证)
安全性 依赖 Cookie 可设置过期时间,防 CSRF
适用场景 传统 Web 应用 前后端分离、移动端

未来 Token 使用示例:

// 登录成功后保存 Token
localStorage.setItem('token', data.token)

// 请求拦截器自动添加 Token
instance.interceptors.request.use(config => {
    const token = localStorage.getItem('token')
    if (token) {
        config.headers['Authorization'] = token
    }
    return config
})
1
2
3
4
5
6
7
8
9
10
11

# 12.2.11 Header 组件实现(根据登录状态动态显示)

文件路径:src/components/Header.vue

<script setup>
import { defineUser } from '../store/userStore'
import { defineSchedule } from '../store/scheduleStore'
import { useRouter } from 'vue-router'

// 获取 Store 实例
const sysUser = defineUser()
const schedule = defineSchedule()

// 获取路由实例
const router = useRouter()

/**
 * 退出登录功能
 * 1. 重置 Store 数据(恢复初始状态)
 * 2. 跳转到登录页面
 */
function logout() {
  // 清空用户信息和日程数据
  sysUser.$reset()
  schedule.$reset()

  // 跳转到登录页(路由守卫会拦截未登录访问)
  router.push('/login')
}
</script>

<template>
  <div>
    <h1 class="ht">欢迎使用日程管理系统</h1>
    <div>
      <div class="optionDiv" v-if="sysUser.username === ''">
        <router-link to="/login">
          <button class="b1s">登录</button>
        </router-link>
        <router-link to="/regist">
          <button class="b1s">注册</button>
        </router-link>
      </div>

      <div class="optionDiv" v-else>
        欢迎 {{ sysUser.username }}
        <button class="b1b" @click="logout()">退出登录</button>
        <router-link to="/showSchedule">
          <button class="b1b">查看我的日程</button>
        </router-link>
      </div>

      <br>
    </div>
  </div>
</template>

<style scoped>
.ht {
  text-align: center;
  color: cadetblue;
  font-family: 幼圆;
}

.b1s {
  border: 2px solid powderblue;
  border-radius: 4px;
  width: 60px;
  background-color: antiquewhite;
}

.b1b {
  border: 2px solid powderblue;
  border-radius: 4px;
  width: 100px;
  background-color: antiquewhite;
}

.optionDiv {
  width: 400px;
  float: right;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

# 12.2.12 路由守卫配置(权限控制)

文件路径:src/router/router.js

import { createRouter, createWebHashHistory } from 'vue-router'
import pinia from '../pinia.js'
import { defineUser } from '../store/userStore.js'

// ✅ 关键:在路由配置文件中访问 Store,需传入 pinia 实例
const sysUser = defineUser(pinia)

// 导入组件
import Login from '../components/Login.vue'
import Regist from '../components/Regist.vue'
import ShowSchedule from '../components/ShowSchedule.vue'

// 创建路由实例
const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    {
      path: '/',
      redirect: '/login'  // 根路径重定向到登录页
    },
    {
      path: '/login',
      component: Login
    },
    {
      path: '/regist',
      component: Regist
    },
    {
      path: '/showSchedule',
      component: ShowSchedule,
      meta: { requiresAuth: true }  // ✅ 标记需要认证
    }
  ]
})

/**
 * 全局前置守卫:权限控制
 * 功能:未登录用户无法访问需要认证的页面
 */
router.beforeEach((to, from, next) => {
  // 判断目标路由是否需要认证
  if (to.path === '/showSchedule' || to.meta.requiresAuth) {
    // 检查用户是否已登录
    if (sysUser.username === '' || sysUser.uid === 0) {
      alert('您尚未登录,请先登录!')
      next('/login')  // 重定向到登录页
    } else {
      next()  // 已登录,放行
    }
  } else {
    next()  // 不需要认证的页面,直接放行
  }
})

export default router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

路由守卫工作原理:

用户访问 /showSchedule
       ↓
触发 beforeEach 守卫
       ↓
检查 sysUser.username
       │
       ├──────────────────────┐
       │                        │
   为空(未登录)          不为空(已登录)
       │                        │
  next('/login')             next()
       │                        │
  跳转登录页                 允许访问
1
2
3
4
5
6
7
8
9
10
11
12
13

最佳实践:

  1. ✅ 使用 meta.requiresAuth 标记需要认证的路由(更灵活)
  2. ✅ 在 pinia.js 中创建 Store 实例,避免循环依赖
  3. ✅ 守卫逻辑应简洁明确,避免复杂判断

# 12.3 日程管理增删改查功能

# 12.3.1 功能概述

  • 日程列表的查询和展示功能,涉及前后端数据交互、Pinia 状态管理和生命周期钩子的使用。
  • 日程的新增和修改功能,涉及两种典型的数据更新模式:
策略 新增日程 修改日程
操作流程 1. 后端创建默认记录
2. 前端刷新列表
1. 前端修改数据
2. 提交到后端
3. 刷新列表
用户体验 需要等待两次请求 修改后需手动保存
实现复杂度 简单(后端控制) 中等(前端校验)
  • 日程的删除功能是 CRUD 操作中最需要谨慎处理的,因为删除操作不可逆,实现带确认机制的删除功能。

# 12.3.2 前端组件实现

文件路径:src/components/ShowSchedule.vue

<script setup>
/* 导入 pinia 数据 */
import {defineUser} from '../store/userStore'
import {defineSchedule} from '../store/scheduleStore'
import {ref, reactive, onUpdated, onMounted} from "vue";
import request from "../utils/request.js";

// 获取 Store 实例
let sysUser = defineUser()
let schedule = defineSchedule()

/**
 * 组件挂载生命周期钩子
 * 功能:挂载完毕后,立刻查询当前用户的所有日程信息,赋值给 pinia 中
 */
onMounted(() => {
  showSchedule()
})

/**
 * 查询日程列表
 * 流程:
 * 1. 发送 GET 请求到后端
 * 2. 传递当前登录用户的 uid
 * 3. 后端返回该用户的所有日程
 * 4. 更新 Pinia Store 中的 itemList
 */
async function showSchedule() {
  try {
    const { data } = await request.get('/schedule/findAllSchedule', {
      params: { uid: sysUser.uid }
    })
    if (data.code === 200) {
      // ✅ 关键:将后端数据更新到 Pinia Store(响应式更新视图)
      schedule.itemList = data.data.itemList
    } else {
      alert('获取日程失败:' + data.message)
    }
  } catch (error) {
    console.error('查询日程异常:', error)
    alert('网络异常,请稍后重试')
  }
}

/**
 * 新增日程
 * 流程:
 * 1. 请求后端创建一条默认日程记录
 * 2. 后端返回成功后,刷新列表展示新记录
 */
async function addItem() {
  try {
    const { data } = await request.get('/schedule/addDefaultSchedule', {
      params: { uid: sysUser.uid }
    })
    if (data.code === 200) {
      // ✅ 成功后立即刷新列表(显示新增的默认记录)
      await showSchedule()
      alert('新增成功,请修改日程内容')
    } else {
      alert('添加失败:' + data.message)
    }
  } catch (error) {
    console.error('新增异常:', error)
    alert('网络异常,请稍后重试')
  }
}

/**
 * 更新日程
 * 流程:
 * 1. 根据索引获取要修改的日程对象
 * 2. 将对象以 JSON 格式发送到后端
 * 3. 后端保存成功后,刷新列表
 *
 * @param {number} index - 日程在 itemList 中的索引
 */
async function updateItem(index) {
  try {
    // 获取要更新的日程对象
    const scheduleItem = schedule.itemList[index]
    // 前端校验:确保标题不为空
    if (!scheduleItem.title || scheduleItem.title.trim() === '') {
      alert('日程标题不能为空!')
      return
    }
    // 发送 POST 请求,将整个对象作为请求体
    const { data } = await request.post('/schedule/updateSchedule', scheduleItem)
    if (data.code === 200) {
      // ✅ 更新成功后刷新列表(保证数据一致性)
      await showSchedule()
      alert('保存成功!')
    } else {
      alert('更新失败:' + data.message)
    }
  } catch (error) {
    console.error('更新异常:', error)
    alert('网络异常,请稍后重试')
  }
}

/**
 * 删除日程
 * 流程:
 * 1. 弹出确认对话框(用户二次确认)
 * 2. 确认后发送删除请求到后端
 * 3. 后端删除成功后刷新列表
 *
 * @param {number} index - 日程在 itemList 中的索引
 */
async function removeItem(index) {
  // ✅ 关键:用户确认机制(防止误删)
  if (!confirm('确定要删除该日程吗?此操作不可恢复!')) {
    return  // 用户点击取消,直接返回
  }
  try {
    // 获取要删除的日程 ID
    const sid = schedule.itemList[index].sid
    // 发送删除请求
    const { data } = await request.get('/schedule/removeSchedule', {
      params: { sid }
    })
    if (data.code === 200) {
      // ✅ 删除成功后刷新列表(移除已删除的记录)
      await showSchedule()
      alert('删除成功!')
    } else {
      alert('删除失败:' + data.message)
    }
  } catch (error) {
    console.error('删除异常:', error)
    alert('网络异常,请稍后重试')
  }
}
</script>

<template>
  <div>
    <h3 class="ht">您的日程如下</h3>
    <table class="tab" cellspacing="0px">
      <thead>
      <tr class="ltr">
        <th>编号</th>
        <th>内容</th>
        <th>进度</th>
        <th>操作</th>
      </tr>
      </thead>
      <tbody>
      <tr class="ltr" v-for="(item, index) in schedule.itemList" :key="index">
        <td>{{ index + 1 }}</td>
        <td>
          <input class="ipt" type="text" v-model="item.title"></input>
        </td>
        <td>
          <input type="radio" value="1" v-model="item.completed"> 已完成
          <input type="radio" value="0" v-model="item.completed"> 未完成
        </td>
        <td class="buttonContainer">
          <button class="btn1" @click="removeItem(index)">删除</button>
          <button class="btn1" @click="updateItem(index)">保存修改</button>
        </td>
      </tr>
      </tbody>
      <tfoot>
      <tr class="ltr buttonContainer">
        <td colspan="4">
          <button class="btn1" @click="addItem()">新增日程</button>
        </td>
      </tr>
      </tfoot>
    </table>
  </div>
</template>

<style scoped>
.ht {
  text-align: center;
  color: cadetblue;
  font-family: 幼圆;
}
.tab {
  width: 80%;
  border: 5px solid cadetblue;
  margin: 0px auto;
  border-radius: 5px;
  font-family: 幼圆;
}
.ltr td{
  border: 1px solid powderblue;
}
.ipt{
  border: 0px;
  width: 50%;
}
.btn1 {
  border: 2px solid powderblue;
  border-radius: 4px;
  width: 100px;
  background-color: antiquewhite;
}
#usernameMsg , #userPwdMsg {
  color: gold;
}
.buttonContainer {
  text-align: center;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
  • 关键知识点:
  1. 新增流程:后端创建 → 前端刷新(两次请求)
  2. 修改流程:前端修改 → 提交后端 → 前端刷新
  3. 数据校验:修改时检查标题是否为空
  4. 错误处理:使用 try-catch 捕获异常,提供用户友好提示
  5. 确认机制:使用 confirm() 弹出确认对话框,防止误删
  6. 索引 → ID 转换:通过索引获取 sid,传递给后端
  7. 刷新列表:删除成功后立即刷新,确保视图与数据库一致

# 12.3.3 后端 Controller 层实现

文件路径:SysScheduleController.java

package com.bombax.schedule.controller;

import com.bombax.schedule.common.Result;
import com.bombax.schedule.pojo.SysSchedule;
import com.bombax.schedule.service.SysScheduleService;
import com.bombax.schedule.service.impl.SysScheduleServiceImpl;
import com.bombax.schedule.util.WebUtil;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.util.List;
import java.util.Map;

/**
 * 日程管理控制器
 * URL 映射:/schedule/*
 */
@WebServlet("/schedule/*")
public class SysScheduleController extends BaseController {

    private SysScheduleService scheduleService = new SysScheduleServiceImpl();

    /**
     * 查询用户所有日程接口
     * 请求地址:GET /schedule/findAllSchedule?uid=1
     * 响应格式:{"code":200,"data":{"itemList":[...]}}
     */
    protected void findAllSchedule(HttpServletRequest req, HttpServletResponse resp) {
        // 1. 获取用户 ID(从 URL 参数中)
        int uid = Integer.parseInt(req.getParameter("uid"));

        // 2. 调用 Service 层查询该用户的所有日程
        List<SysSchedule> itemList = scheduleService.findItemListByUid(uid);

        // 3. 封装响应数据
        Map<String, Object> map = Map.of("itemList", itemList);
        Result<Object> result = Result.ok(map);

        // 4. 返回 JSON 响应
        WebUtil.writeJson(resp, result);
    }

    /**
     * 新增默认日程接口
     * 请求地址:GET /schedule/addDefaultSchedule?uid=1
     * 功能:为指定用户创建一条默认日程记录
     */
    protected void addDefaultSchedule(HttpServletRequest req, HttpServletResponse resp) {
        // 1. 获取用户 ID
        int uid = Integer.parseInt(req.getParameter("uid"));
        // 2. 调用 Service 层创建默认日程
        scheduleService.addDefault(uid);
        // 3. 返回成功响应
        WebUtil.writeJson(resp, Result.ok(null));
    }

    /**
     * 更新日程接口
     * 请求地址:POST /schedule/updateSchedule
     * 请求体:{"sid":1,"uid":1,"title":"学习Vue","completed":1}
     */
    protected void updateSchedule(HttpServletRequest req, HttpServletResponse resp) {
        // 1. 解析 JSON 请求体为 SysSchedule 对象
        SysSchedule schedule = WebUtil.readJson(req, SysSchedule.class);
        // 2. 调用 Service 层更新数据
        scheduleService.updateSchedule(schedule);
        // 3. 返回成功响应
        WebUtil.writeJson(resp, Result.ok(null));
    }

    /**
     * 删除日程接口
     * 请求地址:GET /schedule/removeSchedule?sid=1
     * 功能:根据日程 ID 删除指定日程
     */
    protected void removeSchedule(HttpServletRequest req, HttpServletResponse resp) {
        // 1. 获取要删除的日程 ID
        Integer sid = Integer.parseInt(req.getParameter("sid"));
        // 2. 调用 Service 层删除日程
        scheduleService.removeSchedule(sid);
        // 3. 返回成功响应
        WebUtil.writeJson(resp, Result.ok(null));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85

关键实现要点:

  • 使用 BaseController 实现 RESTful 风格的方法路由
  • 通过 req.getParameter() 获取查询参数
  • 统一使用 Result 对象封装响应数据

# 12.3.4 后端 Service 层实现

Service 接口:SysScheduleService.java

package com.bombax.schedule.service;

import com.bombax.schedule.pojo.SysSchedule;

import java.util.List;

/**
 * 日程管理业务接口
 * @author bombax
 * @date 2025/11/23
 */
public interface SysScheduleService {
    /**
     * 根据用户 ID 查询日程列表
     * @param uid 用户 ID
     * @return 日程列表
     */
    List<SysSchedule> findItemListByUid(int uid);

    /**
     * 新增默认日程
     * @param uid 用户 ID
     * @return 影响行数
     */
    Integer addDefault(int uid);

    /**
     * 更新日程
     * @param schedule 日程对象
     * @return 影响行数
     */
    Integer updateSchedule(SysSchedule schedule);

    /**
     * 删除日程
     * @param sid 日程 ID
     * @return 影响行数
     */
    Integer removeSchedule(Integer sid);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

Service 实现类:SysScheduleServiceImpl.java

package com.bombax.schedule.service.impl;

import com.bombax.schedule.dao.SysScheduleDao;
import com.bombax.schedule.dao.impl.SysScheduleDaoImpl;
import com.bombax.schedule.pojo.SysSchedule;
import com.bombax.schedule.service.SysScheduleService;

import java.util.List;

/**
 * 日程管理业务实现类
 */
public class SysScheduleServiceImpl implements SysScheduleService {

    private SysScheduleDao scheduleDao = new SysScheduleDaoImpl();

    @Override
    public List<SysSchedule> findItemListByUid(int uid) {
        // 直接调用 DAO 层方法(简单业务逻辑无需额外处理)
        return scheduleDao.findItemListByUid(uid);
    }

    @Override
    public Integer addDefault(int uid) {
        return scheduleDao.addDefault(uid);
    }

    @Override
    public Integer updateSchedule(SysSchedule schedule) {
        return scheduleDao.updateSchedule(schedule);
    }

    @Override
    public Integer removeSchedule(Integer sid) {
        return scheduleDao.removeSchedule(sid);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

设计说明:

  • Service 层主要处理业务逻辑(本例查询业务较简单,直接委托给 DAO 层)
  • 如果需要复杂业务处理(如数据过滤、权限校验),应在此层实现

# 12.3.5 后端 DAO 层实现

DAO 接口:SysScheduleDao.java

package com.bombax.schedule.dao;

import com.bombax.schedule.pojo.SysSchedule;

import java.sql.SQLException;
import java.util.List;

/**
 * 日程管理数据访问接口
 */
public interface SysScheduleDao {

    /**
     * 用于向数据库中增加一条日常记录
     * @param schedule 日常数据以 SysSchedule 实体类对象形式入参
     * @return 返回影响数据库记录的行数,行数为 0 说明增加失败,行数大于 0 说明增加成功
     */
    int addSchedule(SysSchedule schedule) throws SQLException;

    /**
     * 查询所有用户的所有日程
     * @return 将所有日志放入一个 List<SysSchedule> 集合中返回
     */
    List<SysSchedule> findAll() throws Exception;

    /**
     * 根据用户 ID 查询日程列表
     * @param uid 用户 ID
     * @return 日程列表
     */
    List<SysSchedule> findItemListByUid(int uid);

    Integer addDefault(int uid);

    Integer updateSchedule(SysSchedule schedule);

    /**
     * 根据日程 ID 删除日程
     * @param sid 日程 ID
     * @return 影响行数
     */
    Integer removeSchedule(Integer sid);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

DAO 实现类:SysScheduleDaoImpl.java

package com.bombax.schedule.dao.impl;

import com.bombax.schedule.dao.BaseDAO;
import com.bombax.schedule.dao.SysScheduleDao;
import com.bombax.schedule.pojo.SysSchedule;

import java.sql.SQLException;
import java.util.List;

/**
 * 日程管理数据访问实现类
 */
public class SysScheduleDaoImpl extends BaseDAO implements SysScheduleDao {
    @Override
    public int addSchedule(SysSchedule schedule) throws SQLException {
        String sql = "insert into sys_schedule values(DEFAULT, ?, ?, ?)";
        return baseUpdate(sql, schedule.getUid(), schedule.getTitle(), schedule.getCompleted());
    }

    @Override
    public List<SysSchedule> findAll() throws Exception {
        String sql = "select sid, uid, title, completed from sys_schedule";
        return baseQuery(SysSchedule.class, sql);
    }

    @Override
    public List<SysSchedule> findItemListByUid(int uid) {
        // SQL 查询:根据用户 ID 查询所有日程
        String sql = "select sid, uid, title, completed from sys_schedule where uid = ?";
        // 调用 BaseDao 封装的查询方法(自动映射为 SysSchedule 对象)
        return baseQuery(SysSchedule.class, sql, uid);
    }

    @Override
    public Integer addDefault(int uid) {
        // SQL:插入一条默认日程记录
        String sql = "insert into sys_schedule values(DEFAULT, ?, '请输入日程', 0)";
        return baseUpdate(sql, uid);
    }

    @Override
    public Integer updateSchedule(SysSchedule schedule) {
        // SQL:根据日程 ID 更新标题和完成状态
        String sql = "update sys_schedule set title = ?, completed = ? where sid = ?";
        return baseUpdate(sql, schedule.getTitle(), schedule.getCompleted(), schedule.getSid());
    }

    @Override
    public Integer removeSchedule(Integer sid) {
        // SQL:根据主键删除记录
        String sql = "delete from sys_schedule where sid = ?";
        return baseUpdate(sql, sid);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

三层架构总结:

层级 职责 本例实现
Controller 接收请求、返回响应 获取参数、调用 Service、封装 JSON
Service 业务逻辑处理 简单委托给 DAO(无复杂业务)
DAO 数据库操作 执行 SQL 查询、返回实体对象列表

CRUD 操作总结:

操作 HTTP 方法 URL 请求参数/请求体 SQL 语句
查询 GET /findAllSchedule ?uid=1 SELECT
新增 GET /addDefaultSchedule ?uid=1 INSERT
修改 POST /updateSchedule JSON 对象 UPDATE
删除 GET /removeSchedule ?sid=1 DELETE

# 12.3.6 删除功能的安全性考虑

前端安全措施:

  1. ✅ 用户确认机制:使用 confirm() 防止误操作
  2. ✅ 错误提示:删除失败时给出明确提示
  3. ✅ 立即刷新:删除成功后刷新列表,避免显示已删除数据

后端安全措施(可选优化):

  1. ⚠️ 权限校验:确认当前用户是否有权删除该日程
    // 伪代码示例
    SysSchedule schedule = scheduleDao.findById(sid);
    if (schedule.getUid() != currentUserId) {
        throw new PermissionDeniedException("无权删除他人日程");
    }
    
    1
    2
    3
    4
    5
  2. ⚠️ 软删除:使用标记删除而非物理删除(可恢复)
    -- 添加 deleted 字段
    ALTER TABLE sys_schedule ADD COLUMN deleted TINYINT DEFAULT 0;
    
    -- 软删除(标记为已删除)
    UPDATE sys_schedule SET deleted = 1 WHERE sid = ?;
    
    -- 查询时过滤已删除记录
    SELECT * FROM sys_schedule WHERE deleted = 0;
    
    1
    2
    3
    4
    5
    6
    7
    8
  3. ⚠️ 级联删除:如果有关联数据,需考虑级联处理
    -- 删除日程前先删除相关评论(假设有评论表)
    DELETE FROM schedule_comments WHERE schedule_id = ?;
    DELETE FROM sys_schedule WHERE sid = ?;
    
    1
    2
    3

# 12.4 日程管理项目总结

# 12.4.1 完整功能清单

功能模块 实现功能 HTTP 方法 前端技术 后端技术
用户认证 登录 POST Pinia、Router 守卫 Servlet、MD5
注册 POST 表单校验 JDBC、参数校验
登出 - $reset()、路由跳转 -
日程管理 查询列表 GET onMounted、Axios SELECT 查询
新增日程 GET 刷新列表 INSERT 插入
修改日程 POST 双向绑定 UPDATE 更新
删除日程 GET confirm 确认 DELETE 删除

# 12.4.2 关键技术点回顾

前端技术要点:

  1. ✅ Pinia 状态管理:跨组件共享用户信息和日程数据
  2. ✅ 路由守卫:未登录用户无法访问日程页面
  3. ✅ 生命周期钩子:onMounted 自动加载数据
  4. ✅ 异步请求:Axios 实现前后端数据交互
  5. ✅ 错误处理:try-catch 捕获异常,提供友好提示

后端技术要点:

  1. ✅ 三层架构:Controller → Service → DAO 分层设计
  2. ✅ RESTful API:统一的接口设计规范
  3. ✅ 统一响应格式:Result 对象封装响应数据
  4. ✅ 密码安全:MD5 加密存储密码
  5. ✅ 参数校验:防止空值和非法数据

# 12.4.3 项目优化建议

功能增强:

  1. 🚀 分页查询:日程数量多时实现分页加载
  2. 🚀 搜索过滤:按标题、状态搜索日程
  3. 🚀 批量操作:批量删除、批量标记完成
  4. 🚀 日程分类:增加日程分类或标签功能

用户体验提升:

  1. 🎨 加载动画:请求过程中显示 loading 效果
  2. 🎨 乐观更新:修改后先更新 UI,再同步后端
  3. 🎨 Toast 提示:使用 Element Plus 的 Message 替代 alert
  4. 🎨 表单验证:使用 Element Plus 的 Form 组件

安全性增强:

  1. 🔐 JWT Token:替代 Session,支持无状态认证
  2. 🔐 权限控制:确保用户只能操作自己的日程
  3. 🔐 防 XSS 攻击:对用户输入进行转义处理
  4. 🔐 防 SQL 注入:使用预编译语句(已实现)

性能优化:

  1. ⚡ 数据缓存:使用 Pinia 持久化插件缓存数据
  2. ⚡ 防抖节流:对搜索、保存等操作进行防抖处理
  3. ⚡ 懒加载:路由组件按需加载
  4. ⚡ 数据库索引:在 uid、sid 字段上建立索引

# 13. 微头条项目开发

微头条项目开发源码 (opens new window)

上次更新: 2026/01/21, 23:09:47
第八章 前端工程化-中
POJO 概念

← 第八章 前端工程化-中 POJO 概念→

最近更新
01
第八章 前端工程化-中
12-11
02
第七章 前端工程化-上
12-04
03
第四章 XML、Tomcat、HTTP
11-04
更多文章>
Theme by Vdoing | Copyright © 2024-2026 bombax | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式