vite创建vue3工程化项目:

接着下载所需要的依赖:

vite的项目结构:


SFC组件(单文件组件):
简单来说就是把css文件,html文件,js文件都放入一个文件中,这个文件的后缀是.vue
工程文件之间的关系:
.html文件是整个项目的入口,其中引入了mian.js文件,main.js文件又引入App.vue文件,而这个vue文件之中就有所有后缀为vue的文件


css样式导入方式:
<script setup>
//import './style/test.css'
//或者在style标签中导入
</script>
<template>
<!--语法上,要求template中只能有一个以及子标签-->
<!--css导入方式
1.在.vue文件中的style标签中
2.将css样式保存到独立的css文件中,有需要就导入
1 script 标签中可以导入
2 在style标签中可以导入
3 如果某个样式需要在所有vue文件中生效,那么可以在main.js中导入
-->
<div>
<span class="s1">你好</span>
</div>
</template>
<style scoped>
@import './style/test.css'
</style>
响应式数据和setup语法糖:
<script setup>
import {ref} from 'vue'
let counter = ref(10)
/*
响应式数据:在数据变化时,vue框架会将变量最新的值更新到dom树中,页面数据就是实时最新的
非响应式数据:在数据变化时,vue框架不会将变量最新的值实时更新到dom树中,页面数据就不是实时最新的
vue3中,数据要经过ref/reactive函数的处理才是响应式的
ref/reactive函数,vue框架给我们提供方法,导入就可以使用
ref处理的响应式数据,在操作时需要注意:
在script标签中,操作ref的响应式数据需要通过.value的方式操作
在template标签中,操作ref的响应式数据不需要通过.value
*/
//定义一些要展示到html上的一些数据
//让counter自增的方法
function counterIncr(){
counter.value++;
}
//让counter自减的方法
function counterDecr(){
counter.value--;
}
</script>
<template>
<div>
<button @click="counterIncr">+</button>
<span v-text="counter"></span>
<button @click="counterDecr">-</button>
</div>
</template>
<style scoped>
</style>
setup语法糖的意思就是:
在script标签中加入setup这个关键字,然后就只用在script标签中写一些必要的东西:导入的方法,函数之类的
插值表达式:
<script setup>
//插值表达式:语法{{数据名字/函数/对象调用API}}
//插值表达式不依赖标签,没有标签可以单独使用
//定义一些常见类型的数据
let msg = "hello vue3"
let getMsg = () =>{
return "hello vue3 message"
}
let age = 19
let bee = "蜜 蜂"
let carts = [{name:"可乐",price:3,number:10},{name:"薯片",price:6,number:8}]
//定义一个购物车总金额的方法
function conpute(){
let count = 0
for(let index in carts){
count += carts[index].price * carts[index].number
}
return count;
}
</script>
<template>
<div>
<h1>{{msg}}</h1>
msg 的值为{{getMsg()}}<br> <!--这里面的函数会直接被替换成这个函数的结果-->
<!--插值表达式支持一些常见的运算符-->
年龄:{{age}},是否成年:{{age > 18 ? "是":"否"}}
<!--插值表达式中支持对象调用一些API-->
<br>
{{bee.split('').reverse().join('')}}
<br>
{{conpute()}}
</div>
</template>
文本渲染命令:
<script setup>
//文本渲染命令
//v-text v-text 不识别html结构的文本
//v-html 可以识别文本中的html代码的命令
//1.命令必须依赖标签,在开始标签中使用
let msg = "hello vue3";
//2.文本渲染命令支持字符串模板,
let haha = "哈哈";
let msg2 = `hello ${haha}`
//3.文本渲染命令支持常见的运算符
let age = 19;
//4.支持常见api的调用
let bee = "蜜 蜂";
//5.命令中支持函数的调用
let getMsg = ()=>{
return "hello"
}
</script>
<template>
<div>
<h1 v-text="msg"></h1>
<h1 v-text="msg2"></h1>
<h1 v-text="`你好 ${haha}`"></h1>
<h1 v-text="age >= 18? '成年':'未成年'"></h1>
<h1 v-text="bee.split('').reverse().join('')"></h1>
<h1 v-text=getMsg()></h1>
</div>
属性渲染命令:
<script setup>
/*
属性渲染命令
v-bind 将数据绑定到元素的属性上
写法: v-bind:属性名="数据名"
或者: :属性名="数据名"
*/
const data ={
logo: "http://www.atguigu.com/images/index_new/logo.png",
name:"尚硅谷",
url:"http://www.atguigu.com"
}
</script>
<template>
<div>
<a v-bind:href="data.url">
<img v-bind:src="data.logo" v-bind:title="data.name">
</a>
</div>
</template>
事件渲染命令:
<script setup>
import {ref} from 'vue'
//事件渲染命令
/*写法:
v-on:事件名称="函数名()"
可以简写成:@事件名
*/
let counter = ref(1);
function fun1(){
alert("hi");
}
function fun2(){
return counter.value++
}
function fun3(event){
let flag = confirm("确定要访问目标链接吗")
if(!flag){
//原生js编码方式阻止组件的默认行为
event.preventDefault()
}
}
function fun4(){
alert("超链接被点击了")
}
</script>
<template>
<div>
<!--事件绑定的写法-->
<button v-on:click="fun1()">hello</button>
<button v-on:click="fun2()">+</button>{{counter}}
<!--内联事件处理器:意思就是如果函数的函数体很简单,也可以直接把函数体写进事件里面-->
<button v-on:click="counter++">+</button>{{counter}}
<!--事件的修饰符 .once事件只绑定一次,意思就是事件只生效一次-->
<button v-on:click.once="counter++">+</button>{{counter}}
<!--时间修饰符 .prevent修饰符阻止组件的默认行为-->
<br>
<a href="http://www.atguigu.com" v-on:click="fun3($event)">尚硅谷</a> <!--原生的js代码就会问要不要访问,使用修饰符的话就直接阻止了-->
<a href="http://www.atguigu.com" v-on:click.prevent="fun4()">尚硅谷</a>
</div>
</template>
响应式数据的处理方式:
<script setup>
import { ref,reactive } from "vue";
/*
让一个普通数据转换为响应式数据的两种方式
1 ref函数 更适合单个变量
在script标签中操作ref响应式数据要通过.value
在template中操作ref响应式数据不需要.value
2 reactive函数 更适合对象
在script , template中操作reactive响应式数据都直接使用对象名.属性名的方式即可
toRef函数 将reactive响应式数据中某个属性转换为ref响应式数据
toRefs函数 同时将reactive响应式数据中的多个属性转换为reg响应式数据
*/
let counter = ref(0);
let person = reactive({
name:"",
age:15
})
function ageIncr(){
person.age++
}
function incr(){
counter.value++;
}
</script>
<template>
<div>
<button @click="incr()">+</button>{{counter}}
<br>
<button @click="ageIncr()">+</button>{{person.age}}
</div>
</template>
条件渲染:
<script setup>
//v-if 条件渲染,意思是先判断条件是否为真,如果是真,就会写入标签中的内容,反之就不会(如果不显示,在dom树中也不会存在)
//v-else 自动和前一个v-if 做取反操作
//v-show="" 数据为true 元素则展示在页面上,否则不展示(但是元素还是会在dom树中,只是不显示)
import {ref} from 'vue'
let flag = ref(true)
</script>
<template>
<div>
<h1 style="color: pink" v-if="flag">哦齁齁齁齁齁齁齁齁</h1>
<h1 style="color:purple" v-else>哟吼吼吼吼吼吼吼吼</h1>
<h1 v-show="flag">桀桀桀桀桀桀桀桀桀桀</h1>
<button @click="flag = !flag">change</button>
</div>
</template>
列表渲染:
v-for 格式是 (当前项, 索引)
一、列表渲染的核心:v-for 指令
Vue3 中通过 v-for 指令实现列表渲染,它的作用类似于 JavaScript 的 for 循环,能将数组 / 对象等可迭代数据批量渲染成 DOM 元素,且完全适配 Vue3 的响应式系统。
1. 基本用法(渲染数组)
这是最常用的场景,语法格式:
vue
<元素 v-for="(item, index) in 数组" :key="唯一标识">
<!-- 渲染内容 -->
</元素>
item:循环的当前项(必选)index:当前项的索引(可选,从 0 开始)in:也可以用of(和 JavaScript 遍历语法一致),推荐用in:key:必须绑定,用于标识节点唯一性
完整示例(Vue3 Setup 语法糖):
vue
<template>
<ul>
<!-- 遍历数组,渲染每一项 -->
<li v-for="(fruit, index) in fruitList" :key="fruit.id">
索引:{{ index }} → 名称:{{ fruit.name }}
</li>
</ul>
</template>
<script setup>
// 响应式数组(Vue3 推荐用 ref 定义简单数组/对象)
import { ref } from 'vue'
const fruitList = ref([
{ id: 1, name: '苹果' },
{ id: 2, name: '香蕉' },
{ id: 3, name: '橙子' }
])
</script>
渲染结果:
<ul>
<li>索引:0 → 名称:苹果</li>
<li>索引:1 → 名称:香蕉</li>
<li>索引:2 → 名称:橙子</li>
</ul>
2. 关键:key 属性的作用
key 是 Vue 虚拟 DOM 算法的核心依赖,必须为 v-for 项绑定唯一的 key:
-
作用:Vue 通过
key识别每个节点的唯一性,在列表更新时(增 / 删 / 改),只重新渲染变化的节点,而非全部重绘,提升性能且避免渲染错误。 -
正确用法:绑定数据的唯一标识 (如 id、手机号等),而非
index(索引)。 -
错误示例(慎用 index 当 key)
- ...
原因:如果删除列表第一项,后续所有项的 index 都会变化,Vue 会误以为所有节点都变了,导致不必要的重绘,甚至出现表单值错乱等问题。
3. 渲染对象
v-for 也能遍历对象的属性,语法:
vue
<元素 v-for="(value, key, index) in 对象" :key="key">
<!-- value:属性值,key:属性名,index:索引(可选) -->
</元素>
示例:
vue
<template>
<div>
<p v-for="(value, key, index) in userInfo" :key="key">
索引{{ index }} → {{ key }}:{{ value }}
</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const userInfo = ref({
name: '张三',
age: 20,
gender: '男'
})
</script>
这里v-for标签中,两个key的含义:
第一个 key:v-for 解构中的 key(遍历对象的键名)
- 位置:
(value, key, index)中的key - 含义:这是
v-for遍历对象 时的对象属性名(键名)。 - 举例:如果
userInfo = { name: '张三', age: 20, gender: '男' },那么这个key依次会是name、age、gender。 - 补充:
v-for遍历对象的完整解构顺序是(值, 键名, 索引),对应(value, key, index)。
(2)第二个 key::key 绑定的 Vue 列表唯一标识
- 位置:
:key="key"中的key(:key是v-bind:key的简写) - 含义:这是 Vue 要求的列表渲染唯一标识,用于让 Vue 精准追踪每个列表项的身份,提升列表更新的性能,避免渲染错误。
- 作用:这里把 "对象的键名" 赋值给了
:key,利用对象键名的唯一性作为列表项的唯一标识。
渲染结果:
<div>
<p>索引0 → name:张三</p>
<p>索引1 → age:20</p>
<p>索引2 → gender:男</p>
</div>
4. 渲染数字(特殊场景)
v-for 遍历数字时,会从 1 开始到指定数字结束:
vue
<template>
<div v-for="n in 5" :key="n">第 {{ n }} 个元素</div>
</template>
渲染结果:
html
预览
<div>第 1 个元素</div>
<div>第 2 个元素</div>
<div>第 3 个元素</div>
<div>第 4 个元素</div>
<div>第 5 个元素</div>
二、列表更新的响应式处理
Vue3 能自动检测数组 / 对象的变化并更新视图,但需遵循特定规则:
1. 数组更新检测
- 能直接检测的方法(变异方法) :这些方法会修改原数组,Vue 能自动响应:
push()(尾部加)、pop()(尾部删)、shift()(头部删)、unshift()(头部加)、splice()(增删改)、sort()(排序)、reverse()(反转)。
示例:
// 给 fruitList 新增一项
fruitList.value.push({ id: 4, name: '葡萄' })
- 无法直接检测的操作 :① 直接通过索引修改数组元素:
fruitList.value[0] = { id: 1, name: '红苹果' }② 直接修改数组长度:fruitList.value.length = 2解决方案:
-
-
用
splice()替代:fruitList.value.splice(0, 1, { id: 1, name: '红苹果' }) -
用 Vue 提供的
set方法:import { ref, set } from 'vue'
set(fruitList.value, 0, { id: 1, name: '红苹果' })
-
-
-
替换原数组(推荐):用
filter/map等返回新数组的方法,直接赋值:javascript运行// 过滤出名称包含"果"的项
fruitList.value = fruitList.value.filter(item => item.name.includes('果'))
-
2. 对象更新检测
-
能直接检测:修改已有属性的值(如
userInfo.value.age = 21)。 -
无法直接检测:新增 / 删除属性(如
userInfo.value.address = '北京')。解决方案 :用set/delete方法:javascript运行import { ref, set, deleteProperty } from 'vue'
// 新增属性
set(userInfo.value, 'address', '北京')
// 删除属性
deleteProperty(userInfo.value, 'gender')
三、常见避坑点
-
v-for 与 v-if 不要一起用 :
<template>v-for优先级高于v-if,会先循环所有项再判断,性能差。解决方案:用计算属性先过滤数组,再渲染:vue - {{ fruit.name }} </template> <script setup> import { ref, computed } from 'vue'
-
避免在 v-for 中直接修改数据:循环内的操作(如点击修改当前项)建议通过方法传递参数,而非直接修改,保证数据流向清晰。
const fruitList = ref([/* 数据 */])
// 计算属性:过滤出名称不是"香蕉"的项
const filterFruits = computed(() => {
return fruitList.value.filter(item => item.name !== '香蕉')
})
</script>
总结
- Vue3 中
v-for是列表渲染的核心指令,支持遍历数组(常用)、对象、数字,语法为(值, 键/索引) in 可迭代数据。 - 必须为
v-for项绑定唯一的 key(推荐数据自身的 id,而非 index),避免渲染错误和性能问题。 - 列表更新需遵循响应式规则:数组用变异方法 / 替换新数组,对象新增 / 删除属性用
set/deleteProperty;避免v-for与v-if混用,优先用计算属性过滤数据。
双向绑定:
单向绑定 v-bind
1. 定义
数据只能从逻辑层(响应式数据) 流向视图层(DOM) ,数据变化会更新视图,但视图手动修改(比如输入框输入内容)不会反向修改数据。这是 Vue 的默认绑定方式,也是最基础、最常用的绑定方式。
2. 常用实现方式
-
{``{ 插值表达式 }}:用于文本内容渲染 -
<template>v-bind:(简写:):用于绑定 HTML 属性(如value、class、style、src等)单向绑定文本:{{ msg }}</template> <script setup> import { ref } from 'vue'<!-- 2. v-bind绑定输入框value属性 --> <input type="text" :value="msg" placeholder="试试输入内容"> <!-- 按钮修改数据,验证视图同步更新 --> <button @click="changeMsg">修改数据</button> <!-- 展示当前数据值,验证视图修改不会改数据 --> <div>当前数据值:{{ msg }}</div>// 定义响应式数据
const msg = ref('初始内容')// 修改数据的方法
const changeMsg = () => {
msg.value = '数据被修改了'
}
</script>
双向绑定 v-model
1. 定义
数据既能从逻辑层 流向视图层 ,也能从视图层 反向流向逻辑层。视图的手动修改会实时更新数据,数据变化也会实时更新视图。
2. 核心实现:v-model 指令
v-model 是 Vue 提供的语法糖 ,本质是「v-bind:value(单向绑定) + v-on:input(监听输入事件)」的组合。
等价转换(以输入框为例):
<!-- 简写形式 -->
<input v-model="msg">
<!-- 等价的完整形式 -->
<input :value="msg" @input="msg = $event.target.value">
3. 适用场景
主要用于表单元素 (input、textarea、select、checkbox、radio等),实现表单交互时的数据实时同步。
<template>
<!-- v-model实现双向绑定 -->
<input type="text" v-model="msg" placeholder="试试输入内容">
<!-- 展示当前数据值,验证视图修改会同步改数据 -->
<div>当前数据值:{{ msg }}</div>
<!-- 按钮修改数据,验证数据修改同步改视图 -->
<button @click="changeMsg">修改数据</button>
</template>
<script setup>
import { ref } from 'vue'
// 定义响应式数据
const msg = ref('初始内容')
// 修改数据的方法
const changeMsg = () => {
msg.value = '数据被修改了'
}
</script>
计算属性:
一、什么是计算属性?
简单来说,计算属性是 Vue 提供的一种基于现有响应式数据派生新数据的方式。你可以把它理解成一个 "智能变量":
- 它的值依赖于 Vue 实例中的响应式数据(比如
data里的属性); - 只有当依赖的数据源发生变化时,它才会重新计算;
- 计算结果会被缓存,多次访问不会重复计算,能提升性能。
二、基本用法(Vue 2/3 选项式 API)
先看一个最简单的示例,比如拼接用户的姓和名:
vue
<template>
<div>
<!-- 直接使用计算属性,不用加括号 -->
<p>完整姓名:{{ fullName }}</p>
</div>
</template>
<script>
export default {
// 响应式数据源
data() {
return {
firstName: "张",
lastName: "三"
};
},
// 计算属性核心
computed: {
// 定义计算属性 fullName(只读型,最常用)
fullName() {
// 依赖 firstName 和 lastName,只有这两个值变了,才会重新执行
return this.firstName + " " + this.lastName;
}
}
};
</script>
运行效果 :页面显示 完整姓名:张 三;如果修改 firstName 为 "李",fullName 会自动变成 "李 三"。
数据监听器:
什么是 Vue3 的数据监听器?
Vue3 中的watch(数据监听器)是用来监听响应式数据变化 的核心 API,当被监听的响应式数据(如ref、reactive声明的数据)发生改变时,会自动触发你预先定义的回调函数,让你可以在数据变化后执行自定义逻辑(比如发起请求、更新 DOM、执行计算等)。
简单类比:就像你盯着一个水杯,当水杯里的水变少(数据变化),你就立刻加水(执行回调)。
import { reactive, ref, watch } from "vue"
//监听单个ref数据
const count = ref(0)
watch(
count,//要监听的目标
(newVlaue,oldValue)=>{
console.log(`${oldValue}变成了${newVlaue}`);
}
)
<button @click="count++" >+</button>{{count}}
//监听reactive对象的单个属性
//如果监听reactive声明的对象的某个属性,需要把监听目标写成函数返回值的形式(因为要获取属性的实时值)
const user = reactive({
name:"张三",
age:20
})
watch(
()=>user.name,
(newValue,oldValue)=>{
console.log(`${oldValue}变成了${newValue}`);
}
)
<p>姓名:{{user.name}}</p>
<button @click="user.name = '李四'">改变名字</button>
<br>
//监听整个reactive对象
//监听整个reactive对象时,无需写函数,直接传对象即可,且默认开启深度监听(即使对象嵌套层级深,内部属性变化也能监听到)
const user1 = reactive({
name:'王五',
info:{
age:20,
address:'北京'
}
})
watch(
user1,
(newValue,oldValue) =>{
console.log(`'user1对象发生变化':${newValue.info.age}`);
}
)
<br>
年龄:{{user1.info.age}}
<br>
<button @click="user1.info.age++">增加年龄</button>
//监听多个数据源
//可以把监听目标写成数组,同时监听多个数据,任意一个数据变化都会触发回调,回调的newVal和oldVal也对应数组形式。
const count1 = ref(0)
const name1 = ref("赵六")
//同时监听count和name
watch(
[count1,()=>name1],
([newCount1,newName1],[oldCount1,oldName1]) =>{
console.log(`count1变化${oldCount1}->${newCount1}`);
console.log(`name1变化${oldName1}->${newCount1}`);
}
)
<br>
count1值:{{count1}}
<button @click="count1++">增加count1的值</button>
name1:{{name1}}
<button @click="name1='妄竹'">改名</button>
//立即执行
//默认情况下,watch只会在数据第一次变化后触发回调;如果设置immediate: true,则组件挂载时就会立即执行一次回调(适合初始化时就需要执行的逻辑,比如监听路由参数时初始化数据)
const count2 = ref(0)
watch(
count2,
(newVal) => {
console.log('count2的值:', newVal)
},
{ immediate: true } // 立即执行
)
count2的值:{{count2}}<br>
<button @click="count2++">增加count2的值</button>
//深度监听
//当监听的是ref声明的复杂类型数据(如对象、数组)时,默认只能监听到引用地址的变化,无法监听到内部属性的变化;设置deep: true可以开启深度监听
//reactive对象的 watch 默认开启deep: true,无需手动设置;ref对象需要手动设置
const user2 = ref({
name:"张三",
info:{
age:20,
address:'北京'
}
})
//// 监听ref对象,开启deep: true
watch(
user2,//监听整个user2对象
(newVal) =>{
console.log(`user2的年龄:${user2.value.info.age}`);
},
{deep:true}
)
<br><br>
<p>user2的年龄:{{user2.info.age}}</p><button @click="user2.info.age++">+</button>
//停止监听
//watch调用后会返回一个停止函数,执行该函数可以停止监听(适合只需要临时监听的场景,比如弹窗关闭后停止监听)
const count3 = ref(0)
const stopWatch = watch(
count3,
(newVal) => {
console.log('count:',newVal)
if(newVal > 5){
stopWatch()
console.log("已停止监听")
}
}
)
<p>count3的值:{{count3}}</p>
<button @click="count3++">+</button>
ref和reactive的watch函数写法的不同之处:
一、核心差异总览
|--------------------|-------------------------|-------------------------|------------------------------|
| 对比维度 | ref(基础类型:数字 / 字符串等) | ref(复杂类型:对象 / 数组) | reactive(对象) |
| 监听目标写法 | 直接传 ref 变量 | 直接传 ref 变量 | 单个属性:传() => 属性 ;整个对象:直接传对象 |
| 深度监听默认行为 | 无需深度监听(无嵌套属性) | 默认关闭(需手动设deep: true ) | 默认开启(无需手动设置) |
| 新旧值(oldVal/newVal) | 能正常区分新旧值 | 需开deep 才触发,新旧值为同一引用 | 监听整个对象:新旧值同一引用;监听单个属性:可正常区分 |
二、具体写法对比(完整可运行示例)
1. 监听 ref(基础类型)------ 最简单的写法
ref 声明的基础类型(数字、字符串等),直接传变量即可,新旧值能正常获取。
vue
<template>
<div style="padding: 20px;">
<p>计数:{{ count }} <button @click="count++">+1</button></p>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
// 写法:直接传ref变量
watch(count, (newVal, oldVal) => {
console.log(`ref基础类型:${oldVal} → ${newVal}`) // 输出:0→1、1→2...
})
</script>
2. 监听 ref(复杂类型:对象)------ 必须手动开深度监听
ref 包裹的对象 / 数组是 "浅层响应式" 的,默认只监听ref.value的引用变化(比如重新赋值),需手动开deep: true才能监听内部属性。
vue
<template>
<div style="padding: 20px;">
<p>年龄:{{ user.value.age }} <button @click="user.value.age++">+1</button></p>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const user = ref({ name: '张三', age: 20 })
// 写法:直接传ref变量 + 手动设置deep: true
watch(user, (newVal, oldVal) => {
// 注意:新旧值是同一对象引用(无法区分旧值)
console.log(`ref复杂类型:年龄变为${newVal.age}`)
}, { deep: true }) // 关键:必须开深度监听,否则修改age不触发
</script>
3. 监听 reactive(单个属性)------ 必须传函数返回值
reactive 是 "代理对象",直接写user.age会传递属性值 (比如 20)而非响应式引用,watch 监听不到变化;需用函数() => user.age实时获取响应式属性。
vue
<template>
<div style="padding: 20px;">
<p>年龄:{{ user.age }} <button @click="user.age++">+1</button></p>
</div>
</template>
<script setup>
import { reactive, watch } from 'vue'
const user = reactive({ name: '张三', age: 20 })
// 写法:传函数返回属性值(不能直接写user.age)
watch(() => user.age, (newVal, oldVal) => {
console.log(`reactive单个属性:${oldVal} → ${newVal}`) // 正常显示新旧值
})
</script>
4. 监听整个 reactive 对象 ------ 直接传对象,默认深监听
监听整个 reactive 对象时,无需写函数,直接传对象即可,且默认开启深度监听(嵌套属性变化也能触发)。
vue
<template>
<div style="padding: 20px;">
<p>嵌套年龄:{{ user.info.age }} <button @click="user.info.age++">+1</button></p>
</div>
</template>
<script setup>
import { reactive, watch } from 'vue'
const user = reactive({
name: '张三',
info: { age: 20 } // 嵌套属性
})
// 写法:直接传reactive对象
watch(user, (newVal, oldVal) => {
// 注意:新旧值是同一引用,无法区分旧值
console.log(`reactive整个对象:年龄变为${newVal.info.age}`)
})
// 无需设置deep: true,默认已开启
</script>
watch 和 watcheffect函数:
核心差异对比
表格
|----------|-----------------------|---------------------|
| 对比维度 | watch | watchEffect |
| 监听方式 | 显式指定监听目标(主动) | 隐式收集依赖(被动) |
| 数据源指定 | 必须传监听目标(变量 / 函数 / 数组) | 无需传目标,自动收集回调中的响应式数据 |
| 新旧值获取 | 能获取newVal 和oldVal | 无法获取,只能拿到最新值 |
| 执行时机 | 默认惰性(数据变化才执行) | 默认立即执行(挂载时执行一次) |
| 适用场景 | 需精准监听特定数据、需区分新旧值 | 副作用依赖多个数据、无需区分新旧值 |
| 停止监听 | 返回停止函数,逻辑一致 | 返回停止函数,逻辑一致 |
| 清理副作用 | 需配合immediate / 手动处理 | 支持回调内返回清理函数(更便捷) |
用 watchEffect 实现(隐式监听,无新旧值)
vue
<template>
<div style="padding: 20px;">
<p>计数:{{ count }} <button @click="count++">+1</button></p>
<p>日志:{{ log }}</p>
</div>
</template>
<script setup>
import { ref, watchEffect } from 'vue'
const count = ref(0)
const log = ref('')
// watchEffect:无需指定监听目标,自动收集count
watchEffect(() => {
// 回调中用到了count.value,自动监听count
log.value = `当前count值是:${count.value}`
// 无法获取oldVal,只能拿到最新值
})
</script>
多数据源监听对比
需求:同时监听 "计数" 和 "姓名" 变化,更新日志。
(1)用 watch 实现(显式指定多个目标)
vue
<script setup>
import { ref, reactive, watch } from 'vue'
const count = ref(0)
const user = reactive({ name: '张三' })
const log = ref('')
// watch:数组形式指定多个监听目标
watch(
[count, () => user.name], // 明确监听count和user.name
([newCount, newName], [oldCount, oldName]) => {
log.value = `count:${oldCount}→${newCount} | 姓名:${oldName}→${newName}`
}
)
</script>
(2)用 watchEffect 实现(自动收集多个依赖)
vue
<script setup>
import { ref, reactive, watchEffect } from 'vue'
const count = ref(0)
const user = reactive({ name: '张三' })
const log = ref('')
// watchEffect:自动收集count和user.name
watchEffect(() => {
// 回调中用到了count.value和user.name,自动监听这两个数据
log.value = `当前count:${count.value} | 当前姓名:${user.name}`
// 无法区分新旧值,只能拿到最新状态
})
</script>
vue的生命周期:

