Vue 组件设计
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 组件设计的更多相关文章
- 一个优质的Vue组件库应该遵循什么样的设计原则
一.组件库的价值 就个人而言,拥有一套自己的组件库,可以让你的开发变得更高效,让你在行业里更有价值. 就团队而言,拥有一套团队的组件库,可以让协同开发变得更高效规范,让你的团队在公司更具有影响力. 就 ...
- vue组件最佳实践
看了老外的一篇关于组件开发的建议(强烈建议阅读英文原版),感觉不错翻译一下加深理解. 这篇文章制定一个统一的规则来开发你的vue程序,以至于达到一下目的. 1.让开发者和开发团队更容易发现一些事情. ...
- Vue组件库的那些事儿,你都知道吗?
前段时间一直在研究Vue组件库,终于在组内派上了用场.来给大家贡献一篇关于Vue组件库的相关知识.经验不多,如果有不合理的地方还请多多指出哦--- 回想一下,在你们公司或者你们小组是否有一个以上的项目 ...
- vue组件推荐
Vue 是一个轻巧.高性能.可组件化的MVVM库,API简洁明了,上手快.从Vue推出以来,得到众多Web开发者的认可.在公司的Web前端项目开发中,多个项目采用基于Vue的UI组件框架开发,并投入正 ...
- python 全栈开发,Day90(Vue组件,前端开发工具包)
昨日内容回顾 1. Vue使用 1. 生成Vue实例和DOM中元素绑定 2. app.$el --> 取出该vue实例绑定的DOM标签 3. app.$data --> 取出该vue实例绑 ...
- vue - 组件的创建
组件的创建 vue的核心基础就是组件的使用,玩好了组件才能将前面学的基础更好的运用起来.组件的使用更使我们的项目解耦合.更加符合vue的设计思想MVVM. 那接下来就跟我看一下如何在一个Vue实例中使 ...
- day 83 Vue学习三之vue组件
本节目录 一 什么是组件 二 v-model双向数据绑定 三 组件基础 四 父子组件传值 五 平行组件传值 六 xxx 七 xxx 八 xxx 一 什么是组件 首先给大家介绍一下组件(componen ...
- Vue 组件 data为什么是函数?
在创建或注册模板的时候,传入一个data属性作为用来绑定的数据.但是在组件中,data必须是一个函数,而不能直接把一个对象赋值给它. Vue.component('my-component', { t ...
- Vue学习笔记之Vue组件
0x00 前言 vue的核心基础就是组件的使用,玩好了组件才能将前面学的基础更好的运用起来.组件的使用更使我们的项目解耦合.更加符合vue的设计思想MVVM. 那接下来就跟我看一下如何在一个Vue实例 ...
随机推荐
- Oracle涂抹oracle学习笔记第8章RMAN说,我能备份
本次测试服务器为172.16.25.33 使用rman连接本地数据库 rman target / 在rman中执行启动与关闭的命令与sqlplus相同 在rman中执行sql语句 sql ‘需要执行的 ...
- Error:(12, 64) java: 未报告的异常错误java.io.IOException; 必须对其进行捕获或声明以便抛出
Error:(12, 64) java: 未报告的异常错误java.io.IOException; 必须对其进行捕获或声明以便抛出 package com.test; import org.apach ...
- Python小功能汇总
1.没有文件夹就新建 适用以下3种情况. (1)文件夹适用 (2)相对路径适用 (3)绝对路径适用 # 判断输出文件夹是否存在.不存在就创建 # 1.output_dir为绝对路径 if os.pat ...
- mysql 数据操作 单表查询 group by 分组 目录
mysql 数据操作 单表查询 group by 介绍 mysql 数据操作 单表查询 group by 聚合函数 mysql 数据操作 单表查询 group by 聚合函数 没有group by情况 ...
- 如何区分不同用户——Cookie/Session机制详解
会话(Session)跟踪是Web程序中常用的技术,用来跟踪用户的整个会话.常用的会话跟踪技术是Cookie与Session.Cookie通过在客户端记录信息确定用户身份,Session通过在服务器端 ...
- java序列化与反序列化(转)
Java序列化与反序列化是什么?为什么需要序列化与反序列化?如何实现Java序列化与反序列化?本文围绕这些问题进行了探讨. 1.Java序列化与反序列化 Java序列化是指把Java对象转换为字节序列 ...
- 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 ...
- 系统管理命令之who am i
who am i 显示的是实际用户的用户名,即用户登陆的时候的用户ID.此命令相当于who -m. 用Linux的术语来解释就是:(实际用户=uid,即user id.有效用户=euid,即effec ...
- E题:Water Problem(快速幂模板)
题目大意:原题链接 题解链接 解题思路:令x=x-1代入原等式得到新的等式,两式相加,将sin()部分抵消掉,得到只含有f(x)的状态转移方程f(x+1)=f(x)+f(x-2)+f(x-3),然后 ...
- CCPC-Wannafly Winter Camp Day7 (Div2, onsite)
Replay Dup4: 啥都不会? 只能看着两位聚聚A题? X: 模拟题不会写, 日常摔锅 u, v分不清, 日常演员 又是自己没理清楚就抢键盘上机导致送了一万个罚时, 日常背锅 A:迷宫 Solv ...