vue 通用模板
vue 组件库
vue 官方推荐第三方组件库,里面列举了各种组件库;
.vue 文件
<!-- vue 3 组合式 api -->
<template>
<div>
<h1>年龄:{{ age }}</h1>
<button @click="addAge">长大</button>
<button v-on:click="addAge">长大(非简写)</button>
</div>
</template>
<!-- name 属性用于定义组件名(keep-alive 缓存、递归组件、devtools 显示都依赖它)
两种写法:① 安装 vite-plugin-vue-setup-extend 后直接写在 <script setup> 标签上(如本例);
② Vue 3.3+ 原生支持,无需插件:在 script 内写 defineOptions({ name: "Test" }) -->
<script setup lang="ts" name="Test">
import { ref } from "vue";
let age = ref(18);
function addAge() {
age.value++;
}
</script>
<style lang="scss" scoped></style><!-- vue 2 选项式 api -->
<template>
<div>
<h1>年龄:{{ age }}</h1>
<button @click="addAge">长大</button>
</div>
</template>
<script>
export default {
name: "Test",
data() {
return {
age: 18,
};
},
mounted() {},
methods: {
addAge() {
this.age++;
},
},
};
</script>
<style lang="scss" scoped></style>知识点
v-show 和 v-if 的区别
- v-if:每次切换会销毁 / 重建组件或元素,并触发对应的生命周期钩子;初始为 false 时不渲染,属于"惰性",首屏开销更低。
- v-show:元素始终渲染,切换只是修改 CSS
display,不触发生命周期钩子。
结论:频繁切换用 v-show,条件很少改变用 v-if。
列表渲染
v-for 用于遍历数组、对象、数字或字符串,语法在 vue2 / vue3 中一致。
<template>
<!-- 遍历数组:(元素, 索引) -->
<li v-for="(item, index) in list" :key="item.id">{{ index }} - {{ item.name }}</li>
<!-- 遍历对象:(值, 键, 索引) -->
<li v-for="(value, key, i) in obj" :key="key">{{ key }}:{{ value }}</li>
<!-- 遍历数字:从 1 开始 -->
<span v-for="n in 5" :key="n">{{ n }}</span>
</template>key 与优先级
- 务必绑定唯一
key(优先用业务 id,别用 index):key帮助 diff 精准复用/移动节点,用 index 在"插入/删除中间项"时会导致状态错位(如输入框内容串位)。 - 不要把
v-for和v-if写在同一元素上:vue2 中v-for优先级更高(每次循环都判断v-if,浪费);vue3 中反而v-if优先(此时拿不到v-for的变量,直接报错)。需要过滤时用计算属性先过滤,或把v-if提到外层<template>。
<!-- 推荐:用计算属性过滤,而不是 v-for + v-if -->
<li v-for="user in activeUsers" :key="user.id">{{ user.name }}</li>
<script setup>
const activeUsers = computed(() => list.value.filter((u) => u.active));
</script>事件处理
@click(v-on:click 简写)绑定事件,可写方法名或内联语句;不传参时回调默认接收原生事件对象,需要同时传参时用 $event 显式获取。
<template>
<button @click="handle">方法名(自动传入 event)</button>
<button @click="handle($event, 1)">内联传参 + 事件对象</button>
</template>事件修饰符(链式可叠加,如 @click.stop.prevent):
| 修饰符 | 作用 |
|---|---|
.stop | 阻止事件冒泡(event.stopPropagation) |
.prevent | 阻止默认行为(event.preventDefault,如表单提交刷新页面) |
.self | 只有点击元素自身才触发(子元素冒泡上来的不算) |
.once | 只触发一次 |
.capture | 使用捕获模式 |
.passive | 滚动等场景提升性能,不能与 .prevent 同用 |
按键 / 鼠标修饰符:@keyup.enter(回车)、@keyup.esc、@keyup.delete、@click.right(右键)等。
<!-- 常见组合:回车提交、阻止表单默认刷新、阻止冒泡 -->
<input @keyup.enter="submit" />
<form @submit.prevent="submit"></form>
<div @click="outer"><button @click.stop="inner">不冒泡</button></div>表单绑定
原生表单用 v-model 双向绑定,不同控件绑定的值类型不同:
<template>
<!-- 文本 / 多行文本:绑字符串 -->
<input v-model="form.name" />
<textarea v-model="form.desc"></textarea>
<!-- 单个复选框:绑布尔值 -->
<input type="checkbox" v-model="form.agree" />
<!-- 多个复选框:绑数组(勾选项的 value 进数组) -->
<input type="checkbox" value="篮球" v-model="form.hobby" />
<input type="checkbox" value="足球" v-model="form.hobby" />
<!-- 单选框:绑选中项的 value -->
<input type="radio" value="男" v-model="form.sex" />
<input type="radio" value="女" v-model="form.sex" />
<!-- 下拉:单选绑字符串,加 multiple 则绑数组 -->
<select v-model="form.city">
<option value="cd">成都</option>
<option value="cq">重庆</option>
</select>
</template>v-model 修饰符:
.lazy:改在change事件同步(失焦/回车),而非每次input。.number:自动转数字(数字输入框常用,否则拿到的是字符串)。.trim:自动去除首尾空格。
<input v-model.trim.number="form.age" />生命周期
组件从创建到销毁会依次触发一系列钩子,vue3 组合式 API 的钩子需从 vue 引入并在 setup 中调用(setup 本身相当于 beforeCreate + created)。
| 阶段 | vue2(选项式) | vue3(组合式) | 常见用途 |
|---|---|---|---|
| 创建前 | beforeCreate | —(用 setup) | — |
| 创建后 | created | —(用 setup) | 发起请求、初始化数据 |
| 挂载前 | beforeMount | onBeforeMount | — |
| 挂载后 | mounted | onMounted | 操作 DOM、初始化图表、请求数据 |
| 更新前 | beforeUpdate | onBeforeUpdate | 拿到更新前的 DOM 状态 |
| 更新后 | updated | onUpdated | DOM 更新完成后的处理 |
| 卸载前 | beforeDestroy | onBeforeUnmount | 清定时器、解绑事件、取消请求 |
| 卸载后 | destroyed | onUnmounted | 收尾清理 |
vue3 中
beforeDestroy/destroyed更名为onBeforeUnmount/onUnmounted;keep-alive缓存组件额外有activated/onActivated、deactivated/onDeactivated。
<script setup lang="ts" name="Test">
import { onMounted, onBeforeUnmount } from "vue";
let timer: number;
onMounted(() => {
// 组件挂载后:适合请求数据、初始化定时器
timer = window.setInterval(() => console.log("tick"), 1000);
});
onBeforeUnmount(() => {
// 组件卸载前:清理,避免内存泄漏
clearInterval(timer);
});
</script><script>
export default {
name: "Test",
data() {
return { timer: null };
},
created() {
// 适合请求数据
},
mounted() {
this.timer = setInterval(() => console.log("tick"), 1000);
},
beforeDestroy() {
clearInterval(this.timer); // 清理定时器
},
};
</script>计算属性
计算属性更多的是用于读取数据,而不是修改数据;
<template>
<div>
<!-- 首字母大写的全名,虽然被多次使用,但只会计算一次 -->
<h1>{{ fullName }}</h1>
</div>
</template>
<script setup lang="ts" name="Test">
import { ref, computed } from "vue";
let firstName = ref("zhang");
let lastName = ref("san");
// 下面「只读」与「可读写」是同一个 fullName 的两种写法,实际二选一(这里为对照才都写出来)
// 只读
let fullName = computed(() => {
return (
firstName.value.slice(0, 1).toUpperCase() +
firstName.value.slice(1) +
lastName.value
);
});
// 可读写--示例
let fullName = computed({
get() {
return (
firstName.value.slice(0, 1).toUpperCase() +
firstName.value.slice(1) +
lastName.value
);
},
set(val) {
// fullName 被修改时调用;val 为被修改的值;
},
});
</script><template>
<div>
<!-- 首字母大写的全名,虽然被多次使用,但只会计算一次 -->
<h1>{{ fullName }}</h1>
</div>
</template>
<script>
export default {
name: "Test",
data() {
return {
firstName: "zhang",
lastName: "san",
};
},
computed: {
// 下面两种写法实际二选一(同名属性只能存在一个,这里为对照才都列出)
// 只读
fullName() {
return (
this.firstName.slice(0, 1).toUpperCase() +
this.firstName.slice(1) +
this.lastName
);
},
// 可读写--示例
fullName: {
get() {
return (
this.firstName.slice(0, 1).toUpperCase() +
this.firstName.slice(1) +
this.lastName
);
},
set(val) {
// fullName 被修改时调用;val 为被修改的值;
},
},
},
};
</script>数据监视
每当数据发生变化时,想额外的处理一些逻辑,可使用数据监视功能;
<!--
vue3 只能监视以下 4 种类型的数据:
1. ref 定义的数据
2. reactive 定义的数据
3. 函数返回一个值
4. 一个包含上述内容的数组
-->
<template>
<div>
<h1>年龄:{{ age }}</h1>
<button @click="addAge">长大</button>
</div>
</template>
<!-- 注意支持 name 属性定义组件名需安装 vite-plugin-vue-setup-extend -->
<script setup lang="ts" name="Test">
import { ref, reactive, watch, watchEffect} from "vue";
// 1. 监视 ref 定义的【基本】数据类型
let age = ref(18);
function addAge() {
age.value++;
}
const myStopWatch = watch(age, (newVal, oldVal) => {
console.log("age 变化了!");
if (newVal > 20) {
myStopWatch(); // 停止监听
}
});
// 2. 监视 ref 定义的【对象】数据类型
// 默认监视的对象地址,若想监视对象属性的变化,需要手动开启深度监视
// 注意,watch 被调用后,newVal 和 oldVal 的值是一样的,原因是因为他们引用的同一个对象
let person = ref({ name: "zs", age: 18 });
watch(person, (newVal, oldVal) => {}, { deep: true,immediate: true }); //immediate 初始化完成后立即执行一次
// 3. 监视 reactive 定义的【对象】数据类型,默认开启深度监视
let person1 = reactive({ name: "zs", age: 18, car: { c1: "宝马" }});
watch(person1, (newVal, oldVal) => {
console.log("变化了!");
});
// 4. 监视 reactive 定义的【对象属性】,即只监听对象中某个属性变化;
watch(
() => person1.car,
(newVal, oldVal) => {
console.log("car 变化了!");
},
{ deep: true }
);
// 5. 监视 reactive 定义的【多个对象属性】(用数组同时监听多个 getter,注意数组后要有逗号)
let person2 = reactive({ name: "zs", age: 18, car: { c1: "宝马" } });
watch(
[() => person2.name, () => person2.car],
(newVal, oldVal) => {
// newVal / oldVal 也是数组,顺序与上面监听源一一对应
console.log("name 或 car 变化了!");
},
{ deep: true }
);
// 6. 监听任意属性(无需像 watch 那样指定要监听的属性)
let person3 = reactive({ name: "zs", age: 18});
watchEffect(()=>{
if(person3.name=="ls" || person3.age>=20){
console.log("变化了");
}
})
</script><!-- vue 2 -->
<script>
export default {
name: "Test",
data() {
return {
age: 18,
name: "zs",
info: {
phone: 15555555555,
},
};
},
methods: {},
watch: {
// 监听简单数据
age: {
immediate: true, // 立即执行一次
// handler 当 age 发生改变时调用
handler(newVal, oldVal) {
console.log("变化了");
},
},
// 监听对象
info: {
deep: true, // 多层对象需开启深度监视才能生效
handler(newVal, oldVal) {
console.log("变化了");
},
},
// 简写形式(不需要立即执行或深度监听时,适用)
name(newVal, oldVal) {
console.log("变化了");
},
},
};
</script>添加属性
以下示例演示了如何给响应式对象添加属性,以及如何给响应式数组添加元素;
Vue3 使用 Proxy 实现响应式,写法比 vue2 简洁许多。而 vue2 使用 defineProperty 实现响应式,需使用 this.$set() 才能实现响应式。
<template>
<div>
<h2>对象操作</h2>
<p>{{ user }}</p>
<button @click="addProperty">添加年龄</button>
<h2>数组操作</h2>
<p>{{ items }}</p>
<button @click="addItem">添加水果</button>
<button @click="updateItem">修改第一个</button>
</div>
</template>
<script setup>
import { reactive } from "vue";
const user = reactive({ name: "张三" });
const items = reactive(["苹果", "香蕉", "橙子"]);
// 对象操作
const addProperty = () => {
user.age = 25; // 直接添加新属性即可
};
// 数组操作
const addItem = () => {
items.push("梨子"); // 直接使用数组方法
};
const updateItem = () => {
items[0] = "红苹果"; // 直接通过索引修改
};
</script><template>
<div>
<h2>对象操作</h2>
<p>{{ user }}</p>
<button @click="addProperty">添加年龄</button>
<h2>数组操作</h2>
<p>{{ items }}</p>
<button @click="addItem">添加水果</button>
<button @click="updateItem">修改第一个</button>
</div>
</template>
<script>
export default {
data() {
return {
user: { name: "张三" },
items: ["苹果", "香蕉", "橙子"],
};
},
methods: {
// 对象操作
addProperty() {
this.$set(this.user, "age", 25); // 正确添加响应式属性
// this.user.age = 25; // 错误写法,视图不会更新
},
// 数组操作
addItem() {
// push, pop, shift, unshift, splice, sort, reverse 是 vue2 内部数组变异方法,会触发视图更新;
this.items.push("梨子"); // 使用数组变异方法
// 或者使用 Vue.set
// this.$set(this.items, this.items.length, '梨子');
},
updateItem() {
this.$set(this.items, 0, "红苹果"); // 正确修改数组元素
// this.items[0] = '红苹果'; // 错误写法,不会触发视图更新
},
},
};
</script>nextTick
Vue 更新 DOM 是异步的:数据改完后,DOM 不会立刻更新。若要在数据变化后拿到更新后的 DOM(如获取新高度、聚焦新元素),需用 nextTick。
<script setup lang="ts">
import { ref, nextTick } from "vue";
const show = ref(false);
const inputRef = ref();
async function openAndFocus() {
show.value = true;
// 此刻 input 还没渲染出来,直接 focus 拿不到
await nextTick(); // 等 DOM 更新完成
inputRef.value.focus();
}
</script><script>
export default {
methods: {
openAndFocus() {
this.show = true;
this.$nextTick(() => {
this.$refs.input.focus();
});
},
},
};
</script>绑定样式
<template>
<div class="basic" :class="bg" :style="styleObj">测试绑定样式</div>
</template>
<script setup lang="ts" name="Test">
import { ref } from "vue";
let bg = ref("happy");
let styleObj = ref({
fontSize: "16px",
});
</script>
<style lang="scss" scoped>
.basic {
width: 100px;
height: 100px;
}
.happy {
background-color: red;
}
</style><template>
<div class="basic" :class="bg" :style="styleObj">测试绑定样式</div>
</template>
<script>
export default {
name: "Test",
data() {
return {
bg: "happy",
styleObj: {
fontSize: "16px",
},
};
},
};
</script>
<style lang="scss" scoped>
.basic {
width: 100px;
height: 100px;
}
.happy {
background-color: red;
}
</style>组件通信
先看一张「按场景选型」的总表,再看各方式的具体写法:
| 方式 | 方向 | 适用场景 | 备注 |
|---|---|---|---|
props | 父 → 子 | 父给子传数据(最常用) | 单向数据流,子组件不应直接改 props |
emit 自定义事件 | 子 → 父 | 子把数据/事件通知父(最常用) | 与 props 配合即"父子双向" |
ref / defineExpose | 父 → 子 | 父需读写子的数据、调子的方法 | 打破封装,非必要不用 |
$parent | 子 → 父 | 子想调父的方法/改父数据 | 耦合强,谨慎使用 |
v-model | 父 ↔ 子 | 子组件做受控输入(表单类封装) | 见下方「组件 v-model」 |
provide / inject | 祖先 → 后代 | 跨多层级传递(如主题、用户信息) | 后代无需逐层透传 |
mitt / 事件总线 | 任意组件 | 无直接关系的组件互相通信 | 需共享同一实例,记得解绑 |
| Pinia / Vuex | 任意组件 | 全局共享状态、跨页面 | 复杂应用首选,另见状态管理专题 |
父传子
props 方式
适用于父组件直接给子组件传递数据(无需获取子组件数据或调用子组件方法)。
vue3<!-- 父组件 --> <template> <Child :info="info" :hobby="hobby" /> </template> <script setup lang="ts" name="Parent"> import Child from "./components/xxx/Child.vue"; import { ref } from "vue"; // 父组件数据 let info = ref({ name: "parent", age: 48 }); let hobby = ref(["fishing", "somking"]); </script> <!-- 子组件 --> <template> <div>父的 info:{{ info }} -- 父:{{ hobby }}</div> </template> <script setup lang="ts" name="Child"> // defineProps / withDefaults 都是编译宏,无需 import // 简单接收 defineProps(["info", "hobby"]); // 类型式声明 + 默认值:类型写在 defineProps<{}>() 的泛型里(括号内不再传参), // 默认值由 withDefaults 的第二个参数(对象)提供;数组/对象默认值必须用工厂函数返回 withDefaults(defineProps<{ hobby?: string[] }>(), { hobby: () => ["fishing"], }); </script>vue2<!-- vue 2 --> <!-- 父组件 Parent.vue --> <template> <div> <Child :info="info" :hobby="hobby" /> </div> </template> <script> import Child from "./Child.vue"; export default { name: "Parent", components: { Child }, data() { return { info: { name: "parent", age: 48 }, hobby: ["fishing", "somking"], }; }, }; </script> <!-- 子组件 Child.vue --> <template> <div>父的 info:{{ info }} -- 父的 hobby:{{ hobby }}</div> </template> <script> export default { name: "Child", // 简单写法 props: ["info", "hobby"], // 完整验证写法 /* props: { info: { type: Object, required: true, default: () => ({ name: 'default', age: 0 }) }, hobby: { type: Array, default: () => ['fishing'] } } */ }; </script>$ref 方式
父组件获取/修改子组件数据、调用子组件方法等(比 props 方式权限更高)。若父组件无需调用子组件方法,更推荐使用 props 方式。
vue3<!-- 父组件 --> <template> <div> <Child ref="sendData" /> <button @click="changeData">修改子组件数据</button> </div> </template> <script setup lang="ts" name="Parent"> import Child from "./components/xxx/Child.vue"; import { ref } from "vue"; // 模板里 ref="sendData",这里同名变量即可拿到子组件实例(<script setup> 中没有 $refs) const sendData = ref(); // 父传子:直接读写子组件 defineExpose 暴露出来的数据 function changeData() { sendData.value.info_child = { name: "child", age: 18 }; } </script> <!-- 子组件 --> <template> <div>父的 info:{{ info_child }} -- hobby:{{ hobby_child }}</div> </template> <script setup lang="ts" name="Child"> import { ref } from "vue"; // 子组件数据 let info_child = ref({}); let hobby_child = ref(["working"]); // 把数据交给外部使用 defineExpose({ info_child, hobby_child }); </script>vue2<!-- vue 2 --> <!-- 这种方式打破了组件封装性,应优先考虑使用 props;若父组件想直接调用子组件方法,可使用 $refs --> <!-- 父组件 Parent.vue --> <template> <div> <Child ref="childRef" /> <button @click="changeData">修改子组件数据</button> </div> </template> <script> import Child from "./Child.vue"; export default { name: "Parent", components: { Child, }, methods: { changeData() { // 通过 ref 直接修改子组件数据 this.$refs.childRef.info_child = { name: "child", age: 18 }; this.$refs.childRef.hobby_child = ["working", "reading"]; }, }, }; </script> <!-- 子组件 Child.vue --> <template> <div>父的 info:{{ info_child }} -- hobby:{{ hobby_child }}</div> </template> <script> export default { name: "Child", data() { return { info_child: {}, hobby_child: ["working"], }; }, }; </script>
子传父
- emit 自定义事件方式
$emit 适合用于子组件向父组件传递数据,而不是传递方法
父组件给子组件绑定一个事件,并为事件设置回调函数,用于接收参数。
子组件声明一个事件,指定事件名称和传递参数。
提示
凡是接收数据,必定绑定事件;凡是提供数据,必定触发事件;
vue3<!-- vue 3 --> <!-- 父组件 --> <template> <div> <!-- 给子组件绑定事件 --> <Child @send-child-age="getChildAge" /> 子的数据:{{ childAge }} </div> </template> <script setup lang="ts" name="Parent"> import Child from "./components/xxx/Child.vue"; import { ref } from "vue"; // 接收子组件数据 let childAge = ref(0); function getChildAge(val: string) { childAge.value = val; } </script> <!-- 子组件 --> <template> <div> age -- {{ childAge }} <button @click="emit('send-child-age', childAge)">按钮触发事件</button> </div> </template> <script setup lang="ts" name="Child"> import { ref, onMounted } from "vue"; let childAge = ref(18); // 声明事件 const emit = defineEmits(["send-child-age"]); // 函数触发 -- 除了用按钮触发,也可以用函数的方式触发 onMounted(() => { emit("send-child-age", childAge.value); // 传值用 .value,别把 ref 对象传出去 }); </script>vue2<!-- vue 2 --> <!-- 父组件 Parent.vue --> <template> <div> <!-- 监听子组件事件 --> <Child @send-child-age="getChildAge" /> 子的数据:{{ childAge }} </div> </template> <script> import Child from "./Child.vue"; export default { components: { Child }, data() { return { childAge: 0 }; }, methods: { getChildAge(val) { this.childAge = val; }, }, }; </script> <!-- 子组件 Child.vue --> <template> <div> age -- {{ childAge }} <button @click="sendAge">按钮触发事件</button> </div> </template> <script> export default { data() { return { childAge: 18 }; }, methods: { sendAge() { // 触发事件并传递数据 this.$emit("send-child-age", this.childAge); }, }, mounted() { // 组件挂载后自动触发 this.$emit("send-child-age", this.childAge); }, }; </script>
$parent 方式
虽然这种方式也能在父组件中获取、修改子组件数据,但更推荐子组件想调用父组件方法的时候使用
vue3<!-- 父组件 --> <template> <div> 子传递的数据 -- {{ money }} <Child /> </div> </template> <script setup lang="ts" name="Parent"> import Child from "./components/xxx/Child.vue"; let money = ref(100); // 父组件数据 // 把数据交给外部使用 defineExpose({ money }); </script> <!-- 子组件 --> <template> <button @click="changeData($parent)">给父组件发送数据</button> </template> <script setup lang="ts" name="Child"> import { ref } from "vue"; // 子传父 function changeData(parent: any) { parent.money -= 1; // 修改父组件数据 } </script>vue2<!-- vue 2 --> <!-- 父组件 Parent.vue --> <template> <div> 子传递的数据 -- {{ money }} <Child /> </div> </template> <script> import Child from "./Child.vue"; export default { components: { Child }, data() { return { money: 100 }; }, }; </script> <!-- 子组件 Child.vue --> <template> <button @click="changeData">给父组件发送数据</button> </template> <script> export default { methods: { changeData() { this.$parent.money -= 1; // 通过 $parent 直接访问父组件 }, }, }; </script>props 方式
此方式适用于父组件需要获取子组件执行异步操作后返回 Promise 结果,使用场景较少,参考即可。
更推荐使用 emit 事件机制来实现子传父,保持明确的数据流向(子组件 emit → 父组件监听)
vue3<!-- vue 3 --> <!-- 思路:父组件把一个「回调函数」当 props 传给子组件,子组件异步完成后调用它,把结果回传上来 --> <!-- 父组件 Parent.vue --> <template> <div> <Child :fetch-data="handleFetchData" /> <p>获取子的数据:{{ apiData }}</p> </div> </template> <script setup lang="ts"> import { ref } from "vue"; import Child from "./Child.vue"; const apiData = ref<any>(null); // 作为回调传给子组件:子组件拿到异步结果后会调用它 const handleFetchData = (data: any) => { apiData.value = data; }; </script> <!-- 子组件 Child.vue --> <template> <button @click="triggerFetch">获取数据</button> </template> <script setup lang="ts"> const props = defineProps<{ fetchData: (data: any) => void; // 父组件传入的回调 }>(); // 模拟异步操作 const mockApiCall = () => new Promise((resolve) => { setTimeout(() => resolve({ age: 25, name: "John" }), 1000); }); const triggerFetch = async () => { const data = await mockApiCall(); props.fetchData(data); // 通过 props 回调把结果交回父组件 }; </script>vue2<!-- vue 2 --> <!-- 父组件 Parent.vue --> <template> <div> <Child :fetch-data="handleFetchData" /> <p>获取子的数据:{{ apiData }}</p> </div> </template> <script> import Child from "./Child.vue"; export default { components: { Child }, data() { return { apiData: null }; }, methods: { // 作为回调传给子组件,接收子组件回传的数据 handleFetchData(data) { this.apiData = data; }, }, }; </script> <!-- 子组件 Child.vue --> <template> <button @click="triggerFetch">获取数据</button> </template> <script> export default { props: { fetchData: { type: Function, required: true, }, }, methods: { mockApiCall() { return new Promise((resolve) => { setTimeout(() => resolve({ age: 25, name: "John" }), 1000); }); }, async triggerFetch() { const data = await this.mockApiCall(); this.fetchData(data); // 调用父组件传入的回调,把结果交回去 }, }, }; </script>
依赖注入
使用 provide--inject 方式可以完成一个父组件相对于其所有的后代组件间相互通信。参考地址
<!-- 父组件 -->
<template>
<div>
后代组件修改数据 -- {{ money }}
<Child />
</div>
</template>
<script setup lang="ts" name="Parent">
import Child from "./components/xxx/Child.vue";
import { provide } from "vue"; // 提供者,为后代提供数据
let money = ref({ count: 100, name: "RMB" });
let phone = ref("110");
function updateMoney(value: number) {
money.value.count -= value;
}
// 把数据提供给后代使用
provide("money", money);
provide("moneyContext", { phone, updateMoney });
</script>
<!-- 后代组件 -->
<template>
<div>
收到上层组件的数据:{{ money.count }}
<button @click="updateMoney(1)">给上层组件发送数据</button>
</div>
</template>
<script setup lang="ts" name="Child">
import { ref } from "vue";
import { inject } from "vue";
// inject 注入数据。它的第二个参数可以设置默认值,以防止模板语法{{ money.count }}发出警告
const money = inject("money", { count: 0, name: "默认值" });
const { phone, updateMoney } = inject("moneyContext", {
phone: "默认",
updateMoney: (x: number) => {},
});
</script><!-- vue 2 -->
<!-- 父组件(提供者) -->
<script>
export default {
name: "Parent",
// provide 可以是对象,也可以写成函数以访问 this(拿到响应式数据)
provide() {
return {
money: this.money,
updateMoney: this.updateMoney,
};
},
data() {
return { money: { count: 100, name: "RMB" } };
},
methods: {
updateMoney(value) {
this.money.count -= value;
},
},
};
</script>
<!-- 后代组件(注入者) -->
<script>
export default {
name: "Child",
// inject 的数组/对象写法;对象写法可设默认值
inject: {
money: { default: () => ({ count: 0, name: "默认值" }) },
updateMoney: { default: () => () => {} },
},
};
</script>提示:vue2 的
provide传普通值不是响应式的。想让后代感知变化,需像上例那样提供整个对象(引用不变、改属性),或提供一个能读到响应式数据的 getter 函数。
任意组件通信
若组件之间不是父子关系,可使用 mitt 实现任意组件间的通信。
安装
$ npm install --save mitt使用示例
还是那句话:凡是接收数据,必定绑定事件;凡是提供数据,必定触发事件;
以下是组件 A 接收组件 B 数据的一个简单示例:
vue3<!-- vue 3 --> <!-- 关键:A、B 必须共用【同一个】 emitter 实例,否则各自 mitt() 出来的实例互不相通! 所以先单独抽一个文件导出,供所有组件 import: // src/utils/emitter.ts import mitt from "mitt"; export default mitt(); --> <!-- 组件 A --> <template> <div>组件 B 的数据:{{ dataB }}</div> </template> <script setup lang="ts" name="A"> import { ref, onUnmounted } from "vue"; import emitter from "@/utils/emitter"; // 共享实例 // 接收数据 let dataB = ref(0); emitter.on("my-foo", (e: any) => (dataB.value = e)); // 组件卸载时解绑事件,避免内存泄漏(类似于 vue2 中的总线 bus) onUnmounted(() => emitter.off("my-foo")); </script> <!-- 组件 B --> <template> <div> <button @click="emitter.emit('my-foo', myData.name)"> 发送数据--按钮触发 </button> </div> </template> <script setup lang="ts" name="B"> import { ref, onMounted } from "vue"; import emitter from "@/utils/emitter"; // 与 A 同一个实例 const myData = ref({ name: "zs" }); // 发送数据 -- 函数触发 onMounted(() => { emitter.emit("my-foo", myData.value.name); }); </script>vue2<!-- vue 2 使用事件总线 --> <!-- app.vue --> <!-- import Vue from 'vue' export const EventBus = new Vue() --> <!-- 组件 A --> <script> import { EventBus } from "./eventBus"; export default { methods: { sendMessage() { EventBus.$emit("message", "Hello from A"); }, }, }; </script> <!-- 组件 B --> <script> import { EventBus } from "./eventBus"; export default { created() { EventBus.$on("message", (msg) => { console.log(msg); // 'Hello from A' }); }, beforeDestroy() { EventBus.$off("message"); // 记得移除监听 }, }; </script>
插槽
插槽用于让父组件向子组件「传入一段模板」,实现内容分发。分默认插槽、具名插槽、作用域插槽三种。
默认插槽 / 具名插槽:父组件传结构,子组件用
<slot>占位。vue3<!-- 子组件 MyCard.vue --> <template> <div class="card"> <header><slot name="title">默认标题</slot></header> <!-- 不写 name 即默认插槽;<slot> 内可放默认内容,父组件没传时兜底 --> <main><slot>默认内容</slot></main> </div> </template> <!-- 父组件 --> <template> <MyCard> <!-- 具名插槽用 #名称(v-slot 的简写) --> <template #title>我是标题</template> <!-- 其余内容进入默认插槽 --> <p>我是正文</p> </MyCard> </template>vue2<!-- 子组件 MyCard.vue(同上,<slot> 写法一致) --> <!-- 父组件:vue2.6+ 具名插槽用 v-slot:名称,旧写法为 slot="名称" --> <template> <MyCard> <template v-slot:title>我是标题</template> <p>我是正文</p> </MyCard> </template>作用域插槽:子组件把自己的数据「回传」给父组件的模板,父组件决定怎么渲染(常用于表格、列表封装)。
vue3<!-- 子组件 MyList.vue --> <template> <ul> <li v-for="(item, i) in list" :key="i"> <!-- 通过 slot 上的属性把数据传出去 --> <slot :item="item" :index="i">{{ item.name }}</slot> </li> </ul> </template> <script setup lang="ts"> defineProps<{ list: { name: string }[] }>(); </script> <!-- 父组件:用 #default="{ item, index }" 接收子组件传出的数据 --> <template> <MyList :list="users"> <template #default="{ item, index }"> {{ index + 1 }} - <b>{{ item.name }}</b> </template> </MyList> </template>vue2<!-- 父组件:slot-scope(2.5-)或 v-slot(2.6+)接收 --> <template> <MyList :list="users"> <template v-slot:default="{ item, index }"> {{ index + 1 }} - <b>{{ item.name }}</b> </template> </MyList> </template>
组件 v-model
v-model 是「父传子 props + 子传父 emit」的语法糖,常用于封装表单类受控组件。vue3 与 vue2 约定不同:
<!-- 子组件:defineModel() 是 3.4+ 的宏,读写它即自动完成 props+emit -->
<template>
<input :value="model" @input="model = $event.target.value" />
</template>
<script setup lang="ts">
const model = defineModel<string>();
</script>
<!-- 父组件 -->
<template>
<MyInput v-model="text" />
</template><!-- 子组件:默认 prop 名为 modelValue,事件名为 update:modelValue -->
<template>
<input
:value="modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup lang="ts">
defineProps<{ modelValue: string }>();
const emit = defineEmits(["update:modelValue"]);
</script><!-- 子组件:默认 prop 名为 value,事件名为 input -->
<template>
<input :value="value" @input="$emit('input', $event.target.value)" />
</template>
<script>
export default {
props: { value: String },
// 如需改默认名:model: { prop: "checked", event: "change" }
};
</script>多个 v-model:vue3 用
v-model:xxx(对应 propxxx+ 事件update:xxx);vue2 单个组件只能有一个 v-model,多个需用.sync修饰符。
动态组件与 keep-alive
动态组件
<component :is>:根据变量切换渲染哪个组件(tab 页、分步表单常用)。is可绑组件对象,也可绑全局注册的组件名。<template> <component :is="tabs[current]" /> <button v-for="(c, name) in tabs" :key="name" @click="current = name"> {{ name }} </button> </template> <script setup> import { shallowRef } from "vue"; import A from "./A.vue"; import B from "./B.vue"; const tabs = { A, B }; const current = shallowRef("A"); </script>keep-alive:缓存被切走的组件实例,保留其状态(如已填表单、滚动位置),避免每次切换重新创建。
<!-- include/exclude 按组件 name 匹配;被缓存组件用 activated/onActivated 感知"重新进入" --> <keep-alive :include="['A']"> <component :is="tabs[current]" /> </keep-alive>
Teleport
把一段模板「传送」到指定 DOM 节点(通常 body)下渲染,但逻辑上仍属于当前组件。常用于弹窗、抽屉、全局提示——避免被父级的 overflow:hidden / transform / z-index 影响定位。
<template>
<button @click="open = true">打开弹窗</button>
<Teleport to="body">
<div v-if="open" class="modal">我被渲染到 body 下,不受父级样式限制</div>
</Teleport>
</template>
<script setup>
import { ref } from "vue";
const open = ref(false);
</script>示例
一个把 ref + computed + watch + 生命周期 串起来的完整小组件(购物车数量):
<template>
<div>
<button @click="count--" :disabled="count <= 0">-</button>
<span>{{ count }}</span>
<button @click="count++">+</button>
<p>合计:¥{{ total }}</p>
</div>
</template>
<script setup lang="ts" name="Cart">
import { ref, computed, watch, onMounted } from "vue";
const price = 9.9;
const count = ref(1);
// 计算属性:数量变化时自动重算合计
const total = computed(() => (count.value * price).toFixed(2));
// 侦听:数量变化时持久化到本地
watch(count, (val) => localStorage.setItem("cart-count", String(val)));
// 挂载时恢复上次数量
onMounted(() => {
const saved = localStorage.getItem("cart-count");
if (saved) count.value = Number(saved);
});
</script>通用 crud
一套「查询 + 分页 + 新增/编辑弹窗 + 删除」的 Element Plus(vue3)骨架,实际项目按需替换接口即可:
<template>
<!-- 查询区 -->
<el-form :inline="true" :model="query">
<el-form-item label="姓名">
<el-input v-model="query.name" placeholder="请输入" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadList">查询</el-button>
<el-button type="success" @click="openDialog()">新增</el-button>
</el-form-item>
</el-form>
<!-- 表格 -->
<el-table :data="list" v-loading="loading" border>
<el-table-column type="index" label="#" width="60" />
<el-table-column prop="name" label="姓名" />
<el-table-column prop="total" label="金额" />
<el-table-column label="操作" width="160">
<template #default="{ row }">
<el-button link type="primary" @click="openDialog(row)">编辑</el-button>
<el-button link type="danger" @click="remove(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
background
layout="total, sizes, prev, pager, next"
:total="total"
v-model:current-page="query.page"
v-model:page-size="query.pageSize"
@change="loadList"
/>
<!-- 新增/编辑弹窗 -->
<el-dialog v-model="dialogVisible" :title="form._id ? '编辑' : '新增'" width="480px">
<el-form :model="form" label-width="80px">
<el-form-item label="姓名">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="金额">
<el-input-number v-model="form.total" :min="0" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="save">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive } from "vue";
import axios from "axios";
import { ElMessage, ElMessageBox } from "element-plus";
const baseUrl = "/api/xxx";
const loading = ref(false);
const list = ref<any[]>([]);
const total = ref(0);
const query = reactive({ name: "", page: 1, pageSize: 10 });
// 查询列表
async function loadList() {
loading.value = true;
try {
const { data } = await axios.get(`${baseUrl}/list`, { params: query });
list.value = data.data.list;
total.value = data.data.total;
} finally {
loading.value = false;
}
}
// 弹窗表单:不传 row 为新增,传 row 为编辑(浅拷贝,避免直接改表格数据)
const dialogVisible = ref(false);
const form = reactive<any>({});
function openDialog(row?: any) {
Object.keys(form).forEach((k) => delete form[k]);
Object.assign(form, row ? { ...row } : { name: "", total: 0 });
dialogVisible.value = true;
}
// 新增 / 编辑共用一个保存入口,按有无 _id 区分
async function save() {
const url = form._id ? `${baseUrl}/edit` : `${baseUrl}/add`;
await axios.post(url, { user: form });
ElMessage.success("保存成功");
dialogVisible.value = false;
loadList();
}
// 删除(二次确认)
async function remove(row: any) {
await ElMessageBox.confirm(`确定删除「${row.name}」?`, "提示", { type: "warning" });
await axios.delete(`${baseUrl}/delete/${row._id}`);
ElMessage.success("删除成功");
loadList();
}
loadList();
</script>通用状态
<el-table-column label="审核状态" align="center">
<template slot-scope="scope">
{{ showStatus(scope.row.checkStatus) }}
</template>
</el-table-column>
<script>
export default {
methods: {
showStatus(status) {
switch (parseInt(status)) {
case 0:
return "审核中";
case 1:
return "已通过";
case 2:
return "已拒绝";
default:
return "未知";
}
},
},
};
</script><el-table-column label="审核状态" align="center">
<template slot-scope="scope">
<span :style="{ color: getStatusText(scope.row.checkStatus).color }">
{{ getStatusText(scope.row.checkStatus).text }}
</span>
</template>
</el-table-column>
<script>
export default {
methods: {
getStatusText(status) {
const map = {
0: { text: "审核中", color: "#E6A23C" }, // 橙色
1: { text: "已通过", color: "#67C23A" }, // 绿色
2: { text: "已拒绝", color: "#F56C6C" }, // 红色
};
return map[status] || { text: "未知", color: "#909399" }; // 灰色
},
},
};
</script>
