Vue 组件设计

Vue 作为 MVVM 框架一员,不管是写业务还是基础服务,都少不了书写组件。本文总结一下书写业务组件的一些心得。

为什么要写组件?

我们知道,只要是组件,就需要在引用的时候与 view 或者其他组件进行相关的交互,即 props 传值,$emit 触发事件,

使用 $refs 调用组件方法等,与写在同一个文件相比,耗费的精力明显更多。那为什么需要拆分出组件呢?我认为有两种目的:

复用和隔离。

复用

在业务代码中,会有大量类似的界面,保证交互唯一,即使我们有了类似 element-ui 或者 iview 这种基础组件库,

我们同样需要为这些基础组件添加 props 或者 events,只有一处使用时,没有任何问题,当你的业务中出现两次、三次甚至更多时,

代码中会出现大量重复的代码,而且这些代码在线上可能会慢慢露出一些深层次的 bug,要修复这些 BUG,就需要 n 倍的时间去写同样的代码,

让人抓狂。所以我们页面同样需要像 js 抽出公用的方法一样抽出公用组件,这就是复用的目的。

隔离

复用针对的是代码重复问题,而隔离则是针对代码逻辑过于复杂的问题。通常我们要实现一个复杂的逻辑,它是一个扁平化的多逻辑并行问题,

人脑对于同时思考是有一定限制的,过于复杂就很难一下考虑全面。

首先需要抽象出它的目的,然后对实现进行分层,让每一层只解决一个简单的问题,这些层合起来形成一个完整的解决方案;

或者将问题拆分成几块,每块之间具有一定的联系,每次思考时只需要考虑局部的逻辑即可。

不管是分层、分块还是混合式,它的目的是对进行隔离,从而简化问题。如果某个页面 js + template 行数非常多(1k+),

这个时候就可以考虑是不是要对部分功能拆解,便于在后续添加新功能,或者修改 BUG 的时候更为方便定位到问题的代码,

不会出现改错函数的问题。

需要注意的是,虽然复用和隔离是让逻辑更为清晰,但使用自己写的组件会让项目的入手难度提高,需要先了解整体的设计,

才能针对性的修改代码或者添加新的功能,得失各半。

组件设计的一些理念

网上有关于组件设计的基本原则:http://www.fly63.com/article/detial/996

内容比较多,下面进行一些常用的原则归纳。

单一职责

之前提到的组件拆分目的:复用与隔离,对于隔离的类型,组件业务必然很重,此时虽然要保证组件尽可能简单,

而复用类型的,通用性更强,所以功能越单一,使用起来就越方便。我们知道 react 有一个概念:container/component,

即 component 只是渲染组件,而 container 才是产生业务的组件,我们 Vue 也可以依照这个理念进行设计。

即把数据处理等带有副作用的工作放在父组件中,而子组件只进行展示或操作,通过事件的方式让父组件进行处理,

保证逻辑归一,后续维护也更为方便。或者使用 slot 等类似高阶组件的方式来简化当前组件的内容。

无副作用/引用透明

和纯函数类似,设计的一个组件不应该对父组件产生副作用,从而达到引用透明(引用多次不影响结果)。

数据操作前必须进行复制。比如需要添加额外的键值,或者需要对数组类型的数据进行操作,会对原始数据产生影响,

需要使用解构的方式进行复制:

const newData = { ...oldData }
const newList = [...oldList]

注:引用类型的 props 千万不要直接修改对象,虽然能够达到传递数据的目的,但会产生副作用,如果有其他地方用到该数据,可能产生未知的影响。

入口和出口正确性检查

Vue 提供了类型检查工具,只在 dev 情况下生效,虽然和 JSON Schema 相比功能比较少,但能够做基本的类型检查了,

我们只需要在 props 时不使用字符串型,而是为它定义详细的类型, 并为它设置默认值(vue-cli 的 eslint 严格模式已经强制要求):

['name1'] // 不规范写法
{
name1: {
type: String,
default: undefined
}
}

组件划分颗粒度

组件拆分出来之后,拆成几层或者是拆成几块,影响文件的数量。如果层级比较多,各种 props 传递,事件传递,维护成本比较高。

举例:如果是一个二级的列表,即有多个一级列表,一级列表各有一级列表,这个时候应该怎么拆分呢?

按单一原则,我们可能需要拆分成以下几个:一级列表卡片本身,二级列表卡片,二级列表承载组件,一级列表承载组件。

这种划分,组件是三级,两块,数据的传递就会比较困难。如果一级卡片列表不复杂,我们可以将几个 v-for 与组件本身合并,

