day05
目录- day05
自定义指令
指令介绍
-
内置指令:
v-html
、v-if
、v-bind
、v-on
... 这都是 Vue 内置的一些指令,提供好的可以直接使用的 -
自定义指令:Vue 也支持让开发者,自己注册一些指令,这些指令被称为自定义指令
-
每个指令都有自己各自独立的功能
自定义指令
- 概念:自己定义的指令,可以封装一些 DOM 操作,扩展额外的功能
自定义指令语法
-
例如想要打开页面完成获取焦点的操作:
<!-- 原本是 --> <input v-focus ref="inp" type="text"> mounted(){ this.$refs.inp.focus() }
- 但每次获取焦点的页面都要加上这几句,所以想是不是可以封装成一个自定义指令
-
全局注册写法
//在main.js中 Vue.directive('指令名', { "inserted" (el) { // el就是指令所绑定的元素,如<input type="text"> // 可以对 el 标签,扩展额外功能 el.focus() } })
- inserted:一个生命周期钩子,表示当指令被绑定的元素被添加到页面中的时候会自动调用
- el:使用指令的那个 DOM 元素
-
局部注册写法
//在Vue组件的配置项中 directives: { "指令名": { inserted (el) { // 可以对 el 标签,扩展额外功能 el.focus() } } }
- 只可以在当前组件范围内使用
-
使用指令
- 注意:在使用指令的时候,一定要先注册,再使用,否则会报错
- 使用指令语法:
v-指令名
,如:<input type="text" v-focus/>
-
注册指令时不用加 v- 前缀,但使用时一定要加 v- 前缀
自定义指令 — 携指令的值
需求
- 实现一个 color 指令 — 传入不同的颜色,给标签设置文字颜色
语法
- 在绑定指令时,可以通过 “ 等号 ” 的形式为指令绑定具体的参数值
<template>
<div>
<!--显示红色-->
<h2 v-color="color1">指令的值1测试</h2>
<!--显示蓝色-->
<h2 v-color="color2">指令的值2测试</h2>
<button>
改变第一个h1的颜色
</button>
</div>
</template>
<script>
export default {
data () {
return {
color1: 'red',
color2: 'blue'
}
}
}
</script>
- 通过 binding.value 就可以拿到指令值,指令值修改会触发 update 函数
directives: {
color: {
inserted (el, binding) {
el.style.color = binding.value
}, // 元素添加到页面时的逻辑 —— 获取
update (el, binding) {
el.style.color = binding.value
} // 提供值发生变化时的逻辑 —— 更新(不需要指定1还是2)
}
}
自定义指令 — v-loading 指令的封装
场景
- 实际开发过程中,发送请求需要时间,在请求的数据未回来时,页面会处于空白状态
需求
- 为了复用,封装一个 v-loading 指令,实现加载中的效果
分析
- 本质 loading 效果就是一个蒙层,盖在了盒子上
- 数据请求中,开启 loading 状态,添加蒙层
- 数据请求完毕,关闭 loading 状态,移除蒙层
实现
- 准备一个 loading 类,通过伪元素定位,设置宽高,实现蒙层
- 开启关闭 loading 状态(就是添加移除蒙层),本质只需要添加移除类即可
- 结合自定义指令的语法进行封装复用
.loading:before {
content: "";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: #fff url("./loading.gif") no-repeat center;
}
准备代码
<template>
<div class="main">
<!-- 写上loading就相当于加上蒙层了:<div class="box loading">,想做成动态可控的,就用自定义指令 -->
<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>
</template>
<script>
// 安装axios => yarn add axios || npm i axios
import axios from 'axios'
export default {
data () {
return {
list: [],
isLoading: true,
isLoading2: false // 可以you
}
},
async created () {
// 1. 发送请求获取数据
const res = await axios.get('http://')
setTimeout(() => {
// 2. 更新到 list 中,用于页面渲染 v-for
this.list = res.data.data
}, 2000) // 加载两秒数据
},
directives: {
loading:{
inserted (el, binding) { // binding.value为初始值的获取
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;
}
</style>
插槽 — 默认插槽
作用
- 让组件内部的一些结构支持自定义
需求
- 例如:将需要多次显示的对话框,封装成一个组件
问题
- 组件的内容部分,不希望写死,希望能使用的时候自定义
插槽的基本语法
- 子组件内需要定制的结构部分,改用
<slot></slot>
占位 - 被使用时,父组件使用标签
<MyDialog>内部</MyDialog>
在内部,传入结构替换 slot - 给插槽传入内容时,可以传入纯文本、html 标签、组件
代码示例
- MyDialog.vue
<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>
- App.vue
<template>
<div>
<MyDialog>
您确定要进行删除操作吗?
<!-- 自定义引入写入 -->
</MyDialog>
</div>
</template>
插槽 — 后备内容(默认值)
插槽的后备内容
- 封装组件时,可以为预留的
<slot>
插槽提供后备内容(默认内容)
语法
- 在 MyDialog.vue 的
<slot>
标签内,放置内容,作为默认显示内容:<slot>默认内容</slot>
效果
-
外部使用组件时,不传内容,则
slot
会显示 “ 默认内容 ” -
外部使用组件时,传东西了,则
slot
整体会被换掉
插槽 — 具名插槽
需求
-
一个组件内有多处结构,需要外部传入标签,进行定制
-
弹框中有三处不同,但是默认插槽只能定制一个位置
具名插槽语法
-
多个 slot 使用 name 属性区分名字
<div class=""> <slot name = "head"></slot> </div> <div class=""> <slot name = "content"></slot> </div> <div class=""> <slot name = "footer"></slot> </div>
-
需要用到
<template>
标签来配合v-slot:
名字来分发对应标签<MyDialog> <template v-slot:head> 大标题 </template> <template v-slot:content> <div>文本内容</div> </template> <template v-slot:footer> <button>按钮</button> </template> </MyDialog>
v-slot 的简写
- v-slot 写起来太长,vue 提供一个简单写法:
#
- 如:
#head
、#content
- 如:
作用域插槽
插槽分类
-
默认插槽
-
具名插槽
- 插槽只有两种,作用域插槽不属于插槽的一种分类
作用
- 定义 slot 插槽的同时,是可以传值的
- 给插槽上可以绑定数据,将来使用组件时可以用
场景
- 封装表格组件
使用步骤
- 给 slot 标签,以添加属性的方式传值
<slot :id="item.id" msg="测试文本"></slot>
- 所有添加的属性,都会被收集到一个对象中
{ id: 3, msg: '测试文本' }
-
在 template 中, 通过
#插槽名= "obj"
接收- 这里的
obj
是一个封装好了的对象,你也可以选择用{}
解构(下面示例有涉及)
- 这里的
-
如果是默认插槽,默认插槽名为 default
<MyTable :list="list">
<template #default="obj">
<button @click="del(obj.id)">删除</button>
</template>
</MyTable>
代码示例
- MyTable.vue
<template>
<table class="my-table">
<thead>
<tr>
<th>序号</th>
<th>姓名</th>
<th>年纪</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="......">
<td>...</td>
<td>...</td>
<td>...</td>
<td>
<!-- 插槽占位,定制非写死的 -->
<slot :row="item" msg="测试文本"></slot>
<!-- 用添加属性的方式传值 -->
<!-- 传值就是一整个对象,即 { row: {item对象}, msg: '测试文本' } -->
</td>
</tr>
</tbody>
</table>
</template>
props:{
data: Array
}
- App.vue
<template>
<div>
<MyTable :data="list">
<!-- 通过template #接收子插槽传来的整个对象obj,可以{{ obj }} 看看 -->
<template #default="obj">
<button @click="del(obj.row.id)">删除</button>
</template>
</MyTable>
<MyTable :data="list2">
<!-- 删除同理,也可以直接解构 -->
<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: 'xxx', age: 18 },
{ id: 2, name: 'yyy', age: 19 },
{ id: 3, name: 'zzz', age: 17 },
],
list2: [
{ id: 1, name: 'aaa', age: 18 },
{ id: 2, name: 'bbb', age: 19 },
{ id: 3, name: 'ccc', age: 17 },
]
}
},
components: {
MyTable
},
methods: {
show (row) {
console.log(row); // 获取的就是含有id、name、age的对象
alert(`姓名:${row.name}; 年纪:${row.age}`)
}
}
}
</script>
综合案例 — 自定义 MyTag 组件控制显示隐藏
- MyTag.vue
<template>
<div class="my-tag">
<input
v-if="isEdit"
v-focus
ref="inp"
......
:value="value"
@blur="isEdit = false"
@keyup.enter="handleEnter"
/>
<div
v-else
@dblclick="handleClick"
class="text">
{{ props接收输入框输入的数据value }}
</div>
</div>
</template>
<script>
export default {
data () {
return {
isEdit: false
}
},
methods: {
handleClick () {
this.isEdit = true
// 不用自定义的v-focus的话,可以
// this.$nextTick(() => {
// this.$refs.inp.focus()
// })
},
handleEnter (e) {
// 非空处理
if (e.target.value.trim() === '') return alert('标签内容不能为空')
this.$emit('input', e.target.value) // 父是v-model,所以用 'input'
// 提交完成,关闭输入状态
this.isEdit = false
}
}
}
</script>
-
@dblclick
: 双击事件 -
v-focus
自定义获取焦点:- main.js:
// 封装全局指令 focus Vue.directive('focus', { // 指令所在的dom元素,被插入到页面中时触发 inserted (el) { el.focus() } })
-
@blur
失去焦点 -
:value
子回传输入的数据,回调回父做更改
综合案例 — 封装 MyTable 组件
-
动态封装表格结构的组件
- 动态渲染数据,表头自定义,显示数据跟着表头变化
- 不写死就用插槽
- 动态结构,具名插槽自定义
- 动态渲染数据,表头自定义,显示数据跟着表头变化
-
App.vue
<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 }">
<!-- 自定义表体,接收子传来的数据obj,也可写成上述样式,对应表头显示几列 -->
<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>
import MyTag from './components/MyTag.vue'
import MyTable from './components/MyTable.vue'
export default {
name: 'TableCase',
components: {
MyTag,
MyTable
},
data () {
return {
....
}
}
</script>
- MyTable.vue
<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传数据到父组件 -->
<slot name="body" :item="item" :index="index" ></slot>
</tr>
</tbody>
</table>
</template>
<script>
export default {
props: {
data: {
type: Array,
required: true
}
}
};
</script>
- 父传总数据给子,子中自定义部分插槽占位,父写进插槽的要与各个子组件相关,所以每个接收到父传来的属于自己的数据要用作用域插槽返还给父
单页应用程序介绍
概念
- 单页应用程序:SPA(Single Page Application)是指所有的功能都在一个 html 页面上实现
具体示例
-
单页应用网站: 网易云音乐 https://music.163.com/
- 根据导航栏的点击改变下面的主体内容,改变的不是整个页面
-
多页应用网站:京东 https://jd.com/
- 点击导航栏会跳到对应的另一个页面
单页应用 VS 多页面应用
-
单页应用类网站:系统类网站 / 内部网站 / 文档类网站 / 移动端站点
-
多页应用类网站:公司官网 / 电商类网站
路由介绍
思考
-
单页面应用程序,之所以开发效率高,性能好,用户体验好
- 最大的原因就是:页面按需更新
- 不是更新整个页面,而是更新这个页面中需要的部分
-
比如点击任意导航栏按钮时,只是更新导航栏下面的部分内容,对于头部的导航栏是不更新的
- 要按需更新,首先就需要明确:访问路径和组件的对应关系
- 访问路径和组件的对应关系的确定就需要:路由
路由的介绍
-
生活中的路由:设备和 ip 的映射关系
- 192.168.0.xx
- 192.168.0.xy
-
Vue 中的路由:路径和组件的映射关系(不同的路径就对应着不同的组件)
http://localhost:8080/#/home
http://localhost:8080/#/car
http://localhost:8080/#/search
路由的基本使用
- 认识插件 VueRouter,掌握 VueRouter 的基本使用步骤
作用
- 修改地址栏路径时,切换显示匹配的组件
说明
- Vue 官方的一个路由插件,是一个第三方包
- Vue Router 官网
VueRouter 的使用(5 + 2)
- 5 个固定的步骤 — 初始
-
下载 VueRouter 模块到当前工程,版本 3.6.5
yarn add vue-router@3.6.5
-
main.js 中引入 VueRouter
import VueRouter from 'vue-router'
-
安装注册
Vue.use(VueRouter)
-
创建路由对象
const router = new VueRouter()
-
注入,将路由对象注入到 new Vue 实例中,建立关联
new Vue({ render: h => h(App), router:router // 只有在第四步const时叫router,这里才能简写为:router }).$mount('#app')
- 当我们配置完以上 5 步之后,就可以看到浏览器地址栏中的路由变成了
localhost:8080/#/
的形式,表示项目的路由已经被 Vue-Router 管理了
两个核心步骤
-
创建需要的组件( views 目录),配置路由规则
- main.js 的 const 实例中,例:
-
配置导航,配置路由出口(路径匹配的组件显示的位置)
- 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>
<router-view>
就是匹配的组件所展示的位置
组件的存放目录问题
- src 里 的 views 文件夹来存放上述组件页面
组件分类
- .vue 文件分为两类,都是 .vue文件(但本质无区别)
- 页面组件(配置路由规则时使用的组件)放 views 里
- 复用组件(多个组件中都使用到的组件)
存放目录
- 分类开来的目的就是为了更容易维护
- src/views 文件夹:
- 页面组件 — 页面展示 — 配合路由用
- src/components 文件夹:
- 复用组件 — 展示数据 — 常用于复用
路由的封装抽离
-
随着日后功能的增加完善,所有的路由配置都在 main.js 中不合适
-
所以要将路由模块抽离出来
- 放在 src 下的 router 文件夹里的 index.js 文件中
-
第一个图就是 router 里的 index.js(别忘了
import Vue from 'vue'
),还要export default router
导出 —— 第三个图 main.js 导入使用 -
./
是在本层里找,../
才是在上一层找- 但这样层级较深或者换目录时就有些复杂,所以:
-
路径简写:
- 脚手架环境下
@
指代src
目录,可以用于快速引入组件
- 脚手架环境下