后盾 Vue3 教程
第二章组件化开发
1.代码视频:代码与视频获取方式
url:https://gitee.com/houdunren/v2021/tree/master/video/vue3
2.全局组件:cdn 开发方式全局组件与 x-template 技巧
Terms:x-template,app.component
- 定义组件 hdXj,使用组件的方式
<hd-xj />
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.31/vue.global.prod.js"></script>
<body>
<div id="app">
<!-- <div>根目录 - <hd-xj /></div> -->
<!-- 需要使用<hd-xj />规范,<hdXj>将失败 -->
</div>
<script type="text/x-template" id="hdTemplate">
<h2>hd-<hd-xj /></h2>
</script>
<script>
//sea require.js vite webpack vue-cli --》 app.js
const app = Vue.createApp({
template: `<div>根目录 - <hd-xj /></div>`,
});
app.component("hd", {
template: "#hdTemplate",
});
app.component("hdXj", {
template: `<div>向军大叔 <hd /></div>`,
});
app.mount("#app");
</script>
</body>
3.局部组件:cdn 开发方式局部组件的声明语法
- 如果对象中的 key 和 value 相同 可以写在一起 {todo:todo} =>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.37/vue.global.prod.js"></script>
<body>
<div id="app">
<todo />
</div>
<script>
const todo = {
data() {
return {
title: "hd",
};
},
template: `<div>{{title}}</div>`,
};
const app = Vue.createApp({
components: { todo },
});
app.mount("#app");
</script>
</body>
4.构建原理:使用 es6 模块构建 VUE3 项目—-模拟工具原理
hd.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>hd</title>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.31/vue.global.prod.js"></script>
</head>
<body>
<div id="app">
<div v-for="data in db" :key="db.id">
<todo :data="data">
</div>
<!-- {{title}} -->
<!-- <todo /> -->
</div>
<script type="module">
import main from './main.js'
</script>
</body>
</html>
main.js
import App from "./App.js";
export default Vue.createApp(App).mount("#app");
App.js
import Todo from "./components/Todo.js";
import db from "./data/db.js";
export default {
components: { Todo },
data() {
return {
// title: 'hd',
db,
};
},
};
components/todo.js
export default {
// data() {
// return {
// content: 'todo',
// }
// },
// template: `<div>{{content}}</div>`,
props: ["data"],
template: `<div style="background-color: #2c3e50;color: #fff;margin-bottom: 30px;padding: 10px;">{{data.title}}</div>`,
};
data/db.js
export default [
{
id: 1,
title: "录制视频是一个很费时的工作,老师需要时间录制更多高质量的视频教程。",
},
{
id: 2,
title:
"不可能做到随时解答问题,我们希望大家可以互相帮助提升技术,而不是直接简单的获取答案。",
},
];
5.文件结构:vite 创建项目的文件结构详解
- public 不需要编译的文件如图片放到这里
yarn create vite vue --template vue
cd vue
yarn
yarn dev // 开发阶段
yarn build // 生成dist文件夹,压缩后的代码
yarn preview // 预览编译的结果
6.type,default:按钮组件 props
- props 下的属性 type,default
- type: String/Boolean/Number/Function/Object
- default: '确定'/default()
- default 根组件没有传递参数才为空,即是否有:content="确定",而非判断"确定"是否为空
components/Button.vue
<template>
<div>{{ content }}</div>
{{ arr }}
</template>
<script>
export default {
props: {
content: {
type: String, //Boolean Number Function Object
default: "确定",
},
arr: {
type: Number,
default() {
return ["1,2", 3];
},
},
},
};
</script>
<style lang="scss" scoped>
div {
background-color: #27ae60;
color: #fff;
padding: 5px 10px;
border-radius: 10px;
opacity: 0.6;
transition: 1s;
display: inline-block;
cursor: pointer;
&:hover {
opacity: 1;
}
}
</style>
App.vue
<template>
<!-- <hd-button :content="btContent" /> -->
<hd-button />
</template>
<script>
import hdButton from "./components/Button.vue";
export default {
components: { hdButton },
data() {
return {
btContent: "保存提交",
};
},
};
</script>
7.自定义样式:自定义按钮样式$attrs.style
terms: $attrs.style
- :class="[type]"其中[]可以放不同的类名
- :style="$attrs.style"当子组件中有多层标签时,指定给确定的标签
App.vue
<template>
<hd-button :content="btContent" type="success" style="margin-right: 10px" />
<hd-button content="删除" type="danger" style="margin-right: 10px" />
<hd-button content="反馈" type="info" />
<!-- <hd-button /> -->
</template>
<script>
import hdButton from "./components/Button.vue";
export default {
components: { hdButton },
data() {
return {
btContent: "保存提交",
};
},
};
</script>
components/Buttons.vue
<template>
<div :class="[type]" :style="$attrs.style">{{ content }}</div>
<span></span>
<!-- {{ arr }} -->
</template>
<script>
export default {
props: {
content: {
type: String,
default: "确定",
},
// arr: {
// type: Number,
// default() {
// return ["1,2", 3];
// },
// },
type: {
type: String,
default: "info",
},
},
};
</script>
<style lang="scss" scoped>
div {
background-color: #27ae60;
color: #fff;
padding: 5px 10px;
border-radius: 10px;
opacity: 0.6;
transition: 1s;
display: inline-block;
cursor: pointer;
&:hover {
opacity: 1;
}
&.info {
background-color: #ddd;
}
&.success {
background-color: #27ae60;
}
&.danger {
background-color: #e74c3c;
}
}
</style>
8.validator:validator 验证与 disabled 禁用按钮
terms: validator
- validator 用于验证传递过来的值是否允许
- :class="[type,{disabled}]" 生成多个类名
<template>
<div :class="[type, { disabled }]" :style="$attrs.style">{{ content }}</div>
</template>
<script>
export default {
props: {
type: {
type: String,
default: "info",
validator(v) {
return ["success", "danger", "info"].includes(v);
},
},
},
};
</script>
<hd-button
content="编辑"
:disabled="true"
type="abc"
style="margin-right: 16px"
/>
</div>
9.json 传数据:v-bind 批量设置 props 与命名规范
- template 中建议使用 hd-button 格式来写组件名与变量名
<!-- 子组件中可用josn格式传递数据 -->
<hd-button
v-bind="{ content: '保存', type: 'success', style: 'margin-right: 16px' }"
/>
10.required:required 验证
content: {
type: String,
required: true,
}
11.单项数据流:单项数据流
- props 是父组件向子组件单向传递数据,不要当做响应式数据进行使用
- 不要修改props
- 如果传递的是数组和对象的引用类型,更改子组件会对父组件影响
父组件
<template>
<hd-button :content="btContent" type="success" />
<hr />
<button @click="btContent = '向军大叔'">父组件</button>
{{ btContent }}
</template>
<script>
export default {
data() {
return {
btContent: "反馈",
};
},
};
</script>
子组件
<template>
<div>{{ content }}</div>
<hr />
<button @click="content = 'hd'">子组件</button>
{{ content }}
</template>
<script>
export default {
props: {
content: { type: String },
},
};
</script>
12.响应方式:子组件中正确的使用 prop 响应的方式
- props 只是用来父组件向子组件传递数值,而非响应式数据.正确的方式赋值给响应式数据
data() {
return {
text: this.content,
};
}
13.watch 监听:使用 watch 监听 prop
- 当 props 中的值改变时,不改变响应数据 text,此时可用 watch 来监听 props 中的值的变化来改变响应式数据
提示
- 实现子元素中的响应式数据,定义一个text接收props中的content,使用 text实现响应式
- 实现通过修改父元素中的content,子元素中的text也发生改变,使用watch监听props中的content的变化,当content发生变化时,text也发生改变
- 使用引用类型
watch: {
content(v) {
this.text = v;
},
}
14.class,id:非 props 属性传递技巧 inheritAttrs.,$attrs.id
terms: inheritAttrs,$attrs.id
- 如果只有一层,class,id 直接放到该层中
- 指定放置,比如放第二层,先禁用 inheritAttrs:false,再在指定的层中放置:id="$attrs.id"
<template>
<section>
<div :id="$attrs.id" :class="[type, { disabled }]" @click="onClick">
{{ text }}
</div>
<!-- <div v-bind="$attrs" :class="[type, { disabled }]" @click="onClick">{{ text }}</div> -->
</section>
</template>
<script>
export default {
inheritAttrs: false,
};
</script>
<hd-button :content="btContent" type="success" class="hd" id="xj" />
15.事件传递:组件间的事件传递
- props 可以传递函数父组件:click="show"子组件@click="click",其中 show 的类型是 Function,即 show 为 function(){console.log("向军")} 此时的父组件中的:click 可以改为@click,此时在子组件中可简化不写@click,而用 v-bind="$attrs",绑定所传递的事件
- 父组件中@click="show",但该点击事件是在子组件中发生
- 如何让父组件中的事件传递到子组件指定的层中,1.先禁用 inheritAttrs: false2.绑定事件 v-bind="$attrs"
App.vue
<template>
<!-- 通过:click来传递函数 -->
<hd-button
class="hd"
id="hd"
:content="btContent"
type="success"
:click="show"
/>
</template>
<template>
<!-- 在父组件中@click,将传递事件给子组件并在子组件中触发该事件 -->
<hd-button
class="hd"
id="hd"
:content="btContent"
type="success"
@click="show"
/>
</template>
components/Button.vue
<template>
<section>
<div :class="[type, { disabled }]">
{{ text }}
</div>
<!-- 再绑定事件 -->
<h2 v-bind="$attrs">向军</h2>
</section>
</template>
<script>
export default {
// 先禁用继承
inheritAttrs: false,
};
</script>
16.event:创建 event 事件项目
- 图片放到/public 中,在 build 后的路径一样,如/public/images/ts.jpg 下的图片,路径用/images/ts.jp
<template>
<div v-for="item in db" :key="item.id">
<img :src="item.preview" alt="" />
</div>
</template>
<script>
import db from "../data/db.js";
export default {
data() {
return {
db,
};
},
};
</script>
17.课程列表:使用 grid 布局课程列表
App.vue
<template>
<div class="lesson">
<lesson v-for="item in db" :key="item.id" :lesson="item" />
</div>
</template>
<script>
import db from "../data/db.js";
import Lesson from "./components/Lesson.vue";
export default {
data() {
return {
db,
};
},
components: { Lesson },
};
</script>
<style lang="scss" scoped>
.lesson {
display: grid;
grid-template-columns: repeat(3, 1fr);
column-gap: 10px;
}
</style>
components/Lesson.vue
<template>
<div>
<img :src="lesson.preview" :alt="lesson.title" />
<h3>{{ lesson.title }}</h3>
<span>x</span>
</div>
</template>
<script>
export default {
props: ["lesson"],
};
</script>
<style lang="scss" scoped>
div {
border: solid 1px #ddd;
text-align: center;
transition: 1s;
&:hover {
box-shadow: 0 0 20px #aaa;
}
h3 {
padding: 0 0 20px 0;
margin: 0;
}
img {
width: 100%;
}
}
</style>
18.删除按钮:添加课程的删除按钮
components/Lesson.vue
div {
border: solid 1px #ddd;
text-align: center;
transition: 1s;
position: relative;
&:hover {
box-shadow: 0 0 20px #aaa;
> span {
opacity: 1;
}
}
span {
display: block;
background-color: #666;
color: #fff;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 10px;
right: 10px;
cursor: pointer;
font-size: 12px;
opacity: 0;
transition: 1s;
}
h3 {
padding: 0 0 20px 0;
margin: 0;
}
img {
width: 100%;
}
}
19.删除:事件传递原理分析
App.vue
<!-- click放到子组件上,默认点击子组件的任何地方都可以删除选项 -->
<template>
<div class="lesson">
<lesson
v-for="item in db"
:key="item.id"
:lesson="item"
@click="show(item)"
class="hd"
/>
</div>
</template>
<script>
import db from "../data/db";
import Lesson from "./components/Lesson.vue";
export default {
data() {
return {
db,
};
},
components: { Lesson },
methods: {
show(item) {
// 删除被点击的项
const index = this.db.findIndex((l) => l.id == item.id);
this.db.splice(index, 1);
},
},
};
</script>
components/Lesson.vue
<!-- 使用v-bind="$attrs"后,把删除事件绑定到span上,但这种做法所有的事件都绑定到这上面了 -->
<template>
<div>
<img :src="lesson.preview" :alt="lesson.title" />
<h3>{{ lesson.title }}</h3>
<span v-bind="$attrs">x</span>
</div>
</template>
<script>
export default {
inheritAttrs: false,
props: ["lesson"],
data() {
return {};
},
};
</script>
20.触发 emit:$emit 触发自定义事件
Terms: 自定义事件,$emit,
提示
- 触发自定义事件流程:父组件调用子组件中自定义事件@del,传递 del 给子组件,子组件接受并定义
emits:['del']
- 当子组件触发事件@click 时,则可执行
$emit('del',lesson)
,并传递参数 lesson - 此时的参数 lesson 将传递回给父组件@del(show)中的 show 函数,并通过 show(lesson)来执行函数
const index = this.db.findIndex((l) => l.id == item.id);
找寻被点击的项itemthis.db.splice(index, 1);
删除被点击的项
<template>
<div class="lesson">
<lesson
v-for="item in db"
:key="item.id"
:lesson="item"
@del="show"
class="hd"
/>
</div>
</template>
<script>
import db from "../data/db";
import Lesson from "./components/Lesson.vue";
export default {
data() {
return {
db,
};
},
components: { Lesson },
methods: {
show(item) {
// console.log(item);
const index = this.db.findIndex((l) => l.id == item.id);
this.db.splice(index, 1);
},
},
};
</script>
<template>
<div>
<img :src="lesson.preview" :alt="lesson.title" />
<h3>{{ lesson.title }}</h3>
<!-- <span @click="$emit('del',lesson)"></span> -->
<span @click="del">x</span>
</div>
</template>
<script>
export default {
inheritAttrs: false,
props: ["lesson"],
emits: ["del"],
methods: {
del() {
this.$emit("del", this.lesson);
},
},
};
</script>
21.验证 emit:验证自定义事件
export default {
inheritAttrs: false,
props: ["lesson"],
// emits: ["del"],
emits: {
// 在这里验证自定义事件,在v传递回父组件前进行验证
del(v) {
if (/^\d+$/.test(v)) {
return true;
}
// console.error('del emit 需要数值参数')
throw new Error("del emit 需要数值参数");
},
},
methods: {
del() {
// this.$emit('del', this.lesson.id)
// this.$emit('del', 'abc')
this.$emit("del", 12);
},
},
};
22.input 同步:自己实现 v-model 功能$event.target.value
Terms: @input,@change,@blur,event.target.value,
- 在函数中用 event.target.value,在元素中用$event.target.value
<input type="text" :value="title" @input="input" /> {{ title }}
input(event) {
console.log(event.target.value);
this.title = event.target.value
}
23.v-model 实现原理
目标: v-model 简化
- $event 的使用
提示
- 用 v-model:value="title" 简化 :value="title" @update:value="title=$event"
<template>
<div class="lesson">
<!-- <hd-input :value="title" @update:value="change" /> -->
<!-- <hd-input :value="title" @update:value="title = $event" /> -->
<hd-input v-model:value="title" />
</div>
<hr />
{{ title }}
</template>
<script>
import HdInput from "./components/HdInput.vue";
export default {
data() {
return {
title: "hd",
};
},
components: { HdInput },
methods: {
// change(v) {
// this.title = v;
// },
},
};
</script>
components/HdInput.vue
<template>
<div>
<input type="text" :value="content" @input="change" />
{{ content }}
</div>
</template>
<script>
export default {
props: ["value"],
emits: ["update:value"],
data() {
return { content: this.value };
},
methods: {
change(event) {
this.content = event.target.value;
this.$emit("update:value", this.content);
},
},
};
</script>
24.简写:v-model 简写格式
目标: v-model 的原理是由原来 props 单向传值与自定义事件 emits 传回值一步步简化而来
Terms: v-model:modelValue,@update:value
- 原 v-model:value 改为 v-model:modelValue 时可以简写为 v-model.父组件传递给子组件的 prop 应一起改为 modelValue,emit 改为 update:modelValue
<hd-input :value="title" @update:value="title = $event" />
=>
<hd-input v-model:value="title" />
=>
<hd-input v-model:modelValue="title" />
=>
<hd-input v-model="title" />
<template>
<div>
<input type="text" :value="content" @input="change" />
{{ content }}
</div>
</template>
<script>
export default {
props: ["modelValue"],
emits: ["update:modelValue"],
data() {
return { content: this.modelValue };
},
methods: {
change(event) {
this.content = event.target.value;
this.$emit("update:modelValue", this.content);
},
},
};
</script>
25.双击修改:vueDevTools 使用与双击修改课程
目标: 使用@dblclick()双击可以修改文字,使用@keyup.enter()回车键@blur 光标离开输入框变回文字
Terms: @dblclick(),@keyup.enter(),@blur()
- 父子组件中响应数据:1.引用特性,2.v-model,
<!-- 双击文本变输入框,失去焦点或回车变回文本 -->
<!-- 使用引用类型lesson时,在子组件中更改时,父组件也会跟着改变,因引用类型共有内存空间 -->
<h3 @dblclick="inputShow = true">
<input
v-if="inputShow"
v-model="lesson.title"
@keyup.enter="inputShow = false"
@blur="inputShow = false"
/>
<strong v-else>{{ lesson.title }}</strong>
</h3>
- 自定义组件中使用 v-model,
- 父组件中使用 v-model="item.title"
- 子组件中 props: ["modelValue"],emits: ["update:modelValue"]
- 子组件中元素使用
event.target.value) - 子组件中传回父组件 db[0].title,如点击的是第一项,传回来的值$event.target.value 即为子组件中被点击的 input 事件的值
父组件
<lesson
v-for="item in db"
:key="item.id"
:lesson="item"
@del="show"
class="hd"
v-model="item.title"
/>
{{ db[0].title }}
子组件
<input
v-if="inputShow"
:value="lesson.title"
@input="$emit('update:modelValue', $event.target.value)"
@keyup.enter="inputShow = false"
@blur="inputShow = false"
/>
props: [ "modelValue"],
emits: {
'update:modelValue': null,
},
26.price:多个 v-model 的使用
目标: 同时使用两个 v-model,例如 v-model:title,v-model:price
父组件
<!-- 添加了v-model:price -->
<lesson
v-for="item in db"
:key="item.id"
:lesson="item"
@del="show"
class="hd"
v-model="item.title"
v-model:price="item.price"
/>
子组件
<h3 @dblclick="inputPriceShow = true">
<input
v-if="inputPriceShow"
:value="lesson.price"
@input="$emit('update:price', $event.target.value)"
@keyup.enter="inputPriceShow = false"
@blur="inputPriceShow = false"
/>
<strong v-else>{{ lesson.price }}</strong>
</h3>
props: ["price"],
data() {
return {
inputPriceShow: false
}
},
emits: {
'update:price': null,
},
27.修饰器:自定义 v-model 修饰器
目标: 在使用 v-model 后面添加自定义的修饰器大写 toupper,截断 2 位 substr_2
v-model.toupper.substr_2
Terms: 修饰器,自定义修饰符,modelModifiers
提示
- v-model.toupper默认修饰符modelModifiers
- v-model:title.toupper修饰符titleModifiers
- 内置修饰符.trim,.number,.lazy
父组件
<!-- v-model:title.toupper.substr_5 -->
<lesson
v-for="item in db"
:key="item.id"
:lesson="item"
@del="show"
class="hd"
v-model:title.toupper.substr_5="item.title"
v-model:price="item.price"
/>
子组件,自定义修饰符
// titleModifiers是根据v-model:title.toupper.substr_5来确定的
props: ["lesson", "title", "price", "titleModifiers"]
// 在元素中调用@input="changeTitle"
changeTitle($event) {
let value = $event.target.value
if (this.titleModifiers.toupper) {
value = value.toUpperCase()
}
const substr = Object.keys(this.titleModifiers).find(m => /^substr_/.test(m));
if (substr) {
let info = substr.split('_');
value = value.substr(0, info[1])
}
this.$emit('update:title', value)
}