近几年前端飞速发展,前端在web开发中的重要性与日俱增,越来越多的项目选择把业务逻辑移到前端来,html,js,css文件越来越多,原有的裸奔式的开发模式渐渐无法支撑随之而来的工程复杂度,组件化开发逐渐成为趋势。
组件化在前端领域可能还算一个略新的名词,在其他软件开发领域却基本上成为了一种共识。
它有两个比较显著的优点:
提升了开发效率
降低了维护成本
看下这张图(出自张云龙):

js模块与css单元以及html组成组件,页面由多个组件组合而成,页面本身也可以看作一个组件。
通过组件的天然解耦,可以多个人同时开发一个页面上的不同组件,最后再像搭积木一样组合成页面。
开发组件时应当遵循这三点:
高内聚
低耦合
高可用
所谓高内聚,低耦合要求组件内部应当包含所有组件需要的资源,尽量不与其他模块互相依赖,开发者能简单的即插即用。
而高可用则有多种含义:得跑的起来,经得起测试,易于扩展。
满足了这三点,可以称之为优雅。

如何进行组件化开发
Web Components标准或者自己实现了组件化的框架,如 Vue,React,Angular,Avalon
Web Components提供了组件化的标准方式:
通过shadow DOM封装组件的内部结构
通过Custom Element对外提供组件的标签
通过Template Element定义组件的HTML模板
通过HTML imports控制组件的依赖加载
但迫于浏览器支持度的问题还少有项目在生产环境中使用。
目前主流的依然是使用那些自己实现了组件化的框架,每个框架的实现方式各不相同,下面主要介绍下 Vue 中的组件。
Vue 组件
组件 (Component) 是 Vue.js 最强大的功能之一,Vue 为组件提供了完整的生命周期钩子让你可以掌控组件的每一个细节。
提供了Props,Events 以及 Slots 可以方便的定义组件接口,同时提供了 *.vue 文件格式的单文件组件的开发方式,配合 webpack 等构建工具可以获得更多能力。
假设我们需要一个toast 组件:
<template>
<toast :msg=”msg” ></toast>
</template>
通过Props传递的msg可以修改toast的内容。
如果这个时候又有个需求,要在msg中加个图标,我们可以使用 Slots:
<template>
<toast> <div>111<img class=”icon”/></div> </toast>
</template>
这样就基本上满足了需求对吗?
但假如你有很多个地方都需要使用这个toast,免不了要在每个要用到的模板里加这么一个标签,免不了会觉得还不如以前的jquery插件用起来方便呢?
是不是有其他的实现方式呢? of course!
先让我们看一看饿了么的 Element 里是怎么做的。
<template>
<el-button :plain="true" @click="open">打开消息提示</el-button>
<el-button :plain="true" @click="openVn">VNode</el-button>
</template>
<script>
export default {
methods: {
open() {
this.$message('这是一条消息提示');
},
openVn() {
const h = this.$createElement;
this.$message({
message: h('p', null, [
h('span', null, '内容可以是 '),
h('i', { style: 'color: teal' }, 'VNode')
])
});
}
}
}
</script>
如此简单,只需传不同的参数给 this.$message 即可根据参数类型来选择展示纯文字或者一个 VNode,看段源码:
if (isVNode(instance.message)) {
instance.$slots.default = [instance.message];
instance.message = null;
}
instance 就是组件实例,判断传入的message 是 VNode后操作实例的$slots.defualt。
$slots:用来访问被 slot 分发的内容。每个具名 slot 有其相应的属性(例如:slot="foo" 中的内容将会在 vm.$slots.foo 中被找到)。default 属性包括了所有没有被包含在具名 slot 中的节点。
通过赋值一个Vnode数组给default,相当于我们上面在模板里手动放入的slot。
instance.vm = instance.$mount();
document.body.appendChild(instance.vm.$el);
这段则是用来手动地挂载一个未挂载的实例并使用原生DOM API把它插入文档中。
Render & JSX
到这里就不得不多讲讲render 和 jsx了,render作为Vue2新增的一大特性让开发者获得了使用js构建template的能力。
比如下面这样一个动态生成 heading 标签的组件,你可能会这样写:
<template>
<h1 v-if="level === 1">
<slot></slot>
</h1>
<h2 v-else-if="level === 2">
<slot></slot>
</h2>
<h3 v-else-if="level === 3">
<slot></slot>
</h3>
<h4 v-else-if="level === 4">
<slot></slot>
</h4>
<h5 v-else-if="level === 5">
<slot></slot>
</h5>
<h6 v-else-if="level === 6">
<slot></slot>
</h6>
</template>
<script>
export default {
props: {
level: {
type: Number
}
}
}
</script>
如此简单的需求却需要这么冗长的代码,下面我们用 render 函数来实现:
<script>
export default {
render: function (createElement) {
return createElement(
'h' + this.level, // tag name 标签名称
this.$slots.default // 子组件中的阵列
)
},
props: {
level: {
type: Number,
required: true
}
}
}
</script>
简洁很多。但是在其他的一些场景,render 函数也有其不足之处,不过,有JSX。
让我们看一看另一足对比:
// render 函数
createElement(
'anchored-heading', {
props: {
level: 1
}
}, [
createElement('span', 'Hello'),
' world!'
]
)
// JSX
<anchored-heading :level="1">
<span>Hello</span> world!
</anchored-heading>
JSX,如果你用过react肯定知道这个all in js 的语法,我一度不能接受react的原因就是不想写JSX,直到我的膝盖中了一箭,写了一段时间个人项目的rn,这才发现了JSX的魅力。
使用JSX其实还能做到更多,但限于篇幅我将通过一个例子展示如何让你的组件获得更灵活的拓展能力。
这是一个Tabs组件的 header部分,通过renderLabel 可以自定义label的样式
<script type="text/jsx">
export default {
props: {
renderLabel: {},
tabs: {
type: Array
}
},
methods: {
renderTab(tab) {
const RenderLabel = this.renderLabel
return (
<div
onClick={ () => this.$parent.handleNavClick(tab) }
class={{ nav: true, active: tab.name === this.$parent.currentName }}>
{
RenderLabel
? <RenderLabel tab={ tab }/>
: <a href="javascript:;">{ tab.label }</a>
}
</div>
)
}
},
render() {
const { tabs, renderTab } = this
return (
<div>
{ tabs.map((tab) => renderTab(tab)) }
</div>
)
}
}
</script>
默认样式如下:

当传递这样一个组件对象的时候:
renderLabel: {
props: ['tab'],
template: `<div>{{tab.label}}-byRenderLabel</div>`
}
Amazing!
以上就是我在使用vue 开发组件的过程中的一点小实践,期待得到你的评论与指点。

本文作者:李梦南(点融黑帮),前端程序猿,写写代码,打打农药,没事撸猫。



