插槽
插槽
以下为学习过程中的极简提炼笔记,以供重温巩固学习
学习准备
准备工作
html+css+JavaScript 3剑客都懂一点,完成AJAX原理,熟悉AJAX函数与调用,熟悉AJAX前端接口处理
学习目的
自定义指令
内置指令:
- 包括:v-html、v-model、v-for等等
- 每个指令各自有独立的功能
- 前面学的 v-指令 是属于vue内置预封装的指令
自定义定义:
- 自己定义的指令,可以封装一些 dom 操作,扩展额外功能
- 我们可以自己封装一些功能业务,封装为自定义指令,方便直接复用
自定义指令封装
需求:自动获取焦点需求:
- 当页面加载时,让某元素将获得焦点(autofocus 在 safari 浏览器有兼容性)
封装前的解决方案:通过操作dom,解决兼容性问题:
dom元素.focus()
封装前的解决方案实现语法:
mounted () {
this.$refs.inp.focus()
}
需求:通过自定义指令封装,简化实现的语法
自定义指令全局注册语法
Vue.directive('指令名', {
"inserted" (el) {
// 可以对 el 标签,扩展额外功能
el.focus()
}
})
- 自定义指令局部注册语法
directives: {
"指令名": {
inserted () {
// 可以对 el 标签,扩展额外功能
el.focus()
}
}
}
- 简化后的自定义指令实现相同功能的语法:
<input v-指令名 type="text">
- 备注:
- inserted是vue2中自定义指令的一个生命周期钩子函数,vue3中这个钩子函数被重命名为mounted
- inserted的定义:当前指令所绑定的元素,被添加到页面中时,会自动调用
inserted (el) {el.focus()}
中,inserted的形参el就是指令所绑定的元素
<template>
<div>
<h1>自定义指令</h1>
//通过 v-focus 使用指令 //
<input v-focus ref="inp" type="text">
</div>
</template>
<script>
export default {
// mounted () {
// this.$refs.inp.focus()
// }
// 2. 局部注册指令
directives: {
// 指令名:指令的配置项
focus: {
inserted (el) {
el.focus()
}
}
}
}
</script>
<style>
</style>
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
// 1. 全局注册指令
Vue.directive('focus', {
// inserted 会在 指令所在的元素,被插入到页面中时触发
inserted (el) {
// el 就是指令所绑定的元素
// console.log(el);
// 获取焦点
el.focus()
}
})
new Vue({
render: h => h(App),
}).$mount('#app')
总结:
- 自定义在局部注册或全局注册都是类似的,写一个大配置项,在配置项中,提供对应钩子逻辑
自定义指令的使用步骤?
- 注册 (全局注册 或 局部注册)
- 在 inserted 钩子函数中,配置指令dom逻辑
- 标签上 v-指令名 使用
- 注册 (全局注册 或 局部注册)
自定义指令 - 指令的值
需求:实现一个 color 指令 - 传入不同的颜色, 给标签设置文字颜色
本质:自定义指令是可以传值的
语法:在绑定指令时,可以通过“等号”的形式为指令 绑定 具体的参数值
<div v-color="color">我是内容</div>
- 通过 binding.value 可以拿到指令值,指令值修改会 触发 update 函数
- 备注:
- inserted是vue2中自定义指令的一个生命周期钩子函数,vue3中这个钩子函数被重命名为mounted
- inserted的定义:当前指令所绑定的元素,被添加到页面中时,会自动调用
inserted (el) {el.focus()}
中,inserted的形参el就是指令所绑定的元素inserted (el, binding) {el.style.color = binding.value},
中,binding是形参el的一个额外配置项,通过配置项binding.value,拿到指令的值- 指令值的修改,会触发 update 函数;
- inserted/mounted 函数是元素被添加到页面中时触发,update 函数是指令的值被修改时触发(前面生命周期钩子学过)
- 如下面案例,自定义指令v-color用在了两个标签上,如果指令的值color1或者color2这两个变量任意一个发生了变化,也就是自定义指令发生了变化;
- 标签每使用一次自定义指令v-color,就会触发一次 update 函数,因此虽然在update 函数中只有一个打印语句,但实际上只要改了颜色,因为使用了两次,就会打印两次
directives: {
color: {
inserted (el, binding) {
el.style.color = binding.value
},
update (el, binding) {
el.style.color = binding.value
}
}
}
<template>
<div>
// 通过v-color使用自定义指令color,通过传参传入变量,el就是指令所绑定的元素,binding.value,拿到指令的值,也就是color1和color2的值
<h1 v-color="color1">指令的值1测试</h1>
<h1 v-color="color2">指令的值2测试</h1>
</div>
</template>
<script>
export default {
data () {
return {
// 提供两个变量传值
color1: 'red',
color2: 'orange'
}
},
directives: {
//定义一个指令,指令名为color
color: {
// 1. inserted 提供的是元素被添加到页面中时的逻辑
inserted (el, binding) {
// console.log(el, binding.value);
// binding.value 就是指令的值
el.style.color = binding.value
// 通过在标签中使用v-color,以使用自定义指令color,通过传参传入变量,el就是指令所绑定的元素,binding.value,拿到指令的值,也就是color1和color2的值
},
// 2. update 指令的值修改的时候触发,提供值变化后,dom更新的逻辑
update (el, binding) {
console.log('指令的值修改了');
el.style.color = binding.value
}
}
}
}
</script>
<style>
</style>
- 总结:
- 通过指令的值相关语法,可以应对更复杂指令封装场景
- 后续v-loading加载中指令,v-lazy图片懒加载都会应用
- 指令值的语法:
- ① v-指令名 = "指令值",通过 等号 可以绑定指令的值
- ② 通过 binding.value 可以拿到指令的值
- ③ 通过 update 钩子,可以监听指令值的变化,进行dom更新操作
自定义指令 综合进阶 - v-loading 指令封装
场景:实际开发过程中,发送请求需要时间,在请求的数据未回来时,页面会处于空白状态 => 用户体验不好
需求:封装一个 v-loading 指令,实现加载中的效果
分析:
- 本质 loading 效果就是一个蒙层,盖在了盒子上
- 数据请求中,开启loading状态,添加蒙层
- 数据请求完毕,关闭loading状态,移除蒙层
实现:
- 准备一个 loading 类,通过伪元素定位,设置宽高,实现蒙层
- 好处:伪元素添加和移除较为方便,只需要添加和移除类名,如果是真实元素,添加和移除还要操作dom
- 开启关闭 loading 状态(添加移除蒙层),本质只需要添加移除类即可
- 结合自定义指令的语法进行封装复用
- 准备一个 loading 类,通过伪元素定位,设置宽高,实现蒙层
<template>
<div class="main">
<div class="box" v-loading="isLoading">
<ul>
<li v-for="item in list" :key="item.id" class="news">
<div class="left">
<div class="title">{{ item.title }}</div>
<div class="info">
<span>{{ item.source }}</span>
<span>{{ item.time }}</span>
</div>
</div>
<div class="right">
<img :src="item.img" alt="">
</div>
</li>
</ul>
</div>
<div class="box2" v-loading="isLoading2"></div>
</div>
</template>
<script>
// 安装axios => yarn add axios
import axios from 'axios'
// 接口地址:http://hmajax.itheima.net/api/news
// 请求方式:get
export default {
data () {
return {
list: [],
isLoading: true,
isLoading2: true
}
},
async created () {
// 1. 发送请求获取数据
const res = await axios.get('http://hmajax.itheima.net/api/news')
setTimeout(() => {
// 2. 更新到 list 中,用于页面渲染 v-for
this.list = res.data.data
this.isLoading = false
}, 2000)
},
directives: {
loading: {
inserted (el, binding) {
binding.value ? el.classList.add('loading') : el.classList.remove('loading')
},
update (el, binding) {
binding.value ? el.classList.add('loading') : el.classList.remove('loading')
}
}
}
}
</script>
<style>
.loading:before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: #fff url('./loading.gif') no-repeat center;
}
.box2 {
width: 400px;
height: 400px;
border: 2px solid #000;
position: relative;
}
.box {
width: 800px;
min-height: 500px;
border: 3px solid orange;
border-radius: 5px;
position: relative;
}
.news {
display: flex;
height: 120px;
width: 600px;
margin: 0 auto;
padding: 20px 0;
cursor: pointer;
}
.news .left {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
padding-right: 10px;
}
.news .left .title {
font-size: 20px;
}
.news .left .info {
color: #999999;
}
.news .left .info span {
margin-right: 20px;
}
.news .right {
width: 160px;
height: 120px;
}
.news .right img {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
- 总结:
- 通过指令相关语法,封装了指令 v-loading 实现了请求的loading效果
- 核心思路:
- (1) 准备类名 loading,通过伪元素提供遮罩层
- (2) 添加或移除类名,实现loading蒙层的添加移除
- (3) 利用指令语法,封装 v-loading 通用指令
- inserted 钩子中,binding.value 判断指令的值,设置默认状态
- update 钩子中,binding.value 判断指令的值,更新类名状态
插槽
定义:
- 封装组件时,组件下有很多地方需要定制,如使用传统的父子关系,通常只能设置普通文本
- 使用插槽,可以更丰富我们的定制方式
案例:
- 封装表格组件
- 封装按钮为可编辑组件
默认插槽
- 作用:让组件内部的一些 结构 支持 自定义
- 本质:
- 是基于组件的、在组件基础上再作自定义的功能
- 子组件上做插槽,父组件上通过标签调用
- 需求: 将需要多次显示的对话框,封装成一个组件

- 问题:组件的内容部分,不希望写死,希望能使用的时候自定义。怎么办?
插槽基本语法:
- 组件内需要定制的结构部分,改用
<slot></slot>
占位 - 使用组件时,
<MyDialog></MyDialog>
在组件标签标签内部, 传入结构替换slot
<template>
<div>
<!-- 2. 在使用组件时,组件标签内填入内容 -->
<MyDialog>
<div>你确认要删除么</div>
</MyDialog>
<MyDialog>
<p>你确认要退出么</p>
</MyDialog>
</div>
</template>
<script>
import MyDialog from './components/MyDialog.vue'
export default {
data () {
return {
}
},
components: {
MyDialog
}
}
</script>
<style>
body {
background-color: #b3b3b3;
}
</style>
<template>
<div class="dialog">
<div class="dialog-header">
<h3>友情提示</h3>
<span class="close">✖️</span>
</div>
<div class="dialog-content">
<!-- 1. 在需要定制的位置,使用slot占位 -->
<slot></slot>
</div>
<div class="dialog-footer">
<button>取消</button>
<button>确认</button>
</div>
</div>
</template>
<script>
export default {
data () {
return {
}
}
}
</script>
<style scoped>
* {
margin: 0;
padding: 0;
}
.dialog {
width: 470px;
height: 230px;
padding: 0 25px;
background-color: #ffffff;
margin: 40px auto;
border-radius: 5px;
}
.dialog-header {
height: 70px;
line-height: 70px;
font-size: 20px;
border-bottom: 1px solid #ccc;
position: relative;
}
.dialog-header .close {
position: absolute;
right: 0px;
top: 0px;
cursor: pointer;
}
.dialog-content {
height: 80px;
font-size: 18px;
padding: 15px 0;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
}
.dialog-footer button {
width: 65px;
height: 35px;
background-color: #ffffff;
border: 1px solid #e1e3e9;
cursor: pointer;
outline: none;
margin-left: 10px;
border-radius: 3px;
}
.dialog-footer button:last-child {
background-color: #007acc;
color: #fff;
}
</style>
- 总结:
- 场景:当组件内某一部分结构不确定,想要自定义怎么办?
- 用插槽 slot 占位封装
- 使用:插槽使用的基本步骤?
- 先在组件内用 slot 占位
- 使用组件时, 传入具体标签内容插入
- 场景:当组件内某一部分结构不确定,想要自定义怎么办?
插槽 - 后备内容(默认值)
插槽后备内容:封装组件时,可以为预留的
<slot>
插槽提供后备内容(默认内容)语法: 在
<slot>
标签内,放置内容, 作为默认显示内容效果:
- 外部使用组件时,不传东西,则slot会显示后备内容
<MyDialog></MyDialog>
- 外部使用组件时,传东西了,则slot整体会被换掉
<MyDialog>我是内容</MyDialog>
- 外部使用组件时,不传东西,则slot会显示后备内容
如以下在子组件上,预设默认显示的内容
<template>
<div class="dialog">
<div class="dialog-header">
<h3>友情提示</h3>
<span class="close">✖️</span>
</div>
<div class="dialog-content">
<slot>我是后备/默认内容</slot>
</div>
<div class="dialog-footer">
<button>取消</button>
<button>确认</button>
</div>
</div>
</template>
<template>
<div>
<MyDialog></MyDialog>
<MyDialog>
你确认要退出么
</MyDialog>
</div>
</template>
<script>
import MyDialog from './components/MyDialog.vue'
export default {
data () {
return {
}
},
components: {
MyDialog
}
}
</script>
<style>
body {
background-color: #b3b3b3;
}
</style>
<template>
<div class="dialog">
<div class="dialog-header">
<h3>友情提示</h3>
<span class="close">✖️</span>
</div>
<div class="dialog-content">
<!-- 往slot标签内部,编写内容,可以作为后备内容(默认值) -->
<slot>
我是默认的文本内容
</slot>
</div>
<div class="dialog-footer">
<button>取消</button>
<button>确认</button>
</div>
</div>
</template>
<script>
export default {
data () {
return {
}
}
}
</script>
<style scoped>
* {
margin: 0;
padding: 0;
}
.dialog {
width: 470px;
height: 230px;
padding: 0 25px;
background-color: #ffffff;
margin: 40px auto;
border-radius: 5px;
}
.dialog-header {
height: 70px;
line-height: 70px;
font-size: 20px;
border-bottom: 1px solid #ccc;
position: relative;
}
.dialog-header .close {
position: absolute;
right: 0px;
top: 0px;
cursor: pointer;
}
.dialog-content {
height: 80px;
font-size: 18px;
padding: 15px 0;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
}
.dialog-footer button {
width: 65px;
height: 35px;
background-color: #ffffff;
border: 1px solid #e1e3e9;
cursor: pointer;
outline: none;
margin-left: 10px;
border-radius: 3px;
}
.dialog-footer button:last-child {
background-color: #007acc;
color: #fff;
}
</style>
- 总结:
- 如何给插槽设置默认显示内容?
- 在slot标签内,写好后备内容
- 什么时候插槽后备内容会显示?
- 当使用组件并未给我们传入具体标签或内容时
- 如何给插槽设置默认显示内容?
插槽 - 具名插槽
需求:一个组件内有多处结构,需要外部传入标签,进行定制
默认插槽:一个的定制位置
具名插槽:支持多个位置定制
具名插槽语法:
- 在多个需要定制的地方,使用多个slot,并且给每一个slot都写上对应的名字,相当于每一个插槽都有唯一标识
- 在通过
<组件名标签对>
使用组件时,在组件标签对中,使用<template></template>
标签对需要定制的内容包裹,并配合v-slot:插槽名字
进行分发
- 子组件上,多个slot使用name属性区分名字
<div class="dialog-header">
<slot name="head"></slot>
</div>
<div class="dialog-content">
<slot name="content"></slot>
</div>
<div class="dialog-footer">
<slot name="footer"></slot>
</div>
- 父组件使用 template 配合v-slot:名字来分发对应标签
<MyDialog>
<template v-slot:head>
大标题
</template>
<template v-slot:content>
内容文本
</template>
<template v-slot:footer>
<button>按钮</button>
</template>
</MyDialog>
- 父组件 v-slot:插槽名 可以简化成 #插槽名
<MyDialog>
<template #head>
大标题
</template>
<template #content>
内容文本
</template>
<template #footer>
<button>按钮</button>
</template>
</MyDialog>
<template>
<div>
<MyDialog>
<!-- 需要通过template标签包裹需要分发的结构,包成一个整体 -->
<template v-slot:head>
<div>我是大标题</div>
</template>
<template v-slot:content>
<div>我是内容</div>
</template>
<template #footer>
<button>取消</button>
<button>确认</button>
</template>
</MyDialog>
</div>
</template>
<script>
import MyDialog from './components/MyDialog.vue'
export default {
data () {
return {
}
},
components: {
MyDialog
}
}
</script>
<style>
body {
background-color: #b3b3b3;
}
</style>
<template>
<div class="dialog">
<div class="dialog-header">
<!-- 一旦插槽起了名字,就是具名插槽,只支持定向分发 -->
<slot name="head"></slot>
</div>
<div class="dialog-content">
<slot name="content"></slot>
</div>
<div class="dialog-footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<script>
export default {
data () {
return {
}
}
}
</script>
<style scoped>
* {
margin: 0;
padding: 0;
}
.dialog {
width: 470px;
height: 230px;
padding: 0 25px;
background-color: #ffffff;
margin: 40px auto;
border-radius: 5px;
}
.dialog-header {
height: 70px;
line-height: 70px;
font-size: 20px;
border-bottom: 1px solid #ccc;
position: relative;
}
.dialog-header .close {
position: absolute;
right: 0px;
top: 0px;
cursor: pointer;
}
.dialog-content {
height: 80px;
font-size: 18px;
padding: 15px 0;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
}
.dialog-footer button {
width: 65px;
height: 35px;
background-color: #ffffff;
border: 1px solid #e1e3e9;
cursor: pointer;
outline: none;
margin-left: 10px;
border-radius: 3px;
}
.dialog-footer button:last-child {
background-color: #007acc;
color: #fff;
}
</style>
- 总结:
- 组件内 有多处不确定的结构 怎么办?
- 具名插槽
- 子组件上,slot占位, 给name属性起名字来区分
- 父组件上,template配合 v-slot:插槽名 分发内容
- v-slot:插槽名 可以简化成什么?
- #插槽名
- 组件内 有多处不确定的结构 怎么办?
插槽 - 作用域插槽
作用域插槽: 是插槽的一个传参语法
- 定义 slot 插槽的同时, 是可以传值的。
- 给 插槽 上可以 绑定数据,将来 使用组件时可以用。
场景:封装表格组件(组件一致数据不一致)
- 父传子,动态渲染表格内容
- 利用默认插槽,定制操作列
- 删除或查看都需要用到 当前项的 id,属于 组件内部的数据,通过 作用域插槽 传值绑定,进而使用
基本使用步骤:
- 在子组件中,给 slot 标签, 以 添加属性的方式传值
<slot :id="item.id" msg="测试文本"></slot>
- 所有添加的属性, 都会被收集到一个对象中
{ id: 3, msg: '测试文本' }
- 在父级组件template中, 通过
#插槽名= "obj"
接收,默认插槽名为 default
<MyTable :list="list">
<template #default="obj">
<button @click="del(obj.id)">删除</button>
</template>
</MyTable>
<template>
<div>
<!-- 0. 通过:data="list"将父级组件的list数据传给子组件 -->
<MyTable :data="list">
<!-- 3. 通过template #插槽名="变量名" 接收 ,此处为默认插槽名字default-->
<template #default="obj">
<button @click="del(obj.row.id)">
删除
</button>
</template>
</MyTable>
<MyTable :data="list2">
<!-- 4. 可以直接解构传入的对象-->
<template #default="{ row }">
<button @click="show(row)">查看</button>
</template>
</MyTable>
</div>
</template>
<script>
import MyTable from './components/MyTable.vue'
export default {
data () {
return {
list: [
{ id: 1, name: '张小花', age: 18 },
{ id: 2, name: '孙大明', age: 19 },
{ id: 3, name: '刘德忠', age: 17 },
],
list2: [
{ id: 1, name: '赵小云', age: 18 },
{ id: 2, name: '刘蓓蓓', age: 19 },
{ id: 3, name: '姜肖泰', age: 17 },
]
}
},
methods: {
del (id) {
this.list = this.list.filter(item => item.id !== id)
},
show (row) {
// console.log(row);
alert(`姓名:${row.name}; 年纪:${row.age}`)
}
},
components: {
MyTable
}
}
</script>
<template>
<table class="my-table">
<thead>
<tr>
<th>序号</th>
<th>姓名</th>
<th>年纪</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<!-- 0. 接收父级传尽量的数据并渲染 -->
<tr v-for="(item, index) in data" :key="item.id">
<td>{{ index + 1 }}</td>
<td>{{ item.name }}</td>
<td>{{ item.age }}</td>
<td>
<!-- 0.5 通过slot占位,以使得不同表格不同功能,当前是默认插槽default -->
<!-- 1. 给slot标签,添加属性的方式传值 -->
<slot :row="item" msg="测试文本"></slot>
<!-- 2. 插槽会将本行row/item遍历到的元素,所有的属性,添加到一个对象中 -->
<!--
{
row: { id: 2, name: '孙大明', age: 19 },
msg: '测试文本'
}
-->
</td>
</tr>
</tbody>
</table>
</template>
<script>
export default {
// 0. 接收父级传过来的数据
props: {
data: Array
}
}
</script>
<style scoped>
.my-table {
width: 450px;
text-align: center;
border: 1px solid #ccc;
font-size: 24px;
margin: 30px auto;
}
.my-table thead {
background-color: #1f74ff;
color: #fff;
}
.my-table thead th {
font-weight: normal;
}
.my-table thead tr {
line-height: 40px;
}
.my-table th,
.my-table td {
border-bottom: 1px solid #ccc;
border-right: 1px solid #ccc;
}
.my-table td:last-child {
border-right: none;
}
.my-table tr:last-child td {
border-bottom: none;
}
.my-table button {
width: 65px;
height: 35px;
font-size: 18px;
border: 1px solid #ccc;
outline: none;
border-radius: 3px;
cursor: pointer;
background-color: #ffffff;
margin-left: 5px;
}
</style>
- 总结:
- 作用域插槽的作用是什么?
- 可以给插槽上绑定数据,供将来使用组件时使用
- 作用域插槽使用步骤?
- (1)给 slot 标签, 以 添加属性的方式传值
- (2)所有属性都会被收集到一个对象中
- (3)template中, 通过
#插槽名= "obj"
接收
- 作用域插槽的作用是什么?
综合案例 - 商品列表
目的:重点练习组件封装
需求说明:
- my-tag 标签组件封装
- (1) 双击显示输入框,输入框获取焦点
- 通过 v-if 和 v-else,实现二选一状态显示隐藏
- 通过@dbclick监听双击,调用 handleClick函数,操作isEdit把编辑状态改成true
- 获取焦点方式一
this.$nextTick(() => {this.$refs.inp.focus()})
- 获取焦点方式二:将方式一的dom操作,封装到指令中,全局注册,后续通过 v-focus使用
- (2) 失去焦点,隐藏输入框
- 注册失去焦点事件 @blur="isEdit = false"
- (3) 回显标签信息
- 双击聚焦后,原标签内容的回显,信息是从父组件传递过来的
- 标签的信息是由外部/父级传入的,并且子组件修改后要往父传递,通过v-model实现能简化代码,
v-model =等价于> :value 和 @input
事件监听组合,在父组件上v-model,子组件上:value="value"
,单腿绑定
- (4) 内容修改,回车 → 修改标签信息
- 回车时将输入固化,监听键盘事件,将v-model拆分的另一个腿
@keyup.enter="handleEnter"
赋值回去handleEnter 函数 - 由于父组件是v-model,需要触发 input 事件
this.$emit('input', e.target.value)
- 通过e,拿到回车时的事件对象,也就是回车那一刻的输入框的实时状态,
e.target
为触发事件的事件源,e.target.value
为值,通过this.$emit
传父级 - 提交完成,关闭输入状态
this.isEdit = false
- 非空处理
if (e.target.value.trim() === '') return alert('标签内容不能为空')
- 回车时将输入固化,监听键盘事件,将v-model拆分的另一个腿
- my-table 表格组件封装
(1) 动态传递表格数据渲染
- 数据不能写死,动态传递表格渲染的数据 props -
required: true
数据不能为空 - 直接解构,类型是一个数组type: Array
- 传过来的数组名叫data,直接v-for in数组名 - 结构不能写死 - 多处结构自定义 【具名插槽】 - 在子组件上,通过slot占位,将数据部分的结构,放到父组件上
- 数据不能写死,动态传递表格渲染的数据 props -
(2) 表头支持用户自定义
(3) 主体支持用户自定义
备注:less是在css的基础上增加了变量、混合及函数等功能的扩展性css
<template>
<div class="my-tag">
<input
v-if="isEdit"
v-focus
ref="inp"
class="input"
type="text"
placeholder="输入标签"
:value="value"
@blur="isEdit = false"
@keyup.enter="handleEnter"
/>
<div
v-else
@dblclick="handleClick"
class="text">
{{ value }}
</div>
</div>
</template>
<script>
export default {
props: {
value: String
},
data () {
return {
isEdit: false
}
},
methods: {
handleClick () {
// 双击后,切换到显示状态 (Vue是异步dom更新)
this.isEdit = true
// // 等dom更新完了,再获取焦点
// this.$nextTick(() => {
// // 立刻获取焦点
// this.$refs.inp.focus()
// })
},
handleEnter (e) {
// 非空处理
if (e.target.value.trim() === '') return alert('标签内容不能为空')
// 子传父,将回车时,[输入框的内容] 提交给父组件更新
// 由于父组件是v-model,触发事件,需要触发 input 事件
this.$emit('input', e.target.value)
// 提交完成,关闭输入状态
this.isEdit = false
}
}
}
</script>
<style lang="less" scoped>
.my-tag {
cursor: pointer;
.input {
appearance: none;
outline: none;
border: 1px solid #ccc;
width: 100px;
height: 40px;
box-sizing: border-box;
padding: 10px;
color: #666;
&::placeholder {
color: #666;
}
}
}
</style>
<template>
<table class="my-table">
<thead>
<tr>
<slot name="head"></slot>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in data" :key="item.id">
{/* :item="item"意为传出item给父组件使用 */}
<slot name="body" :item="item" :index="index" ></slot>
</tr>
</tbody>
</table>
</template>
<script>
export default {
props: {
data: {
type: Array,
required: true
}
}
};
</script>
<style lang="less" scoped>
.my-table {
width: 100%;
border-spacing: 0;
img {
width: 100px;
height: 100px;
object-fit: contain;
vertical-align: middle;
}
th {
background: #f5f5f5;
border-bottom: 2px solid #069;
}
td {
border-bottom: 1px dashed #ccc;
}
td,
th {
text-align: center;
padding: 10px;
transition: all .5s;
&.red {
color: red;
}
}
.none {
height: 100px;
line-height: 100px;
color: #999;
}
}
</style>
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
// 封装全局指令 focus
Vue.directive('focus', {
// 指令所在的dom元素,被插入到页面中时触发
inserted (el) {
el.focus()
}
})
new Vue({
render: h => h(App),
}).$mount('#app')
<template>
<div class="table-case">
<!-- 放表格组件 -->
<MyTable :data="goods">
<template #head>
<th>编号</th>
<th>名称</th>
<th>图片</th>
<th width="100px">标签</th>
</template>
<template #body="{ item, index }">
<td>{{ index + 1 }}</td>
<td>{{ item.name }}</td>
<td>
<img
:src="item.picture"
/>
</td>
<td>
<!-- 放标签组件 -->
<MyTag v-model="item.tag"></MyTag>
</td>
</template>
</MyTable>
</div>
</template>
<script>
// my-tag 标签组件的封装
// 1. 创建组件 - 初始化
// 2. 实现功能
// (1) 双击显示,并且自动聚焦
// v-if v-else @dbclick 操作 isEdit
// 自动聚焦:
// 1. $nextTick => $refs 获取到dom,进行focus获取焦点
// 2. 封装v-focus指令
// (2) 失去焦点,隐藏输入框
// @blur 操作 isEdit 即可
// (3) 回显标签信息
// 回显的标签信息是父组件传递过来的
// v-model实现功能 (简化代码) v-model => :value 和 @input
// 组件内部通过props接收, :value设置给输入框
// (4) 内容修改了,回车 => 修改标签信息
// @keyup.enter, 触发事件 $emit('input', e.target.value)
// ---------------------------------------------------------------------
// my-table 表格组件的封装
// 1. 数据不能写死,动态传递表格渲染的数据 props
// 2. 结构不能写死 - 多处结构自定义 【具名插槽】
// (1) 表头支持自定义
// (2) 主体支持自定义
import MyTag from './components/MyTag.vue'
import MyTable from './components/MyTable.vue'
export default {
name: 'TableCase',
components: {
MyTag,
MyTable
},
data () {
return {
// 测试组件功能的临时数据
tempText: '水杯',
tempText2: '钢笔',
goods: [
{ id: 101, picture: 'https://yanxuan-item.nosdn.127.net/f8c37ffa41ab1eb84bff499e1f6acfc7.jpg', name: '梨皮朱泥三绝清代小品壶经典款紫砂壶', tag: '茶具' },
{ id: 102, picture: 'https://yanxuan-item.nosdn.127.net/221317c85274a188174352474b859d7b.jpg', name: '全防水HABU旋钮牛皮户外徒步鞋山宁泰抗菌', tag: '男鞋' },
{ id: 103, picture: 'https://yanxuan-item.nosdn.127.net/cd4b840751ef4f7505c85004f0bebcb5.png', name: '毛茸茸小熊出没,儿童羊羔绒背心73-90cm', tag: '儿童服饰' },
{ id: 104, picture: 'https://yanxuan-item.nosdn.127.net/56eb25a38d7a630e76a608a9360eec6b.jpg', name: '基础百搭,儿童套头针织毛衣1-9岁', tag: '儿童服饰' },
]
}
}
}
</script>
<style lang="less" scoped>
.table-case {
width: 1000px;
margin: 50px auto;
img {
width: 100px;
height: 100px;
object-fit: contain;
vertical-align: middle;
}
}
</style>
- 总结:
- 商品列表的实现封装了几个组件?
- 2个组件,标签组件 和 表格组件
- 封装用到的核心技术点有哪些?
- (1)props父传子 $emit子传父 v-model
- (2)$nextTick 自定义指令
- (3)插槽:具名插槽,作用域插槽
- 商品列表的实现封装了几个组件?
scoped 属性是一个 CSS 属性,用于将样式限制在它所在的组件或影子 DOM 中。这意味着使用 scoped 属性的样式只适用于组件内部的元素,而不会影响组件外部的元素。
scoped 属性对于创建可重用的组件非常有用,因为它可以防止样式冲突。例如,如果你有两个使用相同类名的组件,但你想让它们的样式彼此独立,你可以使用 scoped 属性来实现这一点。
要使用 scoped 属性,只需将其添加到组件的 <style> 标签中,如下所示:
Copy
<style scoped>
/* 组件的样式 */
</style>
需要注意的是,scoped 属性只在支持 Shadow DOM 的浏览器中受支持。目前,所有主流浏览器都支持 Shadow DOM,但如果你需要支持较旧的浏览器,则需要使用其他方法来限制样式。
示例
以下示例演示了如何使用 scoped 属性来限制样式:
Copy
<!-- 组件 1 -->
<div>
<style scoped>
p {
color: red;
}
</style>
<p>我是组件 1 中的段落。</p>
</div>
<!-- 组件 2 -->
<div>
<style scoped>
p {
color: blue;
}
</style>
<p>我是组件 2 中的段落。</p>
</div>
在这个示例中,组件 1 中的段落将显示为红色,而组件 2 中的段落将显示为蓝色。这是因为 scoped 属性将每个组件的样式限制在组件内部。
优点
使用 scoped 属性有以下优点:
提高样式的可重用性
防止样式冲突
提高代码的可维护性
缺点
使用 scoped 属性也有以下缺点:
只在支持 Shadow DOM 的浏览器中受支持
可能导致样式覆盖问题
总体而言,scoped 属性是一个非常有用的工具,可以用来创建可重用和可维护的组件。
路由入门
单页应用程序: SPA - Single Page Application
单页面应用(SPA): 所有功能在 一个html页面 上实现
具体示例: 网易云音乐 https://music.163.com/
特点:
- 单页面应用,页面功能按模块划分并封装成组件,点击时,变化部分切换到相应的组件,共有的部分使用相同的组件不动
- 单页面应用,需要配合路由语法
- 单一页面包含的内容多,加载相对慢
单页与多页比较
开发分类 | 单页 | 多页 |
---|---|---|
实现方式 | 一个html页面 | 多个HTML页面 |
页面性能 | 按需更新,性能高 | 整页更新,性能低 |
开发效率 | 高 | 中等 |
用户体验 | 非常好 | 一般 |
学习成本 | 高 | 中等 |
首屏加载 | 慢 | 快 |
SEO搜索引擎优化 | 差 | 优 |
单页面应用:系统类网站 / 内部网站 / 文档类网站 /移动端站点(性能和体验第一)
多页面应用(首要考虑SEO搜索引擎优化及首屏加载):公司官网 / 电商类网站
总结:
- 什么是单页面应用程序?
- 所有功能在一个html页面上实现
- 单页面应用优缺点?
- 优点:按需更新性能高,开发效率高,用户体验好
- 缺点:学习成本,首屏加载慢,不利于SEO
- 应用场景?
- 系统类网站 / 内部网站 / 文档类网站 /移动端站点
- 什么是单页面应用程序?
路由的介绍
单页面应用程序,之所以开发效率高,性能高,用户体验好
最大的原因就是:页面按需更新
要按需更新,首先就需要明确:访问路径 和 组件的对应关系!
访问路径 和 组件的对应关系如何确定呢? 路由
- 访问不同的内容,触发切换不同的路径
- 通过路径切换,切换不同的组件显示使用
- 通过路由确定 访问路径 和 组件 的对应关系
生活中的路由:设备和ip的映射关系
Vue中路由:路径 和 组件 的 映射 关系(路径与组件的对应关系)

总结:
- 什么是路由?
- 路由是一种映射关系
- Vue中的路由是什么?
- 路径 和 组件 的映射关系
- 根据路由就能知道不同路径的,应该匹配渲染哪个组件
- 什么是路由?
Vue中的路由VueRouter
- 目标:认识插件 VueRouter,掌握 VueRouter 的基本使用步骤
- 背景:在单页面上,点击不同的功能切换组件时,地址路径会有变化
VueRouter
- 作用:修改地址栏路径时,切换显示匹配的组件
- 说明:Vue 官方的一个路由插件,是一个第三方包
- 官网:https://v3.router.vuejs.org/zh/
VueRouter 的 使用 (5 + 2)
- 技术链版本对应关系
技术链 | 插件版本 |
---|---|
Vue2 | VueRouter3.x 和 Vuex3.x |
Vue3 | VueRouter4.x 和 Vuex4.x |
5个基础步骤 (固定)
- ① 下载: 下载 VueRouter 模块到当前工程,版本3.6.5
yarn add vue-router@3.6.5
- ② 全局引入(案例是在main.js入口文件中,后续可分离)
import VueRouter from 'vue-router'
- ③ 安装注册(插件的初始化,全局注册)注:Vue相关的插件都需要安装注册(案例是在main.js入口文件中,后续可分离)
Vue.use(VueRouter)
// 安装注册Vue.use(Vue插件)
- ④ 创建路由对象(可配置规则)(案例是在main.js入口文件中,后续可分离)
const router = new VueRouter()
// 创建一个空的路由对象
- ⑤ 注入,将路由对象注入到new Vue实例中,建立关联
new Vue({
render: h => h(App),
router
// 完整写法 router:router ,值要与路由对象名一致
}).$mount('#app')
- 当看到路径中出现
#
号时,说明当前Vue实例已被VueRouter所管理

2 个核心步骤(根据项目实际情况配置)
① 创建需要的组件 (views视图目录)
例如:Find.vue 、 My.vue 、 Friend.vue
- 注意:eslink规范,所引入的页面,名字需要是组合单词 multi-word,可以到组件下,配置上name,如
export default {name: 'FindMusic'}
- 注意:eslink规范,所引入的页面,名字需要是组合单词 multi-word,可以到组件下,配置上name,如
② 配置路由规则(数组,一个对象一个规则) 在入口文件main.js中配置引入
import Find from './views/Find.vue'
import My from './views/My.vue'
import Friend from './views/Friend.vue'
const router = new VueRouter({
routes: [
{ path: '/find', component: Find },
{ path: '/my', component: My },
{ path: '/friend', component: Friend },
]
})
- ③ 配置导航,配置路由出口(路径匹配的组件显示的位置);
- 即:配置在切换组件时的路由路径
- 也可以理解为:路由目的组件的路径指向
在App.vue根组件中配置路由出口
<div class="footer_wrap">
<a href="#/find">发现音乐</a>
<a href="#/my">我的音乐</a>
<a href="#/friend">朋友</a>
</div>
<div class="top">
<!-- 控制组件的展示位置 -->
<router-view></router-view>
</div>

import Vue from 'vue'
import App from './App.vue'
// 路由的使用步骤 5 + 2
// 5个基础步骤
// 1. 下载 v3.6.5
// 2. 引入
// 3. 安装注册 Vue.use(Vue插件)
// 4. 创建路由对象
// 5. 注入到new Vue中,建立关联
// 2个核心步骤
// 1. 建组件(views目录),配规则
// 2. 准备导航链接,配置路由出口(匹配的组件展示的位置)
import Find from './views/Find'
import My from './views/My'
import Friend from './views/Friend'
import VueRouter from 'vue-router'
Vue.use(VueRouter) // VueRouter插件初始化
const router = new VueRouter({
// routes 路由规则们
// route 一条路由规则 { path: 路径, component: 组件 }
routes: [
{ path: '/find', component: Find },
{ path: '/my', component: My },
{ path: '/friend', component: Friend },
]
})
Vue.config.productionTip = false
new Vue({
render: h => h(App),
router
}).$mount('#app')
<template>
<div>
<div class="footer_wrap">
<a href="#/find">发现音乐</a>
<a href="#/my">我的音乐</a>
<a href="#/friend">朋友</a>
</div>
<div class="top">
<!-- 路由出口 → 匹配的组件所展示的位置 -->
<router-view></router-view>
</div>
</div>
</template>
<script>
export default {};
</script>
<style>
body {
margin: 0;
padding: 0;
}
.footer_wrap {
position: relative;
left: 0;
top: 0;
display: flex;
width: 100%;
text-align: center;
background-color: #333;
color: #ccc;
}
.footer_wrap a {
flex: 1;
text-decoration: none;
padding: 20px 0;
line-height: 20px;
background-color: #333;
color: #ccc;
border: 1px solid black;
}
.footer_wrap a:hover {
background-color: #555;
}
</style>
<template>
<div>
<p>发现音乐</p>
<p>发现音乐</p>
<p>发现音乐</p>
<p>发现音乐</p>
</div>
</template>
<script>
export default {
name: 'FindMusic'
}
</script>
<style>
</style>
<template>
<div>
<p>我的朋友</p>
<p>我的朋友</p>
<p>我的朋友</p>
<p>我的朋友</p>
</div>
</template>
<script>
export default {
name: 'MyFriend'
}
</script>
<style>
</style>
<template>
<div>
<p>我的音乐</p>
<p>我的音乐</p>
<p>我的音乐</p>
<p>我的音乐</p>
</div>
</template>
<script>
export default {
name: 'MyMusic'
}
</script>
<style>
</style>
- 总结:
- 如何实现 路径改变,对应组件 切换?
- Vue 官方插件 VueRouter
- VueRouter 的使用基本步骤? (5 + 2)
5个基础步骤
- ① 下包
- ② 引入
- ③ Vue.use 安装注册
- ④ 创建路由对象
- ⑤ 注入Vue实例
2个核心步骤
- ① 创建组件,配置规则 (路径组件的匹配关系 )
- ② 配导航,配置路由出口 router-view (组件展示的位置)
路由出口
- 理解路由出口核心概念:
- 路由出口由两部分组成:
- 路由出口标签对
- 路由出口路径
- 路由出口标签对:
- 需要有
<router-view></router-view>
标签对 - 标签对插入的地方,就是作为下一级路由组件插入/作用/展示的地方
- 路由出口标签对 也可以简单理解为 下一级路由组件的插入位置
- 需要有
- 路由出口路径:
- 需要有路由出口路径,一般在标签的元素上
- 后续使用 vue-router 中的全局组件 router-link
<router-link to="/路径值" ></router-link>
- 后续使用其他组件,一般都是匹配标签中的to属性
- 路由出口路径 也可以简单理解为 下一级路由续接在本级路由后面的地址
- 路由出口由两部分组成:
组件存放目录问题(组件分类存放)
- 注意:.vue文件 在本质无区别。
路由相关的组件,为什么放在 views视图 目录呢?
import Find from './views/Find.vue'
import My from './views/My.vue'
import Friend from './views/Friend.vue'
组件分类:
- .vue文件分2类; 页面组件 & 复用组件
- 注意:都是 .vue文件 (本质无区别)
目的:
- 将页面路由组件,与编写的结构组件,按文件夹分类开来存放管理 更易维护
src/views文件夹
- 页面路由组件 - 页面展示 - 配合路由用
src/components文件夹
- 复用结构组件 - 展示数据 - 常用于复用
总结:
- 组件分类有哪两类?分类的目的?
- 页面组件 和 复用组件,便于维护
- 放在什么文件夹?作用分别是什么?
- 页面组件 - views文件夹 => 配合路由,页面展示
- 复用组件 - components文件夹 => 封装复用
- 组件分类有哪两类?分类的目的?