封装Vue组件的原则及技巧

Vue的组件系统

Vue组件的API主要包含三部分:prop、event、slot

  • props表示组件接收的参数,最好用对象的写法,这样可以针对每个属性设置类型、默认值或自定义校验属性的值,此外还可以通过type、validator等方式对输入进行验证
  • slot可以给组件动态插入一些内容或组件,是实现高阶组件的重要途径;当需要多个插槽时,可以使用具名slot
  • event是子组件向父组件传递消息的重要途径

单向数据流

参考:单向数据流-官方文档。

父级 prop 的更新会向下流动到子组件中,但是反过来则不行

单向数据流是Vue组件一个非常明显的特征,不应该在子组件中直接修改props的值

  • 如果传递的prop仅仅用作展示,不涉及修改,则在模板中直接使用即可
  • 如果需要对prop的值进行转化然后展示,则应该使用computed计算属性
  • 如果prop的值用作初始化,应该定义一个子组件的data属性并将prop作为其初始值
    从源码/src/core/vdom/create-component.js/src/core/vdom/helpers/extract-props.js里可以看见,在处理props的取值时,首先从
function extractPropsFromVNodeData(){  const res = {}  const { attrs, props } = data  // 执行浅拷贝  checkProp(res, props, key, altKey, true) || checkProp(res, attrs, key, altKey,  false)   return res
}

在子组件修改props,却不会修改父组件,这是因为extractPropsFromVNodeData中是通过浅复制将attrs传递给props的。

浅复制意味着在子组件中对对象和数组的props进行修改还是会影响父组件,这就违背了单向数据流的设计。因此需要避免这种情况出现。

组件之间的通信

这里可以参考:vue组件通信全揭秘,写的比较全面

  • 父子组件的关系可以总结为prop 向下传递,事件event向上传递
  • 祖先组件和后代组件(跨多代)的数据传递,可以使用provide和inject来实现
    此外,如果需要跨组件或者兄弟组件之间的通信,可以通过eventBus或者vuex等方式来实现。

“绕开”单向数据流

考虑下面场景:父组件将数据通过prop形式传递给子组件,子组件进行相关操作并修改数据,需要修改父组件的prop值(一个典型的例子是:购物车的商品数量counter组件)。

根据组件单向数据流和和事件通信机制,需要由子组件通过事件通知父组件,并在父组件中修改原始的prop数据,完成状态的更新。在子组件中修改父组件的数据的场景在业务中也是比较常见的,那么有什么办法可以“绕开”单向数据流的限制呢?

状态提升

可以参考React的状态提升,直接通过props将父元素的数据处理逻辑传入子组件,子组件只做数据展示和事件挂载即可

<template><div class="counter"><div class="counter_btn" @click="onMinus">-</div><div class="counter_val">{{value}}</div><div class="counter_btn" @click="onPlus">+</div></div>
</template><script>export default {props: {value: {type: Number,default: 0},onMinus: Function,onPlus: Function},};
</script>

然后在调用时传入事件处理函数

<template><div><counter :value="counter2Val" :on-minus="minusVal" :on-plus="plusVal"></counter></div>
</template>
<script>export default {data() {return {counter2Val: 0,}},methods: {minusVal(){this.counter2Val--},plusVal(){this.counter2Val++}}}
</script>

很明显,由于在每个父组件中都需要实现on-minus和on-plus,因此状态提升并没有从根本上解决问题。

v-model语法糖

Vue内置了v-model指令,v-model 是一个语法糖,可以拆解为 props: value 和 events: input。就是说组件只要提供一个名为 value 的 prop,以及名为 input 的自定义事件,满足这两个条件,使用者就能在自定义组件上使用v-model

