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(){
// 设置超长的title
this.$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() {
// 基础组件render
var 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-Tag
nodes = slot().filter(t=>t.tag);
}
return nodes
},
leftButtons() {
// 截取左侧VNodes
return this.menuNodes.slice(0, 3)
},
rightButtons() {
// 截取右侧VNodes
return 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, // 是否激活toast
type: 'normal', // 提示类型, normal success,fail,warning
message: '消息提示', // 消息内容
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,手动添加到body
const 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 组件复用和扩展之道