即一级列表承载组件+一级列表卡片+二级列表卡片,二级列表卡片。这种处理方式保证所有的数据处理在第一层上,二级卡片只做渲染,

保证逻辑处理集中在一个组件,维护也比较方便。当然,如果一级卡片非常复杂,或者数据需要大量的处理,需要根据情况把最细的进行合并。

新功能下添加新属性/新文件

对于通用类型组件,我们要求它尽可能的短小精悍,调用起来更为简单,所以不能设计太多的参数。基础组件库不能符合这个要求,

主要是因为基础组件库需要尽可能增加普适性,不会因为没有某个常用的属性,导致该组件需要复制一份重写,再加上日积月累的 pull request,

属性和参数必然会越来越多。而我们在业务中使用,完全不需要这么多的配置,如果有重大差别,重新复制一份,对于后续的维护反而更方便。

所以是否新增加属性还是拷贝一份,是根据后续该组件是否会产生比较大的发展方向差异来决定的。

Vue 组件之间的交互设计

Vue 组件与 React 组件有比较大的区别,模板的设计更偏向于 HTML,所以要实现类似 react 的高阶组件的需求通常比较少,

而高阶组件集成度过高,对于业务来说,当业务越来越复杂,组件内部逻辑将拆分困难,未必是件好事,所以我们只讨论普通的组件设计。

组件设计是考虑组件通讯方式,主要分为以下几个方面:向下传值,向上传值,伪双向绑定,方法调用。

数据流转

向下传值

向下传值就是父级传给子级数据。前面已经提到了,在 props 传值尽量对传入数据进行类型校验,保证尽快发现问题。除此之外,也有一些注意事项。

传值类型如果是引用类型的 Object 类型,那么尽量给它默认值,防止 undefined。

default: () => ({})

其次,父级在赋值时,不要使用 a=newData 这种写法,而是使用 Object.assign 来保证能准确触发组件更新。

还有另外一种方式,但不方便声明所有对象内的数据时,可以使用 this.$set(this, 'key', newData),保证对象一定会被监听到。

向上传值

Vue 2.0 需要使用 $emit 进行事件向上冒泡, 父组件进行事件的监听就可以进行处理。

伪双向绑定

Vue 2.0 提供了语法糖,支持双向绑定,使得Vue 进行双向传递数据极为方便,不需要既向上传值又向下传值。

当然它不是真正的绑定,而是封装了之前提到的向下传值和向上传值,简单的语法糖。它分为两类:v-model 和 .sync 修饰符

数据传递支持各种类型,不过建议传递的数据使用数组而不要使用对象类型,对象类型可能会出现渲染监听失败的问题。

v-model

v-model 使用的是 value 属性和 input 事件,父组件会自动把 input 事件的值赋给对应的变量。

在设计组件中,如果有双向的数据传递,且符合组件设计目的,应该优先使用 v-model 来实现数据的控制,

这样的组件更符合 Vue 组件的标准。

要注意的是,如果是自行写 render 函数,双向绑定要自己实现。

sync

.sync 修饰符和 v-model 比较类似,不过它的 props 可以是自定义的,而向上传值时方式为:

this.$emit('update:propsName', val)

本质上和 v-model 是类似的。sync 修饰符相比于 v-model,语义化更好,用起来更方便

方法调用

有了 props 和 emit ,我们已经基本能够实现大部分功能了,但总有些子组件的层次控制或者数据控制无法通过这种方式实现,

这个时候,组件间的交互就需要使用子组件的 Methods 来定义,使用 this.$refs.组件ref 来调用它的方法。

比如说 el-tree 组件,设置选中和非选中,只靠数据传递,无法保证设计选中状态,所以它提供了一些方法来进行手动选择。

在设计组件时,使用方法进行控制应该是最后才考虑的,因为我们通常无法一眼看出某个方法是否应该支持外部调用,

只能通过看文档才能得知相关的方法

简化与抽离的其他实现

除组件外,Vue 提供了一些机制用于减少项目中的代码重复率。

使用插件或者 mixins 实现

插件机制需要在 Vue 初始化的时候引入。看下 vue-meta 的插件入口写法:

