Vue组件继承与扩展

Vue组件继承与扩展

前言

与Class继承类似,在Vue中可以通过组件继承来达到复用和扩展基础组件的目的,虽然它可能会带来一些额外的性能损耗和维护成本,但其在解决一些非常规问题时有奇效。本文将通过一些非常规的功能需求来讨论其实现过程。

基础实现

进入正题之前,我们先来看一下Vue2中是如何实现组件逻辑、数据状态复用的(Vue3中推荐使用组合式API,因此不再说明)。

Props

基础组件内容如下,根据传入的type显示不同内容

<template><div><div v-if="type == 1">内容1</div><div v-else-if="type == 2">内容2</div><div v-else-if="type == 3">内容3</div></div>
</template>
<script>
export default {props: ['type']
}
</script>

父组件使用

<base-component :type="1"></base-component>
<base-component :type="2"></base-component>

在这里插入图片描述

这种方式存在明显的问题:组件内如果存在大量条件判断,可读性和可维护性会变差

Slot

基础组件内容如下,父组件可在指定位置自定义内容

<template><div><slot>默认内容</slot><slot name="footer"></slot></div>
</template>

父组件使用

<base-component>替换默认内容<template slot="footer"><button>底部插入按钮</button></template>
</base-component>

在这里插入图片描述

这种方式也存在一个问题:slot内元素从属于父组件的上下文,某些场景下不易拆分逻辑

Mixin

混入 (mixin) 提供了一种非常灵活的方式来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。

假设我们有多个页面需要用到同一个方法,就可以将其抽离到单独文件中

export default {data(){return {count: 0}},methods: {increment(){this.count++;},decrement(){this.count--;}}
}

然后在需要使用的组件中混入即可

<template><div><div>{{ count }}</div><el-button size="small" @click="increment">数量增加</el-button><el-button size="small" @click="decrement">数量减少</el-button></div>
</template><script>
import countMix from './count-mix'
export default {mixins: [ countMix ]
}
</script>

在这里插入图片描述

如果组件内选项与mixin冲突,一般遵循如下规则(也可以通过自定义合并策略改变默认行为,这里不做赘述)

  • 数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先

  • 同名钩子函数将合并为一个数组,因此都将被调用(混入对象的钩子将在组件自身钩子之前调用)

  • 值为对象的选项(如 methods、components 和 directives)将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。

当然mixin也存在一些问题:

  • 多个mixin之间命名冲突
  • 难以定位异常报错来源

SlotScope

有些情况下需要让插槽内容能够访问子组件中的数据,进行自定义展示而非固定显示,此时作用域插槽就派上用场了。

基础组件内容如下,对外暴露一些数据

<template><div><slot :user="user"></slot></div>
</template>
<script>export default {data(){return{user:{name: '张三',age: 18,gender: '男'}}}}
</script>

父组件中使用

<template><div><base-comp><template slot-scope="{user}"><div>姓名:{{ user.name }}</div><div>年龄:{{ user.age }}</div><div>性别:{{ user.gender }}</div></template></base-comp></div>
</template>
<script>export default {components:{baseComp:()=>import('./base-comp.vue')}}
</script>

在这里插入图片描述

在这个例子中,基础组件仅对外提供数据,实际上是不需要定义模板的。针对这种情况,无渲染组件将会是一个很好的方式。

基础组件内容改写如下,使用render代替template,父组件使用方式相同。

<script>
export default {data() {return {user: {name: '张三',age: 18,gender: '男'}}},render() {return this.$scopedSlots.default({user: this.user})}
}
</script>

因无渲染组件与模板无关,仅提供数据,因此非常灵活,可自由组合实现不同展示。但其并不像前几种方式通用,所以一般仅用于组件库开发(下面单独介绍)。

扩展方法

上面几种方式是日常开发中较为常用的实现组件复用和扩展的方式,应对绝大多数开发场景是没问题的。但在一些特殊情况下似乎就不够用了,比如移除项目中所有输入框内容的前后空格。显然我们不可能逐个页面去处理,此时需要考虑如何全局改造。

上述几种实现方式在处理自己封装的组件时非常有效,但在处理第三方组件时似乎就不太好用了,我们没办法直接修改三方组件代码来为我们的实际需求服务。