Vue3 的生命周期指的是组件从创建、挂载、更新到销毁的整个生命周期过程,每个阶段都提供了对应的 "钩子函数"(生命周期钩子),让你能在特定时机执行自定义逻辑
Vue3 的生命周期可分为 4 个核心阶段 + 2 个特殊阶段,整体执行顺序如下:

二、核心生命周期钩子详解
1. 核心阶段(所有组件通用)
表格
|---------------|------------------------------------------------|----------------------------------------------|
| 生命周期钩子 | 执行时机 | 核心作用 |
| beforeCreate | 组件实例刚被创建,数据响应式、事件绑定还未初始化 | 几乎不用(无可用数据 / 方法) |
| created | 组件实例创建完成,data/methods/computed 等已初始化,但 DOM 未生成 | 异步请求数据(无需 DOM 时)、初始化非响应式数据 |
| beforeMount | 模板编译完成,$el 已生成,但还未挂载到页面 DOM 中 | 最后一次修改数据而不触发额外渲染的时机 |
| mounted | 组件挂载到页面 DOM 完成 | DOM 操作(如获取 DOM 元素、初始化第三方插件)、异步请求数据(需要 DOM 时) |
| beforeUpdate | 响应式数据变化后,DOM 重新渲染前 | 获取更新前的 DOM 状态(如滚动位置) |
| updated | DOM 重新渲染完成后 | 处理更新后的 DOM 逻辑(禁止修改响应式数据,否则会无限循环) |
| beforeUnmount | 组件卸载前(实例仍可用) | 清理副作用(定时器、事件监听、订阅、第三方插件实例) |
| unmounted | 组件卸载完成(DOM 移除、指令 / 事件解绑) | 最终清理(如取消网络请求) |
2. 特殊阶段(仅 keep-alive 包裹的组件)
表格
|-------------|-------------------|-----------------|
| 钩子 | 执行时机 | 作用 |
| activated | keep-alive 组件被激活时 | 恢复组件状态(如继续播放视频) |
| deactivated | keep-alive 组件失活时 | 暂停组件状态(如暂停视频) |
组件的传参:
父子组件之间的传参:
流程:
先写父传子。子组件中用defineProps这个方法接收父组件传过来的东西,然后父组件中导入子组件,并且在父组件中绑定要传给子组件的信息。然后写子传父,子组件中用defineEmits这个方法传要给父组件的消息,并定义说话的方法,然后父组件中先存子组件说的话,然后定义监听子组件说话的方法,最后在son标签中增加@要监听的内容,并触发监听方法。就实现父子之间的传参了。
@后面跟的名称,就是父组件要监听的子组件触发的事件名 ------ 这就像父子之间的 "专属暗号",子组件发消息时用的 "暗号" 和父组件监听时用的 "暗号" 必须一字不差(包括大小写、连字符),父组件才能 "听到" 子组件的消息。
父组件:
<template>
<div>我是爸爸</div>
<!--绑定信息,给son组件这么多钱--><!--加@want-toy="listSon",父组件监听儿子的"want-toy"消息,听到后就触发listenSon方法-->
<Son :money="100" @want-toy = "listenSon" />
<!--展示子组件说的话-->
<div>子组件说的话:{{sonWords}}</div>
</template>
<script setup>
//导入son组件
import Son from './Son.vue'
import {ref} from 'vue'
//存子组件说的话
const sonWords = ref('')
//父组件听子组件说话的方法,data就是儿子传过来的内容
const listenSon = (data) =>{
sonWords.value = data;
}
</script>
子组件:
<template>
<div>我是儿子,爸爸给了我 {{ money }} 元零花钱</div>
<!--添加一个按钮,点击就和父组件说想要玩具-->
<button @click="tellDad">跟父组件要玩具</button>
</template>
<script setup>
//先是接收父组件传过来的money
const props = defineProps(['money'])
//然后再给父组件传信息
const emit = defineEmits(['want-toy'])
//点击按钮时,触发刚才传过去的消息,并同时告诉父组件"想要奥特曼"
const tellDad = () =>{
emit('want-toy','我想要奥特曼')
}
</script>
app.vue组件:
<script setup>
import Dad from './components/Dad.vue'
</script>
<template>
<div>
<Dad />
</div>
</template>
<style scoped>
</style>
兄弟之间的传参:
兄弟之间无法直接传参,而是要通过父组件才能传参
流程;
哥哥组件通过defineEmits方法把消息传递给父子间,然后定义传话的方法。接着父组件先保存哥哥组件传过来的内容,再把这个内容传递给弟弟组件,弟弟组件通过defineProps方法接收父组件传递过来的哥哥组件的消息
哥哥组件:
<template>
<div>
<h4>我是哥哥</h4>
<!--点击按钮,给爸爸传数据-->
<button @click="sendToDad">给弟弟传一句话</button>
</div>
</template>
<script setup>
//1.声明要发给父组件的事件
const emit = defineEmits(['brother-a-msg'])
//2.点击按钮时,出发事件,把数据传给父组件
const sendToDad = () =>{
//要传给弟弟的内容,先发给父组件
emit('brother-a-msg','弟弟,我是哥哥,这是我给你的零食')
}
</script>
弟弟组件:
<template>
<div>
<h3>我是弟弟</h3>
<!--展示父组件传过来的,哥哥给的内容-->
<p>哥哥说的话:{{msgFromBrotherA}}</p>
</div>
</template>
<script setup>
//接收父组件传过来的变量
const props = defineProps(['msgFromBrotherA'])
</script>
父组件:
<template>
<div>
<h3>我是父组件,用来中转</h3>
<!--1.监听哥哥的事件,收到数据后存在自己的变量里-->
<BrotherA @brother-a-msg="getFromBrotherA"/>
<!--2.把收到的哥哥的数据,通过props传给弟弟-->
<BrotherB :msgFromBrotherA="brotherMsg" />
</div>
</template>
<script setup>
import BrotherA from './BrotherA.vue'
import BrotherB from './BrotherB.vue'
import {ref} from 'vue'
//先把哥哥传过来的数据保存
const brotherMsg = ref('')
//父组件接收哥哥传过来数据的方法
const getFromBrotherA = (data) => {
//data里就是哥哥传的内容,存在brotherMsg里
brotherMsg.value = data;
console.log('父组件收到的哥哥的消息',data)
}
</script>
app.vue组件:
<script setup>
import Dad from './components/Dad.vue'
</script>
<template>
<div>
<Dad />
</div>
</template>
<style scoped>
</style>
路由(router):

