组件开发
??vue是一个支持组件化开发 的前端框架。组件化开发指的是根据封装 的思想,把页面上可重用的UI结构封装为组件,从而方便项目的开发和维护。vue 中规定组件的后缀名是.vue 。 ??每个.vue 组件都由3部分构成,分别是:①template :组件的ui模板结构;②script :组件的JavaScript行为;③style :组件的样式。
<template>
<!-- 只能有一个根节点,即最外层只有一个div盒子 -->
<div></div>
</template>
<script>
export default{
data(){
return { }
},
methods: { },
computed: { },
}
</script>
<style scoped>
</style>
注意:组件是可复用的 Vue 实例,所以它们与 new Vue 接收相同的选项,例如 data 、computed 、watch 、methods 以及生命周期钩子 等。仅有的例外是像 el 这样根实例特有的选项。
一、组件的结构与注册
1.1 组件的data必须是函数,且必须返回一个实例对象(对象内部保存着数据)
??我们容易发现上述vue文件的 data 并不是像我们之前遇到过的那样直接提供一个对象,现在变成了一个函数。因为组件不可以访问Vue实例中的数据,所以组件应该有保存自己数据的地方。所以一个组件的 data 选项必须是一个函数,这样每个实例就可以维护一份被返回对象的独立的拷贝(即复用多次的同一个组件之间相互独立互不影响)。否则,如果我们在多次复用某个组件时,其中一个变化则会影响到其他实例的变化。
1.2 组件的组织
??通常一个应用会以一棵嵌套的组件树的形式来组织: 其中最上面的节点就是我们所有组件的根组件,然后在根组件App.vue中使用其他组件:
<template>
<div id="app">
<h2>App.vue组件</h2>
<!-- 3、以标签的形式使用注册的组件 -->
<ComponentA></ComponentA>
<ComponentB></ComponentB>
</div>
</template>
<script>
import ComponentA from '@/components/a.vue'
import ComponentB from '@/components/b.vue'
export default {
components: {
ComponentA,
ComponentB
}
}
</script>
1.3 局部组件与全局组件
??上面使用组件的第二个步骤中“使用components节点注册组件”中在组件的 components 节点下注册的组件是私有子组件,是局部的。也就是说,在其他组件中无法使用。例如:在组件A的components节点下注册了组件F,则组件F只能用在组件A中,不能被用在组件C中。要想在其他组件中使用的话,因此通过 Vue.component() 方法注册全局组件。全局组件注册之后可以用在任何新创建的 Vue 根实例 (new Vue) 的模板中。
import ComponentA from '@/components/a.vue'
Vue.component('my-component-name', ComponentA)
“我的附庸的附庸不是我的附庸”
??例如,在根组件App.vue下注册了A和B两个组件,又在A下注册了组件C,可以发现直接在App.vue中使用 <C></C> 将无法编译,即无法使用子组件的子组件。也正如局部组件一样,组件C只能在A中使用,无法在其他组件中使用。
1.4 组件间的样式冲突问题
??默认情况下,写在.vue组件中的样式会全局生效,因此很容易造成多个组件之间的样式冲突问题。导致组件之间样式冲突的根本原因是:在SPA中,所有组件的DOM结构,都是基于唯一的index.html页面进行呈现的,而每个组件中的样式都会影响整个index.html页面中的DOM元素。举个例子,比如父组件和子组件中都有着p标签,此时如果给父组件的p标签加上样式,则会导致子组件的p标签也会发生变化,这就是样式冲突问题。 ??解决办法:为每个组件分配唯一的自定义属性,在编写组件样式时,通过属性选择器来控制样式的作用域。这种自定义属性名前面加上 data-v- ,所有data-v-*属性都存放在 dataset 中,获取/赋值都需要通过dataset。
<!-- 根组件/父组件 -->
<template>
<div id="app" data-v-tip>
<h2 data-v-tip>App根组件</h2>
<p data-v-tip>根组件</p>
</div>
</template>
<style lang="less">
p[data-v-tip] {
color: red,
}
</style>
<!-- 子组件 -->
<template>
<div data-v-tip2>
<p data-v-tip2>子组件</p>
</div>
</template>
上面这种人工给每个组件分配自定义属性的方式较为繁琐,因此,为了提高开发效率和开发体验,vue为 style 节点提供了 scoped 属性,从而防止组件之间的样式冲突问题。style节点的 scoped 属性,用来自动为每个组件分配唯一的“自定义属性”,并自动为当前组件的 DOM标签和 style 样式应用这个自定义属性,防止组件的样式冲突问题。
<style lang="less" scoped>
</style>
样式穿透
??如果给当前组件的style节点添加了scoped 属性,则当前组件的样式对其子组件是不生效的。如果想让某些样式对子组件生效,可以使用 /deep/ 深度选择器。
<style lang="less" scoped>
p {
font-size: 15px;
color: #e81410;
}
/deep/ .b {
color: #e81410;
}
:deep(.b) {
color: #e81410;
}
</style>
注意: /deep/ 是vue2.x中实现样式穿透的方案,在vue3.x中推荐使用 :deep() 替代/deep/。
二、动态组件
??可以通过 Vue 的 <component> 元素加一个特殊的 is 属性来实现在一个多标签的界面里进行不同组件间的动态切换。vue提供了一个内置的 <component> 组件,专门用来实现动态组件的渲染。
<template>
<div id="app">
<h2>Vue动态组件</h2>
<hr />
<!-- 渲染一个“元组件”为动态组件。依 is 的值,来决定哪个组件被渲染。 -->
<component :is="itemName"></component>
<button @click="itemName='A'">切换A页面</button>
<button @click="itemName='B'">切换B页面</button>
<button @click="itemName='C'">切换C页面</button>
</div>
</template>
<script>
import A from '@/components/a.vue'
import B from '@/components/b.vue'
import C from '@/components/c.vue'
export default {
name: 'App',
data(){
return {
itemName: 'A'
}
},
components: {
A,
B,
C
}
}
</script>
2.1 keep-alive
??之前在一个多标签的界面中使用 is 属性来切换不同的组件,但是我们可以发现:当在这些组件之间切换的时候,切换之后前一个组件会被销毁,导致我们在切换回上一个组件时会重新渲染该组件(即在该组件操作之后先切换到其他组件在切换回该组件时,这些操作不会被保存)。重新创建动态组件的行为通常是非常有用的,但是在某些情况下,我们更希望那些标签的组件实例能够被在它们第一次被创建的时候缓存下来。为了解决这个问题,可以用一个 <keep-alive> 元素将其动态组件包裹起来。使用该元素包裹后在进行组件间的切换时,上一个组件将处于失活(inactive )状态,而不会被销毁,从而达到保存该组件的状态的效果。
<template>
<div id="app">
<h2>Vue动态组件</h2>
<hr />
<!-- keep-alive的使用 -->
<keep-alive>
<component :is="itemName"></component>
</keep-alive>
<button @click="itemName='A'">切换A页面</button>
<button @click="itemName='B'">切换B页面</button>
<button @click="itemName='C'">切换C页面</button>
</div>
</template>
注意:这个 <keep-alive> 要求被切换到的组件都有自己的名字,不论是通过组件的 name 选项还是局部/全局注册。
2.1.1 keep-alive的生命周期
??当组件被缓存(失活)时,会自动触发组件的 deactivated 生命周期函数;当组件被激活时,会自动触发组件的 activated 生命周期函数。当组件第一次被创建的时候,既会执行 created 生命周期,也会执行 activated 生命周期;当组件被激活的时候,只会触发 activated 生命周期,不再触发created ,因为组件没有被重新创建。
注意:只有 <keep-alive> 包裹的的组件才有 deactivated 和 activated 生命周期!
2.1.2 keep-alive的include和exclude属性
?? include 属性用来指定只有名称匹配的组件会被缓存,多个组件名之间使用英文的逗号分隔。没有include属性的话默认所有组件在切换后都会被缓存。
<keep-alive include="A,B">
<component :is="itemName"></component>
</keep-alive>
exclude 属性用来指定哪些组件不需要被缓存,多个组件名之间使用英文的逗号分隔。没有exclude属性的话默认所有组件在切换后也都会被缓存,且include属性和exclude属性不能同时使用 。
<keep-alive exclude="C">
<component :is="itemName"></component>
</keep-alive>
2.1.3 组件注册名称和组件声明name的区别
??
components: {
A,
B,
C
}
export default {
name: 'myC'
}
组件的“注册名称”的主要应用场景是以标签的形式把注册好的组件渲染和使用到页面结构之中;组件声明时候的“name”名称的主要应用场景是结合 <keep-alive> 标签实现组件缓存功能以及在调试工具中看到组件的 name名称。
三、Props(重要,父传子)
3.1 传递静、动态props
?? 为了提高组件的复用性,在封装通用组件的时候,组件的DOM结构、Style样式要尽量复用且组件中要展示的数据,尽量由组件的使用者提供。为了方便使用者为组件提供要展示的数据,于是就引入了 props 。props 是组件的自定义属性,组件的使用者可以通过props把数据传递到子组件内部,供子组件内部进行使用(即子组件利用props来接收父组件传递过来的数据),合理地使用props可以极大的提高组件的复用性!另外,可以给 props 传入一个静态的值以及可以通过 v-bind 动态赋值。以一个计数的Count组件:Count.vue为例:
<template>
<div>
<!-- <p>count的值是: {{ count }}}</p> -->
<!-- <button @click="count += 1">+1</button> -->
<!-- 缺点:不同场景下多次使用该组件时可能会有不同的初始值,但data中的count已经固定了初始值
改进:使用props -->
<!-- <p>count的值是: {{ init }}</p> -->
<!-- <button @click="init += 1">+1</button> -->
<!-- 缺点:props是只读的,因此直接更改props会报错
改进:要想修改props中的值,可以把props中的值转存到data中,因为data中的数据是可读可写的-->
<p>count的值是: {{ count }}</p>
<button @click="count += 1">+1</button>
<!-- 传递静态props -->
<p>致橡树的作者:{{author}}</p>
</div>
</template>
<script>
export default {
props: ['init','author'],
data(){
return {
count: this.init
}
},
}
</script>
在a.vue文件中使用count组件,并动态赋值props:
<!-- a.vue -->
<template>
<div>
<!-- 传递动态props:使用v-bind属性绑定的形式,为组件动态绑定 props 的值-->
<Count :init="3"></Count>
</div>
</template>
注意:1、一个组件默认可以拥有任意数量的 prop,任何值(数字,布尔值,数组,对象以及一个对象的所有属性等)都可以传递给任何 prop。能够在组件实例中访问这个值,就像访问 data 中的值一样。 2、props是只读的,不能直接修改 props的值,否则终端会报错! 3、如果父组件给子组件传递了未声明的props属性,则这些属性会被忽略,无法被子组件使用。
3.2 Props验证
??Props验证用来在封装组件时对外界传递过来的props数据进行合法性的校验,从而防止数据不合法。使用 对象类型 的props 节点而不是一个字符串数组,可以对每个prop进行数据类型的校验。其中键是prop属性,值是类型。如果传递的值的类型与prop的类型不合法,则会在浏览器的console调试面板中会提示警告信息: Invalid prop 。 ??对象类型的props节点提供了多种数据验证方案,如①基础的类型检查;②多个可能的类型;③必填项校验;④属性默认值;⑤自定义验证函数等。
export default {
props: {
propA: String,
propB: [String,Number],
propC: {
type: String,
required: true
},
propD: {
type: Number,
default: 100
},
propE: {
type: Object,
default() {
return { message: 'hello' }
}
},
propF: {
validator(value) {
return ['success', 'warning', 'danger'].includes(value)
}
}
}
}
注意:在①基础的类型检查中, null 和 undefined 会通过任何类型验证。另外,prop 会在一个组件实例创建之前进行验证,所以实例的 property (如 data、computed 等) 在 default 或 validator 函数中是不可用的。
四、自定义事件(子传父)
??像常见的click、键盘事件等都是js内置的事件,都是在html元素上使用的;而自定义事件则是区别于这些内置的事件且自定义事件是只能应用在组件上。**它与 props 同样用于组件通信,但是props是 父传子 ,而自定义事件则是 子传父 **。使用自定义事件的三个步骤是:在封装组件时,要先声明自定义事件再触发,然后在使用组件时再监听自定义事件。 ??开发者为自定义组件封装的自定义事件,必须事先在 emits 节点中声明;在emits 节点下声明的自定义事件,可以通过 this.$emit('自定义事件的名称') 方法进行触发(在调用 this.$emit() 方法触发自定义事件时,可以通过第2个参数为自定义事件传参);在使用自定义的组件时,可以通过v-on的形式监听自定义事件。
五、异步组件
在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue 提供了 defineAsyncComponent 方法来实现此功能:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
resolve()
})
})
defineAsyncComponent 方法接收一个返回Promise的加载函数。这个 Promise 的 resolve 回调方法应该在从服务器获得组件定义时调用。你也可以调用 reject(reason) 表明加载失败。而ES模块动态导入 也会返回一个 Promise,所以多数情况下我们会将它和defineAsyncComponent 搭配使用。
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
最后得到的 AsyncComp 是一个外层包装过的组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。它会将接收到的 props 和插槽传给内部组件,所以你可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载。与普通组件一样,异步组件可以使用 app.component() 全局注册,也可以在局部注册组件时使用 defineAsyncComponent 。异步组件可以搭配内置的 <Suspense> 组件一起使用。
加载与错误状态
异步操作不可避免地会涉及到加载和错误状态,因此 defineAsyncComponent() 也支持在高级选项中处理这些状态:
const AsyncComp = defineAsyncComponent({
loader: () => import('./xxx.vue'),
loadingComponent: LoadingComponent,
delay: 200,
errorComponent: ErrorComponent,
timeout: 3000
})
如果提供了一个加载组件,它将在内部组件加载时先行显示。如果提供了一个报错组件,则它会在加载器函数返回的Promise抛错时被渲染。你还可以指定一个超时时间,在请求耗时超过指定时间时也会渲染报错组件。
|