Vue.js凭借简洁高效易用的特点迅速被前端开源社区接受,并借助weex覆盖移动开发场景,逐步演变成一个完整的生态圈,未来充满想象。而组件化的出现是为了确保代码高内聚低耦合并实现高效复用,从而提升开发效率和降低维护成本。Vue.js提供了组件化解决方案,本篇文章将结合开发实践先从零开始探讨组件的意义以及设计组件需要考虑的问题,再向前迈一步,讲解Vue.js组件的各种开发方式及对比。
从零开始,我们为什么需要组件?
换句话说,组件解决了什么问题?回想我们刚接触web开发时,通过标签引入css,通过
标签引入js就能轻松实现简单的静态页面,而随着用户需求的逐步提高,web项目在开发人员规模和代码量两个维度上会越来越庞大,自然会产生以下问题:
为了解决这些问题,需要做到 ①限制作用域 ②抽离公共代码,这不正是组件在做的事情嘛,因此我们可以认为组件化是软件工程变得复杂后的一种解决方案。具体这种解决方案是怎么演变来了,可以参考什么叫组件化开发? - aloo的回答 - 知乎。
既然组件是为了解决代码耦合和代码复用的问题,那我们在设计一个组件的时候,需要逐步思考以下的问题:
解决方案始于问题,也止于新的问题,方案总在变化,理解背后方案背后解决的问题能加深我们对方案的认识,也有助于我们解决新的问题。
从0到1,如何动手打造一个合适的组件?
前面简单了解了组件的出现是为了解决哪些具体问题,这个时候是不是好奇Vue的组件怎么写?学习资料首推Vue官方文档-组件,既简单又全面。为了保持阅读连贯性,这里举个简单的例子。没接触过Vue的同学建议先去看Vue官方文档-入门。
<template>
<div>{{ msg }}div>
template>
<script>
export default {
data() {
return { msg: 'Hello Vue.js' }
}
}
script>
<style scoped>
div { font-size: 20px; }
style>
<template>
<hello>hello>
template>
<script>
// 组件引用
import Hello from '/components/hello.vue';
export default {
// 组件注册
components: {
Hello
},
}
script>
上面我们实现了一个Hello
组件来显示”Hello Vue.js”,Hello
组件采用单文件的形式(借助webpack打包,具体配置可参考Vue脚手架工具:vue-cli生成的项目)来组合组件相关的模板、样式以及逻辑,实现代码层面的高内聚。这时候我们就能在任意地方使用Hello
组件(需引用,局部/全局注册,使用)。
这里假设大家通过浏览Vue官方文档-组件后了解了:
现在是不是特别想动手写自己第一个组件,用于实际业务中?在接触到新的技术时,我也有这种想法和冲动,但写码实在太自由了,实现同样一个小功能,可以有千万种写法(夸张),从多种写法中挑选出大家认可的,实际业务场景检验过的写法,就叫“最佳实践”,因此在动手写码前,不妨来看看Vue.js组件的“最佳实践”或者说主流写法,这常常会帮助我们省去一些填坑的时间。
在阅读一些博客后,我决定去看饿了么Vue组件库 mint-ui源码,最后总结出Vue组件的使用场景以及对应的实现方式,如下表:
序号 | 使用场景 | 实现方式 | 举例 |
---|---|---|---|
1 | 不需要返回js引用对象,纯粹是渲染内容 | 单Vue文件,例如上面的Hello 组件 |
Button |
2-1 | 需要返回js引用对象,例如控制弹窗开关 | 单Vue文件 + js封装对外接口 | Dialog |
2-2 | 需要返回js引用对象,例如控制弹窗开关 | 单Vue文件 + refs引用 | 局部Dialog |
2-3 | 需要返回js引用对象,例如控制弹层 | 单Vue文件 + refs引用 + 挂载引用对象到全局 | 全局Toast |
下面就逐一详细地和大家讲解各种使用场景和对应的实现方式。
PS:在这里特别感谢《Vue组件的三种调用方式》这篇博客给了我启发,不要先去写组件再考虑怎么使用组件,而应该从使用场景出发去设计组件。
这应该是开发中最普遍的场景,三部曲:①import引用组件 ②components注册组件 ③中使用组件,并通过数据传递和事件绑定将组件需要的信息传递过去,完事。这里举一个比上面的
Hello
组件复杂一些的例子:
①业务需求
上面是某个业务需求的视觉稿,咋一看是四个不同样式的卡片,但还是能找到一些共同点:①背景色一致;②阴影一致;③右上角有几种角标;④卡片两侧有缺口;我们对这四个共同点进一步整理和抽象:这是一个背景为白色、带阴影、右上角有不同角标、两侧可能有缺口的容器。下面就是愉快的编码时间啦。
②具体实现
<template>
<div class="card-container">
<slot name="content">slot>
<div v-if="status === 'default'">div>
<div v-else-if="status === 'unqualified'" class="tips unqualified">未达领取资格div>
<div v-else-if="status === 'expired'" class="tips expired">已过期div>
<div v-else-if="status === 'gift'" class="tips gift">平台赠送div>
<div v-if="semiCircle">
<div class="semi-circle">div>
<div class="semi-circle right">div>
div>
div>
template>
<script>
export default {
data () {
return {}
},
props: ['status', 'semiCircle']
}
script>
<style scoped>
/* 样式不是重点,忽略 */
style>
卡片容器实现比较简单,这里使用了插槽 - slot来将内容装载在容器内,再通过props传参来控制容器自身的形态。
③分析
“不需要返回js引用对象,纯粹是渲染内容”覆盖了业务绝大部分场景。如果不嫌麻烦,应该也能覆盖所有的场景。例如,一个Toast,我们期望是Toast.show('hello world')
的方式调用,其实也能这样写:
<template>
<div v-if="show">{{ msg }}div>
template>
<script>
export default {
data() {
return {}
},
props: ['show', 'msg']
}
script>
<template>
<toast :show="toastShow" :msg="toastMsg">toast>
template>
<script>
// 组件引用
import Toast from '/components/toast.vue';
export default {
data() {
return {
toastMsg: '',
toastShow: false
};
},
// 组件注册
components: {
Toast
},
methods: {
showToast: function(msg) {
this.toastMsg = msg;
this.toastShow = true;
}
}
}
script>
虽然上面同样实现了Toast.show('hello world')
的需求,但试想我们在多个页面里都需要用到Toast,每个页面都需要配置toastMsg
、toastShow
变量,showToast
方法,远没有Toast.show('hello world')
用得爽。下面我们来探讨新的解决方案。
上面已经用代码示例和大家展示用配置的方法来实现一个弹窗的开关过程是多么“傻”。有些时候我们需要操作对应的组件对象来完成一些交互,这里还是以Toast.show('hello world')
为目标,看看怎么去满足这种业务场景。
①Show me the code
<template>
<div v-if="showView">{{ msg }}div>
template>
<script>
export default {
data() {
return {
showView: false,
msg: ''
}
},
methods: {
show: function(msg) {
this.msg = msg;
this.showView = true;
},
close: function() {
this.showView = false;
}
}
}
script>
/* /components/toast.js 对上面的Vue组件进行封装,对外暴露接口 */
import Toast from '/components/toast.vue';
export default {
// install为Vue定义的插件引入规范
// 具体可以参考:https://cn.vuejs.org/v2/guide/plugins.html
install(Vue) {
// 由单Vue组件扩展成组件构造器
const constructor = Vue.extend(Toast);
function toast(msg) {
// 实例化
let toast = new constructor();
toast.show(msg);
// 挂载到DOM树上,没有考虑DOM节点复用的问题
if (!toast.$el) {
// 挂载虚拟DOM
let vm = toast.$mount();
document.querySelector('body').appendChild(vm.$el);
}
// 如果需要进一步操作,可返回组件实例
return toast;
}
// 将toast构造方法挂载在Vue上
Vue.toast = toast;
}
}
<template>
<div>div>
template>
<script>
import Vue from 'vue'
import Toast from './index.js'
// 全局注册
Vue.use(Toast)
export default {
mounted: function() {
const toast = Vue.toast('Hello world');
toast.close();
}
}
script>
②基本思路 - 从开源项目源码中总结得来:
按照上面的操作,我们已经能全局使用Vue.toast('Hello world')
。有时候我们的.vue
文件里并没有import Vue from 'vue'
,因此我们可以更改一下上面的步骤2,将构造函数挂载在window对象中,调用方法就变成toast('Hello world')
。
至此,我们已经知道如何返回组件实例操作对象供交互使用,但回顾一下整个解决方案,我们需要额外多写一个封装组件对外暴露构造方法的js文件,对于Vue新人来说这个过程会稍微有些复杂。
怎么理解复杂的事情?抽象是一个好方法。
③额外的思考
剥去外壳,直击本质,使用组件其实是将组件挂载到视图上并与组件进行逻辑交互的一个过程。基于这个本质,我们只用考虑两个问题:
由这个思考,针对场景2(需要返回js引用对象),产生了下面2-2、2-3两种稍微不一样的方案。
Vue在标签里已经给我们提供挂载组件的能力,同时利用VNode引用 - ref我们能获取到组件实例,有了组件实例,就能调用组件的方法进行各种交互。稍微改造一下之前的代码:
<template>
<div v-if="showView">{{ msg }}div>
template>
<script>
export default {
data() {
return {
showView: false,
msg: ''
}
},
methods: {
show: function(msg) {
this.msg = msg;
this.showView = true;
},
close: function() {
this.showView = false;
}
}
}
script>
<template>
<toast ref='myToast'>toast>
template>
<script>
// 组件引用
import Toast from '/components/toast.vue';
export default {
// 组件注册
components: {
Toast
},
mounted: function() {
// 获取组件引用
const toast = this.$refs.myToast;
// 调用组件方法实现逻辑交互
toast.show('Hello world');
toast.close();
}
}
script>
利用来挂载组件省去了我们使用Vue API来构建组件,挂载组件的麻烦,同时通过ref引用我们能快速获取组件实例,完成需要的逻辑交互。需要注意的是ref只能获取子组件的引用,因此只能在当前挂载作用域使用,在别的文件里访问不了
this.refs.myToast
,更进一步,如果我们需要一个全局使用的组件呢?。
说不定大家都已经猜到了,把上面的this.refs.myToast
挂载在window对象不就实现了全局调用接口吗?代码和上面及其相似就不展示了,这里再温习一遍操作流程:
挂载
;this.refs.myToast
获取组件引用;自此,大家可以开始结合自己的需求,挑选合适的方案来设计适合自己的组件了,欢迎来到组件化时代。
刚开始写的时候博客标题是《Vue.js组件开发从0到1到100》,后来写着写着发现从0到1内容就很多了,故拆开,后续会有《Vue.js组件开发从1到100》,分享内容可能有:
1. 组件的基本逻辑复用 - mixin
2. 你并不总需要组件 - 指令
3. 组件库的技术架构
4. 高阶组件 - 内置的transition组件分析
5. 复杂项目中的状态管理
6. 组件与SSR
欢迎大家关注。
好久不见,我又回来啦。过去17年探索了很多新的领域,希望18年能收收心,一专多长,加速成长。期待与大家共同进步。