缘起
新的一天又开始啦,大家也应该看到我的标题了,是滴,Vue基础基本就到这里了,咱们回头看看这一路,如果你都看了,并且都会写了,那么现在你就可以自己写一个Demo了,如果再了解一点路由,ajax请求(这里是axios),那么你就可以准备面试前端了,哈哈,当然没有这么夸张,往后的路还很长,至少咱们基础都会了。
这里咱们再温习下之前讲了哪些基础知识:
《十五 ║Vue前篇:了解JS面向对象原理 & 学会嵌套字面量等4种函数定义 & this指向》
《十六 ║Vue前篇:ES6知识详细说明 & 如何在JS中进行模块化编程》
《十七 ║Vue基础:第一次页面引入Vue.js,了解 Vue实例是如何通过数据驱动来操作DOM的》
《十八 ║Vue基础: 学习了常用的十大指令并一一举例,然后了解到了Vue的计算属性,侦听器,当然还有过滤器》
《十九 ║Vue基础: 通过样式的动态绑定,进一步学习Vue是如何操作DOM的,然后深入了解生命周期的八个阶段,以及其中的职能》
一共是五篇,基本已经涵盖了Vue的基础知识,今天呢,再说完组件以后,明天就正式开始搭建本地脚手架,终于开始安装软件了[ 哭笑 ],我这几天考虑了一下,在之后的讲解中,可能需要两个小项目的讲解,第一个就是我现在自己用到的一个,大家其实也可以看看 http://vue.blog.azlinli.com(买的服务器不好,首次加载慢),也不是啥隐私,这个是我之前练习的时候自己瞎玩的,只有首页和详情页,用的数据就是咱们在之前系列里讲到的.net core api,这个可能在本周,或者本周末说到,主要的就是把之前的讲解给穿起来,然后再说下如何使用路由 v-router 和 ajax请求——axios,这样我这个项目就说到这里,然后第二个也是一个博客系统,用的是一套数据,只不过是用到了 Nuxt 框架了,基本结构又发生了变化,项目整体被封装了,更趋于工程化,至于为什么要用到这个,就是因为它可以解决 MVVM 前后端分离的 SEO 的老大难的问题,大家可以先问问度娘,到时候都会说到滴。好啦,开始今天的基础篇最后一章节 —— 深入了解下组件。
零、今天要完成天青色的部分
一、组件的基本使用
1、注册组件
上篇文件我们也说到了,注册组件的其中一个办法就是 Vue.component()
方法,先传入一个自定义组件的名字,然后传入这个组件的配置。
Vue.component('mycomponent',{
template: `<div>我的组件</div>`,
data () {
return {
message: '老张的哲学'
}
}
})
定义好后,我们就可以在Vue实例所定义的DOM元素内使用它(就是我们在new vue的时候的 el 节点),这里我们的页脚组件,全局组件,可以在其他地方使用
<div id="app">
<mycomponent></mycomponent>
<my-component></my-component>
</div>
<script> //注意要在vue实例之前去定义,不然渲染页面的时候,会报错
// 定义一个名为 footer-vue 的新组件
Vue.component('footer-vue', {
template: `
<div id="footer-vue">
<p> <a href="#">LZ's Blog</a> - Hosted by <a href="#" style="font-weight: bold">Coding Pages</a></p>
<p>
<a href="#">京ICP备00000000号</a>
</p>
</div>
`,
data () {
return {
message: 'hello world'
}
}
}) var app = new Vue({
el: '#app',//没错,vue实例所定义的DOM元素就是这个,超过了这个区域,定义的组件会无效
data: {
},
})
</script>
上边,我们定义的组件是一个全局的组件,也就是说如果我们定义了多个 vue实例,我们都可以使用这一个组件,这就是全局的,
当然,既然有全局的,我们也有局部的(我们对联系方式定义局部组件,表示只有在当前页面的app元素内使用):
注意:全局的组件是 component,而 局部的是 components
var app = new Vue({
el: '#app',
data: {
},
components: {
'my-component': {//这个就是我们局部组件的名字 在页面内使用 <my-component></my-component>
template: `
<ul class ="contact-list non-style-list">
<li>
<b class ="twitter">TWITTER</b>: <a href="#">@laozhang</a>
</li>
<li>
<b class ="weibo">微博</b>: <a href="#">@laozhang</a>
</li>
<li>
<b class ="zhihu">知乎</b>: <a href="#" ></a>
</li>
<li>
<b class ="github">GITHUB</b>: <a href="https://github.com/anjoy8">anjoy8</a>
</li>
<li>
<b class ="email">EMAIL</b>: <a href="mailto:laozhang@azlinli.com">randypriv at azlinli</a>
</li>
</ul>
`,
data () {
return {
message: 'hello world two'
}
},
directives:{//自定义局部指令,使用的时候,直接可以 <my-component v-focus><my-component>
focus;{
inserted(el){
el.focus();
}
}
}
}
}
})
2、组件的规范定义——单个根元素 + Data函数化
观察一下上边两种写法与的特点,大家应该也能说出来:
相同点:组件的模板只能有一个根节点,或者说根标签只能有一个(第一个的根元素是 <div>,第二个根元素是 <ul>),如果定义成这样就是不允许的,这里是两个根元素 <div> 和 <a>:
template: `<div>我的地盘听我的,哈哈,只能在当前我的Vue实例中使用</div>
<a>我的一个标签</a>
`,
我们看到在定义组件的时候和平时定义的 data 不一样,这里的定义一定要是一个函数,因为如果像Vue实例那样,传入一个对象,由于JS中对象类型的变量实际上保存的是对象的引用
,所以当存在多个这样的组件时,会共享数据,导致一个组件中数据的改变会引起其他组件数据的改变。而使用一个返回对象的函数,每次使用组件都会创建一个新的对象,这样就不会出现共享数据的问题来了。
Vue.component('footer-vue', {
template: `
<div id="footer-vue">
<p>
<a href="#">京ICP备00000000号</a>
</p>
</div>
`,
data :{//这是错误栗子
message: 'hello world'//我们用普通属性的方法
}
})
如果我们按照一个属性的写法的话,页面会成功的报了一个这样的错误,而且大家注意,这个错误是出现在哪里的,没错就是挂载结束前,也就是说,和实例化数据没影响,但是在挂载到页面,页面渲染的时候,出现了这个错误,所以大家在初学的时候,还是要多了解下生命周期的概念。
注意:因为组件是可复用的 Vue 实例,所以它们与
new Vue
接收相同的选项,例如data
、computed
、watch
、methods
以及生命周期钩子等。仅有的例外是像el
这样根实例特有的选项。
3、另外一种注册方式,通过 全局API:Vue.extend()创建,然后由component来注册,两步
// extend 创建组件
var MyComponent = Vue.extend({
template: '<div>A custom component!</div>'
}); // component注册 组件
Vue.component('my-component', MyComponent);//使用到了 extend 创建的组件
var vm = new Vue({
el: '#example',
data: { }
})
两种写法没有什么太多的区别,基本来说
extend 是构造创建一个组件的语法器,你给它参数 他给你创建一个组件, 然后这个组件,你可以作用到Vue.component 这个全局注册方法里, 也可以在任意vue模板里使用apple组件
var apple = Vue.extend({
….
})
Vue.component(‘apple’,apple)你可以作用到vue实例或者某个组件中的components属性中并在内部使用apple组件
new Vue({
components:{
apple:apple
}
})
可见上边的定义过程比较繁琐,也可以不用每次都调用两个,可以直接用 Vue.component 创建 ,也可以取组件 例如下
var apple = Vue.component(‘apple’)
4、模板html 可以单写出来
<template id="temApp">//这里是定义一个id,
<div>
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">Form</router-link> |
<router-link to="/Vuex">Vuex</router-link>
</div>
<router-view/>
</div>
</template> //在使用的时候,直接引用 #temApp
Vue.component('footer-vue', {
template:'#temApp',
data () {
return {
message: 'hello world'
}
}
})
5、动态组件
你一定在开发中会遇到这样的需求,就是一个banner的切换:
我们这时候可以使用动态组件,很容易实现这个需求,通过 Vue 的 <component>
元素加一个特殊的 is
特性来实现:
<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component v-bind:is="currentTabComponent"></component>
在上述示例中,currentTabComponent
可以包括
- 已注册组件的名字,或
- 一个组件的选项对象
<div id="dynamic-component-demo" class="demo">
<button
v-for="tab in tabs"//for 循环展示 banner
v-bind:key="tab"
v-bind:class="['tab-button', { active: currentTab === tab }]" //这里语法错误,删除本行注释!绑定样式,当前组件增加 active 样式
v-on:click="currentTab = tab"
>{{ tab }}</button>
<!-- 组件的使用 通过currentTabComponent 来动态展示是哪一个组件 -->
<component
v-bind:is="currentTabComponent" //这里语法错误,删除本行注释!通过 is 特性,来动态实现组件,核心
class="tab"
></component>
</div>
//定义三个组件,可以比如是我们的三个页面,
Vue.component('tab-home', {
template: '<div>Home component</div>' //组件1,也就是页面1
})
Vue.component('tab-posts', {
template: '<div>Posts component</div>' //组件2,页面2
})
Vue.component('tab-archive', {
template: '<div>Archive component</div>' //组件3,页面3
}) new Vue({
el: '#dynamic-component-demo',
data: {
currentTab: 'Home',//当前banner名称
tabs: ['Home', 'Posts', 'Archive']//定义三个banner
},
computed: {//计算属性,实时监控获取当然banner的值,并返回到页面
currentTabComponent: function () {
return 'tab-' + this.currentTab.toLowerCase()//组件名称拼串,一定要和上边的三个组件名对应
}
}
})
注意:这里可以使用 <keep-alive> 来缓存当然组件的内容
<keep-alive>
<component :is="currentTabComponent"></component>
<keep-alive>
二、属性Props —— 父子通讯(父传子)
在 Vue 中,父子组件的关系可以总结为 prop
向下传递,事件
向上传递。父组件通过 prop
给子组件下发数据,子组件通过事件
给父组件发送消息,这里咱们先说下向下传递,通过属性Props属性来完成。
1、使用动态属性Props,可以将父组件的数据传递给子组件,从而达到父子通讯的第一步,举个栗子
还记得之前咱们说的,Vue 其实就是由许许多多个组件拼接而成,高效复用,相互之间可以传值,但是又不受影响,最常见的应用就是:组件 A 在它的模板中使用了组件 B。它们之间必然需要相互通信:父组件可能要给子组件下发数据,子组件则可能要将它内部发生的事情告知父组件。大家第一次使用的时候可能会有点儿不舒服,但是使用熟练以后,就会发现真的得心应手,所以咱们就先看看组件是如何通讯的。
首先大家还记得咱们定义的页脚组件么,就是刚刚说到的。咱们看到最下边是备案号,现在想在备案号旁边加上咱的昵称”老张的哲学“,想想很简单嘛,想想肯定不能直接写死数据吧,正好看到页面内定义vue实例的时候,有这个属性嘛,直接放到咱们的页脚组件里,嗯就是这样:
// 定义一个名为 footer-vue 的新组件
Vue.component('footer-vue', {
template: `
<div id="footer-vue">
<p> <a href="#">LZ's Blog</a> - Hosted by <a href="#" style="font-weight: bold">Coding Pages</a></p>
<p>
<a href="#">京ICP备00000000号{{authorHtml}}</a>
</p>
</div>
`,
data () {
return {
message: 'hello world'
}
}
})
然后满怀开心的刷新页面一看,额报错了:
然后很熟练的必应翻译了一下(这是一个反面教材,大家要好好学习英文,多看国外教程 [苦笑] ),得到这样的:属性或方法 "authorHtml " 不应该是在实例上定义的, 而是在呈现过程中引用的。通过初始化属性, 确保此属性在数据选项或基于类的组件中是被动的。说人话就是,这里不能使用实例上定义的值,好吧,查阅资料发现,组件只能被动的接受父组件的传参数,嗯于是乎我们这样写:
<footer-vue :foo="author"></footer-vue>//在自定义的组件上,新增一个动态属性,然后属性的值 author 是父组件的,这个时候父组件就是已经把值给发过去了
这个时候,我们就需要在子组件里接收一下
// 定义一个名为 footer-vue 的新组件
Vue.component('footer-vue', {
template: `
<div id="footer-vue">
<p> <a href="#">LZ's Blog</a> - Hosted by <a href="#" style="font-weight: bold">Coding Pages</a></p>
<p>
<a href="#">京ICP备00000000号{{foo}}</a>//这里根据自身的props的参数来赋值
</p>
</div>
`,
props: ['foo'],//这里根据组件的props属性,来被动接受组件传递来的参数
data () {
return {
message: 'hello world'
}
}
})
刷新页面,这时候就真正的成功了。
2、使用静态Props传值
这里我们得到的结果和上边的是一样的,直接是一个字符串结果,而不是一个变量属性。
Vue.component('child', {
// 声明 props
props: ['message'],
// 就像 data 一样,prop 也可以在模板中使用
// 同样也可以在 vm 实例中通过 this.message 来使用
template: '<span>{{ message }}</span>'
}) <child message="老张的哲学"></child>
上边咱们可以看到,通过父子传值,我们把数据传递了过去,但是这个时候你需要思考,我们只能传递数据 data 么,答案是否定的,我们还可以传递方法事件 Function !很简单,直接看代码:
// 实例化 Vue
var V = new Vue({
el: '#app',
data: {
now:'',
lists:[
{id:,name:'孙悟空'},
{id:,name:'猪八戒'},
{id:,name:'沙和尚'},
{id:,name:'唐僧'},
{id:,name:'小白龙'},
], answer: 'Time is:'
},
watch: {
},
methods: {
run(){
console.log()
} },
mounted() {
//this.now=this.dateFtt("yyyy-MM-dd hh:mm:ss",new Date());; }
}) // 调用子组件,传递数据 lists 和事件 run
<child :lists="lists" :run="run">
<template slot-scope="a">
{{a}}
</template>
</child> // 定义子组件
Vue.component('child',{
props:['lists','run'],
methods:{
testRun(){
this.run()
}
},
template:`
<div @click='testRun'>
<ul> <li v-for="list in lists">
<slot :bbbbb="list"></slot>
</li> </ul>
</div>
`
});
然后我们点击 div,就会触发父组件事件:
是不是很简单,父子传值,方法事件也是特别常用的,大家要多学习学习。
3、注意大小写的命名的写法
注意:HTML 特性是不区分大小写的。所以,当使用的不是字符串模板时,camelCase (驼峰式命名) 的 prop 需要转换为相对应的 kebab-case (短横线分隔式命名),比如 如何上边的foo 写成了 fooPro ,那我们定义属性的时候,就应该写 foo-pro
<footer-vue :foo-pro="author"></footer-vue>
如果我们写了 fooPro 这样的写法,挂载页面的时候,就会警告,并且不会成功渲染
如果你使用字符串模板,则没有这些限制。(字符串模板:指的是在组件选项里用 template:"" 指定的模板,换句话说,写在 js 中的 template:"" 中的就是字符串模板。)
4、通过订阅发布来实现通讯
这个其实很简单,使用的也一般,不是很多,需要安装一个库 pubsub-js ,然后我们就可以在任何组件内来实现订阅发布,从而来实现通讯了:
父组件 —— 订阅消息(绑定事件监听)
子/孙组件 —— 发布消息(触发事件)
三、自定义事件 —— 子传父
我们知道,父组件使用 prop 传递数据给子组件。但子组件怎么跟父组件通信呢?这个时候 Vue 的自定义事件系统就派得上用场了。
1、使用 v-on 绑定自定义事件
每个 Vue 实例都实现了事件接口,即:
- 使用 $on(eventName) 监听事件
- 使用 $emit(eventName) 触发事件
Vue 的事件系统与浏览器的 EventTarget API
有所不同。尽管它们的运行起来类似,但是 $on
和 $emit
并不是addEventListener
和 dispatchEvent
的别名。
另外,父组件可以在使用子组件的地方直接用 v-on
来监听子组件触发的事件。
//1、子组件内,有一个click,当点击的时候 触发 incrementCounter 方法
//2、方法被触发以后,向父组件 发送一个信号广播,并传递参数 counter,名字就是 increment。
//3、父组件通过监听,来获取到这个广播信号 increment ,然后触发 incrementTotal 方法
//4、incrementTotal 被触发,获取到参数 counter 值,执行相应的操作
<div id="counter-event-example">
<p>{{ total }}</p>
<button-counter v-on:increment="incrementTotal"></button-counter>//3、父组件通过监听,来获取到这个广播信号 increment ,然后触发 incrementTotal 方法
</div> Vue.component('button-counter', {
template: '<button v-on:click="incrementCounter">{{ counter }}</button>',//1、子组件内,有一个click,当点击的时候 触发 incrementCounter 方法
data: function () {
return {
counter:
}
},
methods: {
incrementCounter: function () {
this.counter +=
this.$emit('increment',this.counter)//2、方法被触发以后,向父组件 发送一个信号广播,并传递参数 counter,名字就是 increment。
}
},
mounted(){//常用,挂载完成后执行
this.incrementCounter();
}
}) new Vue({
el: '#counter-event-example',
data: {
total:
},
methods: {
incrementTotal(counter) {//4、incrementTotal 被触发,获取到参数 counter 值,执行相应的操作
this.total = counter
}
}
})
上边注释的已经很清楚,就好像冒泡一样的,往上走,和 父传子,正好相反。
如果你这个看懂了,可以把上一节中的,通过属性传递方法的那种操作给改一下,改成我们的自定义事件的方式:
<custom-input @run="run"></custom-input>
<br/>
{{something}} Vue.component('custom-input', {
template: '<input type="text" @input="updateValue($event.target.value)"/>',
methods:{
updateValue:function(value){
this.$emit('run', value)
}
}
}) var vm = new Vue({
el: '#app',
data: {
something:''
},
methods:{
run(value){
this.something=value
}
} })
2、使用自定义事件的表单输入组件
自定义事件可以用来创建自定义的表单输入组件,使用 v-model 来进行数据双向绑定。要牢记:
<input v-model="something">
这不过是以下示例的语法糖:
<input
v-bind:value="something"
v-on:input="something = $event.target.value">
所以在组件中使用时,它相当于下面的简写:
<custom-input
v-bind:value="something"
v-on:input="something = arguments[0]">
</custom-input>
<div id="app">
<custom-input v-model="something"></custom-input>
<br/>
{{something}}
</div> // 注册
Vue.component('custom-input', {
props:['something'],
template: '<input type="text" v-bind:value="something" v-on:input="updateValue($event.target.value)"/>',
methods:{
updateValue:function(value){
this.$emit('input', value)
}
}
})
var vm = new Vue({
el: '#app',
data: {
something:''
} })
四、使用插槽slot分发内容(完善中)
2019-06-27 更新:
普通子组件
<div id="app">
<child-component></child-component>
</div>
<script>
Vue.component('child-component',{
template:`
<div>Hello,World!</div> `
})
let vm = new Vue({
el:'#app',
data:{ }
})
</script>
Vue.component('child-component',{
template:`
<div>
Hello,World!
<slot></slot>
</div> `
})
Vue.component('child-component',{
template:`
<div>
<h4>Header.vue</h4>
<slot name="girl"></slot>
<div style="height:1px;background-color:red;"></div>
<slot name="boy"></slot>
<div style="height:1px;background-color:red;"></div>
<slot></slot>
</div>
`
})
<child-component>
<template slot="girl">
<p style="color:red;">漂亮、美丽、购物、逛街</p>
</template>
<template slot="boy">
<strong>帅气、才实</strong>
</template>
<div>
<a href='#' >
我是一个没有表情的 a 标签
</a>
</div>
</child-component>
<child-component>
<template slot="boy">
<strong>帅气、才实</strong>
</template>
<div>
<div >
我这里需要一个div就行
</div>
</div>
</child-component>
Vue.component('child',{
props:['lists'],
template:`
<div>
<ul>
<li v-for="list in lists">
<slot :scope="list"></slot>
</li>
</ul>
</div>
`
});
var V = new Vue({
el: '#app',
data: {
lists:[
{id:1,name:'孙悟空'},
{id:2,name:'猪八戒'},
{id:3,name:'沙和尚'},
{id:4,name:'唐僧'},
{id:5,name:'小白龙'},
]
},
})
<div id="app">
<child :lists="lists">
<template slot-scope="a">
<div v-if='a.scope.id==1'>你好:<span>{{a.scope.name}}</span></div>
<div v-else>{{a.scope.name}}</div>
</template>
</child>
</div>
2018-09-14 更新:
如果我们定义了一个组件 O,需要在 A、B、C三个页面使用,在O中有一部分是三个子组件相同的,有一部分是各自的不同的,这个时候我们就可以使用 slot 分发;
内容分发的作用,就是为了组件的灵活性,在一个模板中,可以供调用者自定义部分
//声明模板
<template id="mysolt">
<div>
<h3>我的公共的相同的内容</h3>
<slot name="s1"></slot>
</div>
</template> //定义组件
Vue.component('my-solt', {
template:'#mysolt',
data () {
return {
message: 'hello world'
}
}
}) //在a页面
<body>
<div id="app">
<my-solt>
<ul slot="s1">
<li>a</li>
<li>a</li>
<li>a</li>
</ul>
</my-solt> </div> </body> //在b页面
<body>
<div id="app">
<my-solt>
<ul slot="s1">
<p>b</p>
<p>b</p>
<p>b</p>
</ul>
</my-solt> </div> </body> //在c页面
<body>
<div id="app">
<my-solt>
<ul slot="s1">
<div>ccc</div>
</ul>
</my-solt> </div> </body>
在使用组件时,我们常常要像这样组合它们:
<app>
<app-header></app-header>
<app-footer></app-footer>
</app>
注意两点:
-
<app>
组件不知道它会收到什么内容。这是由使用<app>
的父组件决定的。 -
<app>
组件很可能有它自己的模板。
为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。使用特殊的 <slot>
元素作为原始内容的插槽。
一个常见错误是试图在父组件模板内将一个指令绑定到子组件的属性/方法:
<!-- 无效 -->
<child-component v-show="someChildProperty"></child-component>
正确做法:
Vue.component('child-component', {
// 有效,因为是在正确的作用域内
template: '<div v-show="someChildProperty">Child</div>',
data: function () {
return {
someChildProperty: true
}
}
})
1、单个插槽
假定 my-component 组件有如下模板:
<div>
<h2>我是子组件的标题</h2>
<slot>
只有在没有要分发的内容时才会显示。
</slot>
</div>
父组件模板:
<div>
<h1>我是父组件的标题</h1>
<my-component>
<p>这是一些初始内容</p>
<p>这是更多的初始内容</p>
</my-component>
</div>
渲染结果:
<div>
<h1>我是父组件的标题</h1>
<div>
<h2>我是子组件的标题</h2>
<p>这是一些初始内容</p>
<p>这是更多的初始内容</p>
</div>
</div>
五、结语
今天简单的说了下关于组件的一些问题,因为事件的问题,还没有说完,还在进一步整理当中,大家可以以后进一步的浏览本博文,通过组件的学习,大家在Vue开发的道路上又进了一步,好啦,关于Vue的基础知识就是这么多了,明天开始进入代码练习啦~~