路由的基本使用:
mian.js:
import { createApp } from 'vue'
import App from './App.vue'
//在整个App.vue中可以使用路由
import router from './routers/router.js'
const app = createApp(App)
app.use(router)
app.mount('#app')
app.vue:
<script setup>
</script>
<template>
<div>
App 开头的内容<br>
<router-link to="/home">home页</router-link><br>
<router-link to="/list">list页</router-link><br>
<router-link to="/add">add页</router-link><br>
<router-link to="/update">update页</router-link><br>
<hr>
<!--该标签会被替换为具体的.vue-->
<router-view></router-view>
<hr>
App结尾的内容
</div>
</template>
<style scoped>
</style>
自己创建的router.js文件:
/*导入 创建路由对象需要使用的函数*/
import {createRouter,createWebHashHistory} from 'vue-router'
//导入.vue组件
import Home from '../components/Home.vue'
import List from '../components/List.vue'
import Update from '../components/Update.vue'
import Add from '../components/Add.vue'
//创建一个路由对象
const router = createRouter({
//history属性用于记录路由的历史
history:createWebHashHistory(),
//用于定义多个不同的路径和组件之间的对应关系
routes:[
{
path:"/home",
component:Home
},
{
path:"/list",
component:List
},
{
path:"/update",
component:Update
},
{
path:"/add",
component:Add
},
{
path:"/",
component:Home
}
]
})
//向外暴露router
export default router;
一些页面的vue文件,放一起了:
<template>
<div>
<router-link to="/home">home页</router-link>
<h1>Updata</h1>
</div>
</template>
<template>
<div>
<router-link to="/home">home页</router-link>
<h1>List</h1>
</div>
</template>
<template>
<div>
<h1>Home</h1>
<router-link to="/add">add页</router-link><br>
<router-link to="/list">list页</router-link><br>
<router-link to="/update">update页</router-link><br>
<router-link to="/add">add页</router-link><br>
</div>
</template>
<template>
<div>
<router-link to="/home">home页</router-link>
<h1>Add</h1>
</div>
</template>
这里面的router-link标签就是和链接标签差不多的意思,可以通过这个切换页面
路由的重定向:
在main.js文件中:
{
path:"/showAll",
redirect:"/list"
}
当访问showALL时,会自动跳转到 list 这个页面
在main.js文件中使用router-link也是同样的效果
一个视图上是可以同时存在多个router-view的,每个router-view都可以设置专门用来展示哪个组件(但大部分情况下,一个router-view就能满足99%的业务)
写法:
app.vue:
<hr>
<!--该标签会被替换为具体的.vue-->
<!--一个视图上是可以同时存在多个router-view的,
每个router-view都可以设置专门用来展示哪个组件
-->
home页<router-view name="homeView"></router-view><hr>
list页<router-view name="listView"></router-view><hr>
add页<router-view name="addView"></router-view><hr>
update页<router-view name="updateView"></router-view>
<hr>
自己创建的router.js文件中:
routes:[
{
path:"/home",
components:{
homeView:Home
}
},
{
path:"/list",
components:{
listView:List
}
},
{
path:"/update",
components:{
updateView:Update
}
},
{
path:"/add",
components:{
addView:Add
}
},
{
path:"/",
components:{
homeView:Home
}
},
{
path:"/showAll",
components:{
homeView:Home
}
}
]
})
编程式路由:
import {ref} from 'vue'
const router = useRouter()
function showList(){
//编程式路由实现页面跳转
//router.push(path:"/list")
router.push({path:"/list"})
}
let myPath = ref("")
function goMyPage(){
router.push(myPath.value)
}
</script>
<template>
<div>
<!-- 声明式路由 -->
<router-link to="/home">home页</router-link><br>
<router-link to="/list">list页</router-link><br>
<router-link to="/add">add页</router-link><br>
<router-link to="/update">update页</router-link><br>
<!--编程式路由-->
<button @click="goMyPage()">Go</button><input type="text" v-model="myPath">
<router-view></router-view>
</div>
路由传参:

