Vue组件继承与扩展

news2024/10/10 19:14:47

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 组件复用和扩展之道

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

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

相关文章

广州自闭症寄宿学校有哪些?选择最适合孩子的学校

在广州这座繁华而充满人文关怀的城市里&#xff0c;有一群特殊的孩子&#xff0c;他们被称为“星星的孩子”——自闭症儿童。他们生活在自己的世界里&#xff0c;对外界的刺激反应迟钝或过度敏感&#xff0c;社交互动困难&#xff0c;语言表达受限。然而&#xff0c;在广州&…

高中数学基础

1.1函数的定义与性质 01函数定义 D是一个非&#xff0c;空时数集&#xff0c;对于D中的每一个X都有一个对应的规则f&#xff0c;能相应只对应唯一的一个实数Y&#xff0c;那么可以称Yf(X)&#xff0c;是一个函数。 判断是否为同一个函数&#xff0c;需要满足两个条件&#xff…

基于springboot和vue.js 养老院管理系统设计与实现

博主介绍&#xff1a;专注于Java&#xff08;springboot ssm springcloud等开发框架&#xff09; vue .net php phython node.js uniapp小程序 等诸多技术领域和毕业项目实战、企业信息化系统建设&#xff0c;从业十五余年开发设计教学工作 ☆☆☆ 精彩专栏推荐订阅☆☆☆…

LLM-RAG相关常见面试题

#############【持续更新】############## LLM-RAG相关常见面试题 1. RAG技术体系的总体思路 RAG可分为5个基本流程&#xff1a;知识文档的准备&#xff1b;嵌入模型&#xff08;embedding model&#xff09;&#xff1b;向量数据库&#xff1b;查询检索和生产回答。 参考&a…

山西省中小学生学籍照片手机拍照集中采集指南

随着山西省教育信息化的持续发展&#xff0c;学校管理的数字化转型中&#xff0c;学籍信息的精确记录变得尤为关键。在这一背景下&#xff0c;学籍管理系统的优化升级显得尤为重要。为了保障学生资料的精确无误&#xff0c;山西省对中小学生学籍系统中的照片采集和上传流程提出…

5本一投就中的极速期刊,性价比高,1周-1个月录用,见刊极快!

在当今快节奏的学术界&#xff0c;研究者们不仅追求高质量的研究成果&#xff0c;还希望能够迅速地将这些成果分享给全球的同行。为此&#xff0c;科检易学术精心挑选了10本以高效审稿流程著称的期刊&#xff0c;这些期刊不仅性价比高&#xff0c;而且从投稿到录用的时间极短&a…

使用API有效率地管理Dynadot域名,设置域名服务器(NS)的ip信息

前言 Dynadot是通过ICANN认证的域名注册商&#xff0c;自2002年成立以来&#xff0c;服务于全球108个国家和地区的客户&#xff0c;为数以万计的客户提供简洁&#xff0c;优惠&#xff0c;安全的域名注册以及管理服务。 Dynadot平台操作教程索引&#xff08;包括域名邮箱&…

决策树(descision tree)

一&#xff1a;决策树的基础介绍 决策树(descision tree)是一种基本的分类与回归的方法。决策树是一种对实例进行预测的树型结构。 下面是一个完整的二叉决策树&#xff0c;根据西瓜的几个特征判断西瓜的好坏。 纹理<1.5代表第一个判断条件&#xff0c;根据纹理<1.5是…

【JDK17 | 16】Java 17 深入剖析:密封类(二)

一、密封类的使用场景和优势 什么是密封类&#xff1f; 密封类&#xff08;sealed class&#xff09;是 Java 17 引入的一种新特性&#xff0c;允许开发者控制哪些类可以继承或实现某个类或接口。通过使用密封类&#xff0c;开发者可以定义一组特定的子类&#xff0c;从而提供…

【springboot9733】基于springboot+vue的藏区特产销售平台

作者主页&#xff1a;Java码库 主营内容&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app等设计与开发。 收藏点赞不迷路 关注作者有好处 文末获取源码 项目描述 “互联网”的战略实施后&#xff0c;很多行业的信息化水…

好用的AI工具

