1. 组件的三大组成部分(结构/样式/逻辑)
1.1 scoped 样式冲突
App.vue
<template>
<!-- template 只能有一个根元素 -->
<div id="app">
<BaseOne></BaseOne>
<BaseTwo></BaseTwo>
</div>
</template>
<script>
/*
组件的样式冲突 scoped
默认情况:写在组件中的样式会 全局生效 → 因此很容易造成多个组件之间的样式冲突问题。
1. 全局样式: 默认组件中的样式会作用到全局
2. 局部样式: 可以给组件加上 scoped 属性, 可以让样式只作用于当前组件
scoped原理?
1. 当前组件内标签都被添加 data-v-hash值 的属性
2. css选择器都被添加 [data-v-hash值] 的属性选择器
最终效果: 必须是当前组件的元素, 才会有这个自定义属性, 才会被这个样式作用到
*/
import BaseOne from "./components/BaseOne.vue";
import BaseTwo from "./components/BaseTwo.vue";
export default {
components: {
BaseOne,
BaseTwo,
},
};
</script>
<style>
/* el 根实例独有, data 是一个函数, 其他配置项一致 */
</style>
BaseOne.vue
<template>
<div class="BaseOne">one</div>
</template>
<script>
export default {};
</script>
<style scoped>
div {
width: 50px;
height: 50px;
background-color: pink;
}
</style>
BaseTwo.vue
<template>
<div class="BaseTwo">
two
<h4>h4</h4>
</div>
</template>
<script>
export default {};
</script>
<style scoped>
div {
width: 100px;
height: 100px;
background-color: green;
}
</style>
1.2 data是一个函数
App.vue
<template>
<div id="app">
<!-- 每个实例有自己独立的状态和方法 -->
<BaseCount></BaseCount>
<BaseCount></BaseCount>
<BaseCount></BaseCount>
</div>
</template>
<script>
import BaseCount from "./components/BaseCount.vue";
export default {
components: {
BaseCount,
},
};
</script>
<style>
</style>
BaseCount.vue
<template>
<div class="BaseCount">
<button @click="count--">-</button>
<span>{{ count }}</span>
<button @click="count++">+</button>
</div>
</template>
<script>
export default {
// `data` property in component must be a function
// data:{}
// 一个组件的 data 选项必须是一个函数。→ 保证每个组件实例,维护独立的一份数据对象。
// 每次创建新的组件实例,都会新执行一次 data 函数,得到一个新对象。
data() {
return {
count: 100,
};
},
};
</script>
<style scoped>
span {
margin: 20px;
}
</style>
2. 组件通信
2.1 组件通信语法
/*
组件通信, 就是指 组件与组件 之间的数据传递。
1. 组件的数据是独立的,无法直接访问其他组件的数据。
2. 想用其他组件的数据 → 组件通信
组件关系分类:1. 父子关系 2. 非父子关系
组件通信解决方案:
1. 父子关系: props $emit
2. 非父子关系: provide&inject eventbus
3. 通用解决方案: Vuex(适合复杂业务场景)
*/
2.2 父子通信
App.vue
<template>
<!-- 03-src-父子通信 -->
<div id="app">
<!-- 1. 父 → 子: 父组件通过 props 将数据传递给子组件 -->
<!-- 1.1 给子组件以添加属性的方式传值 :title="myTitle" -->
<!-- <MySon :title="myTitle"></MySon> -->
<!-- 2.2 父组件监听$emit触发的事件 @changeTitle="handleChange" -->
<MySon :title="myTitle" @changeTitle="handleChange"></MySon>
</div>
</template>
<script>
import MySon from "./components/MySon.vue";
export default {
components: {
MySon,
},
data() {
return {
myTitle: "你好,世界",
};
},
methods: {
// 2.3 提供处理函数,在函数的形参中获取传过来的参数
handleChange(val) {
this.myTitle = val;
},
},
};
</script>
<style>
</style>
MySon.vue
<template>
<div class="MySon">
子组件
<!-- 1.3 模板中直接使用 props 接收的值 {{ title }} -->
{{ title }}
<button @click="changeFn">修改</button>
</div>
</template>
<script>
export default {
// 1.2 子组件内部通过 props 接收 props: ["title"]
props: ["title"],
methods: {
changeFn() {
// console.log(this);
// 2. 子 → 父: 子组件利用 $emit 通知父组件,进行修改更新
// 2.1 $emit 触发事件,给父组件发送消息通知
// 触发一个名为 changeTitle 的事件,并传递数据 '修改成功'
// this.$emit("事件名称", "传递修改的内容")
this.$emit("changeTitle", "修改成功");
},
},
};
</script>
<style scoped>
span {
margin: 20px;
}
</style>
2.3 props 传值 - 案例个人信息
App.vue
<template>
<!-- 04-src-props传值 -->
<div id="app">
<!-- 1.2 父级填坑 :username="username" -->
<!-- 2.2 父级收到通知 @changeName="handleName" -->
<UserInfo
:username="username"
:age="age"
:isSingle="isSingle"
:car="car"
:hobby="hobby"
@changeName="handleName"
></UserInfo>
</div>
</template>
<script>
import UserInfo from "./components/UserInfo.vue";
export default {
data() {
return {
username: "小帅",
age: 28,
isSingle: true,
car: {
brand: "宝马",
},
hobby: ["篮球", "足球", "羽毛球"],
};
},
components: {
UserInfo,
},
methods: {
handleName(val) {
// 2.3 父级修改数据
this.username = val;
},
},
};
</script>
<style>
</style>
UserInfo.vue
<template>
<div class="userinfo">
<h3>我是个人信息组件</h3>
<!-- 1.3 子级用 -->
<div>
姓名:{{ username }}
<button @click="changeNameFn">修改名称</button>
</div>
<div>年龄:{{ age }}</div>
<div>是否单身:{{ isSingle ? "是" : "否" }}</div>
<div>座驾:{{ car.brand }}</div>
<div>兴趣爱好:{{ hobby.join("、") }}</div>
</div>
</template>
<script>
export default {
// props 组件上注册的一些自定义属性 → 向子组件传递数据用
// 特点 1. 可以 传递 任意数量 的prop 2. 可以 传递 任意类型 的prop
// 1. 拿到父级数据
// 1.1 子级挖坑
props: ["username", "age", "isSingle", "car", "hobby"],
methods: {
changeNameFn() {
// 2. 修改父级数据
// 2.1 给父级发通知
this.$emit("changeName", "chl");
},
},
};
</script>
<style>
.userinfo {
width: 300px;
border: 3px solid #000;
padding: 20px;
}
.userinfo > div {
margin: 20px 10px;
}
</style>
2.4 props 校验
App.vue
<template>
<!-- 05-src-props校验 -->
<div id="app">
<BaseProgress :w="width"></BaseProgress>
</div>
</template>
<script>
import BaseProgress from "./components/BaseProgress.vue";
export default {
data() {
return {
width: 40,
};
},
components: {
BaseProgress,
},
};
</script>
<style>
</style>
BaseProgress.vue
<template>
<div class="base-progress">
<div class="inner" :style="{ width: w + '%' }">
<span>{{ w }}%</span>
</div>
</div>
</template>
<script>
export default {
// props: ['w'],
// props校验
// 为组件的 prop 指定验证要求,不符合要求,控制台就会有错误提示 → 帮助开发者,快速发现错误
// props校验完整写法
/* props: {
校验的属性名: {
// Number String Boolean ... (类型单词首字母大写)
type: 类型,
// default和required一般不同时写(因为当时必填项时,肯定是有值的)
// 是否必填
required: true,
// 默认值
// default后面如果是简单类型的值,可以直接写默认。如果是复杂类型的值,则需要以函数的形式return一个默认值
default: 默认值,
// 自定义校验逻辑: return 布尔值
validator(value) {
return 是否通过校验;
},
},
}, */
props: {
w: {
// 类型校验 可简写为 w: Number
type: Number,
// required: true,
default: 0,
validator(value) {
if (value >= 0 && value <= 100) {
console.log("满足规则");
return true;
} else {
console.log("不满足规则");
return false;
}
},
},
},
};
</script>
<style scoped>
.base-progress {
height: 26px;
width: 400px;
border-radius: 15px;
background-color: #272425;
border: 3px solid #272425;
box-sizing: border-box;
margin-bottom: 30px;
}
.inner {
position: relative;
background: #379bff;
border-radius: 15px;
height: 25px;
box-sizing: border-box;
left: -3px;
top: -2px;
}
.inner span {
position: absolute;
right: 0;
top: 26px;
}
</style>
3. 综合案例 - 小黑记事本(组件版)
App.vue
持久化存储
<template>
<!-- 07-src-小黑记事本 -->
<!-- 主体区域 -->
<section id="app">
<!-- 输入框 -->
<TodoHeader @addTodoList="handleAdd"></TodoHeader>
<!-- 列表区域 -->
<TodoMain :list="list" @delTodoList="handleDel"></TodoMain>
<!-- 统计和清空 -->
<TodoFooter :list="list" @clearList="handleClear"></TodoFooter>
</section>
</template>
<script>
// * 大驼峰命名法(PascalCase)有助于区分组件和普通 HTML 元素,从而提高代码的可读性和一致性。
// * todoHeader × 创建组件时不遵循大驼峰命名 可能会出现错误 可更改(最好不要)
// 大驼峰命名法本身不会直接导致脚手架报错,
// 但不符合命名约定可能会引发 ESLint 警告、Vue DevTools 显示问题以及第三方库或插件的限制。
/*
vue.config.js 中更改组件命名规则
module.exports = defineConfig({
lintOnSave: true,
});
*/
import TodoHeader from "./components/TodoHeader.vue";
import TodoMain from "./components/TodoMain.vue";
import TodoFooter from "./components/TodoFooter.vue";
export default {
components: {
TodoHeader,
TodoMain,
TodoFooter,
},
data() {
return {
list: JSON.parse(localStorage.getItem("todoList")) || [
{ id: 1, name: "吃饭" },
{ id: 2, name: "喝水" },
{ id: 3, name: "睡觉" },
],
};
},
methods: {
handleAdd(todo) {
this.list.unshift({
id: +new Date(),
name: todo,
});
},
handleDel(id) {
this.list = this.list.filter((item) => item.id !== id);
},
handleClear() {
this.list = [];
},
},
watch: {
// 复杂类型 → 深度监听
list: {
deep: true,
handler(newList) {
localStorage.setItem("todoList", JSON.stringify(newList));
},
},
},
};
</script>
<style>
</style>
TodoHeader.vue
添加任务
<template>
<header class="header">
<h1>小黑记事本</h1>
<!-- @keyup.enter="addTodo" 回车添加任务 -->
<input
placeholder="请输入任务"
class="new-todo"
v-model.trim="todo"
@keyup.enter="addTodo"
/>
<button class="add" @click="addTodo">添加任务</button>
</header>
</template>
<script>
export default {
data() {
return {
todo: "",
};
},
methods: {
addTodo() {
if (this.todo === "") return alert("请输入有效内容");
this.$emit("addTodoList", this.todo);
this.todo = "";
},
},
mounted() {
// 打开页面 → 输入框获取焦点
document.querySelector(".new-todo").focus();
},
};
</script>
<style>
</style>
TodoMain.vue
渲染待办任务
删除任务
<template>
<section class="main">
<ul class="todo-list">
<li class="todo" v-for="(item, index) in list" :key="item.id">
<div class="view">
<span class="index">{{ index + 1 }}.</span>
<label>{{ item.name }}</label>
<button class="destroy" @click="del(item.id)"></button>
</div>
</li>
</ul>
</section>
</template>
<script>
export default {
props: {
list: Array,
},
methods: {
del(id) {
this.$emit("delTodoList", id);
},
},
};
</script>
<style>
</style>
TodoFooter.vue
底部合计 和 清空功能
<template>
<footer class="footer">
<!-- 统计 -->
<span class="todo-count"
>合 计:<strong> {{ list.length }} </strong></span
>
<!-- 清空 -->
<button class="clear-completed" @click="clear">清空任务</button>
</footer>
</template>
<script>
export default {
props: {
list: Array,
},
methods: {
clear() {
this.$emit("clearList");
},
},
};
</script>
<style>
</style>
4. 非父子通信
4.1 event bus 事件总线
utils/EventBus.js
// 非父子通信 (拓展) - event bus 事件总线
// 作用:非父子组件之间,进行简易消息传递。(复杂场景 → Vuex)
// 1. 创建一个都能访问到的事件总线 (空 Vue 实例) → utils/EventBus.js
import Vue from "vue";
const Bus = new Vue();
export default Bus;
BaseB.vue
<template>
<div class="base-b">
B组件:发布方 <button @click="send">发送消息</button>
</div>
</template>
<script>
import Bus from "../utils/EventBus";
export default {
methods: {
send() {
// 2. B 组件(发送方),触发 Bus 实例的事件
Bus.$emit("sendMsg", '368复机,密码"爱你一万年"');
},
},
};
</script>
<style scoped>
.base-b {
width: 300px;
height: 100px;
border: 3px solid #000;
border-radius: 3px;
margin: 10px;
}
</style>
BaseA.vue
<template>
<div class="base-a">
A组件:接收方
<h3>{{ title }}</h3>
</div>
</template>
<script>
import Bus from "../utils/EventBus";
export default {
data() {
return {
title: "",
};
},
created() {
// 3. A 组件(接收方),监听 Bus 实例的事件
Bus.$on("sendMsg", (msg) => {
this.title = msg;
});
},
};
</script>
<style scoped>
.base-a {
width: 300px;
height: 100px;
border: 3px solid #000;
border-radius: 3px;
margin: 10px;
}
h3 {
color: blueviolet;
}
</style>
4.2 provide & inject
App.vue
<template>
<!-- 09-provide和inject -->
<div class="app">
我是APP组件
<button @click="change">修改数据</button>
<SonA></SonA>
<SonB></SonB>
</div>
</template>
<script>
// 非父子通信 (拓展) - provide & inject
// provide & inject 作用:跨层级共享数据。
import SonA from "./components/SonA.vue";
import SonB from "./components/SonB.vue";
export default {
// 1. 父组件 provide 提供数据
/*
注意
- provide提供的简单类型的数据不是响应式的,复杂类型数据是响应式。(推荐提供复杂类型数据)
- 子/孙组件通过inject获取的数据,不能在自身组件内修改
*/
provide() {
return {
// 普通类型【非响应式】
color: this.color,
// 复杂类型【响应式】
userInfo: this.userInfo,
};
},
data() {
return {
color: "pink",
userInfo: {
name: "zs",
age: 18,
},
};
},
methods: {
change() {
this.color = "blue";
this.userInfo.name = "李四";
},
},
components: {
SonA,
SonB,
},
};
</script>
<style>
.app {
border: 3px solid #000;
border-radius: 6px;
margin: 10px;
}
</style>
GrandSon.vue
<template>
<div class="grandSon">
我是GrandSon
<p>{{ color }}</p>
<p>{{ userInfo.name }}</p>
</div>
</template>
<script>
export default {
// 2. 子/孙组件 inject 取值使用
inject: ["color", "userInfo"],
created() {
// console.log(this.color, this.userInfo);
},
};
</script>
<style>
.grandSon {
border: 3px solid #000;
border-radius: 6px;
margin: 10px;
height: 100px;
}
</style>
5. 进阶语法
5.1 v-model 原理
<template>
<!-- 10-v-model原理 -->
<div id="app">
<!-- v-model -->
<input type="text" v-model="msg1" /> <br /><br />
<!--
原理:v-model本质上是一个语法糖。
例如应用在输入框上,就是 value属性 和 input事件 的合写。
作用:提供数据的双向绑定
1. 数据变,视图跟着变 :value
2. 视图变,数据跟着变 @input
不同的表单元素,v-model 在底层的处理机制是不一样的。
比如给 checkbox 使用 v-model 底层处理的是 checked 属性和 change 事件。
-->
<!-- change函数不能加() 加() → 有参数未传, 事件对象得不到数据 -->
<input type="text" :value="msg2" @input="change" /> <br /><br />
<!-- $event 用于在模板中,获取事件的形参
提供的在template里面获取事件对象的参数 -->
<input type="text" :value="msg3" @input="msg3 = $event.target.value" />
</div>
</template>
<script>
export default {
data() {
return {
msg1: "",
msg2: "",
msg3: "",
};
},
methods: {
change(e) {
this.msg2 = e.target.value;
},
},
};
</script>
<style>
</style>
5.2 表单类组件封装 & v-model 简化代码
App.vue
<template>
<!-- 11-src-下拉封装 -->
<div class="app">
<BaseSelect :cityId="selectId" @changeId="handleChangeId"></BaseSelect>
</div>
</template>
<script>
import BaseSelect from "./components/BaseSelect.vue";
export default {
data() {
return {
selectId: "102",
};
},
components: {
BaseSelect,
},
methods: {
handleChangeId(id) {
this.selectId = id;
},
},
};
</script>
<style>
</style>
BaseSelect.vue
<template>
<div>
<!--
表单类组件封装 & v-model 简化代码
实现子组件和父组件数据的双向绑定 (实现App.vue中的selectId和子组件选中的数据进行双向绑定)
下拉菜单 → value 和 change 事件的语法糖
model 不能用 → 双向绑定,代表要修改数据 → cityId来自于父组件,子组件不能直接修改
-->
<select :value="cityId" @change="changeId">
<option value="101">北京</option>
<option value="102">上海</option>
<option value="103">武汉</option>
<option value="104">广州</option>
<option value="105">深圳</option>
</select>
</div>
</template>
<script>
export default {
props: {
cityId: String,
},
methods: {
changeId(e) {
// 通知父组件修改数据 → 当前下拉菜单的value值
this.$emit("changeId", e.target.value);
},
},
};
</script>
<style>
</style>