先配置路由:
// src/routers/router.js(路由配置文件)
import { createRouter, createWebHistory } from 'vue-router'
// 假设有两个页面:Home.vue(首页)、User.vue(用户页)
import Home from '@/views/Home.vue'
import User from '@/views/User.vue'
const routes = [
{ path: '/', name: 'Home', component: Home }, // 首页
{ path: '/user', name: 'User' , component: User }, // 用户页(query传参用)
{ path: '/user/:id', name: 'UserDetail', component: User } // 用户详情页(params传参用,:id是占位符)
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
然后在main.js里引入路由:
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router' // 引入上面的路由配置
createApp(App).use(router).mount('#app')
query传参:键值对传参:
方式 1:用<router-link>(点击跳转,不用写 JS)
Home.vue:
<div>
<router-link :to="{name:'User',query:{id:1,name:'张三'}}">跳转到用户页</router-link>
</div>
方式 2:用 JS 点击跳转(编程式导航)
Home.vue:
<script setup>
//第一步:引入路由
import {useRouter} from 'vue-router'
const router = useRouter()
//第二步:定义跳转函数
const goTOUser = () => {
router.push({
name:'User',//对应路由配置里的name
query:{id:1,name:'张三'}//要传的参数
})
}
</script>
<template>
<div>
<!--编程式导航-->
<button @click="goTOUser">点击跳转到用户页 </button>
</div>
</template>
接收参数(在 User.vue 里) :
<script setup>
//引入useRoute 拿到路由对象
import {useRoute} from 'vue-router'
const route = useRoute()
</script>
<template>
<div>
<p>接收的ID:{{route.query.id}}</p>
<p>接收的姓名:{{route.query.name}}</p>
</div>
</template>
<style scoped>
App.vue里的代码:只用加一行 router-view标签就行了
关键注意点(就 1 条):
query 传参不用改路由配置(不用加:id),参数直接写在 query 里就行,地址栏能看到,刷新不丢。
params传参:路径传参:
而是嵌在地址栏路径里,比如:http://localhost:5173/user/1(1 就是 id 参数),刷新页面也不丢,但需要先在路由配置里加占位符(:id)。
如果是params传参的话:就要在配置文件routers里加id
import {createRouter,createWebHashHistory} from 'vue-router'
import Home from '../components/Home.vue'
import User from '../components/User.vue'
const routes = [
{path:'/',name:'Home',component:Home}, //首页
{path:'/',name:'User',component:User},//用户页
{path:'/user/:id',name:'UserDetail',component:User}//用户详情页
]
const router = createRouter({
history:createWebHashHistory(),
routes
}
)
export default router;
- 传参(重点:只能用 name,不能用 path!)
<!-- Home.vue 里的代码 -->
<script setup>
//第一步:引入路由
import {useRouter} from 'vue-router'
const router = useRouter()
//第二步:定义跳转函数
const goToUserDetail =()=>{
router.push({
name:'UserDetail',//必须用路由的name,不能写path
params:{id:1}//传的参数,对应路由里的 :id
})
}
</script>
<template>
<div>
<button @click="goToUserDetail">跳转到用户详情页 </button>
</div>
</template>
2.接收参数:在User.vue
<script setup>
//引入useRoute 拿到路由对象
import {useRoute} from 'vue-router'
const route = useRoute()
console.log('Params的id:',route.params.id)
</script>
<template>
<div>
<p>Params传参的ID:{{route.params.id}}</p>
</div>
app.vue文件中的router-view标签是通用的
路由守卫:
路由守卫(Navigation Guards)是 Vue Router 提供的导航钩子函数,本质是拦截路由跳转的 "关卡"------ 在路由从「离开当前页」到「进入目标页」的整个生命周期中,你可以插入自定义逻辑(比如登录验证、权限检查、表单未保存提示等),从而控制路由是否允许跳转、跳转到哪里。
前置准备:基础路由环境
先搭建一个最基础的 Vue3+Vue Router 4 项目结构,后续所有守卫都基于这个环境演示:
# 安装依赖(确保是Vue Router 4,适配Vue3)
npm install vue-router@4
// src/router/router.js(路由核心文件)
import { createRouter, createWebHistory } from 'vue-router'
// 示例组件(你需要自己创建或替换)
import Home from '@/views/Home.vue'
import Login from '@/views/Login.vue'
import User from '@/views/User.vue'
import Admin from '@/views/Admin.vue'
// 路由规则
const routes = [
{ path: '/', name: 'Home', component: Home },
{ path: '/login', name: 'Login', component: Login },
{ path: '/user', name: 'User', component: User },
{ path: '/admin', name: 'Admin', component: Admin }
]
// 创建路由实例
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), // Vue3 Vite环境
routes
})
// 后续所有"全局守卫"都写在这里(router实例上)
export default router
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
守卫分类:
全局守卫(作用于所有路由)
全局守卫是挂载在router实例上的钩子,对所有路由跳转生效,核心有 3 个:
表格
|-----------------|------------------------|--------------------------------|
| 守卫类型 | 触发时机 | 核心用途 |
| beforeEach | 路由跳转之前(最常用) | 登录验证、全局权限控制、加载提示 |
| beforeResolve | 所有组件内守卫和异步路由组件解析后、跳转之前 | 比 beforeEach 晚,适合需要等待异步操作完成的校验 |
| afterEach | 路由跳转之后 | 页面埋点、修改页面标题、关闭加载提示 |
全局前置守卫 router.beforeEach(最核心)
用法 :在router/index.js中,router实例创建后添加:
router.js文件:
import {createRouter,createWebHashHistory} from 'vue-router'
import Home from '../src/components/Home.vue'
import Login from '../src/components/Login.vue'
import User from '../src/components/User.vue'
import Admin from '../src/components/Admin.vue'
const routes = [
{path:'/',name:'Home',component:Home},
{path:'/login',name:'Login',component:Login},
{path:'/user',name:'User',component:User},
{path:'/admin',name:'Admin',component:Admin}
]
const router = createRouter({
history:createWebHashHistory(import.meta.env.BASE_URL),//vue3 vite环境
routers
})
//后续所有全局守卫都写在这里(router实例上)
export default router
router.beforeEach((to,from,next) => {
// 参数说明:
// to:目标路由对象(要跳转到哪里)
// from:当前路由对象(从哪里跳走)
// next:控制跳转的函数(Vue Router4中可选,推荐用return替代)
console.log('全局前置守卫:从',from.path,'跳向',to.path)
// 实战场景:未登录用户禁止访问/user和/admin,重定向到登录页
const isLogin = localStorage.getItem('token') // 假设登录后存token
// 需要登录的路由白名单
const needLoginRoutes = ['User', 'Admin']
if (needLoginRoutes.includes(to.name) && !isLogin) {
// 方式1:用next重定向(Vue2/3兼容)
// next('/login')
// 方式2:Vue3推荐------return重定向(更简洁)
return '/login'
//需要注意的是:如果定义了next就不能再用return,如果用了return就不能定义next
}
// 允许跳转(必须写,否则路由会卡住)
// next() // 兼容写法
return true // Vue3推荐
}
)
全局解析守卫 router.beforeResolve
触发时机比beforeEach晚,会等待所有异步路由组件加载完成 、组件内守卫执行完后才触发,适合需要 "等所有准备工作完成" 的全局校验:
js
router.beforeResolve(async (to, from) => {
console.log('全局解析守卫:所有异步操作完成后触发')
// 示例:加载目标路由的权限配置(异步)
// const permission = await getPermission(to.path)
// if (!permission) return '/403'
return true
})
全局后置钩子 router.afterEach
跳转完成后触发,没有 next/return 参数(无法控制跳转),只能做 "收尾工作":
js
router.afterEach((to, from) => {
// 示例1:修改页面标题
document.title = to.name ? `XX系统-${to.name}` : 'XX系统'
// 示例2:关闭全局加载弹窗
// ElLoading.close()
console.log('全局后置钩子:跳转完成,当前页', to.path)
})
关键注意事项
- next/return 的使用 :Vue Router4 中推荐用
return替代next(更符合 ES6 规范),比如:
-
- 放行:
return true(或不写,默认放行) - 重定向:
return '/login' - 取消跳转:
return false
- 放行:
-
异步逻辑处理 :如果守卫中有异步操作(比如请求接口校验权限),需要返回
Promise:jsrouter.beforeEach(async (to, from) => {
const permission = await api.checkPermission(to.path)
if (!permission) return '/403'
return true
}) -
执行顺序:全局前置守卫 → 路由独享守卫 → 组件内 beforeRouteEnter → 全局解析守卫 → 路由跳转 → 全局后置钩子 → 组件挂载。
promise:
回调函数:

