封装Vue组件的一些技巧

封装Vue组件的一些技巧

本文同步在个人博客shymean.com上,欢迎关注

写Vue有很长一段时间了,除了常规的业务开发之外,也应该思考和反思一下封装组件的正确方式。以弹窗组件为例,一种实现是在需要模板中引入需要弹窗展示的组件,然后通过一个flag变量来控制弹窗的组件,在业务代码里面会充斥着冗余的弹窗组件逻辑,十分不优雅。

本文整理了开发Vue组件的一些技巧,包含大量代码示例。

开发环境

vue-cli3提供了非常方便的功能,可以快速编写一些测试demo,是开发组件必备的环境。下面是安装使用步骤

// 全局安装vue-cli3
npm install -g @vue/cli
vue -V // 查看版本是否为3.x

// 安装扩展,此后可以快速启动单个vue文件
npm install -g @vue/cli-service-global

// 快速启动demo文件
vue serve demo.vue
复制代码

如果需要scss,则还需要在目录下安装sass-loader等。

下面是使用vue-cli3可能会遇见的几个问题,更多使用教程可以参考:一份超级详细的Vue-cli3.0使用教程[赶紧来试试!]

自定义入口文件

如果需要(比如需要开发移动端的组件),可以在使用vue serve时自定义html入口文件,在根目录下编写index.html,并确保页面包含#app的dom即可。

引入公共混合文件

通过style-resources-loader在每个文件引入公共样式混合等,参考自动化导入

需要访问Vue全局对象

在某些时候需要放问全局Vue对象,如开发全局指令、插件时

import Vue from "vue"
import somePlugin from "../src/somePlugin"

Vue.use(somePlugin)
复制代码

上面这种写法并不会生效,这是因为vue serve xxx.vue仅仅只能作为快速原型开发的方案,使用的Vue与 import引入的Vue不是同一个对象。一种解决办法是手动指定vue serve的入口文件

// index.js
import Vue from "../node_modules/vue/dist/vue.min"
import placeholder from "../src/placeholder/placeholder"

Vue.use(placeholder)