比如下面这种实现方式:二次封装输入框组件my-input,替换原有组件el-input。

<template><el-input v-model.trim="newValue" @change="handleChange"></el-input>
</template>
<script>export default {name: 'my-input',data(){return {newValue: ''}},props: {value: {type: String,default: ''}},watch:{value(val){this.newValue = val;}},methods: {handleChange(val){this.$emit('input', val)}}}
</script>

看似没有问题,但我们仍然需要确认几个关键问题,比如

  • 全局替换的工作量和覆盖率
  • 做了一层封装会不会对原功能造成影响,比如一些自定义事件会不会被覆盖

显然我们不能保证其完全没问题,因此我们需要一些更加合理且精简的做法。接下来以这个输入框的需求来介绍几种常见的实现思路。

Fork+PR

拉取对应的第三方包的源代码仓库,修改源代码后发布到公服(非同名)或私服(非同版本)即可。这种方式较为常规且简单,但我们需要考虑两种情况:

如果需求点是一个稳定Bug或者通用需求,就可以提交一个PR。如果你的PR被作者接受并且合并到主线版本,那么就可以把项目中的包换回官方的包,而无需继续维护自己的版本。

而如果需求点仅仅是自己项目的定制化需求,那么提PR显然就不合理了。而单独维护自己的包又会涉及到同步官方版本等相关问题,后续处理相对麻烦。

就像上面提到移除输入框前后空格的需求,单独为了这个点而维护一个包,显然得不偿失。此时可以考虑一种较为简单的做法:源码补丁。

patch-package

即在修改node_modules中包的源码后,将修改的部分进行打补丁操作(生成对应的补丁文件),方便团队共享修改内容。简单使用方法如下

修改包源码
// node_modules/element-ui/lib/input.js
created: function created() {this.$on('inputSelect', this.select);
+    this.$on('change',(value)=>{
+        this.$emit('input', value.trim());
+    })
},
安装依赖
npm install patch-package --save-dev
或者
yarn add patch-package postinstall-postinstall
生成补丁
// 添加命令 package.json
"scripts": {
+    "postinstall": "patch-package"
}
// 执行命令 npx patch-package package-name
npx patch-package element-ui

在这里插入图片描述

验证

删除node_modules,重新安装依赖,此时会自动执行命名,将补丁内容更改到源码中

在这里插入图片描述
在这里插入图片描述

这种实现方式建议在依赖包版本锁定的情况下使用,否则会导致一些异常。

Coverage

如果觉得打补丁的方式还是太麻烦,还有一种简单粗暴的实现方式:通过同名组件覆盖的方法来替换掉三方库的组件。简单实现如下

复制node_module/element-ui/packages/input/src/input.vue文件到本地,根据需求修改代码

// src/components/input.vue
created: function created() {this.$on('inputSelect', this.select);
+    this.$on('change',(value)=>{
+        this.$emit('input', value.trim());
+    })
},

全局注册同名组件

import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI)
// 注册同名组件
import Input from './components/input.vue'
Vue.component('ElInput',Input)

同样建议在依赖包版本锁定的情况下使用,否则会导致一些异常。

这种实现方式在实际开发中非常有效,比如在三方组件中多加几个插槽、改动DOM结构,如果直接改动源码,代价很大;而复制一份组件代码到本地修改,全局注册覆盖,则非常简便。当然为了避免影响到其它功能,也可以不用同名,完全当做一个新的组件处理。

Component.extends

如果你觉得上面几种方式还是太复杂,不够优雅,那么我们可以用到官方提供的一个组件扩展方式:extends。

extends允许一个组件扩展另一个组件,继承其组件选项,从实际效果上看几乎与mixins一致,但两者的含义完全不同,mixins主要用于组合功能块,而extends主要用于继承(合并策略一致)。一般使用形式如下

const CompA = { ... }const CompB = {extends: CompA,...
}

由此上述需求就可以改写为

import ElementUI from 'element-ui'
// 扩展
Vue.component('el-input', {extends: ElementUI.Input,created(){this.$on('change', (value) => {this.$emit('input', value.trim())})}
})

是不是相当简便?没有额外的操作,几行代码就可以替换上面几种方式的所有操作。

利用这种方式可以处理很多特殊场景,如下拉框因为某条数据过长导致宽度很大,影响样式美观

