Vue框架
Vue3组件基础:ref引用、动态组件、插槽、自定义指令
前面简单介绍了组件的相关,比如监听,组件关系和组件的数据共享,还有自定义事件,这样一个界面就可以为不同的组件相互作用结合形成,数据传递则依靠指令和之间【后代可以使用provide和inject便捷传输】 复杂的数据传递一般使用vuex
其实很多得概念之前就提到了,比如这里得ref引用;【这个在Spring的IOC的属性注入的位置,对象属性就是ref,普通的属性就是value】,之前的响应式数据传递,就配置全局数据项,就可以不写.value
ref引用就是对象的引用,ref是帮助programmer在不依赖JQuery的情况下,获取DOM元素或者组件的引用【之前Jq最方便的就是简化了对象的操作,特别是$(选择器:过滤器)】
Vue里面是不建议使用Jq的 ,因为容易造成引用的混乱,所以就要使用ref来操作DOM或者组件。每一个vue的组件实例上,都包含一个$refs对象,里面存储着对应的DOM元素或者对于组件的引用,默认情况下,其默认指向一个空对象
之前分享组件的基础的时候,就提过this代表的就是当前组件的实例,这里可以看一下其内容
[[Target]]: Object
num: (...)
username: (...)
$: (...)
$attrs: (...)
$data: (...)
$el: (...)
$emit: (...)
$forceUpdate: (...)
$nextTick: (...)
$options: (...)
$parent: (...)
$props: (...)
$refs: Object
$root: (...)
$slots: (...)
$watch: (...)
_: Object
可以看到有很多的内置的对象,这里就有一个$refs
如果想要对页面上的某个DOM元素进行操作,就给需要操作的DOM结点添加ref属性
<h1 ref="myh1">LifeCircle子组件h1>
通过this.$refs.引用名称
,获取到dom元素的引用
通过该引用操作DOM元素
methods:{
getDom() {
console.log(this.$refs.myh1) //因为会将带有ref属性的结点注册到$refs中,所以这里想要使用myh1,就直接.引用即可
}
}
这里打印的结果就是: < h1>LifeCircle子组件< /h1>;成功获取到了该组件对应页面上的DOM结点,比如这里想要修改文本的颜色: this.$refs.myh1.style.color = 'red’就可以设置红色
如果想要引用页面上的组件【组件对于父组件来说也是一个标签,和上面的DOM一样是一个结点】
这里比如在consum-kid组件中定义了方法setColor,可以获取组件页面中的dom元素修改其背景颜色,在App中点击按钮达到效果
//consum-kid.vue
X * 2 + 1的结果为 :{{count * 2 + 1}}
methods:{
setColor() { //在子组件中定义了一个set方法,在另一个组件中点击按钮就可以执行该方法,那么就要使用组件的引用
this.$refs.myspan.style.backgroundColor = 'pink'
}
}
//父组件App.vue
methods:{
changeColor(){
this.$refs.conKid.setColor() //获取子组件调用其set方法修改颜色
}
}
这样通过ref引用就可以方便操作组件及上面的DOM结点
通过布尔值inputVisible来控制组件中的文本框于按钮的按需切换
当为true的时候就可以展示文本输入框,要想展示的一瞬间获得焦点,就使用元素js的方法.focus()即可;所以通过ref引用获取文本框即可
methods:{
showInput() {
this.inputVisible = true
this.$refs.ipt.focus()
}
}
这样点击按钮就可显示文本框,但是控制台报错:Uncaught TypeError: Cannot read properties of undefined (reading ‘focus’) — 也就是浏览器并没有检测到文本框undefined
DOM元素的更新是异步的,也就是异步的任务【因为要重新渲染结点,属于宏任务】,在EventLoop中,这是在一个消息队列中,主任务就是优先执行,所以会立即去执行this.$refs,这个时候并没有获取到结点,报错
上面就发现因为组件的更新是异步的,而js是将栈中的所有的主任务执行完毕才会执行异步任务,所以调用不应该在主任务,vue提供了内部函数$nextTick(cb),会将回调函数推迟到下一个DOM更新周期之后执行,也就是登组件重新渲染更新完毕之后,才会执行cb函数,保证cb操作的最新的回调函数
methods:{
showInput() {
this.inputVisible = true
//把对dom元素的操作推迟到页面的DOM更新完毕之后执行
this.$nextTick(() => {
this.$refs.ipt.focus()
})
}
}
这里将执行语句放到匿名回调函数中,等待更新完毕之后执行,相当于也是异步的任务
动态组件指的是动态切换组件的显示和隐藏,vue提供了一个内置的< compnent>组件,用来实现组件的动态渲染 【之前的普通的DOM结点的显示可以使用v-if或者v-show】
也就是使用component标签占据位置,通过这个标签的is属性就可以动态绑定组件
姓名
data() {
return {
conName:'',
showComp() {
this.conName = 'LifeCircle'
}
这样只要点击按钮,就可以显示LifeCircle组件
这里的component占位的效果,其实也是动态创建或者销毁组件实例,当切换其他的组件的时候,之前的组件实例就被销毁,第二次切换回来的时候就是重新创建组件实例,所以之前的状态就会恢复到最初的状态, 如果想要不销毁,那么就要使用keep-alive
默认情况下,切换动态组件时 无法保持组件的状态,这个时候可以使用vue内置的< keep- alive>组件包裹component标签保持动态组件的状态
这样组件实例切换的时候就不会被销毁,组件实例就会被缓存到浏览器的内存中,一般都会使用keep-alive来保持之前页面的状态
插槽是vue为组件的封装er提供的能力,允许programmer在封装组件的时候,把不确定的、希望由用户指定的部分定义为插槽
这样就可以通过插槽将html小的页面插入到封装好的组件中,插槽就是在组件的封装期间,为用户预留的占位符;上面的动态组件的component也是占位符,但是占的是整个组件的位,这里占位站的就只是DOM的位置
在封装组件期间,可以通过< slot>元素定义插槽,从而为用户预留的内容进行占位
这是子组件的第一个p标签
这是子组件的第二个P标签
使用者,也就是父组件调用的时候,使用双目的子组件的标签,标签之间内容都会被填充到插槽
这里可以演示一下
SlotTest组件 ---- 这是第一个p标签;下面就是使用者自定义的内容
这是第二个p标签
-------------------父组件App.vue进行调用--------------
我是App根组件自定义的内容
就是如果在组件中没有定义slot插槽,那么就算在父组件使用时,在标签间定义了内容,也不会填充,因为没有插槽,那么效果就是会被自动舍弃
封装组件时,可以为预留的slot 插槽提供后备内容【默认内容】,如果使用者没有为插槽提供内容,那么就会使用后备内容,提供了就会覆盖
第一个p标签
插槽的后备内容都在这里 【 如果使用者不提供,改内容生效】 --- 这样也实现动态页面
第二个p标签
上面的只是定义了一个插槽,插槽标签slot中的内容就是后备内容;如果需要在封装组件的时候预留多个插槽结点,则需要为每一个插槽指定具体的name名称,这种含有名称的插槽为具名插槽
后备内容
这是一个默认的插槽
如果没有指定name名称的插槽,那么这个插槽的名称为default — 所以只能由一个default
比如这里定义了3个插槽
后备内容 ---- header
默认的插槽
在App根组件中要使用这个组件,传入了需要插入的自定义的内容
春晓
孟浩然
春眠不觉晓,处处闻啼鸟
夜来风雨声,花落知多少
最终渲染的结果就是: 所有的内容都进入了默认插槽;也就是说,在不特殊说明的情况下,插入的内容都会进入默认插槽
要让内容进入具名插槽,需要使用template标签包裹内容,并且使用v-slot指定剧名插槽的名称
春晓
孟浩然
春眠不觉晓,处处闻啼鸟
夜来风雨声,花落知多少
对要插入的内容使用template标签包裹,并且使用v-slot指明具名插槽的名称,使用: ,不是=号,并且直接写名称,不用加引号,不然会报错
Error: Codegen node is missing for element/if/for node. Apply appropriate transforms first.
这里的问题就是因为:template标签放的位置有问题,template必须是最外层的标签,外面不能包裹div了,报错就是因为组件标签里的内容包裹在div中:happy:,template应该是最外面 ----- div也是属于DOM结点,template才能将其渲染到界面上 ---- 所以template必须包裹所有的DOM
春晓
作者:孟浩然
春眠不觉晓,处处闻啼鸟
夜来风雨声,花落知多少
默认插槽可以省略template,具名插槽不能省略
和v-on和v-bind一样,v-slot也可以简写,把参数之前的内容v-slot:替换为字符#, 比如
这里可以修改上面的代码
春晓
作者:孟浩然
春眠不觉晓,处处闻啼鸟
夜来风雨声,花落知多少
之前的作用域插槽slot-scope已经弃用,在封装组件的过程中,可以为预留的slot插槽绑定props数据,带有props数据的slot就是作用域插槽
也就是说插槽中有数据,就是为slot动态绑定数据
使用者使用的时候传入数据的时候,还可以指定prop绑定的数据
xxx
在插槽的名称后面通过=‘scope’ ;然后这个scope就可以接收到上面的属性prop, 也就是实现了子组件的数据传输到了父组件
data() {
return {
infomation:{
stuname: 'Cfeng',
stuClass: 'HC2001',
stuMajor: 'CS'
},
message: 'Hello,Cfeng!!'
}
}
--------------------------------------------------------------
{{scope}}
然后打印的结果就是: { “info”: { “stuname”: “Cfeng”, “stuClass”: “HC2001”, “stuMajor”: “CS” }, “msg”: “Hello,Cfeng!!” }】
也就是scope会接收插槽传递的所有的属性形成的JSON对象
之前已经使用过很多次解构了,就是可以直接加上{},然后就可以直接取出对象中的属性;
这样就可以直接使用msg的值,而不再需要使用scope.msg来获得数据
作用域插槽的最主要的特点就是组件的多态,就类似之前的java的abstract一样;不会具体定义,交给用户来决定数据的样式
比如这里子组件提供了一个table
序号
姓名
状态
{{item.id}}
{{item.name}}
{{item.state}}
这里的子组件将数据直接给到了template中,但是问题就是这里的state,并不知道用户想要一个什么样子的样式,是复选框,还是什么;所以这里不要直接将数据按照特定的格式渲染,而是交给使用者App来按需操作
序号
姓名
状态
{{item.id}}
{{item.name}}
{{item.state}}
在App.vue中通过作用域插槽的操作,来进行具体的渲染
{{item.id + 1}}
{{item.name}}
父组件就可以解构出item属性然后使用,自定义样式;比如这里将state渲染成了一个复选框的样式
⚠: Whitespace was expected. 这是因为v-for指令和:key两个属性之间需要空格
//因为这是属性绑定,key不是修饰符,绑定的key属性,两个不同的指令或者属性之间要有空格
自定义指令
vue官方提供了v-for,v-bind等内置指令,除此之外vue还允许开发者自定义指令,vue自定义指令主要有两类:
- 私有自定义指令
- 全局自定义指令
私有自定义指令 directives结点下声明
这里先定义一个组件MyHome,其中有一个文本框,现在的需求: 当渲染出组件的时候,文本框自动获得焦点
- 可以使用之前的ref来动态获得组件【这里使用的是动态显示组件,点击按钮会显示MyHome组件,然后这里的渲染必须是异步任务,所以需要使用$nextTick,这里前面是获取组件,后面是获取组件上的结点
showComp() {
this.conName = 'MyHome'
this.$nextTick(() => {//变成异步任务
this.$refs.myHome.$refs.ipt.focus()
})
}
这里直接给动态组件加上了ref属性,也是可以获取到这里具体的组件的
这里可以使用自定义指令,比如为input绑定自定义指令v-focus
[Vue warn]: Failed to resolve directive: focus --自定义指令必须要进行声明
使用自定义指令,必须以v-开头,但是在directives下面声明私有指令时,不用v- 这里的directives结点和data等结点平级
directives: {
//自定义一个私有指令
focus: {
//当绑定元素插入到DOM时,自动会触发mounted函数
mounted(e1) {
e1.focus()
}
}
}
这里mounted的参数就是自定义指令绑定的DOM结点;mounted函数的执行时机: 指令绑定的结点渲染到DOM结构的时候自动触发
directives:{
focus: {
mounted(e) {
e.focus()
}
}
}
自动执行mounted方法,获取焦点; 所以自定义的私有指令就是在对应的组件的directives结点中声明指令,然后就可以调用了,mounted函数可以获取到绑定的元素DOM
全局自定义指令
上面的v-focus指令声明在MyHome组件中,在其他的组件中是不能使用这个指令的;就类似之前的组件的全局注册一样;只要在main.js进行指令声明就可以使用,使用spa_app.directive方法可以声明,第一个是名称,第二个是指令的方法体
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import './assets/css/bootstrap.css'
//导入axios
import axios from 'axios'
const spa_app = createApp(App)
//在mount之前进行配置
//声明请求的相对路径
axios.defaults.baseURL = 'https://www.escook.cn/api'
//全局注册挂载
spa_app.config.globalProperties.$ajax = axios
spa_app.config.unwrapInjectedRef = true
spa_app.directive('focus',{
mounted(e) {
e.focus()
}
})
spa_app.mount('#app')
还是要在spa_appmout之前进行声明,和组件的全局注册类似
全局声明自定义指令之后,不管在哪个组件中都可以使用该指令,这里将私有的v-focus声明去除;在MyHome组件中还是可以使用
updated函数保持v-focus持续触发
上面的v-focus指令有点缺陷: — mounted函数只是在元素第一次插入DOM时被调用,当DOM更新时mounted函数不会被触发; 与之相比,updated函数【之前分享组件的生命周期的时候,组件的几个函数和DOM的函数相同,mounted就是渲染到页面后触发,而updated就是每次页面更新都会触发】
spa_app.directive('focus',{
mounted(e) {
e.focus()
},
updated(e) {
e.focus()
}
})
这样声明之后就可以渲染和更新的时候都会获取焦点【和组件的生命周期函数是相同的】组件也可以看作父组件的一个DOM结点
MyHome组件 ---- {{count}}
这里点击按钮页面更新,这个时候会执行updated函数,还是会获取焦点
在vue2项目中使用自定义指令,是没有ed的,就是mount和update【提一句】
函数简写
如果mounted函数和updated函数的逻辑完全相同,就可以将方法的第二项完全变成一个方法,而不是方法体
spa_app.directive('focus',(e) => {
e.focus()
})
指令的参数值
自定义指令声明的方法的第一个形参指代的DOM对象,第二个形参指定的时指令绑定的参数对象,通过.value获取参数对象的具体值
vue提供的指令都是支持参数的,自定义指令也是支持的,在绑定指令的时候,可以通过等号
的形式为指令绑定具体的参数值
比如上面就为v-color自定义指令绑定了参数red;
声明指令的时候,可以通过函数的形参binding来接收绑定的参数
,第一个参数是指代的DOM对象,第二个参数就是binding参数; 注意binding接收的是一个对象,获取red要使用binding.value
spa_app.directive('color', (e,binding) => {
e.style.backgroundColor = binding.value
})
子组件使用v-color指令时,需要注意里面还有单引号; 因为如果时属性绑定,后面的变量就要包裹在引号中,现在这个变量时字符串,所以需要有两重的引号
Table案例
案例达到的效果就是商品列表,主要步骤
首先建立一个vite项目table-demo,初始化项目
npm init vite-app table-demo
cd table-demo
npm i
npm i less -D
npm i axios -S
npm run dev
同时要导入bootstap.css,修改index.css全局的样式
:root {
font-size: 12px;
}
body {
padding: 8px;
}
- 请求商品列表的数据 ---- 上面安装了axios包,接下来就是在mian.js中进行配置
methods:{
async getGoodsList() {
const {data: res} = await this.$ajax.get('/goods')
if(res.status !== 0) return console.log("请求商品列表失败")
this.goodsList = res.data
}
},
components: {
},
created() {
this.getGoodsList()
}
}
这里需要注意的就是定义的全局属性$ajax要通过this进行调用,不能直接写出,不然undefined
- 封装MyTable组件
封装的要求: 用户通过名为data的prop属性,为MyTable组件指定数据源; 在Mytable组件中,预留名称为header的具名插槽;同时要预留名为body的作用域插槽
这里模板结构还是基于的bootstrap进行渲染,这里的作用域插槽携带的数据就是row当前的数据对象和当前的索引index
#
商品名称
价格
标签
操作
{{index + 1}}
{{row.goods_name}}
¥{{row.goods_price}}
{{row.tags}}
- 实现删除的功能
为按钮添加事件处理函数,根据id删除goodsList中的数据
<button type="button" class="btn btn-danger btn-sm" @click="onRemoveGoods(row.id)">删除</button>
onRemoveGoods(id) {
this.goodsList = this.goodsList.filter(x => x.id !== id) //过滤出不是id的所有的数据
}
- 实现添加标签的功能
{{tag}}
标签tags也是一个数组,所以需要使用class进行渲染
文本输入框的焦点事件blur,就是失去焦点的时候就会触发
这里的所有的代码都是在App.vue完成的
#
商品名称
价格
标签
操作
{{index + 1}}
{{row.goods_name}}
¥{{row.goods_price}}
{{tag}}
这样就完成了列表的标签的添加
vue中关于组件部分就介绍到这里,接下来就是路由了