欢迎大家前往腾讯云 + 社区,获取更多腾讯海量技术实践干货哦~
本文由蔡述雄发表于云 + 社区专栏
需求背景
组件库是做 UI 和前端日常需求中经常用到的,把一个按钮,导航,列表之类的元素封装起来,方便日常使用,调用方法只需直接写上 <qui-button></qui-button> 或者 <qui-nav></qui-nav> 这样的代码就可以,是不是很方便呢,接下来我们将要完成以下页面:
这是我们组件库的首页,包含三个子页面,按钮页面、列表页面、导航页面;点击进去子页面会由路由来配置。先看我们的目录结构:
pages 目录存放我们的页面,包括首页和三个子页面;components 目录存放我们的具体组件,包括按钮组件,箭头组件,列表组件和导航组件(组件和页面其实是一样的文件类型,只是由于功能不一样,我们就叫不同的称呼)
先看路由配置的代码吧!
路由配置
import Vue from ‘vue’
import Router from ‘vue-router’
// 引用页面模板 -> 供路由使用
import index from ‘../pages/index.vue’
import pageQuiButton from ‘../pages/pageQuiButton.vue’
import pageQuiList from ‘../pages/pageQuiList.vue’
import pageQuiNav from ‘../pages/pageQuiNav.vue’
Vue.use(Router)
export default new Router({
routes: [
{
path: ‘/’,
name: ‘index’,
component: index
},
{
path: ‘/btn’,
name: ‘btn’,
component: pageQuiButton
},
{
path: ‘/list’,
name: ‘list’,
component: pageQuiList
},
{
path: ‘/nav’,
name: ‘nav’,
component: pageQuiNav
}
]
})
有了上一篇的分析之后,这里应该很容易看出来几个路由地址
首页:http://localhost:8080/#/
按钮子页:http://localhost:8080/#/btn
列表子页:http://localhost:8080/#/list
导航子页:http://localhost:8080/#/nav
具体每一页的内容分别对应每一页的.vue 文件,不知大家是否还记得入口页 App.vue,这个文件承载着一些公用的元素,还有就是一个路由容器,我们的首页 index.vue 到时候也是挂载在路由容器中的,看看 App.vue 的代码
入口页 App.vue
<template>
<div id=”app”>
<h1 class=”page-title”><a href=”#/”> 开发组件库 </a></h1>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: ‘app’
}
</script>
<style scoped>
@import ‘./assets/css/App.css’;
</style>
简单分析一下入口页的代码,h1 标签是一个公用元素,也就是说到时候每个子页面都会带着这个 h1,他的作用就是方便我们快速回到首页,子页面的内容会注入到 router-view 中。这里值得关注的地方是 style 标签,我们可以在 style 标签里面直接写样式,也可以直接引入一个样式文件,scoped 关键字表示这个样式是私有的,也就是说,即使两个组件写着一样的 #app{}样式也不会冲突,程序会加上命名空间,这也就是为什么在 script 标签中有个 name 参数。
首页 index.vue
<template>
<div class=”mod-module mod-parallel”>
<div class=”img-list type-full”>
<div class=”img-box”>
<p class=”img-item”>
<a class=”page-link” href=”#/btn”> 按钮 </a>
</p>
</div>
<div class=”img-box”>
<p class=”img-item”>
<a class=”page-link” href=”#/list”> 列表 </a>
</p>
</div>
<div class=”img-box”>
<p class=”img-item”>
<a class=”page-link” href=”#/nav”> 导航 </a>
</p>
</div>
</div>
</div>
</template>
<style scoped>
@import ‘./css/index.css’;
</style>
首页的代码也是非常简单,和我们平时写 html 差不多,就是几个跳转链接跳到对应的子页面,程序运行的时候,会将 <template> 标签里面的内容都注入到 App.vue 页面中的 router-view 标签中,从而实现无刷新的路由跳转。
从下面的内容开始,我们的知识将会深入一些。我们先不急着看其他几个子页面,因为子页面里面只是引用对应的组件,所以我们先从组件开始入手。
按钮组件 quiButton.vue
<template>
<button class=”qui-btn”>
<span>{{msg}}</span>
</button>
</template>
<script>
export default {
data:function(){
return {
msg:’ 下载 ’
}
}
}
</script>
<style scoped>
@import ‘./css/reset.import.css’;
@import ‘./css/qui-btn.import.css’;
</style>
按钮组件很简单,就是一个正常的 button 标签,script 标签中暴露这个组件的 data 属性(data 是 Vue 的属性值,不是乱写的~~)。当按钮组件被初始化的时候,msg 自定义属性会被绑定到 <span> 标签中的 {{msg}} 中,两个花括号用来绑定属性,这种写法学过模版化前端代码的人应该都比较熟悉。这里需要注意一个地方,如果不是组件的话,正常 data 的写法可以直接写一个对象,比如 data:{msg:‘ 下载 ‘},但由于组件是会在多个地方引用的,JS 中直接共享对象会造成引用传递,也就是说修改了 msg 后所有按钮的 msg 都会跟着修改,所以这里用 function 来每次返回一个对象实例。
这就是一个非常简单的按钮组件,结构、样式 + 文案。
这时候问题来了,按钮中的文案我希望可以异化,不能每次都初始化一个叫做“下载”文案的按钮吧,希望可以以属性的方式来使用,比如这样子写就可以改变我们的按钮文案:
<qui-btn msg=” 确定 ” class=”small”></qui-btn>
没问题,属性的接口暴露只需要写在 prosp 里面就可以了,如下所示修改下 script 标签的内容:
<script>
export default {
props: {
msg: {
default: ‘ 下载 ’
}
}
}
</script>
把属性写在 props 里面,就可以暴露给其他页面调用了,在组件中,props 是专门用来暴露组件的属性接口的,这里给了一个默认值‘下载’,后面我们要使用的话,只需要 <btn msg=” 确认 ”></btn> 就可以修改按钮的默认文案了。
我们在上一篇文章的开头就讲了 Vue 是数据驱动模式的,当我在 btn 结构写上 msg=” 确认 ” 的时候,对应 script 里面的 msg 属性就会自动修改了。
按钮事件
按钮总少不了点击事件吧,那在 Vue 中怎么绑定事件呢,用 methods 属性,看下代码:
<template>
<button class=”qui-btn” v-on:click=”btnClickEvent”>
<span>{{msg}}</span>
</button>
</template>
<script>
export default {
props: {
msg: {
default: ‘ 下载 ’
}
},
methods: {// 绑定事件的关键代码
btnClickEvent: function(){
alert(this.msg);
}
}
}
</script>
methods 属性中可以写任何的自定义函数,写完之后绑定的方式也很简单,在 button 上写关键字 v -on:click,把对应的事件写上就可以了,以上代码实现的就是点击按钮弹出按钮中的文案,v-XXX 是 Vue 里的一些关键字,叫做指令,我们后面会慢慢学到更多的指令;v-on:click 可以缩写为 @click,当然还有其他的事件比如 v -on:tab 等等;
使用按钮组件 pageQuiButton.vue
现在我们大致做了一个按钮组件了,那么怎么调用它呢,去到我们的 pageQuiButton 子页面。
//pageQuiButton.vue
<template>
<div id=”pageQuiButton”>
<!– 使用 –>
<qui-btn msg=” 确定 ” class=”small”></qui-btn>
</div>
</template>
<script>
import quiBtn from ‘../components/quiButton.vue’ /* 引用 */
export default {
name: ‘pageQuiButton’,
components: {
‘qui-btn’: quiBtn /* 注册自定义标签 */
}
}
</script>
从 script 开始解析,首先引入我们的组件赋值给变量 quiBtn,使用时候直接将 quiBtn 作为对象的一部分写进 components 属性,这是 Vue 用来存储引用组件的关键字,同时对应我们自定义的标签 “qui-btn”,完成这些操作之后,我们就可以在 template 中使用自定义的按钮组件 <qui-btn> 上面也说了用 msg 属性来自定义按钮的文案。完成之后,我们就可以在页面中看到具体效果,点击按钮弹出对应的文案。
上述我们将按钮事件写成默认的 alert(this.msg),如果有些按钮想要异化怎么办。之前说了 msg 属性可以支持自定义,那么按钮的点击事件如何支持自定义呢?
//pageQuiButton.vue
// 监听子组件的事件
<qui-btn v-on:btnClickEvent=”doSth” msg=” 我可以点击 ” ></qui-btn>
上面的代码在引用组件的时候,注册了一个事件,这个 btnClickEvent 事件是之前我们在按钮组件中绑定到按钮的 click 事件中的,然后我们给这个事件一个自定义的方法 doSth,同时,在 script 中声明这个自定义的方法如下:
//pageQuiButton.vue
// 页面中引用子组件并监听子组件的事件
<script>
import quiBtn from ‘../components/quiButton.vue’
export default {
name: ‘pageQuiButton’,
components: {
‘qui-btn’: quiBtn
},
methods: {
doSth: function(){
alert(‘ 你点击了组件的 click:btnClickEvent’);
}
}
}
</script>
专业一点的说,这种做法叫做监听,由引用方(暂且叫做父组件)监听子组件的内置方法;同时在子组件中,需要触发这个事件,以下是在子组件中的关键代码:
//quiButton.vue
// 子组件中的代码
<script>
export default {
props: {
msg: {
default: ‘ 下载 ’
}
},
methods: {
btnClickEvent: function(){
alert(“ 先弹出默认的文案 ”);
this.$emit(‘btnClickEvent’);// 关键代码父组件触发自定义事件
}
}
}
</script>
这里的关键代码就是 $emit,也叫触发机制,父组件监听,子组件触发。如果觉得绕,以下描述可能会比较好理解:小 B(子组件)有一个电话号码(子组件注册的事件),有一天小 B 把电话号码告诉了小 A(父组件),让小 A 打电话给他,于是小 A 就拨打了小 B 的电话号码(监听),这时候整个沟通流程没有结束,必须要小 B 接听了电话(触发),两人之间才算完成了打电话这件事情。
完成这步之后,引用方(父组件)就可以给不同子组件调用不同的事件处理了:
<qui-btn v-on:btnClickEvent=”doSth1″ msg=” 确定 ” ></qui-btn>
<qui-btn v-on:btnClickEvent=”doSth2″ msg=” 取消 ” ></qui-btn>
<script>
/* 这里只是简单展示 */
methods: {
doSth1: function(){
alert(‘111’);
},
doSth2: function(){
alert(‘222’);
}
}
</script>
给按钮加图标
有时候单纯的文案异化还不够,比如一些按钮是图标 + 文字类型的,而且图标还可能不一样,那应该怎么办呢?
如果按钮组件的结构除了开发时候预设的那些 dom 结构之外,允许我们在调用的时候添加一些自己想要的结构,那是不是解决了呢,是的,Vue 早就为我们考虑了这一点,他就是 slot 标签。
下面给我们的按钮组件加上一段结构
//quiButton.vue
<template>
<button class=”qui-btn” v-on:click=”btnClickEvent”>
<slot name=”icon”></slot><!– 重点在这里 –>
<span>{{msg}}</span>
</button>
</template>
加入了关键字 slot 并赋予一个 name 值之后,我们再看看引用如何使用
//pageQuiButton.vue
<qui-btn msg=” 下载 ” class=”with-icon”>
<img slot=”icon” class=”ico” src=”xxx.png” />
</qui-btn>
img 上有个关键字 slot=”icon” 对应组件中的 name=”icon”,渲染的时候,会将 img 整个替换掉组件中的对应 name 的 <slot> 标签,其实很好理解,slot 的翻译是插槽的意思,相当于把 img 这块内容插到一个名叫 icon 的插槽里面去。
中场休息一下
学到这里,我们已经学会了用 props 给按钮自定义文案,用 on 和 emit 给按钮自定义点击触发,用 slot 给按钮添加一些自定义结构。当你回头去翻文档的时候,你会发现 props,事件,slot 这三样刚好就是学习组件通讯中最最最关键的三个环节。将这三个环节以实际案例解析出来后,好像也没有那么难了吧~!
上述我们已经讨论了如何制作一个按钮组件,以及如何使用我们的按钮组件。
接下来我们通过制作一个导航组件,来了解 Vue 中对于 for 循环的巧妙使用。
导航组件 quiNav.vue
我们将完成这样一个导航组件,点击导航中的 tab,可以给当前 tab 加上一个 active 类,同时切换底部的黄色滑条,并且输出当前 tab 的文案,同时支持自定义事件。
由于在现实项目中,我们导航的 tab 个数是不定的,所以制作组件的时候,我们希望可以暴露一个属性来支持导航的 tab 个数,而 tab 的长相和应用其实是一样的,那么这时候我们可以用一个 for 循环来输出每一个 tab。先看看关键代码:
//quiNav.vue
<template>
<div class=”qui-nav nav-type-1″>
<a v-for=”(item, index) in items” ><!– 关键代码 v -for–>
<span class=”nav-txt”>{{item.text}}</span>
</a>
</div>
</template>
<script>
export default {
data:function(){
return {
items:[
{
text: ‘ 首页 ’,
active : true
},
{
text: ‘ 列表 ’,
active : false
},
{
text: ‘ 关于 ’,
active : false
},
{
text: ‘ 招聘 ’,
active : false
}
]
}
}
}
}
</script>
该段代码的关键地方在于 a 标签上 v -for 关键字(还记得我们在前面说过的 v -on 绑定事件吗,v-XXX 关键字是 Vue 预留的)可以把它理解为 js 中的 for in 循环,items 是我们在 data 里面定义的对象(还记得为什么 data 要写在 function 中返回吗?)。v-for=”(item,index) in items” 暴露了 item 和 index 两个接口,这是 Vue 提供的,代表 items 中的每一项以及该项对应的下标,接着我们就可以在标签中使用绑定{{item.text}}了。
这段代码理解了之后,我们再延伸一个动态添加 class 的概念。我们希望每个 tab 都有默认的 class 类名(比如 nav-item 类),在点击每个 tab 的时候,当前 tab 添加 active 类,其他的 tab 删除这个 active 类。在 Vue 怎么实现呢?
动态类名
//quiNav.vue
<template>
<div class=”qui-nav nav-type-1″>
<a v-for=”(item, index) in items” :class=”[commonClass,item.active ? activeClass : ”]” >
<span class=”nav-txt”>{{item.text}}</span>
</a>
</div>
</template>
<script>
export default {
data:function(){
return {
commonClass:’nav-item’,
activeClass:’active’,
items:[
…// 数据
]
}
}
}
</script>
在 template 中添加了一句关键代码
:class=”[commonClass,item.active ? activeClass : ”]”
:class 给组件绑定一个 class 属性(类似 jQuery 中的 attr 方法),这里的写法是缩写,他的全拼应该是 v -bind:(又一个 v -XXX 写法)。注意到最前面有个冒号,:class=XXX 和 class=XXX 的区别在于不带冒号的是静态的字符串绑定,带冒号的是动态的变量绑定。我们给 class 绑定了一个数组,这个数组带有变量,先看 commonClass,这个变量在 data 中定义了,然后数组的第二个元素是一个 JS 的三元运算符:item.active?activeClass:”,当每个 item 中的 active 值为 true 时,绑定 activeClass 变量对应的类,如果为 false,则为空。最后的结果是当 item.active 为 true 时候,tab 的 class 值为 ’nav-item active’,当为 false,就只有 ’nav-item’。
上面的代码可以理解的话,那么我们切换 tab 的 active 类,就转换为修改每个 item 里面的 active 的值(再次体现数据驱动)。
那么问题来了,怎么去修改每个 item 里面的 active 值呢?没错,给每个 tab 绑定一个点击事件,当点击事件触发的时候,修改当前 tab 对应 item 的 active 值。于是代码变成了如下:
<template>
<div class=”qui-nav nav-type-1″>
<a v-for=”(item, index) in items” :class=”[commonClass,item.active ? activeClass : ”]” v-on:click=”navClickEvent(items,index)” >
<span class=”nav-txt”>{{item.text}}</span>
</a>
</div>
</template>
<script>
export default {
data:function(){
return {
commonClass:’nav-item’,
activeClass:’active’,
items:[
{
text: ‘ 首页 ’,
active : true
},
……
]
}
},
methods:{
navClickEvent:function(items,index){
/* 默认切换类的动作 */
items.forEach(function(el){
el.active = false;
});
items[index].active = true;
/* 开放用户自定义的接口 */
this.$emit(‘navClickEvent’,items,index);
}
}
}
</script>
我们利用 for 循环给每个 a 标签绑定了一个 click 事件,对应 methods 中定义的 navClickEvent,接收两个参数 items 和 index(你也可以传人 item 和 index,看个人代码喜好),然后当点击的时候,把 items 中的每个 item.active 置为 false,把当前的 tab 的 active 值置为 true,这样就可以动态切换 active 类了。最后再触发一次自定义事件(参考按钮制作自定义事件)。
以上就是我们导航组件的内容了,回想下我们做了啥?for 循环输出每个 tab,为每个 tab 绑定动态的 class 类名,同时在点击事件中动态切换类(底部的小黄条其实是利用 active 类做的 CSS)
小结
回顾下我们这一篇章都学了什么内容。
页面路由的配置
按钮组件自定义属性 props
按钮组件自定义事件 $on $emit
按钮组件自定义子块 slot
for 循环实现导航组件
动态类名
上述内容已经基本上涵盖了组件的重要知识点,主要是父组件(页面)和子组件之间的调用和通讯(数据交互绑定),好好消耗一下我们会发现,其实 Vue 的总体逻辑思想和 jQuery 是一样的,毕竟最后都回归到 javascript,只是由于代码设计角度的不同,我们可能看到和以前调用 jQuery 时候的写法不一致,但其实都有对方的影子在里面,相信理解了 Vue 的代码思想之后,以后我们学习 React 等其他类似的框架的时候,也会比较得心应手了。
下一篇文章《包学会之浅入浅出 Vue.js:结业篇》中,我们将会学习如何用多个组件来组成一个大的组件,也就是真正意义上的父子组件之间的关系。再忍耐一下,就可以出山了,新领域的大门就在前面,让我们大步往前跨吧。
文末附上所有相关代码和官方文档地址~~~
http://cn.vuejs.org/v2/guide/
相关阅读【每日课程推荐】机器学习实战!快速入门在线广告业务及 CTR 相应知识