在这里插入图片描述

我们希望下拉那部分的宽度要和上方保持一致,就可以这么处理

// 扩展el-select,设置下拉宽度
Vue.component('el-select', {extends: ElementUI.Select,mounted(){// 设置下拉宽度与上方输入框一致this.$refs.popper.$el.style.width = `${this.$el.offsetWidth}px`;}
})
// 扩展el-option,设置超长tip
Vue.component('el-option', {extends: ElementUI.Option,mounted(){// 设置超长的titlethis.$el.setAttribute('title',this.currentLabel||'')}
})

在这里插入图片描述

这种实现方式可以应对除修改template外的几乎所有需求,且非常高效。

Render

如果觉得上面几种实现还是不够灵活的话,那么render将会是一个终极解决方案(这一节仅作基础知识点说明,实际应用在后面两节中体现)。

在绝大多数情况下Vue 推荐使用模板来创建HTML,但在一些场景中需要JavaScript的完全编程能力,这时可以用渲染函数,它比模板更接近编译器。

简单来说,在Vue中我们一般使用模板语法构建页面,使用render函数可以让我们通过JavaScript来构建DOM,这样可以免去转译的过程,灵活且高效。

这部分内容较多,大家可以直接查阅官方文档:https://v2.cn.vuejs.org/v2/guide/render-function.html,下面列出一些关键点

基础

假设有这么一个组件:根据传入的值动态生成h1-h4标题

<template><div><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></div>
</template><script>
export default {props: {level: {type: Number,default: 1,required: true}}
}
</script>

显然这个场景下使用模板并不是最好的选择:不但代码冗长,而且在每一个级别的标题中重复书写了slot。于是我们可以尝试使用 render函数重写上面的例子:

<script>
export default {props: {level: {type: Number,default: 1,required: true}},render: function (h) {return h('h' + this.level, // 标签名称this.$slots.default // 子节点)},
}
</script>

在了解渲染函数之前,需要先了解浏览器的工作原理。示例如下

<div><h1>My title</h1>Some text content<!-- TODO: Add tagline -->
</div>

当浏览器读到这些代码时,会建立一个“DOM 节点”树来保持追踪所有内容。如下图所示

在这里插入图片描述

每个元素都是一个节点,每段文字也是一个节点,甚至注释也都是节点,一个节点就是页面的一个部分。

高效地更新所有节点会是比较困难的,好在Vue已经帮我们处理了这个复杂的过程。我们仅需告知Vue页面上的HTML是什么即可,它可以在一个模板里:

<h1>{{ title }}</h1>

也可以在一个渲染函数中

render: function (createElement) {return createElement('h1', this.title)
}

这两种写法,Vue都会自动保持页面的更新,即便 title 发生了改变。

虚拟DOM

Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实DOM,如下代码

return createElement('h1', this.title)

createElement返回的内容并不是一个真实的DOM元素,而是节点的相关信息,因此它更应该被叫做createNodeDescription。它所包含的信息会告诉Vue页面上需要渲染什么样的节点,包括及其子节点的描述信息。这样的节点描述称之为“虚拟节点 (virtual node)”,简写为“VNode”。“虚拟 DOM”是对由 Vue 组件树建立起来的整个 VNode 树的统称。

createElement

标准用法如下

// @returns {VNode}
createElement(// {String | Object | Function}// 一个 HTML 标签名、组件选项对象,或者// resolve 了上述任何一种的一个 async 函数。必填项。'div',// {Object}// 一个与模板中 attribute 对应的数据对象。可选。{// 内容较多,见官方文档:https://v2.cn.vuejs.org/v2/guide/render-function.html#深入数据对象},// {String | Array}// 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,// 也可以使用字符串来生成“文本虚拟节点”。可选。['先写一些文字',createElement('h1', '一则头条'),createElement(MyComponent, {props: {someProp: 'foobar'}})]
)

插槽

可以通过 this.$slots 访问静态插槽的内容,每个插槽都是一个 VNode 数组:

render: function (createElement) {// `<div><slot></slot></div>`return createElement('div', this.$slots.default)
}

也可以通过 this.$scopedSlots 访问作用域插槽,每个作用域插槽都是一个返回若干 VNode 的函数:

props: ['message'],
render: function (createElement) {// `<div><slot :text="message"></slot></div>`return createElement('div', [this.$scopedSlots.default({text: this.message})])
}

如果要用渲染函数向子组件中传递作用域插槽,可以利用 VNode 数据对象中的 scopedSlots 字段:

render: function (createElement) {// `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`return createElement('div', [createElement('child', {// 在数据对象中传递 `scopedSlots`// 格式为 { name: props => VNode | Array<VNode> }scopedSlots: {default: function (props) {return createElement('span', props.text)}}})])
}

JSX和函数式组件

假设有这么一段简单的模板代码

<anchored-heading :level="1"><span>Hello</span> world!
</anchored-heading>

如果使用渲染函数,会发现非常复杂

createElement('anchored-heading',{props: {level: 1}},[createElement('span', 'Hello'),' world!']
)

因此通过Babel 插件在 Vue 中使用 JSX 语法,可以让我们的书写行为更加贴合模板语法

render: function (h) {return (<AnchoredHeading level={1}><span>Hello</span> world!</AnchoredHeading>)
}

函数式组件也非常重要,限于篇幅此处不做详述,可查阅官方文档。

操作模板

上述内容都是继承一个组件并做功能的修改或扩展,这里我们思考最后一个场景:怎么实现继承并修改template模板?

问题

Vue的继承、合并策略中其实是不包含template的,若是在继承的组件中定义模板内容又会覆盖原有模板(所以下面不讨论直接重写模板的情况)。

实现方式

假设我们有一个基础组件,它只有一个简单模板内容,示例如下

<template><div>基础组件,插槽:<slot name="menu"><el-button size=small>默认按钮</el-button></slot></div>
</template>

此时我们要实现两个操作:一是在原模板的基础上追加自定义内容,二是替换原插槽中的默认内容。(下面两种方式仅作参考)

追加内容

既然没办法直接修改模板,那么可以尝试通过render更改渲染逻辑

<script>
import baseComp from './baseComp.vue'
export default {extends: baseComp,render() {// 基础组件rendervar parentRenderer = baseComp.render.apply(this, arguments);// 创建自定义内容var prefix = <span>扩展前置内容</span>var sufix = <span>扩展后置内容</span>// 在原组件基础上添加自定义内容return <div>{prefix}{parentRenderer}{sufix}</div>}
}
</script>

效果如下,在原组件前后各添加一些内容

在这里插入图片描述

替换插槽

上面的方法可以很轻松的在原组件的前后添加自定义内容,而如果我们要把原组件插槽中的按钮换成自定义内容,又该如何处理?

<script>
import baseComp from './baseComp.vue'
export default {extends: baseComp,render() {// 创建自定义内容var prefix = <span>扩展前置内容</span>var sufix = <span>扩展后置内容</span>// 创建插槽内容this.$slots.menu = [h('el-button', {attrs: {type: 'primary'},on: {click: () => { console.log('自定义按钮1') },},}, '新按钮1'),h('el-button', {attrs: {type: 'danger'},on: {click: () => { console.log('自定义按钮2') },},}, '新按钮2'),h('el-button', {attrs: {type: 'plain'},on: {click: () => { console.log('自定义按钮3') },},}, '新按钮3'),];// 在原组件基础上替换插槽内容var parentRenderer = baseComp.render.apply(this, h);// 或 baseComp.render.apply(this, [this.$slots.menu]);return <div>{prefix}{parentRenderer}{sufix}</div>}
}
</script>

效果如下,替换原组件的默认插槽内容

在这里插入图片描述

问题

通过上述方法可以实现简单的“模板合并”,当然在实际开发中并不推荐使用这种方式实现此类需求。修改或替换模板内容可能会导致某些异常,如

  • 样式丢失:原样式依赖DOM层级结构
  • 功能丢失:原组件依赖特定元素
  • 事件丢失:原组件定义事件监听、委托依赖某些元素
  • 性能问题:重写或改变模板可能会导致额外的DOM操作
  • 维护问题:修改原模板会导致组件强耦合

实战应用

假设有这么一个功能:有封装好的列表组件,如Avue这种,只需要传入列配置和数据源即可展示列表。现在要求列表的操作最多显示3个按钮,超出部分收起放入下拉中,类似这样的效果

!在这里插入图片描述

针对已有项目该如何处理(项目有几百个页面,难道要逐个去手动判断?如果这些按钮是带权限的,总不能写死哪些按钮是收起来的吧)。

于是基本原则如下:最好底层适配改造,不要让每个研发去逐个页面修改,否则工作量和稳定性无法保证。

尝试几个方案:

  • 添加额外插槽,同时给dropdown添加一个参数,控制在没有子元素的情况下隐藏
  • 获取插槽的VNode节点,手动渲染成button或者dropdown-item
  • 直接操作DOM,将超出3个的按钮移入到dropdown中
  • 逐个页面修改,将原有插槽写法改为functionList,由底层再进行一次处理

这几种方式虽然能实现功能,但不够优雅。其中甚至有直接操作DOM的做法,显然是不能接受的。

经过思考和实践,最后在第二种方式的基础上,结合render的相关方法,形成了最终方案,下面介绍实现过程。

基础封装

便于大家理解,先做一个简单的table封装,模拟crud组件

<template><el-table :data="data"><!-- 循环列 --><el-table-column v-for="col in columns" :key="col.prop" :label="col.label" :prop="col.prop"></el-table-column><!-- 固定操作列 --><el-table-column fixed="right" label="操作"><!-- 按钮插槽 --><template slot-scope="{row,$index}"><slot name="menu":row="row":index="$index"></slot></template></el-table-column></el-table>
</template><script>
export default {props: {columns: {type: Array,default: () => []},data: {type: Array,default: () => []}}
}
</script>

使用及实现效果如下

<template><my-table :data="tableData" :columns="columns"><template slot="menu" slot-scope="scope"><el-button type="text" @click="handleEdit(scope.row)">编辑</el-button><el-button type="text" @click="handleDelete(scope.row)">删除</el-button></template></my-table>
</template>
<script>export default {components: {myTable: () => import('./myTable')},data(){return {columns: [{ prop: 'name', label: '姓名' },{ prop: 'age', label: '年龄' }],tableData: []}},methods: {// 省略相关数据方法}}
</script>

在这里插入图片描述

改写渲染逻辑

考虑到直接使用插槽会被默认渲染,那么我们就要移除原有插槽,改为手动渲染

<el-table-column fixed="right" label="操作"><!-- 按钮插槽 --><template slot-scope="{row,$index}"><!-- <slot name="menu":row="row":index="$index"></slot> --><render-button :row="row" :index="$index"></render-button></template>
</el-table-column>

封装一个替换组件,实现超出3个按钮放入下拉中

<!-- renderButton.vue -->
<template><div class="column-flex-btn"><!-- 左侧按钮渲染区域 --><!-- leftButtons --><el-dropdown size="small" v-if="rightButtons.length"><el-button icon="el-icon-more" type="text" round size="small"></el-button><el-dropdown-menu slot="dropdown"><!-- 右侧按钮渲染区域 --><!-- rightButtons --></el-dropdown-menu></el-dropdown></div>
</template><script>
export default {props: ['row','index'],computed: {leftButtons() {return []},rightButtons() {return []}}
}
</script>

获取插槽内容

有了渲染容器之后,我们需要拿到用户自定义的按钮信息,显然此时只能通过插槽来获取,那么如何获得插槽内容?输出$scopedSlots会发现menu插槽是一个函数

在这里插入图片描述

直接执行,发现其内容为传入按钮对应的VNode数组

this.$scopedSlots.menu();

在这里插入图片描述

按照需求拆分左右两部分数据

computed: {menuNodes(){let slot = this.table.$scopedSlots.menu;let nodes = [];if(slot){// 排除一些换行、空格等特殊情况,也可以直接过滤button-Tagnodes = slot().filter(t=>t.tag);}return nodes},leftButtons() {// 截取左侧VNodesreturn this.menuNodes.slice(0, 3)},rightButtons() {// 截取右侧VNodesreturn this.menuNodes.slice(3)}
}

渲染子节点

根据Render一节的内容,渲染一个button元素可以通过无模板方式创建,示例如下

render: function (createElement) {// `<el-button>按钮名称</el-button>`return createElement('el-button','按钮名称')
}

createElement返回的是对应的VNode节点,这刚好就是我们执行插槽得到的结果。因此,我们封装一个单纯的render组件,接收VNode,通过render直接返回结果即可

// renderNode.vue
export default {props: ['node'],render() {return this.node}
}

效果如下,实现基本布局

在这里插入图片描述

设置插槽作用域

此时我们点击按钮,发现无法获得行数据

在这里插入图片描述

还是根据Render一节中插槽的相关内容,设置作用域数据只需要在创建或执行插槽时传入对应数据即可

menuNodes(){let slot = this.table.$scopedSlots.menu;let nodes = [];if(slot){nodes = slot({row: this.row,index: this.index}).filter(t=>t.tag);}
}

查看效果,数据获取正常。到此这个需求完美解决。

在这里插入图片描述

插件式组件

梳理上述内容,会发现还剩一大类场景没有覆盖:

  • 现有组件都是固定模板,动态渲染组件如何处理?
  • 现有组件都是在app容器中渲染,如何实现全局(body)组件?
  • js调用组件

因此我们需要一个高自由的组件创建方式,它至少要满足以下两点:

  • 能被随时创建
  • 可以任意指定其渲染位置

这个时候就要考虑使用Vue.extend了。

基础用法

使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。

Vue.extend(options)

示例如下,首先定义一个容器(挂载位置)

<div id="mount-node"></div>

创建构造器,实例化并挂载(这里的模板、数据、挂载位置都可以动态处理)

// 创建构造器
var extConstructor = Vue.extend({template: '<div><h1>{{title}}</h1><h3>{{content}}</h3></div>',data: function () {return {title: '自定义标题',content: '自定义文本内容',}}
})
// 创建实例,并挂载到一个元素上。
new extConstructor().$mount("#mount-node")
// 或
new extConstructor({el:"#mount-node"})

在这里插入图片描述

在示例中,extConstructor是构造器,而非普通组件,因此需要实例化后使用

在这里插入图片描述

需要注意的是,挂载完成后(无论是否指定挂载容器),都可以通过$el获取到对应的DOM,这一点非常重要。因为拿到对应的DOM后,你可以通过任意方式去处理成你想要的结果。

在这里插入图片描述

实现原理

  • extend:主要实现Vue的继承并添加一些方法到子类
  • _init():是Vue实例初始化过程中的核心方法,它完成了组件实例的初始化、状态的初始化、事件的初始化、渲染相关的属性和方法等工作。

内容较多,这里不做赘述,感兴趣的可查看源码:

extend:vue/src/core/global-api/extend.ts

_init():vue/src/core/instancei/init.ts

全局Toast实现

下面来看一个全局toast提示框的简单实现过程。

定义模板
<!--toast.vue-->
<template><div class="my-toast" :class="type" v-if="showToast">{{ message }}</div>
</template><script>
export default {name: 'MyToast',data () {return {showToast: false, // 是否激活toasttype: 'normal', // 提示类型, normal success,fail,warningmessage: '消息提示', // 消息内容duration: 3000 // 显示时间}},
}
</script><style scoped>/**省略**/
</style>
定义入口
// toast.js
import Vue from 'vue'
import myToast from './toast.vue'
const ToastConstructor = Vue.extend(myToast)
// 定义弹出函数
function showToast ({message, type = 'normal', duration = 2000}) {// 实例化const _toast = new ToastConstructor({data () {return {showToast: true,type: type,message: message,duration: duration}}})// 获取真实dom,手动添加到bodyconst element = _toast.$mount().$el;document.body.appendChild(element);// 间隔时间结束,隐藏setTimeout(() => { _toast.showToast = false }, duration)
}// 暴露注册事件,全局挂载
showToast.install = (Vue) => {Vue.prototype.$toast = showToast
}export default showToast
全局引入
// main.js
import toast from './toast,js'
Vue.use(toast)
实现效果

在这里插入图片描述

后记

本文主要讨论了Vue中继承和扩展组件的几种实现方式,一般情况下可以满足大部分场景需求。值得注意的是,扩展组件要比书写一般组件更加严格,需要防止对原有功能或性能上造成大的影响。如果无法确认扩展的可行性,建议还是书写普通组件,即使它会浪费一些时间去维护重复内容。

简单做个总结

  • 普通需求(处理本地开发组件):可以通过props、Slot、SlotScope、Mixin等方式处理
  • 定制需求(处理第三方组件):可以通过Fork仓库+PR、公服或私服、patch-package、组件同名覆盖、extends等方式处理
  • 特殊需求(统一处理底层逻辑):可以通过render、操作子模板、构造器等方式处理

如果最后还是未能实现你的需求,那么别忘了前端有三大核心:HTML、JavaScript、CSS,任何前端框架都脱离不了这三个基础。对于前端研发人员而言,只要能拿到HTML,就可以利用JavaScript和CSS对其处理去实现各种功能。

在实际开发中,不仅仅是为了实现业务功能,还要注意代码书写的稳定性和可扩展性,遵循开闭原则。在遇到特殊场景而没有思路时,可从最基础的方法开始开始尝试,直到最终的底层解决方案。掌握以上方法几乎可以应对现阶段能遇到的所有业务场景和技术需求,这部分内容非常重要,谨慎对待。

参考

vue2中文网

vue extends继承后修改template的解决方案

Vue.js 组件复用和扩展之道

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1559883.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

锐明Crocus系统 RepairRecord.do SQL注入漏洞

0x01 产品描述&#xff1a; 明锐技术是一家专注于AI和视频技术的商用车智能物联&#xff08;AIoT&#xff09;解决方案提供商&#xff0c;Crocus系统是其核心产品之一。该系统旨在利用人工智能、高清视频、大数据和自动驾驶技术&#xff0c;提高企业或车队的运营效率&#xff0…

python31_format方法的使用

format方法的使用 def str_format(c, d):"""格式化字符串函数本函数的目的是通过给定的参数c和d&#xff0c;去填充一个预设的字符串模板。模板中包含了{name1}和{name2}两个占位符&#xff0c;分别用参数c和d的值来替换。这个过程演示了Python中字符串格式化的…

AR虚拟试用,让网购不再只靠想象!

在数字化浪潮席卷全球的今天&#xff0c;电子商务已成为我们日常生活中不可或缺的一部分。然而&#xff0c;传统的网购体验往往受限于二维图片和文字描述&#xff0c;消费者在购买前只能依靠想象来构建商品的模样与适用性&#xff0c;这无疑增加了购物的不确定性和风险。幸运的…

基于卷积神经网络的脊柱骨折识别系统,resnet50,mobilenet模型【pytorch框架+python】

更多目标检测和图像分类识别项目可看我主页其他文章 功能演示&#xff1a; 基于卷积神经网络的脊柱骨折识别系统&#xff0c;resnet50&#xff0c;mobilenet【pytorch框架&#xff0c;python&#xff0c;tkinter】_哔哩哔哩_bilibili &#xff08;一&#xff09;简介 基于卷…

Java面试宝典-Java集合02

目录 Java面试宝典-Java集合02 21、TreeMap 和 TreeSet 在排序时如何比较元素&#xff1f; 22、ArrayList 和 LinkedList 的区别是什么&#xff1f; 23、ArrayList 和 Vector 的区别&#xff1f; 24、队列和栈是什么&#xff1f;有什么区别&#xff1f; 25、Queue和Deque的区别…

版本控制系统Helix Core的常见使用误区及解决办法、实用工具及新功能介绍

日前&#xff0c;Perforce携手合作伙伴龙智一同亮相Unreal Fest 2024上海站&#xff0c;分享Helix Core版本控制系统及其协作套件的强大功能与最新动态&#xff0c;助力游戏创意产业加速前行。 Perforce解决方案工程师Kory Luo在活动主会场&#xff0c;带来《Perforce Helix C…

谈谈英国硕士毕业论文如何收集问卷数据

在之前几期文章中&#xff0c;英国翰思教育小编介绍了在写英国硕士毕业论文的时候如何设计问卷&#xff0c;以及设计问卷时需要注意的一些原则和细节。希望通过那些往期内容&#xff0c;可以帮助同学们正确认识毕业论文问卷设计&#xff0c;也可以帮助大家找到合适的问卷设计思…

vantUI用popup和picker和field做关键词搜索功能

<van-field class"mainFileCss" v-model"currentflowmeterLocation" left-icon"search" click"openPicker" center label"位号"placeholder"请输入位号"><template slot"button"></temp…

Linux查看下nginx及使用的配置文件

1、查到nginx进程 ps -aef | grep nginx2、通过进行pid查到nginx路径 pwdx <pid>3、根据路径得到配置文件 path***/nginx -t如下&#xff1a;

基于xml配置文件的Spring事务

在项目中对事务属性通常传播属性&#xff0c;回滚属性&#xff0c;隔离级别&#xff0c;超时属性都取默认值&#xff0c;只有只读属性会如下的配置&#xff1a; 什么意思&#xff1a;Service层你的类里的方法&#xff0c;以get&#xff0c;find&#xff0c;select等开头的方法是…

shell注意点

变量命名&#xff1a; 注意&#xff0c;变量名和等号之间不能有空格&#xff0c;定义变量时&#xff0c;变量名不加美元符号$&#xff0c;这可能和你熟悉的所有编程语言都不一样。同时&#xff0c;变量名的命名须遵循如下规则&#xff1a; 只包含字母、数字和下划线&#xff…

【text2sql】ReFSQL检索生成框架

论文标题为《ReFSQL: A Retrieval-Augmentation Framework for Text-to-SQL Generation》&#xff0c;发表在 EMNLP 2023 上。ReFSQL框架通过结构增强检索器来获取与当前问题语义和模式结构相似的样本&#xff0c;然后通过对比学习机制来引导模型学习到这些样本的特定知识&…

10款超实用的Sketch插件合集,别错过!

在 UI 界面设计领域&#xff0c;Sketch 以其高效、轻便的优势获得了不少设计团队的青睐&#xff0c;帮助全球设计师创造了许多妙不可言的作品。在使用 Sketch 的过程中&#xff0c;使用一些 辅助用的 Sketch 插件&#xff0c;可以让我们更加高效地完成设计任务。本文将将揭秘大…

jenkins中的allure和email问题梳理

一、allure相关 1、我安装了jenkins之后需要再安装allure吗&#xff1f;在jenkins插件中心直接安装allure 1.Allure Jenkins Plugin 只是一个集成插件&#xff0c;它要求你在 Jenkins 服务器上安装 Allure 命令行工具&#xff08;Allure Commandline&#xff09;来实际生成报…

Spring Boot教学资源库:从入门到精通

1绪 论 1.1研究背景 目前&#xff0c;在网络大环境下&#xff0c;越来越多高校开始实行网络教学&#xff0c;利用网络教学方式有利于学生更好的学习。 网络教学是指以计算机及网络为基础&#xff0c;来实现教学资源的上传、存储、传播和共享的教学手段。它是一种教学活动&#…

伪随机调制

伪随机调制是一种利用伪随机序列来调制信号的技术&#xff0c;广泛应用于现代通信系统中。其主要目的在于增强信号的抗干扰能力和隐蔽性&#xff0c;同时提升数据传输的安全性。以下是关于伪随机调制的详细介绍&#xff0c;包括原理、数学表达以及应用。 一、原理 1. 伪随机序…

ITSS-IT服务工程师和ITSS-IT服务经理的区别

培训目的差异&#xff1a;ITSS 服务项目经理的培训旨在为应对 IT 服务行业一线管理人员短缺的问题提供有力支持&#xff1b;而 ITSS 服务工程师的培训则致力于向 IT 服务业输送初级人才&#xff0c;协助企业将一部分内部成本转移至社会。 培训方向区分&#xff1a;ITSS 服务经理…

基于Springboot+Vue的智能推荐旅游平台 (含源码数据库)

1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: SpringBoot自带 apache tomcat 主要技术: Java,Springboot,mybatis,mysql,vue 2.视频演示地址 3.功能 系统中…

【Python】Conda离线执行命令

以下链接证明了想要离线使用conda命令的方法 启用离线模式 — Anaconda documentation 基本上大部分的命令都会提供网络选项 例如creat命令 conda create — conda 24.7.1 文档 - Conda 文档

英国商科毕业论文选题常见错误与解决思路

每到毕业季&#xff0c;英国商科毕业论文的选题和确定题目往往是留学生头痛的一大难题。如何有效确定英国毕业论文题目&#xff1f;如何避免题目中出现一些错误导致研究不能进行&#xff1f;在这篇文章中&#xff0c;翰思教育小编将逐一分析各种可能的问题以及如何避免这些问题…