软件编程有一个重要的原则是 D.R.Y(Don't Repeat Yourself),讲的是尽量复用代码和逻辑,减少重复。组件扩展可以避免重复代码,更易于快速开发和维护。那么,扩展 Vue 组件的最佳方法是什么?

Vue 提供了不少 API 和模式来支持组件复用和扩展,你可以根据自己的目的和偏好来选择。

本文介绍几种比较常见的方法和模式,希望对你有所帮助。


扩展组件是否必要

要知道,所有的组件扩展方法都会增加复杂性和额外代码,有时候还会增加性能消耗。

因此,在决定扩展组件之前,最好先看看有没有其他更简单的设计模式能完成目标。

下面几种组件设计模式通常足够替代扩展组件了:

  • props 配合模板逻辑
  • slot 插槽
  • JavaScript 工具函数

props 配合模板逻辑

最简单的方法是通过props结合模板条件渲染,来实现组件的多功能。

比如通过 type 属性:

MyVersatileComponent.vue

<template>
<div class="wrapper">
<div v-if="type === 'a'">...</div>
<div v-else-if="type === 'b'">...</div>
<!--etc etc-->
</div>
</template>
<script>
export default {
props: { type: String },
...
}
</script>

使用组件的时候传不同的type值就能实现不同的结果。

// *ParentComponent.vue*
<template>
<MyVersatileComponent type="a" />
<MyVersatileComponent type="b" />
</template>

如果出现下面两种情况,就说明这种模式不适用了,或者用法不对:

  1. 组件组合模式把状态和逻辑分解成原子部分,从而让应用具备可扩展性。如果组件内存在大量条件判断,可读性和可维护性就会变差。
  2. props 和模板逻辑的本意是让组件动态化,但是也存在运行时资源消耗。如果你利用这种机制在运行时解决代码组合问题,那是一种反模式。

slot(插槽)

另一种可避免组件扩展的方式是利用 slots(插槽),就是让父组件在子组件内设置自定义内容。

// *MyVersatileComponent.vue*
<template>
<div class="wrapper">
<h3>Common markup</div>
<slot />
</div>
</template>
// *ParentComponent.vue*
<template>
<MyVersatileComponent>
<h4>Inserting into the slot</h4>
</MyVersatileComponent>
</template>

渲染结果:

<div class="wrapper">
<h3>Common markup</div>
<h4>Inserting into the slot</h4>
</div>

这种模式有一个潜在约束, slot 内的元素从属于父组件的上下文,在拆分逻辑和状态时可能不太自然。scoped slot会更灵活,后面会在无渲染组件一节里提到。

JavaScript 工具函数

如果只需要在各组件之间复用独立的函数,那么只需要抽取这些 JavaScript 模块就行了,根本不需要用到组件扩展模式。

JavaScript 的模块系统是一种非常灵活和健壮的代码共享方式,所以你应该尽可能地依靠它。

MyUtilityFunction.js

export default function () {
...
}

MyComponent.vue

import MyUtilityFunction from "./MyUtilityFunction";
export default {
methods: {
MyUtilityFunction
}
}

扩展组件的几种模式

如果你已经考虑过以上几种简单的模式,但这些模式还不够灵活,无法满足需求。那么就可以考虑扩展组件了。

扩展 Vue 组件最流行的方法有以下四种:

每一种方法都有其优缺点,根据使用场景,或多或少都有适用的部分。

Composition 函数

组件之间共享状态和逻辑的最新方案是 Composition API。这是 Vue 3 推出的 API,也可以在 Vue 2 里当插件使用。

跟之前在组件定义配置对象里声明datacomputedmethods等属性的方式不同,Composition API 通过一个 setup 函数声明和返回这些配置。

比如,用 Vue 2 配置属性的方式声明 Counter 组件是这样的:

Counter.vue

<template>
<button @click="increment">
Count is: {{ count }}, double is: {{ double }}
</button>
<template>
<script>
export default {
data: () => ({
count: 0
}),
methods: {
increment() {
this.count++;
}
},
computed: {
double () {
return this.count * 2;
}
}
}
</script>

用 Composition API 重构这个组件,功能完全一样:

Counter.vue

<template><!--as above--><template>
<script>
import { reactive, computed } from "vue"; export default {
setup() {
const state = reactive({
count: 0,
double: computed(() => state.count * 2)
}); function increment() {
state.count++
} return {
count,
double,
increment
}
}
}
</script>

用 Composition API 声明组件的主要好处之一是,逻辑复用和抽取变得非常轻松。

进一步重构,把计数器的功能移到 JavaScript 模块 useCounter.js中:

useCounter.js

import { reactive, computed } from "vue";

export default function {
const state = reactive({
count: 0,
double: computed(() => state.count * 2)
}); function increment() {
state.count++
} return {
count,
double,
increment
}
}

现在,计数器功能可以通过setup函数无缝引入到任意 Vue 组件中:

MyComponent.vue

<template><!--as above--></template>
<script>
import useCounter from "./useCounter"; export default {
setup() {
const { count, double, increment } = useCounter();
return {
count,
double,
increment
}
}
}
</script>

Composition 函数让功能模块化、可重用,是扩展组件最直接和低成本的方式。

Composition API 的缺点

Composition API 的缺点其实不算什么——可能就是看起来有点啰嗦,并且新的用法对一些 Vue 开发者来说有点陌生。

关于 Composition API 优缺点的讨论,推荐阅读:When To Use The New Vue Composition API (And When Not To)

mixin

如果你还在用 Vue 2,或者只是喜欢用配置对象的方式定义组件功能,可以用 mixin 模式。mixin 把公共逻辑和状态抽取到单独的对象,跟使用 mixin 的组件内部定义对象合并。

我们继续用之前的Counter组件例子,把公共逻辑和状态放到CounterMixin.js模块中。

CounterMixin.js

export default {
data: () => ({
count: 0
}),
methods: {
increment() {
this.count++;
}
},
computed: {
double () {
return this.count * 2;
}
}
}

使用 mixin 也很简单,只要导入对应模块并在mixins数组里加上变量就行。组件初始化时会把 mixin 对象与组件内部定义对象合并。

MyComponent.vue

import CounterMixin from "./CounterMixin";

export default {
mixins: [CounterMixin],
methods: {
decrement() {
this.count--;
}
}
}

选项合并

如果组件内的选项跟 mixin 冲突怎么办?

比如,给组件定义一个自带的increment 方法,哪个优先级更高呢?

MyComponent.vue

import CounterMixin from "./CounterMixin";

export default {
mixins: [CounterMixin],
methods: {
// 自带的 `increment`` 方法会覆盖 mixin 的`increment` 吗?
increment() { ... }
}
}

这个时候就要说到 Vue 的合并策略了。Vue 有一系列的规则,决定了如何处理同名选项。

通常,组件自带的选项会覆盖来自 mixin 的选项。但也有例外,比如同类型的生命周期钩子,不是直接覆盖,而是都放进数组,按顺序执行。

你也可以通过 自定义合并策略 改变默认行为。

mixin 的缺点

作为扩展组件的一种模式,mixin 对于简单的场景还算好用,一旦规模扩大,问题就来了。不仅需要注意命名冲突问题(尤其是第三方 mixin),使用了多个 mixin 的组件,很难搞清楚某个功能到底来自于哪里,定位问题也比较困难。

高阶组件

高阶组件(HOC)是从 React 借用的概念,Vue 也能使用。

为了理解这个概念,我们先抛开组件,看看两个简单的 JavaScript 函数,increment 和 double

function increment(x) {
return x++;
} function double(x) {
return x * 2;
}

假设我们想给这两个函数都加一个功能:在控制台输出结果。

为此,我们可以用高阶函数模式,新建一个 addLogging函数,接受函数作为参数,并返回一个带有新增功能的函数。

function addLogging(fn) {
return function(x) {
const result = fn(x);
console.log("The result is: ", result);
return result;
};
} const incrementWithLogging = addLogging(increment);
const doubleWithLogging = addLogging(double);

组件如何利用这种模式呢?类似地,我们创建一个高阶组件来渲染Counter组件,同时添加一个decrement方法作为实例属性。

实际代码比较复杂,这里只给出伪代码作为示意:

import Counter from "./Counter";

// 伪代码
const CounterWithDecrement => ({
render(createElement) {
const options = {
decrement() {
this.count--;
}
}
return createElement(Counter, options);
}
});

HOC 模式比 mixin 更简洁,扩展性更好,但是代价是增加了一个包裹组件,实现起来也需要技巧。

无渲染组件

如果需要在多个组件上使用相同的逻辑和状态,只是展示方式不同,那么就可以考虑无渲染组件模式。

该模式需要用到两类组件:逻辑组件用于声明逻辑和状态,展示组件用于展示数据。

逻辑组件

还是回到Counter的例子,假设我们需要在多个地方重用这个组件,但是展示方式不同。

创建一个CounterRenderless.js 用于定义逻辑组件,包含逻辑和状态,但是不包含模板,而是通过 render函数声明 scoped slot

scoped slot暴露三个属性给父组件使用:状态count,方法increment 和计算属性 double

CounterRenderless.js

export default {
data: () => ({
count: 0
}),
methods: {
increment() {
this.count++;
}
},
computed: {
double () {
return this.count * 2;
}
},
render() {
return this.$scopedSlots.default({
count: this.count,
double: this.double,
increment: this.toggleState,
})
}
}

这里的scoped slot是这种模式里逻辑组件的关键所在。

展示组件

接下来是展示组件,作为无渲染组件的使用方,提供具体的展示方式。

所有的元素标签都包含在scoped slot里。可以看到,这些属性在使用上跟模板直接放在逻辑组件里没什么两样。

CounterWithButton.vue

<template>
<counter-renderless slot-scope="{ count, double, increment }">
<div>Count is: {{ count }}</div>
<div>Double is: {{ double }}</div>
<button @click="increment">Increment</button>
</counter-renderless>
</template>
<script>
import CounterRenderless from "./CountRenderless";
export default {
components: {
CounterRenderless
}
}
</script>

无渲染组件模式非常灵活,也容易理解。但是,它没有前面那几种方法那么通用,可能只有一种应用场景,那就是用于开发组件库。

模板扩展

上面的 API 也好,设计模式也罢,都有一种局限性,就是无法扩展组件的模板。Vue 在逻辑和状态方面有办法重用,但是对于模板标签就无能为力了。

有一种比较 hack 的方式,就是利用 HTML 预处理器,比如 Pug,来处理模板扩展。

第一步是创建一个基础模板.pug文件,包含公共的页面元素。还要包含一个 block input ,作为模板扩展的占位符。

BaseTemplate.pug

div.wrapper
h3 {{ myCommonProp }} <!--common markup-->
block input <!--extended markup outlet -->

为了能扩展这个模板,需要安装 Vue Loader 的 Pug 插件。然后就可以引入基础模板并利用block input语法替换占位部分了:

MyComponent.vue

<template lang="pug">
extends BaseTemplate.pug
block input
h4 {{ myLocalProp }} <!--gets included in the base template-->
</template>

一开始你可能会认为它跟 slot 的概念是一样的,但是有个区别,这里的基础模板不属于任何单独的组件。它在编译时跟当前组件合并,而不是像 slot 那样是在运行时替换。

参考资料:

原文

看到这个颇有气质的 logo,不来关注下吗?

Vue.js 组件复用和扩展之道的更多相关文章

  1. vue.js组件化开发实践

    前言 公司目前制作一个H5活动,特别是有一定统一结构的活动,都要码一个重复的轮子.后来接到一个基于模板的活动设计系统的需求,便有了下面的内容.借油开车. 组件化 需求一到,接就是怎么实现,技术选型自然 ...

  2. VUE.JS组件化

    VUE.JS组件化 前言 公司目前制作一个H5活动,特别是有一定统一结构的活动,都要码一个重复的轮子.后来接到一个基于模板的活动设计系统的需求,便有了下面的内容.借油开车. 组件化 需求一到,接就是怎 ...

  3. Vue.js 组件编码规范

    本规范提供了一种统一的编码规范来编写 Vue.js 代码.这使得代码具有如下的特性: 其它开发者或是团队成员更容易阅读和理解. IDEs 更容易理解代码,从而提供高亮.格式化等辅助功能 更容易使用现有 ...

  4. 浅尝Vue.js组件(一)

    本篇目录: 组件名 组件注册 全局注册 基础组件的自动化全局注册 局部注册 在模块系统中局部注册 Prop 单向数据流 Prop验证 类型检查 非Prop特性 替换/合并已有的特性 禁用特性继承 自定 ...

  5. Vue.js 组件的三个 API:prop、event、slot

    组件的构成 一个再复杂的组件,都是由三部分组成的:prop.event.slot,它们构成了 Vue.js 组件的 API.如果你开发的是一个通用组件,那一定要事先设计好这三部分,因为组件一旦发布,后 ...

  6. 如何理解vue.js组件的作用域是独立的

    vue.js组件的作用域是独立,可以从以下三个方面理解: 1.父组件模板在父组件作用域内编译,父组件模板的数据用父组件内data数据:2.子组件模板在子组件作用域内编译,子组件模板的数据用子组件内da ...

  7. Vue.js组件的重要选项

    Vue.js组件的重要选项 实例化Vue对象一些很重要的选项,Vue的所有数据都是放在data里面的,Vue的参数是个对象,对象里面的字段叫做data,data里面也是对象,data也可以写作是thi ...

  8. 打造 Vue.js 可复用组件

    Vue.js 是一套构建用户界面的渐进式框架.我们可以使用简单的 API 来实现响应式的数据绑定和组合的视图组件. 从维护视图到维护数据,Vue.js 让我们快速地开发应用.但随着业务代码日益庞大,组 ...

  9. Vue.js组件学习

    组件可以扩展HTML元素,封装可重用的HTML代码,我们可以将组件看作自定义的HTML元素.组件系统提供了一种抽象,让我们可以使用独立可复用的小组件来构建大型应用. 一个简单组件例子(全局注册) &l ...

随机推荐

  1. Java实现 LeetCode 695 岛屿的最大面积(DFS)

    695. 岛屿的最大面积 给定一个包含了一些 0 和 1 的非空二维数组 grid . 一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直方向上相 ...

  2. Java实现蓝桥杯历届试题区间移位

    问题描述 数轴上有n个闭区间D1,-,Dn.其中区间Di用一对整数[ai, bi]来描述,满足ai < bi.已知这些区间的长度之和至少有10000.所以,通过适当的移动这些区间,你总可以使得他 ...

  3. Java实现 洛谷 P1421 小玉买文具

    import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner in = ...

  4. Java实现 蓝桥杯 历届试题 波动数列

    问题描述 观察这个数列: 1 3 0 2 -1 1 -2 - 这个数列中后一项总是比前一项增加2或者减少3. 栋栋对这种数列很好奇,他想知道长度为 n 和为 s 而且后一项总是比前一项增加a或者减少b ...

  5. Python学习之输入输出入门 A+B篇

    描述 求两个整数之和. 输入 输入数据只包括两个整数A和B. 输出 两个整数的和. 样例输入 1 2 样例输出  3 a=input().split() print(int(a[0])+int(a[1 ...

  6. 深入剖析AQS

    目录 摘要 AbstractQueuedSynchronizer实现一把锁 ReentrantLock ReentrantLock的特点 Synchronized的基础用法 ReentrantLock ...

  7. 利用mitmproxy实现抖音Cookie,设备ID获取(一)

    先讲解一下思路,是利用mitmproxy代理https协议,从而判定抖音个人信息接口,在个人信息接口的返回体接收时将用户信息数据,以及Header头(主要是Cookie),Query体(包含设备ID) ...

  8. 【深度思考】JDK8中日期类型该如何使用?

    在JDK8之前,处理日期时间,我们主要使用3个类,Date.SimpleDateFormat和Calendar. 这3个类在使用时都或多或少的存在一些问题,比如SimpleDateFormat不是线程 ...

  9. v-model 指令来实现双向数据绑定

    <div id="app"> <p>{{ message }}</p> <input v-model="message" ...

  10. centos7 安装 jdk8

    安装主要分为两种方式 linux中的yum命令 手动解压安装(自己下载官网jdk.tar.gz) yum 安装 可以使用yum -y list java*命令,查询可安装的版本.版本后面要带-deve ...