/**
* Plugin install function.
* @param {Function} Vue - the Vue constructor.
*/
export default function VueMeta (Vue, options = {}) {
// set some default options
const defaultOptions = {
keyName: VUE_META_KEY_NAME,
contentKeyName: VUE_META_CONTENT_KEY,
metaTemplateKeyName: VUE_META_TEMPLATE_KEY_NAME,
attribute: VUE_META_ATTRIBUTE,
ssrAttribute: VUE_META_SERVER_RENDERED_ATTRIBUTE,
tagIDKeyName: VUE_META_TAG_LIST_ID_KEY_NAME
}
// combine options
options = assign(defaultOptions, options) // bind the $meta method to this component instance
Vue.prototype.$meta = $meta(options) // store an id to keep track of DOM updates
let batchID = null // watch for client side component updates
Vue.mixin({
beforeCreate () {
// Add a marker to know if it uses metaInfo
// _vnode is used to know that it's attached to a real component
// useful if we use some mixin to add some meta tags (like nuxt-i18n)
if (typeof this.$options[options.keyName] !== 'undefined') {
this._hasMetaInfo = true
}
// coerce function-style metaInfo to a computed prop so we can observe
// it on creation
if (typeof this.$options[options.keyName] === 'function') {
if (typeof this.$options.computed === 'undefined') {
this.$options.computed = {}
}
this.$options.computed.$metaInfo = this.$options[options.keyName]
}
},
created () {
// if computed $metaInfo exists, watch it for updates & trigger a refresh
// when it changes (i.e. automatically handle async actions that affect metaInfo)
// credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux)
if (!this.$isServer && this.$metaInfo) {
this.$watch('$metaInfo', () => {
// batch potential DOM updates to prevent extraneous re-rendering
batchID = batchUpdate(batchID, () => this.$meta().refresh())
})
}
},
activated () {
if (this._hasMetaInfo) {
// batch potential DOM updates to prevent extraneous re-rendering
batchID = batchUpdate(batchID, () => this.$meta().refresh())
}
},
deactivated () {
if (this._hasMetaInfo) {
// batch potential DOM updates to prevent extraneous re-rendering
batchID = batchUpdate(batchID, () => this.$meta().refresh())
}
},
beforeMount () {
// batch potential DOM updates to prevent extraneous re-rendering
if (this._hasMetaInfo) {
batchID = batchUpdate(batchID, () => this.$meta().refresh())
}
},
destroyed () {
// do not trigger refresh on the server side
if (this.$isServer) return
// re-render meta data when returning from a child component to parent
if (this._hasMetaInfo) {
// Wait that element is hidden before refreshing meta tags (to support animations)
const interval = setInterval(() => {
if (this.$el && this.$el.offsetParent !== null) return
clearInterval(interval)
if (!this.$parent) return
batchID = batchUpdate(batchID, () => this.$meta().refresh())
}, 50)
}
}
})
}

要的本质是使用 prototype 设置独立变量,然后使用 mixins 注入相关的方法。可以看到,基本上每个生命周期都会处理到。

mixin 不仅使用在插件中,直接使用也是可以的。关于 mixins 可看官方文档:https://cn.vuejs.org/v2/guide/mixins.html.

事件与属性透传

之前提到组件尽可能参数少,但参数过少,组件无法实现某些定制化的要求,而我们组件可能有多个层次,

这种情况下我们需要将当前组件的父组件的其他属性透传给子组件,将父组件其他事件监听给子组件,写法如下:

<div name="main">
<input v-on='$listeners' v-bind="$attrs" />
</div>

其他注意事项

DOM 操作

正常情况下是不推荐业务组件直接操作 DOM 的,但有时候要写组件监听事件,这种情况下一定要注意在 destroyed 时候进行 removeEventListener。