<template><div><button @click="changeValue(-1)">-1</button><span>{{currentVal}}</span><button @click="changeValue(1)">+1</button></div>
</template><script>
export default {props: {value: {type: Number // 定义value属性}},data() {return {currentVal: this.value};},methods: {changeVal(val) {this.currentVal += parseInt(val);this.$emit("input", this.currentVal); // 定义input事件}}
};
</script>

然后调用的时候只需要传入v-model指令即可

<counter v-model="counerVal"/>

使用v-model,可以很方便地在子组件中同步父组件的数据。在2.2之后的版本中,可以定制v-model指令的prop和event名称,参考model配置项

export default {    model: {        prop: 'value',        event: 'input'    },    // ... }

获得组件实例的引用
在开发组件中,获取组件实例是一个非常有用的方法。组件可以通过$ refs、$ parents、$children等方式获得vm实例引用

  • $refs在组件(或者dom上)增加ref属性即可
  • $parents获取子组件挂载的父组件节点
  • $children,获取组件的所有子节点
    这些接口返回的都是vnode,可以通过vnode.componentInstance获得对应的组件实例,然后直接调用组件的方法或访问数据。虽然这种方式多多少少有些违背组件的设计理念,增加了组件之间的耦合成本,但代码实现会更加简洁。

表单验证组件

通常情况下,表单验证是表单提交前一个十分常见的应用场景。那么,如何把表单验证的功能封装在组件内部呢?

下面是一个表单组件的示例,展示了通过获得组件的引用来实现表单验证功能。

首先定义组件的使用方式,

  • xm-form接收model和rule两个prop

    • model表示表单绑定的数据对象,最后表单提交的就是这个对象
    • rule表示验证规则策略,表单验证可以使用async-validator插件
  • xm-form-item接收的prop属性,对应form组件的model和rule的某个key值,根据该key从model上取表单数据,从rule上取验证规则
    下面是使用示例代码
<template><div class="page"><xm-form :model="form" :rule="rule" ref="baseForm"><xm-form-item label="姓名" prop="name"><input v-model="form.name"/></xm-form-item><xm-form-item label="邮箱" prop="email"><input v-model="form.email"/></xm-form-item><xm-form-item><button @click="submit">提交</button></xm-form-item></xm-form></div>
</template><script>import xmForm from "../src/form/form"import xmFormItem from "../src/form/form-item"export default {components: {xmForm,xmFormItem,},data() {return {form: {name: "",email: ""},rule: {name: [{required: true, message: '用户名不能为空', trigger: 'blur'}],email: [{required: true, message: '邮箱不能为空', trigger: 'blur'},{type: 'email', message: '邮箱格式不正确', trigger: 'blur'}],}}},methods: {submit() {// 调用form组件的validate方法this.$refs.baseForm.validate().then(res => {console.log(res)}).catch(e => {console.log(e)})}}}
</script>

接下来让我们实现form-item组件,其主要作用是放置表单元素,及展示错误信息

<template><label class="form-item"><div class="form-item_label">{{label}}</div><div class="form-item_mn"><slot></slot></div><div class="form-item_error" v-if="errorMsg">{{errorMsg}}</div></label>
</template>
<script>export default {name: "form-item",props: {label: String,prop: String},data() {return {errorMsg: ""}},methods: {showError(msg) {this.errorMsg = msg}}}
</script>

然后让我们来实现form组件

通过calcFormItems获取每个xm-form-item的引用,保存在formItems中
暴露validate接口,内部调用AsyncValidator,并根据结果遍历formItems中每个表单元素的prop属性,处理对应的error信息

<template><div class="form"><slot></slot></div>
</template><script>import AsyncValidator from 'async-validator';export default {name: "xm-form",props: {model: {type: Object},rule: {type: Object,default: {}}},data() {return {formItems: []}},mounted() {this.calcFormItems()},updated() {this.calcFormItems()},methods: {calcFormItems() {// 获取form-item的引用if (this.$slots.default) {let children = this.$slots.default.filter(vnode => {return vnode.tag &&vnode.componentOptions && vnode.componentOptions.Ctor.options.name === 'form-item'}).map(({componentInstance}) => componentInstance)if (!(children.length === this.formItems.length && children.every((pane, index) => pane === this.formItems[index]))) {this.formItems = children}}},validate() {let validator = new AsyncValidator(this.rule);let isSuccess = truelet findErrorByProp = (errors, prop) => {return errors.find((error) => {return error.field === prop}) || ""}validator.validate(this.model, (errors, fields) => {this.formItems.forEach(formItem => {let prop = formItem.proplet error = findErrorByProp(errors || [], prop)if (error) {isSuccess = false}formItem.showError(error && error.message || "")})});return Promise.resolve(isSuccess)}}}
</script>

这样我们就完成了一个通用的表单验证组件。从这个例子中可以看出获取组件引用,在组件开发中是一个非常有用的方法。

封装API组件

一些组件如提示框、弹出框等,更适合单独的API调用方式,如

import MessageBox from '@/components/MessageBox.vue'MessageBox.toast('hello)

如何实现制这种不需要手动嵌入模板里面的组件呢?原来,除了在通过在模板中嵌入组件到children挂载组件,Vue还为组件提供了手动挂载的方法$mount

let component = new MessageBox().$mount()document.getElementById('app').appendChild(component.$el)

通过这种方式,我们就是可以封装API形式调用组件,下面是一个alert消息提示的接口封装

消息弹窗组件

一个消息组件就是在页面指定绘制展示提示消息的组件,下面是简单实现

<template><div class="alert"><div class="alert-main" v-for="item in notices" :key="item.name"><div class="alert-content">{{ item.content }}</div></div></div>
</template><script>let seed = 0;function getUuid() {return 'alert_' + (seed++);}export default {data() {return {notices: []}},methods: {add(notice) {const name = getUuid();let _notice = Object.assign({name: name}, notice);this.notices.push(_notice);// 定时移除,单位:秒const duration = notice.duration;setTimeout(() => {this.remove(name);}, duration * 1000);},remove(name) {const notices = this.notices;for (let i = 0; i < notices.length; i++) {if (notices[i].name === name) {this.notices.splice(i, 1);break;}}}}}
</script>

下面来实现消息组件挂载到页面的逻辑,并对外暴露展示消息的接口

import Vue from 'vue';// 具体的组件
import Alert from './alert.vue';
Alert.newInstance = properties => {const props = properties || {};// 实例化一个组件,然后挂载到body上const Instance = new Vue({data: props,render (h) {return h(Alert, {props: props});}});const component = Instance.$mount();document.body.appendChild(component.$el);// 通过闭包维护alert组件的引用const alert = Instance.$children[0];return {// Alert组件对外暴露的两个方法add (noticeProps) {alert.add(noticeProps);},remove (name) {alert.remove(name);}}
};// 提示单例
let messageInstance;
function getMessageInstance () {messageInstance = messageInstance || Alert.newInstance();return messageInstance;
}
function notice({ duration = 1.5, content = '' }) {// 等待接口调用的时候再实例化组件,避免进入页面就直接挂载到body上let instance = getMessageInstance();instance.add({content: content,duration: duration});
}// 对外暴露的方法
export default {info (options) {return notice(options);}
}

然后就可以使用API的方式来调用弹窗组件了

import alert from './alert.js'
// 直接使用
alert.info({content: '消息提示', duration: 2})
// 或者挂载到Vue原型上
Vue.prototype.$Alert = alert
// 然后在组件中使用
this.$Alert.info({content: '消息提示', duration: 2})

高阶组件

高阶组件可以看做是函数式编程中的组合。可以把高阶组件看做是一个函数,他接收一个组件作为参数,并返回一个功能增强的组件。

高阶组件是一个接替Mixin实现抽象组件公共功能的方法,不会因为组件的使用而污染DOM(添加并不想要的div标签等)、可以包裹任意的单一子元素等等

在React中高阶组件是比较常用的组件封装形式,在Vue中如何实现高阶组件呢?

在组件的render函数中,只需要返回一个vNode数据类型即可,如果在render函数中提前做一些处理,并返回this.$slots.default[0]对应的vnode,就可以实现高阶组件。

内置的keep-alive

Vue内置了一个高阶组件keep-alive,查看源码可以发现其实现原理,就是通过维护一个cache,并在render函数中根据key返回缓存的vnode,来实现组件的持久化。

throttle

节流是web开发中处理事件比较常见的需求。常见的场景有及时搜索框避免频繁触发搜索接口、表单按钮防止在短暂时间误重复提交等

首先来看看Throttle组件的使用方式,接收两个props

  • time表示节流的时间间隔
  • events表示需要处理的事件名,多个事件用逗号分隔
    在下面的例子中,通过Throttle组件来控制其内部button的点击事件,此时连续点击多次,触发clickBtn的次数要比点击的次数小(节流函数通过一个定时器进行处理)。
 <template><div><Throttle :time="1000" events="click"><button @click="clickBtn">click {{count}}</button></Throttle></div>
</template>

下面是具体实现,实现高阶组件的主要功能是在render函数中对当前插槽中的vnode进行处理

const throttle = function (fn, wait = 50, ctx) {let timerlet lastCall = 0return function (...params) {const now = new Date().getTime()if (now - lastCall < wait) returnlastCall = nowfn.apply(ctx, params)}
}export default {name: 'throttle',abstract: true,props: {time: Number,events: String,},created() {this.eventKeys = this.events.split(',')this.originMap = {}this.throttledMap = {}},// render函数直接返回slot的vnode,避免外层添加包裹元素render(h) {const vnode = this.$slots.default[0]this.eventKeys.forEach((key) => {const target = vnode.data.on[key]if (target === this.originMap[key] && this.throttledMap[key]) {vnode.data.on[key] = this.throttledMap[key]} else if (target) {// 将原本的事件处理函数替换成throttle节流后的处理函数this.originMap[key] = targetthis.throttledMap[key] = throttle(target, this.time, vnode)vnode.data.on[key] = this.throttledMap[key]}})return vnode},
}

我们还可以进一步封装,通过debounce函数来实现Debounce组件,可见高阶组件的作用,就是为了增强某个组件而存在的。关于高阶组件的其他应用,可以参考HOC(高阶组件)在vue中的应用。

小结
本文整理了几种实现Vue组件的技巧

  • 以counter计数器组件为例,展示了通过v-model语法糖同步父子组件的方式
  • 以表单验证组件为例,展示了通过获取子组件的实例来封装组件的方法
  • 以全局弹窗组件为例,展示了手动mount挂载组件封装API组件的方式
  • 以throttle节流组件为例,展示了在vue中一种实现高阶组件的方式
    在了解Vue的API之后,理解上面的概念都比较轻松,封装组件,除了对于API的熟练度之外,更多地是考察JavaScript基础。Vue入门十分轻松,但是要写好优雅的Vue代码,也是一份不小的学问。

封装Vue组件的原则及技巧相关推荐

  1. Vue2组件封装 Vue组件封装

    写在前面 虽然是Vue2组件封装,主要的内容是记录一下我对封装组件的一些要点和我的看法 --原学习视频来源于b站黑马从0到1封装组件库 什么是组件 都说Vue是组件化开发,确实有道理,别说按钮输入框这 ...

  2. 封装 vue 组件的过程记录

    在我们使用vue的开发过程中总会遇到这样的场景,封装自己的业务组件. 封装页面组件前要考虑几个问题: 1.该业务组件的使用场景 2.在什么条件下展示一些什么数据,数据类型是什么样的,及长度颜色等 3. ...

  3. 提升效率的Vue组件开发和实战技巧

    大家好我是若川. 现在的大前端时代,是一个动荡纷争的时代,江湖中已经分成了很多门派,主要以Vue,React还有Angular为首,形成前端框架三足鼎立的局势.Vue在前端框架中的地位就像曾经的 jQ ...

  4. 手把手教你封装 Vue 组件并使用 NPM 发布

    Vue 开发插件 我们可以先查看Vue的插件的开发规范 我们开发的之后期望的结果是支持 import.require 或者直接使用 script 标签的形式引入,就像这样: ps: 这里注意一下包的名 ...

  5. vscode svn使用_使用Typescript封装Vue组件

    一.搭建项目以及初始化配置 vue create ts_vue_btn 这里使用了vue CLI3自定义选择的服务,我选择了ts.stylus等工具.然后创建完项目之后,进入项目.使用快捷命令code ...

  6. 简述封装vue组件的过程

    组件可以提升整个项目的开发效率,能够把页面抽象成多个相对独立的模块,解决 了我们传统项目开发:效率低.难维护.复用性等问题. 分析需求:确定业务需求,把页面中可以复用的结构,样式以及功能,单独抽离成一 ...

  7. 灵活的Vue组件——原来这么简单

    本篇学习目标 能够理解vue组件概念和作用 能够掌握封装组件能力 能够使用组件之间通信 能够完成todo案例 1. vue组件 1.0_为什么用组件 以前做过一个折叠面板 需求: 现在想要多个收起展开 ...

  8. 【Vue知识点- No4.】vue组件、组件通信、Todo案例

    知识点自测 this指向 let obj = {fn: function(){// this指向此函数的调用者},fn () {// this指向当前函数的调用者 (如果都是在vue里, this指向 ...

  9. vue组件_组件通信_todo案例

    今日学习目标 能够理解vue组件概念和作用 能够掌握封装组件能力 能够使用组件之间通信 能够完成todo案例 1. vue组件 1.0_为什么用组件 以前做过一个折叠面板 [外链图片转存失败,源站可能 ...

最新文章

  1. 使用PhoneGap开启移动开发之旅
  2. MarkDown常用技巧总结
  3. 1270: [BeijingWc2008]雷涛的小猫
  4. UI5 setBusyIndicatorDelay
  5. ASP.NET Core 3.0 迁移避坑指南
  6. 7-3 堆中的路径 (25 分)
  7. C++学习之路 | PTA(天梯赛)—— L2-013 红色警报 (25分)(带注释)(并查集)(精简)
  8. SQL Server -- SQL NULL值,ISNull(),Oracal NVL(),MYSQL IFNULL(),COALESCE()
  9. JVM初学之类的加载过程
  10. Xshell连接Linux下Oracle无法回退的解决办法
  11. IT民工系列——c#操作开心网001,实现几乎所有SNS操作!
  12. Hyper-V用差异磁盘克隆系统
  13. 解决ifconfig没有网卡问题
  14. 逆向libbaiduprotect(二)
  15. 如何搭建Vue开发环境的步骤
  16. 密码学--CTF Crypto
  17. UPC6605 所罗门王的宝藏
  18. 小程序_图片剪切功能(支持多图片上传)
  19. 黑苹果 wifi android,黑苹果目前已可以完美驱动内置intel WiFi
  20. Failed connect to github-production-release-asset-2e65be.s3.amazonaws.com:443问题

热门文章

  1. STM32 HAL库学习笔记1-HAL库简介
  2. OpenNI 1.3.2.1发布
  3. 第七届中国云计算大会 | 首批主会演讲嘉宾和议题公布(表)
  4. 谷歌将投资10亿美元在旧金山湾区新建约2万套住房
  5. python web server 知乎_“知乎网”技术方案初探
  6. 知名软件 XMind 竟然请求其它网站发布其盗版软件
  7. 省首例AI犯罪案:归咎技术是欲加之罪?
  8. 小米Java笔试_JAVA后端笔试试题(一)
  9. 测试工具和测试自动化
  10. 高斯定理证明(HTML)