3 组件化开发
为了提高代码复用性,使用组件化开发,将一串拥有独立功能、可以接受参数、可渲染控件的代码封装起来,并给与一个名字,在使用时与其他html基础元素相同。
组件其实就是可复用的Vue实例,和new Vue一样,存在data、computed、watch、methods等属性,而el是根实例特有的!
注意:vue2要求组件模板为单根元素,vue3无该要求
组件构建三步骤:
- 创建组件构造器:
Vue.extend()
- 注册组件:
Vue.component()
- 使用
注意:目前建议直接使用注册组件方法一步到位合并1,2两步:
Vue.component("abc",{
template:`<div></div>`,
})
组件使用:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="../js/vue.js"></script>
</head>
<body>
<div id="app">
<button-counter></button-counter>
</div>
<script>
// 全局组件注册
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
new Vue({
el: '#app'
})
</script>
</body>
</html>
对于组件的构成,分为单文件组件和多文件组件,单文件组件,对每个组件来说,只有一个文件同时包含html、css、js,而多文件则这几样分开存放,然后引入html作为组件
- 单文件组件
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>项目</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
<div id="root"></div>
<script>
const Child = Vue.extend({
template: `<div>child</div>`
});
const Parent = Vue.extend({
template: `<div>parent<Child></Child></div>`,
components:{
Child
}
});
new Vue({
el: "#root",
template: `
<div>
<button @click='show'>点击show</button>
<Parent></Parent>
</div>
`,
components: {
Parent
},
methods: {
show() {
alert("success");
}
}
});
</script>
</body>
</html>
- 多文件组件(需要vue脚手架支持)
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png" width="25%">
<HelloWorld msg="Hello Vue in CodeSandbox!"/>
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld";
export default {
name: "App",
components: {
HelloWorld
}
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
3.1 全局组件和局部组件
全局组件:可以在所有的Vue实例下使用(可以有多个Vue实例,但是不推荐)
Vue.component('abc',abc)
局部组件:是在Vue实例下注册的组件
new Vue({
components:{
'abc':abc
}
})
3.2 父组件和子组件
在父组件构造器中注册的组件称为父组件的子组件
const cp2 = Vue.extend({
template:`
<div>
<p>222</p>
</div>
`,
})
const cp1 = Vue.extend({
template:`
<div>
<p>111</p>
<cp2></cp2>
</div>
`,
components:{
cp2:cp2
}
})
注意:如果想在当前作用域中使用自定义组件,必须在当前的作用域中注册该组件,不存在传递!
其实在父组件中,vue编译父组件的时候,子组件就被替换成对应的模板了,最终当前作用域中只存在父组件编译好的所有模板。即子组件不是在当前作用域中编译解析的,所以在当前作用域中需要使用子组件必须再次注册。
3.2.1 父组件直接访问子组件 $children
父组件如果向直接访问子组件可以使用:$children
或$refs
this.$children
获取到的是当前组件的所有子组件,可以通过遍历来访问子组件中的方法或者属性
更好的目的性更明确地是使用$refs
,使用方法:
- 在需要访问的组件上使用ref定义属性:
<abc ref="a"></abc>
- 在需要访问的地方使用:
this.$refs.a
即可
3.2.2 子组件访问父组件 $parent
使用方法:this.$parent
不建议使用,耦合性太高了
3.2.3 直接访问Vue实例(顶层)$root
this.$root
3.3 模板抽离
将模板抽离出js内,放到html中去写,这样可以使用编辑器的代码提示功能,同时看起来比较方便
- html中使用
script
标签,类型为text/x-template
<html>
<body><div id="root"></div></body>
<script type="text/x-template" id="cp1">
<div>
<h2>abc</h2>
</div>
</script>
<script>
Vue.component('cp1',{template:'#cp1'}
</script>
</html>
- 使用
template
标签
<template id="cp1">
<div>
<h2>abc</h2>
</div>
</template>
<script>
Vue.component('cp1',{template:'#cp1'}
</script>
3.4 组件data
组件的data
必须是一个函数! 不能是一个对象,这样每个组件实例才能获得一份独立的数据拷贝!否则所有组件状态会相同!
data: function () {
return {
count: 0
}
}
因为如果不是函数,则每次复用组件会导致组件内部状态一样,因为获取到的是同一个对象
3.5 组件传值props(父组件–>子组件)
Prop可以在组件上注册自定义的属性,props
属性接收一个变量数组[]
:
Vue.component('blog-post', {
props: ['title'],
template: '<h3>{{ title }}</h3>'
})
<blog-post title="My journey with Vue"></blog-post>
<!--如果需要传递变量过去,需要使用v-bind-->
<blog-post :title="msg"></blog-post>
props支持验证和默认值设置,这需要使用对象来传递
Vue.component('blog-post', {
props: {
title: String,
message: [String, Number],
name: {
type: String,
required: true,
default: "pp"
}
hobby: {
type: Array,
default: function(){return ["jump","eat"]}
}
other: {
type: Object,
default: function(){return {abc:"aaa"}}
}
},
template: '<h3>{{ title }}</h3>'
})
注意:这几种props写法都可以,一种是数组,一种是对象。其中对象写法可以定义类型和是否必须,当类型为数组或者对象时,对于低版本Vue的默认值必须是一个函数
类型可以定义为:String|Number|Boolean|Array|Object|Date|Function|Symbol
当然也可以自定义类型:
function Person(a, b){
this.a = a
this.b = b
}
props:{
author: Person
}
注意:在标签属性中不支持驼峰命名,如果在vue对象中的变量是驼峰命名,可以在v-bind的属性上使用-
进行分割,如:v-bind:abC=""
转化成v-bind:ab-c=""
3.6 组件事件监听$emit(子组件–>父组件)
情景:当子组件事件需要让父组件监听行为并作出一些改变时:
方法一:子组件无需复杂操作,仅仅传递参数给父组件:使用v-on:自定义事件="函数"
和v-on:事件=$emit("自定义事件")
组合来实现
方法二:子组件需要复杂操作,并传递参数给父组件:使用props传递函数
3.6.1 方法一
父组件:定义监听自定义事件
<blog-post v-on:enlarge-text="run"></blog-post>
子组件:定义触发方法,抛出对应的自定义事件名
<button v-on:click="$emit('enlarge-text')">Enlarge text</button>
<!--或者在子组件的方法中使用:this.$emit(自定义事件,参数):methods:{crun(x){this.$emit('enlarge-text',x)}}-->
<button v-on:click="crun">Enlarge text</button>
相当于子组件通过标准事件抛出自定义事件,然后父组件监听到该自定义事件,并触发相应函数进行操作
有时候,需要对自定义事件传参,则须要用到$emit(自定义事件名,参数)
,使用第二个参数进行传参,传递的参数在监听函数中使用$event
来获取,此处的$event
就是参数,可以直接使用
<!--方法中:this.$emit('enlarge-text',0.1)-->
<button v-on:click="$emit('enlarge-text', 0.1)">Enlarge text</button>
<blog-post v-on:enlarge-text="postFontSize += $event"></blog-post>
更常用的是通过函数参数来获取
methods: {
onEnlargeText: function (enlargeAmount) {
this.postFontSize += enlargeAmount
}
}
3.6.2 方法二
当然,对于子组件向父组件发起控制也可以通过props来做,具体思路:(由于回调函数定义在父组件中,可以更改父组件状态,而触发操作在子组件中,相当于子组件控制父组件)
- 父组件定义一个回调函数,将这个回调函数传递给子组件的绑定属性
- 子组件某个操作调用过程中调用父组件传过来的回调函数,并传递值即可
3.7 组件插槽slot
在 2.6.0 中,具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slot 和 slot-scope 这两个目前已被废弃但未被移除且仍在文档中的 attribute。新语法的由来可查阅这份 RFC。
插槽:让组件扩展性更强,抽取共性,插槽用来替换个性
插槽用于在自定义组件中放置可以显示在自定义标签中间的内容,即自定义标签中间的部分会替换到插槽位置
Vue.component('alert-box', {
template: `
<div class="demo-alert-box">
<strong>Error!</strong>
<slot></slot>
</div>
`
})
<alert-box>
Something bad happened.
</alert-box>
插槽中间可以放其他标签,作为默认值,如果自定义标签中间存在标签,则默认值被该标签替换,否则显示默认标签
<slot><div>123</div></slot>
如果存在多个插槽,则会重复替换,如果有多个标签,则会一起替换到插槽位置
特别注意:插槽上不要写css或者判断,因为会被直接替换掉!可以采取间接方法用div包裹插槽,然后在div上进行判断和样式
3.7.1 具名插槽
多插槽情况,如果插槽具有name
属性,则必须替换时使用slot
属性指定替换的插槽,v-slot:
可以简写为#
:
<!--使用:废弃语法-->
<abc>
<div slot="a">444</div>
</abc>
<!--使用:新语法-->
<abc>
<div v-slot:a>444</div>
<div #a>444</div>
</abc>
<div class="abc">
<strong>Error!</strong>
<slot name="a"></slot>
<slot name="b"></slot>
<slot name="c"></slot>
</div>
3.7.2 作用域插槽
作用域插槽相当于将本作用域的一些数据提供到外部使用,即暴露本作用域中的内容
<!--组件中-->
<template id="myabc">
<div>
<slot :mydata="abc">
<span>默认显示:{{abc}}</span>
</slot>
</div>
</template>
<script>
..
// 注册组件
myabc: {
template: "#myabc",
data:function(){
return{
abc: 1111
}
}
}
...
</script>
<!--使用:废弃语法-->
<myabc>
<template slot="default" slot-scope="myslot">
<span>改写显示:{{myslot}}</span>
</template>
</myabc>
<!--使用:最新语法-->
<myabc>
<template v-slot:default="myslot">
<span>改写显示:{{myslot}}</span>
</template>
</myabc>
分析使用过程:组件中在slot
标签上定义自定义属性,然后赋值,在组件外部(即使用该组建的组件中)可以通过:slot-scope
获取到内部的slot
上定义的自定义属性!最终展示结果为:改写显示:{ "mydata": 1111 }
注意:默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确。只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template>
的语法。
3.7.3 解构插槽
//作用域插槽的内部工作原理是将你的插槽内容包裹在一个拥有单个参数的函数里:
function (slotProps) {
// 插槽内容
}
//这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。
//所以在支持的环境下 (单文件组件或现代浏览器),你也可以使用 ES2015 解构来传入具体的插槽 prop,如下:
<current-user v-slot="{ user }">
{{ user.firstName }}
</current-user>
//这样可以使模板更简洁,尤其是在该插槽提供了多个 prop 的时候。
//它同样开启了 prop 重命名等其它可能,例如将 user 重命名为 person:
<current-user v-slot="{ user: person }">
{{ person.firstName }}
</current-user>
//你甚至可以定义后备内容,用于插槽 prop 是 undefined 的情形:
<current-user v-slot="{ user = { firstName: 'Guest' } }">
{{ user.firstName }}
</current-user>
3.7.4 动态插槽
2.6.0新增动态插槽
语法:
<template v-slot:[dynamicSlotName]>
...
</template>
3.8 动态组件
动态组件用于在事件触发后动态加载组件,改变is
的值即可改变渲染的对象,接收的对象都被视为Vue组件(value需要加.prop)
<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component v-bind:is="currentTabComponent"></component>
4 模块化开发
原生js进行编写时会导致全局变量同名问题,还有引入顺序也会导致一些问题,解决方法:模块化封装
模块化封装基本思想:使用匿名函数立即调用形式+闭包:(function(){})()
,因为函数有自己的作用域,不会导致同名,但是又出现代码复用性无的问题,因为拿不到函数内的变量或者函数,所以还需要通过闭包
var moduleA = (
function(){
var obj = {}
var f = function(x){console.log(x)}
var name = "pp"
obj.name = name
obj.f = f
return obj
}
)()
// 此时moduleA中存在自己的作用域和属性,只需要保证全局中module名不重复即可
常见的模块化规范:CommonJS | AMD | CMD | ES6Modules
nodejs中模块化使用的CommonJS规范
模块化规范核心:导入、导出
4.1 CommonJS
// 导出
module.export = {
flag: true,
run(){},
name: "pp"
}
// 导入
let {flag,run,name} = require('./xxx.js)
// 或
let x = require('./xxx.js)
let flag = x.flag
4.2 ES6 export/import
// 导出:写法1
export let name = 'pp
export let age = 12
// 导出:写法2
let name = "pp"
let age = 213
export {name,age}
// 导入
import {flag,sum} from './xxx.js'
console.log(flag)
// html中使用
<script src="./xxx.js" type="module"></script>
4.2.1 export default
导出默认模块,普通导出:导出的名字和导入的名字必须相同才能正确获取
export default
导出则为匿名,可以在导入的时候去自定义名字,一个文件default
导出的只能存在一个,导入的时候不需要{}
,表示导入默认导出项
export default x
import y from 'xxx.js'
4.2.2 统一全部导出
由于对于非default
导入需要 保证导入的变量名和导出的相同,当导入变量很多,则可以采用导入全部作为一个变量,然后去使用
// 全部导出
import * as x from 'xxx.js'
console.log(x.name)