new Vue({
    el: "#app",
    template: ``,
    created(){},
})
复制代码

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-minuson-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接收modelrule两个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 = true

                let findErrorByProp = (errors, prop) => {
                    return errors.find((error) => {
                        return error.field === prop
                    }) || ""
                }

                validator.validate(this.model, (errors, fields) => {
                    this.formItems.forEach(formItem => {
                        let prop = formItem.prop
                        let 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>
复制代码

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

// alert.js
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 timer
    let lastCall = 0
    return function (...params) {
        const now = new Date().getTime()
        if (now - lastCall < wait) return
        lastCall = now
        fn.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] = target
                this.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.js

原文地址:https://www.cnblogs.com/mouseleo/p/11064773.html

时间: 2024-10-12 13:19:28

封装Vue组件的一些技巧的相关文章

vue组件篇(2)---封装组件并发布到npm

瞎说说 vue小白,要进行应用开发:你们都用什么UI框架,求指教,不想踩坑(想太多,不踩坑???不存在的!!!too young too simple-) 同学1:pc端我用element UI,感觉组件的API方法.属性比较完善...?? 同学2:哈?我个人感觉他的样式有些生硬,不够炫酷美观??,用iView吧,组件丰富,动画效果cool~~~?? 同学3:真的吗?阔是有人说,iView组件虽然完美的把其他UI组件有的缺点完美避过,但是某某组件不够人性化....?? 同学4:用vue-beau

vue单文件组件构建vue项目的若干技巧或建议

接触vue一年了,由于早前都是碎片化的学习与练习,缺乏深入的理解与总结,所以感觉收效甚微.最近利用闲暇时间撸了一个基于vue全家桶的天气应用webapp并做一些项目总结,以巩固自己所学,加深对vue的理解. 前端框架的选择 在web移动端应用快速崛起的今天,webapp,hybird app挤压传统原生app的趋势越发明显,产品的快速更新迭代.跨平台适配,催生了越来越多五花八门的前端框架.为了应对各式各样的产品需求,适应快速高效的开发工作,我们也必须跟上步伐,所以掌握一门甚至多门前端框架显得尤为

JS组件系列——又一款MVVM组件:Vue(二:构建自己的Vue组件)

阅读目录 一.为什么组件很重要 二.Vue里面的组件基础知识 1.组件的概念 2.组件原理 3.组件使用 三.封装自己的Component 1.使用Component封装bootstrapTable 2.封装select 3.查看其他Vue框架源码 四.总结 正文 前言:转眼距离上篇 JS组件系列--又一款MVVM组件:Vue(一:30分钟搞定前端增删改查) 已有好几个月了,今天打算将它捡起来,发现好久不用,Vue相关技术点都生疏不少.经过这几个月的时间,Vue的发展也是异常迅猛,不过这好像和博

Vue组件基础用法

前面的话 组件(Component)是Vue.js最强大的功能之一.组件可以扩展HTML元素,封装可重用的代码.根据项目需求,抽象出一些组件,每个组件里包含了展现.功能和样式.每个页面,根据自己所需,使用不同的组件来拼接页面.这种开发模式使前端页面易于扩展,且灵活性高,而且组件之间也实现了解耦.本文将详细介绍Vue组件基础用法 概述 组件是一个自定义元素或称为一个模块,包括所需的模板.逻辑和样式.在HTML模板中,组件以一个自定义标签的形式存在,起到占位符的功能.通过Vue.js的声明式渲染后,

vue组件讲解(is属性的用法)

什么是组件? 在说之前我们先搞清楚什么是组件?这样对我们下边的学习是很有帮助的. 组件(Component)是Vue.js最强大的功能之一.组件可以扩展HTML元素,封装可以重复使用的代码.在较高层次上,组件就是自定义元素,Vue.js的编辑器为它添加特殊功能.在有些情况下,组件也可以是原生元素的形式,以is特性进行扩展. 怎么使用组件? 说道这里,组件是什么,我们都有了清楚的了解,但是怎么使用呢? 首先,我们要进行注册,才能进行使用 注册 之前我们也说过,可以通过new来创建一个VUE实例 n

react初探(二)之父子组件通信、封装公共组件

一.前言 在组件方面react和Vue一样的,核心思想玩的就是组件,下面举两个组件常用的情景. 场景一:假如我们现在有一个页面包含表格以及多个弹框,这种时候如果将这个页面的业务代码写在一个组件中,那么这一块的代码会看着非常恶心.如果这个时候我们将这个页面的表格以及弹框这些单独的模块分别写成组件的形式,然后再在这个页面中将这些组件引入进来,那样我们的代码会看着非常整洁.这样做会需要使用到父子组件之间的通信,下面会详细解释. 场景二:日常项目中我们会经常遇到某一个功能会在不同地方使用,但是每次使用的

【转】Vue组件一-父组件传值给子组件

Vue组件一-父组件传值给子组件 开始 Vue组件是学习Vue框架最比较难的部分,而这部分难点我认为可以分为三个部分学习,即 组件的传值 - 父组件向子组件中传值 事件回馈 - 子组件向父组件发送消息,父组件监听消息 分发内容 整个博客使用的源代码-请点击 所以将用三篇博客分别进行介绍以上三种情况和使用 Vue的设计者对组件的理解 Vue的设计者,对组件和父组件之间的关系流上做了阐述,即单向数据流图:父组件向子组件传递数据,子组件回馈事件 组件意味着协同工作,通常父子组件会是这样的关系:组件 A

Vue 使用中的小技巧

在vue的使用过程中会遇到各种场景,当普通使用时觉得没什么,但是或许优化一下可以更高效更优美的进行开发.下面有一些我在日常开发的时候用到的小技巧,在下将不定期更新~ 1.多图表resize事件去中心化 1.1 一般情况 有时候我们会遇到这样的场景,一个组件中有几个图表,在浏览器resize的时候我们希望图表也进行resize,因此我们会在父容器组件中写: mounted() { setTimeout(() => window.onresize = () => { this.$refs.char

vue组件间通信六种方式(完整版)

vue组件间通信六种方式(完整版) 前言 组件是 vue.js最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着不同组件之间的数据无法相互引用.一般来说,组件可以有以下几种关系: 如上图所示,A 和 B.B 和 C.B 和 D 都是父子关系,C 和 D 是兄弟关系,A 和 C 是隔代关系(可能隔多代). 针对不同的使用场景,如何选择行之有效的通信方式?这是我们所要探讨的主题.本文总结了vue组件间通信的几种方式,如props.$emit/$on.vuex.$parent / $chil