vue 开发组件化
vue 开发组件化
Jinvue-cli
什么是 vue- cli
tip vue-cli 是 Vue.js 开发的标准工具。它简化了程序员基于 webpack 创建工程化的 Vue 项目的过程。
安装与使用
安装
npm -g @vue/cli
使用 快速初始化
vue create 项目名字
运行流程
vue 通过 main.js 把 app.vue 渲染到 index.html 指定区域
- app.vue 用来编写待渲染的模板结构
- index.html 中需要预留 el 区域
vue 组件的三个组成部分
template(组件的模板结构)
<template>
<div>
<!-- 定义当前组件的 DOM 结构 -->
</div>
</template>
- 每个组件的模板结构 ,需要调用到 template
- template 的 vue 提供的容器标签 ,只有包裹性作用,不会渲染为真正的DOM元素
- template 只能包含唯一的根节点
- 简单理解里面写 HTML即可
script(组件的 JavaScript 行为)
<script>
export default {
// 封装组件的 JS 业务逻辑
}
</script>
style (组件的样式)
<style>
/* css代码 */
</style>
添加less 语法支持
<style leng="less">
/* less代码 */
</style>
每个组件中必须包含 template 模板结构,而 script 行为和 style 样式是可选的组成部分
组件关系
封装好 App.vue Left.vue Right.vue 之后,彼此间相互独立不存在关系
使用组件
<template>
<div>
<!-- 步骤三:以标签形式使用组件 -->
<Left></Left>
</div>
</template>
<script>
// 步骤一:使用 import 导入需要的组件
import Left from "@/components/Left.vue"
export default{
// 步骤二:使用 components 结点注册组件
components:{
Left,
}
}
</script>
全局组件
通过 components 注册的是私有子组件
在组件 A 的 components 节点下,注册了组件 F。
则组件 F 只能用在组件 A 中;不能被用在组件C 中。
注册全局组件
在 vue 项目的 main.js
入口文件,通过 Vue.component()
注册全局组件
// 导入需要注册的全局组件
import Count from "@/components/Count.vue"
props (自定义属性)
tip props 是组件的自定义属性,在封装通用组件的时候,合理地使用props 可以极大的提高组件的复用性!
导入组件后以标签形式使用组件 在标签即可使用 自定义属性
<!-- 绑定自定义属性 -->
<!-- 自定义属性有小驼峰命名 改成连字符-命名 -->
<count :init="1" :cmt-count="2"></count>
// count.vue
export default {
// 组件的自定义属性
// 格式一
// props: ["自定义属性1","自定义属性2","自定义属性3"],
// props: ["init"],
// 格式二:props:{
// 自定义属性1:{...}
// 自定义属性2:{...}
// 自定义属性3:{...}
// }
props: {
// 自定义属性名
init: {
// 默认值,如果用户不传递默认的值
default: 0,
// 类型 ,传递过来的值必须是规定类型的值
type: Number,
// 必选项,开启后必须传递值 ,这个组件才可以使用
require: true,
// props 自定义属性为只读值,不能用户修改
},
// 如果自定义属性名为小驼峰命名
// 建议绑定属性时可改成连字符-命名
cmtCount:{
default: 0,
type: Number,
}
},
data() {
return {
// props 的值转存到 data
count: this.init,
};
},
};
tip
vue 规定:组件中封装的自定义属性是只读的,程序员不能直接修改 props 的值
要想修改 props 的值,可以把 props 的值转存到 data 中,因为 data 中的数据都是可读可写的!
组件冲突问题
默认情况下,写在 .vue 组件中的样式会全局生效,因此很容易造成多个组件之间的样式冲突问题。
导致组件之间样式冲突的根本原因是:
- 单页面应用程序中,所有组件的 DOM 结构,都是基于唯一的 index.html 页面进行呈现的
- 每个组件中的样式,都会影响整个 index.html 页面中的 DOM 元素
style 节点的 scoped 属性
为了提高开发效率和开发体验,vue 为 style 节点提供了 scoped 属性,从而防止组件之间的样式冲突问题
<style leng="less" scoped >
/* less代码 */
</style>
- 原理:为当前组件所有 DOM 元素分配唯一的自定义属性,写样式时使用属性选择器防止样式冲突问题
scoped
只给子组件最外层的 div 添加了自定义属性[data-v-xxx]
,子组件内部的标签并没有添加。因此父组件只能修改子组件最外层的 div 样式,修改子组件内层元素的样式是不可行的- 若想让某些样式对子组件生效,需使用
/deep/
深度选择器 (常用于修改 第三方 ui 组件库 )
/* 细细品味 */
<style lang="less" scoped>
.title {
/* 不加 /deep/,选择器格式为 .title[data-v-052242de] */
color: blue;
}
/deep/ .title {
/* 加 /deep/,选择器格式为 [data-v-052242de] .title */
color: blue;
}
</style>
组件数据共享
父子间的共享
父向子共享
父组件向子组件共享数据需要使用自定义属性 (props)
父组件
<son :msg="message" :user="userinfo"></son>
<script>
export default{
data(){
return {
// 定义要传递的值
message:'hello vue.js'
userinfo:{name:'张三',age:18}
}
}
}
</script>
子组件
<p>父组件传递过来的 msg 是 :{{msg}}</p>
<p>父组件传递过来的 user 是 :{{user}}</p>
<script>
export default{
// 定义自定义属性
props:['msg','user']
}
</script>
子向父共享
子组件向父组件共享数据使用自定义事件。
子组件
export default{
data(){
return {
// 定义要传递的值
count: 0
}
},
methods:{
add(){
this.count++
// 修改数据时 ,通过 $emit() 触发自定义事件
// 参数1:自定义事件的名字,
// 参数2:要传递的值
this.$emit("numChange",this.count)
}
}
}
父组件
<son @numChange="getCount"></son>
<script>
export default{
data(){
return {
// 定义储存传递过来的值
countFromSon: 0
}
},
methods:{
// 定义触发自定义事件 numChange 的函数
// val 接收 son 自定义事件发送过来的值
getCount(val){
// 转存到countFromSon
this.countFromSon=val
}
}
}
</script>
兄弟间共享
在 vue2.x 中,兄弟组件之间数据共享的方案是EventBus。
EventBus 的使用步骤:
- 创建
eventBus.js
模块,并向外共享一个Vue 的实例对象
- 在数据发送方,调用
bus.$emit('事件名称', 要发送的数据)
方法触发自定义事件 - 在数据接收方,调用
bus.$on('事件名称', 事件处理函数)
方法注册一个自定义事件
兄弟组件 A(数据发送方)
<!--定义一个按钮 触发 -->
<button @click="sendMsg">发送数据给 兄弟组件 C</button>
<script>
// 导入 EventBus 模块
import bus from "./eventBus.js";
export default {
data() {
return {
// 共享的数据
msg: "hello vue.js",
};
},
methods: {
sendMsg() {
// 自定义事件
// 参数:事件名称, 要发送的数据
bus.$emit("share", this.msg);
},
},
};
</script>
eventBus.js
import Vue from 'vue'
// 向外共享 Vue 的实例对象
export default new Vue()
兄弟组件 C(数据接收方)
<p>兄弟组件 A 共享的数据 :{{ msgFromLeft }}</p>
<script>
// 导入 EventBus 模块
import bus from "./eventBus.js";
export default {
data() {
return {
// 定义用来转存 发送过来的值 的变量
msgFromLeft: "",
};
},
// 在 created 钩子中注册函数
created() {
// 使用箭头函数,则 this 指向该组件而非 bus
// 参数:事件名称, 事件处理函数
// 触发兄弟组件 A 自定义事件 share
// 函数处理 把传递过来的值转存到 msgFromLeft
bus.$on("share", (val) => {
this.msgFromLeft = val;
});
},
};
</script>
生命周期
概念:每个组件从创建 -> 运行 -> 销毁的一个过程,强调的是一个时间段!
tip 生命周期阶段
tip 官方生命周期图示
ref 引用
每个 vue 的组件实例上,都包含一个$refs
对象,里面存储着对应的DOM 元素或组件的引用。默认情况下,组件的 $refs
指向一个空对象。
引用 dom 元素
<!-- 使用 ref 属性,为对应的 dom 元素添加引用的名称 -->
<h3 ref="myH3Ref"> myRef 组件</h3>
<!-- 点击调用getRef -->
<button @click='getRef'>获取 $refs 引用</button>
<script>
export default {
methods:{
getRef(){
// 通过 this.$refs.引用名称
// 可以得到 dom 元素引用
console.log(this.$refs.myH3Ref)
// 操作 dom 元素,把文本颜色改为红色
this.$refs.myH3Ref.style.color='red'
}
}
}
</script>
引用组件实例
<!-- 使用 ref 属性,为对应的组件添加引用的名称 -->
<my-counter ref="counterRef"></my-counter>
<button @click='getRef'>获取 $refs 引用</button>
<script>
export default {
methods:{
getRef(){
// 通过 this.$refs.引用名称
// 可以得到组件实例引用
console.log(this.$refs.counterRef)
// 可以访问获取后的组件数据和方法
this.$refs.counterRef.count = 1
this.$refs.counterRef.add()
}
}
}
</script>
this.$nextTick(cb) 方法
组件的 $nextTick(cb)
方法,会把 cb 回调推迟到下一个 DOM 更新周期之后执行,即在 DOM 更新完成后再执行回调,从而保证 cb 回调可以获取最新的 DOM 元素
methods: {
showInput() {
this.inputVisible = true
// 对输入框的操作推迟到 DOM 更新完成之后
this.$nextTick(() => {
this.$refs.input.focus()
})
}
}
动态插件
vue
提供了一个内置的<component>
组件,专门用来实现动态组件的渲染
<script>
data(){
return {
// 定义要渲染组件的名称
comName:'Left'
}
}
</script>
<!-- 通过 js 动态绑定指定的渲染组件 -->
<component is:"conName" ><component>
<!-- 点击 动态切换组件 -->
<button @click="comName = 'Left'">展示Left组件</button>
<button @click="comName = 'Right'">展示Right组件</button>
keep-alive
在默认情况下,切换组件会将不需要的组件销毁,导致组件的数据清空,切换回来时数据不能还原到切换前
为了保持组件的状态可以使用 vue 内置的 <keep-alive>
组件
<keep-alive>
<component :is="comName"></component>
</keep-alive>
使用 <keep-alive>
组件后,动态切换组件时不需要的组件就会被缓存,切换回来时重新被激活
keep-alive 对应的生命周期函数
- 组件被激活时,触发组件的
deactived
生命周期函数 - 组件被销毁时,触发组件的
activated
生命周期函数
export default {
activated(){
console.log('组件被激活了')
}
deactived(){
console.log('组件被销毁了')
}
}
keep-alive 的 include
属性
include
属性用来指定,只有匹配名称的组件才会被缓存,其他销毁
<keep-alive include="Left">
<component :is="comName"></component>
</keep-alive>
keep-alive 的 exclude
属性
功能与 include
相反,排除不缓存的组件
如果同时使用 include, exclude, 那么 exclude 优先于 include
include 和 exclude 避免混乱只用一个就好
插槽
插槽(slot)是封装组件时,把不确定的,希望由用户指定部分定义为插槽
简单来讲,组件封装期间,为用户预留的内容的占位符。
基础用法
封装组件时,用 <slot>
元素定义插槽,预留占位符
<!-- my-com.vue -->
<template>
<p> 这是 MyCom 组件的第一个 p标签 </p>
<!-- 通过 slot 占位符,为用户预留位置 -->
<slot></slot>
<p> 这是 MyCom 组件的最后的 p标签 </p>
</template>
使用
<my-com>
<!-- 使用 my-com 组件时,为插槽指定具体内容 -->
<p> 这是用户自定义的 p标签 ~~~ </p>
<my-com>
如果在封装组件时没有预留 slot
任何插槽,用户自定义的内容不生效,会被丢弃
后备内容
在封装组件时,可以为预留的 <slot>
插槽的提供后备内容(默认内容)
<slot>
<p> 这是后备内容的 p标签 ~~~ </p>
</slot>
如果组件使用者没有为插槽提供任何的内容,后备内容就会生效
用户提供内容后,后备内容就会被覆盖掉
具名插槽
如果在封装组件时需要预留多个插槽节点,则需要为每个 <slot>
插槽指定具体的 name 名称。这种带有具体名称的插槽叫做“具名插槽”。
<!-- my-com.vue -->
<div class="container">
<header>
<!-- 希望页头放这里 -->
<slot name="header"></slot>
</header>
<main>
<!-- 希望主要内容放这里 -->
<slot name="main"></slot>
</main>
<footer>
<!-- 希望主要页脚放这里 -->
<slot name="footer"></slot>
</footer>
</div>
为具名插槽提供内容,在一个 <template>
元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称
<my-com>
<!-- v-slot 的属性 要和希望放到 slot 插槽name属性名一致 -->
<template v-solt:header>
<h1>头部 </h1>
</template>
<template v-solt:main>
<p> 主要内容 </p>
</template>
<template v-solt:footer>
<p> 页脚 </p>
</template>
</my-com>
注意:没有指定 name 名称的插槽,会有隐含的名称叫做 “default”。
具名插槽的简写形式: (v-slot:)
替换为字符 #
,例如 v-slot:header
可以被简写为 #header
作用域插槽
在封装组件过程中,为预留的 solt 插槽绑定 props 数据,叫做作用域插槽
数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定
<template>
<slot v-for="item in list" :user='item'>
</template>
<script>
export default {
data() {
return {
list: [
{
id: 1,
name: 'Lily',
state: true,
},
{
id: 2,
name: 'Ben',
state: false,
},
{
id: 3,
name: 'Water',
state: true,
},
],
}
},
}
</script>
可以使用 v-slot: 的形式,接收作用域插槽对外提供的数据
<my-com>
<template v-slot:default="scope">
<p>作用域插槽提供的数据: {{scope}}</p>
</template>
</my-com>
接收到的数据 scope
是一个对象。
// scope 的内容
{
'user': {
'id': 1,
'name': 'Lily',
'state': true
}
}
在接收作用域插槽提供的数据时可以使用解构赋值。
<my-com>
<template #default="{user}">
<p>id:{{ user.id }}</p>
<p>name:{{ user.name }}</p>
<p>state:{{ user.state }}</p>
</template>
</my-com>
自定义指令
私有自定义指令
在每个 vue 组件中,可以在 directives 节点下声明私有自定义指令
directives: {
color:{
// 为绑定的元素设置红色字体
bind(el){
// el 是绑定此指令的 dom 对象
el.style.color='red'
}
}
}
使用:需要加上 v- 前缀
<!-- 上面声明的自定义指令名为 color -->
<!-- 使用自定义指令名 color,需要加上 v- 前缀 -->
<h1 v-color>app组件</h1>
动态绑值
data:{
return{
color:'red'
}
}
<h1 v-color="color">app组件</h1>
通过 binding 获取指令的参数值
directives: {
color:{
bind(el,binding){
// el 是绑定此指令的 dom 对象
// 通过 binding 对象.value 属性获取动态的参数值
el.style.color=bing.value
}
}
}
update 函数
bind 函数只调用 1 次:当指令第一次绑定到元素时调用,当 DOM 更新时 bind 函数不会被触发。 update 函数会在每次 DOM 更新时被调用
directives: {
color:{
// 指令第一次绑定元素时调用
bind(el,binding){
// el 是绑定此指令的 dom 对象
// 通过 binding 对象.value 属性获取动态的参数值
el.style.color=bing.value
}
// 每次 dom 更新都调用
update(el,binding){
el.style.color=bing.value
}
}
}
简写
insert 和update 函数中的逻辑完全相同,则对象格式的自定义指令可以简写成函数格式
directives: {
color(el,binding){
el.style.color=bing.value
}
}
全局自定义指令
全局共享的自定义指令需要通过 Vue.directive()
进行声明
// 全局写法
Vue.directive('color', (el, binding) => {
el.style.color = binding.value
}))
注意事项
- 自定义指令使用时需要添加
v-
前缀 - 指令名如果是多个单词,要使用
camel-case
短横线命名方式,不要用camelCase
驼峰命名 - 自定义指令三个函数里的
this
指向window
<span v-big-number="n"></span>
data() {
return {
n: 1
}
},
directives: {
// 添加引号才是对象键名完整写法
// 平时不加引号都是简写形式
// 遇到短横线的键名就必须添加引号
'big-number': {
bind(el, binding) {
console.log(this) // Window
el.innerText = binding.value * 10
}
}
}