由于本vue教程的参考视频太旧,不再更新,请移步新版尚硅谷Vue技术全家桶(1)
组件化
组件化概念:
注:组件化和模块化是有区别的
组件的基本使用步骤
组件初步接触:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app1">
<h2>h2-context</h2>
<p>p-context</p>
<!-- 在vue接管的元素内组件复用才有效 -->
<my-cpn></my-cpn>
</div>
<!-- vue接管不到 -->
<my-cpn></my-cpn>
<div id="app2">
<!-- 全局组件vue接管元素下下到处可用 -->
<my-cpn></my-cpn>
<!-- 局部组件 -->
<cpn></cpn>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
// 组件使用:
// 1.创建组件构造器对象
const cpnConstructor = Vue.extend({
// ES6新语法,使用飘号可以换行连续写一个整体的代码,而不需要加号连接
template: `
<div>
<h2>h2-context</h2>
<p>p-context</p>
</div>`
})
// 2.注册组件,这种方式是注册的全局组件,所有的vue接管元素下都可复用
Vue.component('my-cpn', cpnConstructor);
// 3.使用组件
// 在上面用注册的名字my-cpn做标签名使用组件
const app1 = new Vue({
el: "#app1",
data: {
},
methods: {
},
computed: {
},
filters: {
}
})
const app2 = new Vue({
el: "#app2",
// vue实例内部创建的组件就是局部组件
components:{
// 标签名:构造器名
cpn: cpnConstructor
}
})
</script>
</body>
</html>
父子组件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<cpn2></cpn2>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
// 子组件
const cpnC1 = Vue.extend({
template: `
<div>
<h2>cpnC1-h2-context</h2>
<p>p-context</p>
</div>`
})
// 父组件
const cpnC2 = Vue.extend({
template: `
<div>
<h2>cpnC2-h2-context</h2>
<p>p-context</p>
<cpn1></cpn1>
</div>
<cpn1></cpn1>`
,
// 组件构造器cpnC1放入2中,在2的template中复用1
components: {
cpn1: cpnC1
}
})
// 在app元素中注册局部组件2
// 可以当成root组件
const app = new Vue({
el: "#app",
components: {
cpn2: cpnC2
}
})
</script>
</body>
</html>
组件语法糖形式:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<cpn1></cpn1>
<cpn2></cpn2>
<cpn3></cpn3>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
// 原先方式
// 1.
const cpn1 = Vue.extend({
template: `
<div>
<h2>h2-context</h2>
<p>p-context</p>
</div>`
});
// 2.
Vue.component('cpn1', cpn1);
// 全局组件语法糖形式:
// 1+2
const Feature = Vue.component('cpn2', {
template: `
<div>
<h2>h2-context</h2>
<p>p-context</p>
</div>`
});
const app = new Vue({
el: "#app",
components: {
// 局部组件语法糖
cpn3: {
template: `
<div>
<h2>h2-context</h2>
<p>p-context</p>
</div>`
}
}
})
</script>
</body>
</html>
理论上说,js代码里是不该出现html代码的,这样会导致混乱,因此,组件中的template应该分离出去:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<cpn1></cpn1>
<cpn2></cpn2>
<cpn3></cpn3>
</div>
<!-- 1.第一种写法 -->
<script type="text/x-template" id="cpn1">
<div>
<h4>1-h4-context</h4>
</div>
</script>
<!-- 2.第二种写法 -->
<template id="cpn2">
<div>
<h4>2-h4-context</h4>
</div>
</template>
<!-- 3.组件嵌套尝试 -->
<template id="cpn3">
<div>
<h4>3-h4-context</h4>
<cpn1></cpn1>
</div>
<!-- div外面的报错、不显示,和之前父子组件时如出一辙,原因不明 -->
<cpn2></cpn2>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
Vue.component('cpn1', {
template: "#cpn1"
});
Vue.component('cpn2', {
template: "#cpn2"
});
Vue.component('cpn3', {
template: "#cpn3"
});
const app = new Vue({
el: "#app",
components: {
}
})
</script>
</body>
</html>
上面只是简单分离写法,在cli脚手架部分有最常规的分离写法。
组件数据
关于组件的data必须是个函数的解释:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<cpn1></cpn1>
<!-- cpn2的data隔离的 -->
<cpn2></cpn2>
<cpn2></cpn2>
<!-- cpn3的data由于返回的是个静态全局变量,是通用的 -->
<cpn3></cpn3>
<cpn3></cpn3>
</div>
<template id="cpn1">
<div>
<h4>cpn1-h4-context</h4>
<p>{{message}}</p>
</div>
</template>
<!-- 计数器组件template -->
<template id="cpn2">
<div>
<h4>counter:{{counter}}</h4>
<button @click="increase()">+</button>
<button @click="decrease()">-</button>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
Vue.component('cpn1', {
template: "#cpn1",
// 组件数据的存放位置
// data必须是个方法,并且内部的数据是要return一个对象返回的
// 关于为什么数据要被封装成一个方法返回的问题,是因为如果是常规的变量情况下,
// 当组件复用时,由于使用的是同一个组件类的数据,数据就会一处变动,所有的同组件跟着变动
// 这是因为return{}会返回一个新的对象
data() {
return {
message: 'message-context'
}
}
});
Vue.component('cpn2', {
template: "#cpn2",
data() {
return {
counter: 0
}
},
methods: {
increase() {
this.counter++
},
decrease() {
this.counter--
}
}
});
const obj = {
counter: 0
}
Vue.component('cpn3', {
template: "#cpn2",
data() {
return obj
},
methods: {
increase() {
this.counter++
},
decrease() {
this.counter--
}
}
});
const app = new Vue({
el: "#app",
components: {
},
})
</script>
</body>
</html>
父子组件通信:
就目前对知识的理解来看:由于组件之间往往是嵌套的,实际开发中,一般是最外层的大组件发url请求到数据,然后将数据通过父子组件的通信传递给子组件,子组件再传给孙子组件,一层一层传下去;而且由于全部数据的信息量庞大,一般会把整体框架类的数据传过来,图片视频什么的开销大的数据再慢慢传。
如何实现:
- 通过props向子组件传递数据
- 通过事件向父组件发送消息
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<!-- 将vue实例data中的数据绑定到组件里面的数据上 -->
<cpn v-bind:cmovies="movies" :cmessage="message"></cpn>
<!-- cmessage没传时设置了默认值 -->
<cpn v-bind:cmovies="movies"></cpn>
<cpn1 :movies="movies"></cpn1>
<cpn1 ></cpn1>
</div>
<template id="cpn">
<div>
<p v-for="movies in cmovies">{{movies}}</p>
<h4>{{cmessage}}</h4>
</div>
</template>
<template id="cpn1">
<div>
<select v-model="selectedmovie">
<option v-for="movie in movies" :value="movie">{{movie}}</option>
</select>
<h4>{{selectedmovie}}</h4>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
// 练习:试验传参全局组件
const Feature = Vue.component('cpn1',{
template: "#cpn1",
props:{
movies: {
type: Array,
default(){
return ['one','two','three']
}
}
},
data() {
return {
selectedmovie: '',
};
},
});
const app = new Vue({
el: "#app",
data: {
message: 'message-context',
movies: ['海王', '海贼王', '海尔兄弟']
},
components: {
// 局部组件
cpn: {
template: "#cpn",
// 在template中要用的变量
// 1.第一种方式,数组
// props: ['cmovies', 'cmessage']
// 2.第二种方式,对象
props: {
// 这种方式会带上数据类型,可以进行数据验证
cmovies: Array,
// cmovies:{
// type: Array,
// default(){
// return ['one','two']
// }
// },
// 或者更详细的对象写法
cmessage: {
type: String,
// 可以提供默认值
// 如果类型是object或array,默认值必须是个函数:
// default(){
// return []
// }
default: 'aaaaaaa',
// 是否为必须传值,若不传,浏览器控制台会报错
required: true
}
}
}
}
})
</script>
</body>
</html>
驼峰标识的问题:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<!-- //v-bind不支持驼峰,驼峰的地方使用-分割 -->
<cpn1 :c-info="info"></cpn1>
</div>
<template id="cpn1">
<h2>{{cInfo}}</h2>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
Vue.component('cpn1', {
template: "#cpn1"
});
const app = new Vue({
el: "#app",
data: {
info: {
name: 'why',
age: 18,
height: 1.88
}
},
components: {
cpn1: {
template: "#cpn1",
props: {
cInfo: {
type: Object,
default(){
return {}
}
}
}
}
}
})
</script>
</body>
</html>
组件通信:子传父
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<!-- 监听事件名和子组件发射的事件名相同(这里没用驼峰标识转短横杠,用的话有问题,暂时未知),是个自定义事件,事件引起的方法是handler -->
<!-- 事件方法不写参数默认是传递事件,但自定义事件默认是传递子组件发送的东西(item) -->
<cpn1 @item-click="handler"></cpn1>
<cpn2 :selected-id="selectedId"></cpn2>
</div>
<template id="cpn1">
<div>
<button v-for="item in categories" @click="btnClick(item)">{{item.name}}</button>
</div>
</template>
<template id="cpn2">
<div>
<h4>id:{{selectedId}}</h4>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
//子组件1
const cpn1 = {
template: '#cpn1',
data() {
return {
categories: [
{ id: 001, name: '热门推荐' },
{ id: 002, name: '手机数码' },
{ id: 003, name: '家用家电' },
{ id: 004, name: '电脑办公' }
]
}
},
methods: {
btnClick(item) {
console.log('btnClick', item)
//发射事件,(事件名,传值)
this.$emit('item-click', item);
},
},
}
子组件2
const cpn2 = {
template: '#cpn2',
data() {
return {
}
},
props: {
selectedId: {
type: Number
}
}
}
//父组件
const app = new Vue({
el: '#app',
data() {
return {
selectedId: 001,
};
},
components: {
cpn1,
cpn2
},
methods: {
handler(item) {
console.log('handler', item)
this.selectedId = item.id
}
}
});
</script>
</body>
</html>
后面要用的父子组件的使用演示:
如果是像上面那样每个组件都有个template\data\methods\props之类的,组件多了太乱。
单个组件写法:
组件里用别的组件就import导入、再在components里注册
父子组件通信+双向绑定案例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<cpn1 :number1="num1" :number2="num2" @datanumber1change="dn1c" @datanumber2change="dn2c"></cpn1>
</div>
<template id="cpn1">
<!-- 模板组件应该只包含一个根元素,所以使用div将模板内容包起来。不然多标签会当成多根元素 -->
<div>
<!-- v-model的拆分操作 -->
<input type="text" :value="datanumber1" @input="inputnumber1">
<h4>number1:{{number1}}</h4>
<h4>datanumber1:{{datanumber1}}</h4>
<input type="text" :value="datanumber2" @input="inputnumber2">
<h4>number2:{{number2}}</h4>
<h4>datanumber2:{{datanumber2}}</h4>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data() {
return {
num1: 1,
num2: 2
};
},
methods: {
dn1c(value) {
//传过来的value是string,转成number
this.num1 = parseFloat(value)
},
dn2c(value) {
this.num2 = parseFloat(value)
}
},
components: {
cpn1: {
template: "#cpn1",
data() {
return {
// datanumber数据来源于-->number-->num
datanumber1: this.number1,
datanumber2: this.number2
};
},
props: {
number1: {
type: Number
},
number2: {
type: Number
}
},
methods: {
inputnumber1(event) {
//v-model的模拟
this.datanumber1 = event.target.value
// input修改传个事件给父组件
this.$emit('datanumber1change', this.datanumber1);
// datanumber的修改会引起另一个datanumber的改变
this.datanumber2=this.datanumber1*10
},
inputnumber2(event) {
this.datanumber2 = event.target.value
this.$emit('datanumber2change', this.datanumber2);
this.datanumber1=this.datanumber2/10
},
}
}
}
})
</script>
</body>
</html>
上面这个案例有点像汇率转化:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<cpn1></cpn1>
</div>
<template id="cpn1">
<div>
汇率:<input type="number" :value="exchange" @input="inputexchange">
货币1:<input type="number" :value="money1" @input="inputmoney1">
货币2:<input type="number" :value="money2" @input="inputmoney2">
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const cpn1 = {
template: "#cpn1",
data() {
return {
exchange: 6.4,
money1: 0,
money2: 0
};
},
methods: {
inputmoney1(event) {
this.money1=event.target.value
this.money2=this.money1*this.exchange
},
inputmoney2(event) {
this.money2=event.target.value
this.money1=this.money2/this.exchange
},
inputexchange(event){
this.exchange=event.target.value
this.money2=this.money1*this.exchange
}
},
}
const app = new Vue({
el: "#app",
components: {
cpn1
}
})
</script>
</body>
</html>
父子组件的访问:
父访问子(父直接操作子,调用子的方法)$children,$refs.组件ref属性名:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<cpn1></cpn1>
<!-- 通过refs方式拿组件需要ref标识 -->
<cpn1 ref="aaa"></cpn1>
<button @click="btnClick1">this.$children</button>
<button @click="btnClick2">this.$refs</button>
</div>
<template id="cpn1">
<div>
<h4>cpn1</h4>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const cpn1 = {
template: "#cpn1",
data() {
return {
};
},
methods: {
showMessage() {
console.log('showMessage');
},
},
}
const app = new Vue({
el: "#app",
components: {
cpn1
},
methods:{
btnClick1(){
// 拿到的是个子组件数组
console.log(this.$children);
// 通过下标来拿子组件是个硬编码,如果父组件中的子组件进行了增删改,这个下标就要跟着变,所以一般不用下标操作
this.$children[0].showMessage();
},
btnClick2(){
// 拿到的是个空对象:{}
// 当有子组件使用ref标识后,拿到的就不是空对象了:{aaa: VueComponent}
console.log(this.$refs);
//因为refs是个对象,所以$refs.对象名就可以拿到相应组件
// 这种情况下使用唯一标识ref拿组件,所以不是硬编码,灵活性高
this.$refs.aaa.showMessage();
}
}
})
</script>
</body>
</html>
子访问父:$parent,$root
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<cpn1></cpn1>
</div>
<template id="cpn1">
<div>
<button @click="btnClick1">this.$parent</button>
<button @click="btnClick2">this.$root</button>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const cpn1 = {
template: "#cpn1",
data() {
return {
};
},
methods: {
btnClick1() {
// 发现是个Vue{}对象实例,这是因为父组件是个 new Vue()
// 如果父组件也是个组件的话那拿到的就是个VueComponent{}
console.log(this.$parent);
// 一般组件里不进行子访问父操作,因为不能保证这个组件在所有情况下父组件里都有访问的方法或数据,复用性不强,和特定父组件耦合度太高
this.$parent.parentMethod();
},
btnClick2() {
// 发现是个Vue{}对象实例,这是因为根组件是个 new Vue()
console.log(this.$root);
// 访问根组件用的比访问父组件还少,因为根组件(即Vue实例)一般不放东西,只放些路由之类的东西
this.$root.parentMethod();
}
},
}
const app = new Vue({
el: "#app",
components: {
cpn1
},
methods:{
parentMethod(){
console.log('parentMethod()');
}
}
})
</script>
</body>
</html>
slot插槽:
比如下图的例子,都有个导航栏,因此导航栏可以抽成个复用的组件,但这个导航栏在不同情境下略有区别,因此,可以开放成插槽,需要什么子组件再添加,提高了组件的扩展性。
slot的基本使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<!-- 通过在组件标签里套别的东西来放到插槽里 -->
<cpn1>
<!-- 放button -->
<button>button</button>
</cpn1>
<cpn1>
<!-- 放span -->
<span>span</span>
</cpn1>
<!-- 根据插槽中的默认东西显示 -->
<cpn1></cpn1>
</div>
<template id="cpn1">
<div>
<h4>cpn1</h4>
<!-- slot插槽 -->
<!-- <slot></slot> -->
<!-- 在插槽里提前放好东西表示插槽不放东西时的默认东西 -->
<slot><button>button</button></slot>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const cpn1 = {
template: "#cpn1",
data() {
return {
};
},
}
const app = new Vue({
el: "#app",
components: {
cpn1
}
})
</script>
</body>
</html>
具名插槽
多个插槽的话会造成混乱,所以使用具名插槽一一对应。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<!-- 在替换的东西上标注slot名即可精确替换插槽 -->
<cpn1><button slot="slot1">button</button></cpn1>
</div>
<template id="cpn1">
<div>
<h4>cpn1</h4>
<!-- 通过给插槽取名字达到精准使用的目的 -->
<slot name="slot1"><span>slot1</span></slot>
<slot name="slot2"><span>slot2</span></slot>
<slot name="slot3"><span>slot3</span></slot>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const cpn1 = {
template: "#cpn1",
data() {
return {
};
},
}
const app = new Vue({
el: "#app",
components: {
cpn1
}
})
</script>
</body>
</html>
编译作用域
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<!-- app中的isShow为true -->
<cpn1 v-show="isShow"></cpn1>
</div>
<template id="cpn1">
<div>
<h4>cpn1</h4>
<!-- cpn1中的isShow为false -->
<button v-show="isShow"></button>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const cpn1 = {
template: "#cpn1",
data() {
return {
isShow: flase
};
},
}
const app = new Vue({
el: "#app",
components: {
cpn1
},
data() {
return {
isShow: true
};
},
})
</script>
</body>
</html>
作用域插槽
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<cpn1></cpn1>
<!-- 目的是获取子组件中的pLanguages -->
<cpn1>
<template slot-scope="slot">
<span v-for="item in slot.data">{{item}}-</span>
</template>
</cpn1>
<cpn1>
<template slot-scope="slot">
<!-- 以*连接并且不会在后面多出个连接符 -->
<span>{{slot.data.join('*')}}</span>
</template>
</cpn1>
</div>
<template id="cpn1">
<div>
<!-- 通过slot的 :data 获取组件中的pLanguages -->
<!-- 这里命名的data,所以上面的cpn1的slot.name就是slot.data -->
<slot :data="this.pLanguages">
<ul>
<li v-for="item in pLanguages">{{item}}</li>
</ul>
</slot>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const cpn1 = {
template: "#cpn1",
data() {
return {
pLanguages: ['javaScript', 'Python', 'Swift', 'Go', "C++"]
};
},
}
const app = new Vue({
el: "#app",
components: {
cpn1
}
})
</script>
</body>
</html>