Vue.js 组件复用和扩展之道
作者简介:
李中凯
八年多工作经验 前端负责人,
擅长JavaScript/Vue。
掘金文章专栏:https://juejin.im/user/57c7cb8a0a2b58006b1b8666/posts
公众号:1024译站
软件编程有一个重要的原则是 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>
如果出现下面两种情况,就说明这种模式不适用了,或者用法不对:
组件组合模式把状态和逻辑分解成原子部分,从而让应用具备可扩展性。如果组件内存在大量条件判断,可读性和可维护性就会变差。
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 API
mixin
高阶组件(HOC)
无渲染组件
每一种方法都有其优缺点,根据使用场景,或多或少都有适用的部分。
Composition API
组件之间共享状态和逻辑的最新方案是 Composition API。这是 Vue 3 推出的 API,也可以在 Vue 2 里当插件使用。
跟之前在组件定义配置对象里声明data
,computed
,methods
等属性的方式不同,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 开发者来说有点陌生。新技术总有个适应的过程,迟早会大面积应用。
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.wrapperh3 {{ myCommonProp }} <!--common markup-->block input <!--extended markup outlet -->
为了能扩展这个模板,需要安装 Vue Loader 的 Pug 插件。然后就可以引入基础模板并利用block input
语法替换占位部分了:
MyComponent.vue
<template lang="pug">extends BaseTemplate.pugblock inputh4 {{ myLocalProp }} <!--gets included in the base template-->
</template>
一开始你可能会认为它跟 slot 的概念是一样的,但是有个区别,这里的基础模板不属于任何单独的组件。它在编译时跟当前组件合并,而不是像 slot 那样是在运行时替换。
作者简介:
李中凯
八年多工作经验 前端负责人,
擅长JavaScript/Vue。
掘金文章专栏:https://juejin.im/user/57c7cb8a0a2b58006b1b8666/posts
公众号:1024译站
本文已经获得李中凯老师授权转发,其他人若有兴趣转载,请直接联系作者授权。
Vue.js 组件复用和扩展之道相关推荐
- Vue.js组件化开发实践
Vue.js组件化开发实践 前言 公司目前制作一个H5活动,特别是有一定统一结构的活动,都要码一个重复的轮子.后来接到一个基于模板的活动设计系统的需求,便有了一下的内容.首先会对使用Vue进行开发的一 ...
- VUE.JS 组件化开发实践
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家.点击跳转到教程. 前言 公司目前制作一个H5活动,特别是有一定统一结构的活动,都要码一个重复的轮子.后来接到一个基于模 ...
- Vue.js 组件编码规范
目标 本规范提供了一种统一的编码规范来编写 Vue.js 代码.这使得代码具有如下的特性: 其它开发者或是团队成员更容易阅读和理解. IDEs 更容易理解代码,从而提供高亮.格式化等辅助功能 更容易使 ...
- 构建你的第一个Vue.js组件
我记得当时我拿起CakePHP,我很喜欢,开始使用它是多么容易.这些文档不仅结构合理,详尽无遗,而且用户友好.多年以后,这正是我在Vue.js中感受到的.然而,与Cake相比,Vue文档仍然缺少一件事 ...
- 三十六、深入Vue.js组件Component(上篇)
@Author:Runsen @Date:2020/6/15 人生最重要的不是所站的位置,而是内心所朝的方向.只要我在每篇博文中写得自己体会,修炼身心:在每天的不断重复学习中,耐住寂寞,练就真功,不畏 ...
- (24)Vue.js组件—组件注册
一.Vue.js组件注册内容 • 全局注册 • 组件基础 • 局部注册 二.全局注册 • 全局注册的组件在注册后可以用于任意实例或组件中. <body><div id="a ...
- (23)Vue.js组件介绍
一.Vue.js组件介绍 组件用于封装页面的部分功能,将功能的结构.样式.逻辑代码封装为整体.提高功能的复用性与可维护性,更好的专注于业务逻辑. 二.Vue.js组件使用 组件使用时为自定义 HTML ...
- 使用 Vue.js 构建 VS Code 扩展
使用 Vue.js 构建 VS Code 扩展 Visual Studio (VS) Code 是开发人员在日常任务中最喜欢使用的代码编辑器之一.它的构建考虑到了可扩展性.在某种程度上,VS Code ...
- 三十七、深入Vue.js组件Component(下篇)
@Author:Runsen @Date:2020/7/4 人生最重要的不是所站的位置,而是内心所朝的方向.只要我在每篇博文中写得自己体会,修炼身心:在每天的不断重复学习中,耐住寂寞,练就真功,不畏艰 ...
最新文章
- 清除messages
- CSS3的background-size:设置背景图片大小
- Java常用API(五)Math 数学类
- nmon监控linux内存,使用Nmon监控Linux系统性能
- MySQL的MVCC底层原理二
- python缺失值处理 fillna能否用scala来处理_数据清洗(一)丨处理缺失数据
- 把人工智能、机器学习、深度学习串一串,串一个同心圆
- 打印输出Arraylist对整型字符增删改查方法开始结束的用时
- [python高德]行政区查询、调用wbe服务api获取省、市、区经纬度信息
- Yate for Mac(音乐标签管理工具)
- python哈姆雷特词频统计_python day 17 文本词频统计
- 公钥密码学 公钥基础设施_简单解释公钥密码学
- 关于ruby on rails启动服务器 Webpacker configuration file not found问题
- Chi-squared 卡方检验
- ballerina 学习二十七 项目k8s部署 运行
- android之银联支付,android端银联支付
- TCP Socket与TCP 连接
- 计算机网络原理第二章答案
- python蜂鸣器_Micropython之蜂鸣器1
- hQsv2Flv主页
热门文章
- win7使用共享打印机提示需要输入用户名和密码?
- 小学生计算机知识课堂,让小学信息技术课堂活起来
- SVN 提交代码后面如何修改日志信息
- 【ARM微控制器】STM32L486VGT6【FPGA】XC3S700A-4FTG256I现场可编程门阵列基本介绍
- DRF自定义响应数据格式
- RK3588在SSH上启动OpenCV报错:Can‘t initialize GTK backend in function ‘cvInitSystem‘
- SDK实战(小米支付流程)
- WebP 探寻之路,vue双向绑定
- 6年保险从业者告诉你,关于保险你应该知道的一些事
- linux android 手机调试,android手机在slackware linux上的调试