执行顺序为:

回调函数就是一种未来会执行的函数,在这个函数执行之前,其他代码该干什么干什么
promise的作用就是可以把普通函数转化成回调函数



then括号里的函数会一直等到出了结果才决定调用哪个函数

async和await:
async:

async function fun1() {
//return 10;//如果return的是个数字,那么就会走成功后的函数
//throw new Error("出错了");//异常状态就会走失败后的函数
let promise = Promise.resolve("成功");//resolve方法会快速产生一个成功的结果
return promise;
}
let promise = fun1()
promise.then(
function(value){
console.log("success +" + value)
//这里面写的是成功之后的函数
}
).catch(
function(value){
console.log("fail" + value)
//这里面写的是失败之后的函数
}
)
await:

这是成功情况的写法:
async function fun2() {
return 10;
}
async function fun3() {
let res = await fun2();
console.log("await got:" + res)
}
fun3();
失败情况的写法:
async function fun4() {
//let res = await fun()
try{
let res = await Promise.reject("something wrong")
}catch(e){
console.log("catch got:" + e)
}
}
fun4()
//在async方法内,await后面的代码都会等await结束之后才继续进行
Axios:
简而言之:就是把原生js代码封装成一个方法,可以快速向服务器发送请求

axios 基本使用
1. GET 请求(最常用,用于获取数据)
GET 请求的参数有两种传递方式:
方式 1:参数拼接在 URL 中
// 基础 GET 请求
axios.get('https://api.example.com/user/123')
.then(response => {
// 请求成功,处理响应数据
console.log('请求成功:', response.data);
})
.catch(error => {
// 请求失败,处理错误
console.error('请求失败:', error);
});
方式 2:通过 params 配置传递参数(推荐,自动编码)
// 带参数的 GET 请求
axios.get('https://api.example.com/user', {
params: {
id: 123,
name: '张三' // 会自动拼接到 URL:?id=123&name=张三
},
// 可选:设置请求超时时间(毫秒)
timeout: 5000
})
.then(response => {
console.log('响应数据:', response.data);
})
.catch(error => {
console.error('错误信息:', error);
});
2. POST 请求(用于提交数据,如表单、新增数据)
POST 请求的参数放在 data 中(默认传递 JSON 格式):
// 基础 POST 请求(发送 JSON 数据)
axios.post('https://api.example.com/user', {
name: '李四',
age: 25,
gender: '男'
})
.then(response => {
console.log('新增用户成功:', response.data);
})
.catch(error => {
console.error('新增用户失败:', error);
});
// 若需要发送表单格式数据(application/x-www-form-urlencoded)
const params = new URLSearchParams();
params.append('name', '王五');
params.append('age', 30);
axios.post('https://api.example.com/user', params)
.then(res => console.log(res.data))
.catch(err => console.error(err));
另外的例子:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>axios 极简使用</title>
<!-- 1. 引入axios(不用安装,直接用CDN) -->
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<!-- 点击按钮触发请求 -->
<button onclick="sendGet()">点我发GET请求(拿数据)</button>
<button onclick="sendPost()">点我发POST请求(传数据)</button>
<script>
// 测试接口(免费的,不用自己搭后端,直接能访问)
const url = 'https://jsonplaceholder.typicode.com/posts';
// 2. GET请求:从后端"拿"数据(最常用)
async function sendGet() {
// try...catch:防止请求失败时页面报错
try {
// 核心代码:axios.get(接口地址, 可选参数)
const result = await axios.get(url + '/1', {
// GET请求的参数:会拼在地址后面,比如 ?name=张三&age=20
params: {
name: '张三',
age: 20
}
});
// 请求成功:打印拿到的数据(重点看result.data)
console.log('GET请求拿到的数据:', result.data);
alert('GET请求成功!数据在控制台(F12)里看~');
} catch (err) {
// 请求失败:打印错误原因
console.log('GET请求失败:', err.message);
alert('请求失败了,看看控制台~');
}
}
// 3. POST请求:给后端"传"数据(最常用)
async function sendPost() {
try {
// 核心代码:axios.post(接口地址, 要传的数据, 可选参数)
const result = await axios.post(url, {
// POST要传给后端的数据(比如表单提交的内容)
title: '我是前端传的标题',
content: '我是前端传的内容'
});
// 请求成功:打印后端返回的结果
console.log('POST请求返回的数据:', result.data);
alert('POST请求成功!数据在控制台(F12)里看~');
} catch (err) {
console.log('POST请求失败:', err.message);
alert('请求失败了,看看控制台~');
}
}
</script>
</body>
</html>
除了 axios.get()、axios.post(),还有一个更通用的写法 axios(config),可以应对所有请求类型,尤其适合需要动态切换请求方式的场景。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>axios 通用请求方式</title>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<button onclick="sendRequest('get')">通用方式发GET</button>
<button onclick="sendRequest('post')">通用方式发POST</button>
<script>
const baseURL = 'https://jsonplaceholder.typicode.com/posts';
// 通用请求函数:传不同的method,发不同类型请求
async function sendRequest(method) {
try {
// 核心:用一个axios(),通过配置对象指定请求类型、地址、参数/数据
const result = await axios({
method: method, // 请求方式:get/post/put/delete
url: method === 'get' ? `${baseURL}/1` : baseURL, // 请求地址
params: method === 'get' ? { name: '张三' } : null, // GET参数(POST不需要)
data: method === 'post' ? { title: '通用请求测试', content: '测试内容' } : null // POST数据(GET不需要)
});
console.log(`${method}请求结果:`, result.data);
alert(`${method}请求成功!`);
} catch (err) {
console.error(`${method}请求失败:`, err.message);
}
}
</script>
</body>
</html>
解释:
axios(config)是最底层的用法,config是一个配置对象,里面可以指定method(请求方式)、url(地址)、params(GET 参数)、data(POST 数据);- 这种写法的好处是:如果需要根据条件切换请求方式(比如有时 GET、有时 POST),不用写两个
axios.get/post,改method就行。
四、响应结构解析
axios 请求成功后,then 中接收的 response 对象包含以下核心属性:
axios.get('https://api.example.com/user/123')
.then(response => {
console.log(response.data); // 服务器返回的核心数据(最常用)
console.log(response.status); // HTTP 状态码(如 200、404、500)
console.log(response.statusText); // 状态文本(如 OK、Not Found)
console.log(response.headers); // 响应头信息
console.log(response.config); // 请求时的配置对象
});
拦截器:
拦截器到底是什么?
把 axios 请求的完整流程比作「快递寄件」:
- 你(前端)准备好包裹(请求参数 / 数据)→ 交给快递员(axios)
- 快递员出发前,要经过「快递站检查点」(请求拦截器):检查包裹是否合规、贴快递单(加请求头 /token)、称重(加参数)
- 快递员送到目的地(后端)→ 对方签收 / 拒收后,快递员返回
- 快递员回来后,又经过「快递站检查点」(响应拦截器):检查包裹是否完好(响应是否正常)、拆包裹(提取 data)、如果包裹损坏(请求失败)则通知你
简单说:拦截器是请求 "发出去前" 和 "响应回来后" 的钩子函数,能统一处理所有请求 / 响应的通用逻辑,不用在每个请求里重复写代码。
一、拦截器的分类与核心语法
axios 有两种拦截器,核心语法高度相似,只是作用时机不同:
1. 核心语法模板
javascript
运行
// 1. 创建axios实例(推荐用实例加拦截器,避免污染全局axios)
const request = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com',
timeout: 5000
});
// 2. 请求拦截器:请求发送前执行
request.interceptors.request.use(
// 成功回调:请求配置没问题时执行(必须返回config!)
(config) => {
// 对请求配置做修改(比如加token、改请求头)
return config;
},
// 失败回调:请求配置本身出错时执行(极少用,比如参数格式错)
(error) => {
// 抛出错误,让后续catch能捕获
return Promise.reject(error);
}
);
// 3. 响应拦截器:响应返回后执行
request.interceptors.response.use(
// 成功回调:HTTP状态码2xx时执行(必须返回数据!)
(response) => {
// 对响应数据做处理(比如只返回data)
return response.data;
},
// 失败回调:HTTP状态码非2xx/网络错误时执行(常用!)
(error) => {
// 统一处理错误(比如401登录过期、404地址错)
return Promise.reject(error);
}
);
坑点:
- 请求拦截器的成功回调必须返回 config:否则请求会被中断(相当于快递员没拿到包裹,没法出发);
- 响应拦截器的成功回调必须返回数据 (比如
response或response.data):否则后续await拿不到数据; - 失败回调必须返回 Promise.reject(error):否则错误无法被外层的
try/catch捕获; - 拦截器是给指定 axios 实例生效 的:如果用
axios.interceptors加拦截器,会影响所有 axios 请求;用request.interceptors只影响当前实例,更安全。
请求拦截器:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>请求拦截器实战</title>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<button onclick="testRequest()">触发请求(看拦截器效果)</button>
<script>
// 1. 创建实例
const request = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com',
timeout: 5000
});
// 2. 请求拦截器:重点讲解每一行
request.interceptors.request.use(
(config) => {
console.log('===== 请求拦截器(出发前) =====');
// 场景1:添加登录token(最常用)
const token = localStorage.getItem('token'); // 假设登录后存了token
if (token) {
// 给请求头加Authorization,后端用来验证登录状态
config.headers.Authorization = `Bearer ${token}`;
console.log('已添加token到请求头:', token);
}
// 场景2:统一设置请求头(比如指定JSON格式)
config.headers['Content-Type'] = 'application/json;charset=utf-8';
console.log('请求头:', config.headers);
// 场景3:过滤GET请求的无效参数(去掉undefined/null的参数)
if (config.method === 'get' && config.params) {
// 遍历params,删除值为undefined/null的参数
Object.keys(config.params).forEach(key => {
if (config.params[key] === undefined || config.params[key] === null) {
delete config.params[key];
}
});
console.log('过滤后的GET参数:', config.params);
}
// 场景4:显示加载动画(比如页面上的loading)
// document.getElementById('loading').style.display = 'block';
// 必须返回config!否则请求中断
return config;
},
(error) => {
// 请求配置出错(比如method写了个不存在的值,如'method: 'get1'')
console.error('请求配置出错:', error);
return Promise.reject(error);
}
);
// 测试请求
async function testRequest() {
try {
// 模拟:先存一个token到本地
localStorage.setItem('token', 'test-token-123456');
const result = await request.get('/posts/1', {
params: {
name: '张三',
age: undefined, // 无效参数,会被拦截器过滤
sex: '男'
}
});
console.log('请求结果:', result);
} catch (err) {
console.error('请求失败:', err);
} finally {
// 隐藏加载动画
// document.getElementById('loading').style.display = 'none';
}
}
</script>
</body>
</html>
响应拦截器:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>响应拦截器实战</title>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<button onclick="testResponse()">触发请求(看响应拦截器)</button>
<button onclick="testError()">触发404错误(看错误处理)</button>
<script>
const request = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com',
timeout: 5000
});
// 先加请求拦截器(和上面一样,省略)
request.interceptors.request.use(config => {
return config;
}, error => Promise.reject(error));
// 响应拦截器:重点讲解每一行
request.interceptors.response.use(
(response) => {
console.log('===== 响应拦截器(成功返回) =====');
// 场景1:简化返回值,只返回data(最常用)
const data = response.data;
console.log('原始响应:', response);
console.log('简化后返回的数据:', data);
// 场景2:处理业务错误(比如后端约定code≠200是业务失败)
// 注意:jsonplaceholder接口没有code,这里模拟后端返回格式
if (data.code && data.code !== 200) {
alert(`业务失败:${data.msg}`);
return Promise.reject(new Error(data.msg));
}
// 场景3:隐藏加载动画
// document.getElementById('loading').style.display = 'none';
// 必须返回数据!否则await拿不到值
return data;
},
(error) => {
console.log('===== 响应拦截器(失败) =====');
let errMsg = '请求失败';
// 场景1:判断错误类型(关键!)
if (error.response) {
// 情况1:发了请求,后端有响应,但状态码非2xx(401/404/500)
const { status, data } = error.response;
console.log('HTTP状态码:', status);
console.log('后端返回的错误信息:', data);
// 按状态码分类处理
switch (status) {
case 401:
errMsg = '登录过期,请重新登录';
localStorage.removeItem('token'); // 清除无效token
// 跳转到登录页
// window.location.href = '/login.html';
break;
case 404:
errMsg = '接口地址不存在';
break;
case 500:
errMsg = '服务器内部错误,请稍后重试';
break;
default:
errMsg = data.msg || `请求失败(${status})`;
}
} else if (error.request) {
// 情况2:发了请求,但没收到后端响应(比如断网、超时)
errMsg = '网络异常,请检查网络或稍后重试';
} else {
// 情况3:请求配置出错(比如method写错)
errMsg = `请求配置错误:${error.message}`;
}
// 场景2:统一弹错误提示
alert(errMsg);
console.error('错误详情:', error);
// 场景3:隐藏加载动画
// document.getElementById('loading').style.display = 'none';
// 必须返回Promise.reject!否则外层catch捕获不到
return Promise.reject(error);
}
);
// 测试成功请求
async function testResponse() {
try {
const result = await request.get('/posts/1');
console.log('最终拿到的数据:', result); // 直接是data,不用写.result.data
} catch (err) {
console.error('外层catch:', err);
}
}
// 测试404错误
async function testError() {
try {
// 故意访问不存在的接口
await request.get('/xxx/1');
} catch (err) {
console.error('外层catch:', err);
}
}
</script>
</body>
</html>