目录 1.常用AI工具 1.1.语音助手 1.1.1. 华为小艺 1.1.2. 小米小爱同学 1.2.智能客服 1.2.1. 银行客服 1.2.2. 酒店语音电话 1.3.编程助手 1.3.1. 百度Comate 1.3.2. 华为CodeArts 2.创新AI应用 2.1. 生成式AI 2.2. 自动驾驶技术 2.3. 医疗AI 2.4. 智能客服机器…

简单的maven nexus私服学习

简单的maven nexus私服学习 1.需求 我们现在使用的maven私服是之前同事搭建的&#xff0c;是在公司的一台windows电脑上面&#xff0c;如果出问题会比较难搞&#xff0c;所以现在想将私服迁移到我们公司的测试服务器上&#xff0c;此处简单了解一下私服的一些配置记录一下&am…

conda新建环境中存在大量ros相关python包

1 问题现象 新建的conda环境&#xff0c;执行pip list&#xff0c;出现了大量的ros相关包&#xff0c;环境不纯净。重新安装anaconda没有用。 2 问题原因 2.1 执行python -m site 执行python -m site获得以下结果 其中sys.path包含了’/opt/ros/noetic/lib/python3/dist-…

想要项目顺利进行,企业如何做好节点计划管理?

项目的成功实施对于企业的发展和竞争力提升至关重要。然而&#xff0c;要确保项目顺利进行并非易事&#xff0c;其中做好节点计划管理是关键所在。一个精心策划和有效执行的节点计划&#xff0c;能够为项目的推进提供清晰的路线图&#xff0c;帮助企业合理分配资源、控制进度、…

VR虚拟场景:重塑沉浸式购物体验的新篇章

在科技日新月异的今天&#xff0c;虚拟现实&#xff08;VR&#xff09;技术正以前所未有的速度改变着我们的生活方式&#xff0c;特别是在消费领域&#xff0c;它正引领着一场前所未有的购物体验革命。通过构建高度逼真的虚拟场景&#xff0c;VR技术为消费者打造了一个超越现实…

修改 antd a-popover气泡卡片弹窗背景颜色

antdv 中 a-popover 样式修改不生效的问题 因为 popover 元素添加到了 body 下面&#xff0c;增加下面这几行代码&#xff0c;将 popover 添加到它原本的父级下面&#xff0c;然后用 ::v-deep 去修改样式就可以 1.效果图 2.代码 主要的代码就是 :getPopupContainer"(tri…

【笔记】Day2.4表设计说明

主键ID一般使用bigint类型 运送类型 使用比int更小的tinyint类型 eg&#xff1a;普快代表1 特快代表2&#xff08;没写反&#xff09; 关联城市 varchar 2代表京津冀 3代表江浙沪 4代表川渝 首重和续重都有小数点 故使用double 轻抛系数都为整数 故使用int 创建时间和修改…

Perforce静态分析工具2024.2新增功能:Helix QAC全新CI/CD集成支持、Klocwork分析引擎改进和安全增强

Perforce Helix QAC和Klocwork的最新版本对静态分析工具进行了重大改进&#xff0c;通过尽早修复错误、降低开发成本和加快发布速度&#xff0c;使开发团队实现左移。 本文中&#xff0c;我们将概述2024.2版本的新特性和新功能。 CI/CD和左移以实现持续合规性 现代软件开发实…

《Programming from the Ground Up》阅读笔记:p217-p238

《Programming from the Ground Up》学习第11天&#xff0c;p217-p238总结&#xff0c;总计22页。 一、技术总结 1.C compiling p216, C compiling is split into two stages - the preprocessor and the main compiler。 注&#xff1a;感觉这个写法不好&#xff0c;因为p…

开源AI智能名片链动2+1模式S2B2C商城小程序源码与工业4.0的融合发展:机遇与挑战

摘要&#xff1a;本文探讨了工业4.0的三大主题&#xff0c;即智能工厂、智能生产和智能物流&#xff0c;分析在各主题下开源AI智能名片链动21模式S2B2C商城小程序源码与之融合的可能性、带来的机遇以及面临的挑战&#xff0c;旨在为相关产业的协同发展提供理论参考。 一、引言 …