Vue 组件设计的更多相关文章

  1. 一个优质的Vue组件库应该遵循什么样的设计原则

    一.组件库的价值 就个人而言,拥有一套自己的组件库,可以让你的开发变得更高效,让你在行业里更有价值. 就团队而言,拥有一套团队的组件库,可以让协同开发变得更高效规范,让你的团队在公司更具有影响力. 就 ...

  2. vue组件最佳实践

    看了老外的一篇关于组件开发的建议(强烈建议阅读英文原版),感觉不错翻译一下加深理解. 这篇文章制定一个统一的规则来开发你的vue程序,以至于达到一下目的. 1.让开发者和开发团队更容易发现一些事情. ...

  3. Vue组件库的那些事儿,你都知道吗?

    前段时间一直在研究Vue组件库,终于在组内派上了用场.来给大家贡献一篇关于Vue组件库的相关知识.经验不多,如果有不合理的地方还请多多指出哦--- 回想一下,在你们公司或者你们小组是否有一个以上的项目 ...

  4. vue组件推荐

    Vue 是一个轻巧.高性能.可组件化的MVVM库,API简洁明了,上手快.从Vue推出以来,得到众多Web开发者的认可.在公司的Web前端项目开发中,多个项目采用基于Vue的UI组件框架开发,并投入正 ...

  5. python 全栈开发,Day90(Vue组件,前端开发工具包)

    昨日内容回顾 1. Vue使用 1. 生成Vue实例和DOM中元素绑定 2. app.$el --> 取出该vue实例绑定的DOM标签 3. app.$data --> 取出该vue实例绑 ...

  6. vue - 组件的创建

    组件的创建 vue的核心基础就是组件的使用,玩好了组件才能将前面学的基础更好的运用起来.组件的使用更使我们的项目解耦合.更加符合vue的设计思想MVVM. 那接下来就跟我看一下如何在一个Vue实例中使 ...

  7. day 83 Vue学习三之vue组件

    本节目录 一 什么是组件 二 v-model双向数据绑定 三 组件基础 四 父子组件传值 五 平行组件传值 六 xxx 七 xxx 八 xxx 一 什么是组件 首先给大家介绍一下组件(componen ...

  8. Vue 组件 data为什么是函数?

    在创建或注册模板的时候,传入一个data属性作为用来绑定的数据.但是在组件中,data必须是一个函数,而不能直接把一个对象赋值给它. Vue.component('my-component', { t ...

  9. Vue学习笔记之Vue组件

    0x00 前言 vue的核心基础就是组件的使用,玩好了组件才能将前面学的基础更好的运用起来.组件的使用更使我们的项目解耦合.更加符合vue的设计思想MVVM. 那接下来就跟我看一下如何在一个Vue实例 ...

随机推荐

  1. Oracle涂抹oracle学习笔记第8章RMAN说,我能备份

    本次测试服务器为172.16.25.33 使用rman连接本地数据库 rman target / 在rman中执行启动与关闭的命令与sqlplus相同 在rman中执行sql语句 sql ‘需要执行的 ...

  2. Error:(12, 64) java: 未报告的异常错误java.io.IOException; 必须对其进行捕获或声明以便抛出

    Error:(12, 64) java: 未报告的异常错误java.io.IOException; 必须对其进行捕获或声明以便抛出 package com.test; import org.apach ...

  3. Python小功能汇总

    1.没有文件夹就新建 适用以下3种情况. (1)文件夹适用 (2)相对路径适用 (3)绝对路径适用 # 判断输出文件夹是否存在.不存在就创建 # 1.output_dir为绝对路径 if os.pat ...

  4. mysql 数据操作 单表查询 group by 分组 目录

    mysql 数据操作 单表查询 group by 介绍 mysql 数据操作 单表查询 group by 聚合函数 mysql 数据操作 单表查询 group by 聚合函数 没有group by情况 ...

  5. 如何区分不同用户——Cookie/Session机制详解

    会话(Session)跟踪是Web程序中常用的技术,用来跟踪用户的整个会话.常用的会话跟踪技术是Cookie与Session.Cookie通过在客户端记录信息确定用户身份,Session通过在服务器端 ...

  6. java序列化与反序列化(转)

    Java序列化与反序列化是什么?为什么需要序列化与反序列化?如何实现Java序列化与反序列化?本文围绕这些问题进行了探讨. 1.Java序列化与反序列化 Java序列化是指把Java对象转换为字节序列 ...

  7. php5.6安装gd库

    rpm -Uvh http://ftp.iij.ad.jp/pub/linux/fedora/epel/6/x86_64/epel-release-6-8.noarch.rpm rpm -Uvh ht ...

  8. 系统管理命令之who am i

    who am i 显示的是实际用户的用户名,即用户登陆的时候的用户ID.此命令相当于who -m. 用Linux的术语来解释就是:(实际用户=uid,即user id.有效用户=euid,即effec ...

  9. E题:Water Problem(快速幂模板)

    题目大意:原题链接  题解链接 解题思路:令x=x-1代入原等式得到新的等式,两式相加,将sin()部分抵消掉,得到只含有f(x)的状态转移方程f(x+1)=f(x)+f(x-2)+f(x-3),然后 ...

  10. CCPC-Wannafly Winter Camp Day7 (Div2, onsite)

    Replay Dup4: 啥都不会? 只能看着两位聚聚A题? X: 模拟题不会写, 日常摔锅 u, v分不清, 日常演员 又是自己没理清楚就抢键盘上机导致送了一万个罚时, 日常背锅 A:迷宫 Solv ...