概述
为何只谈 vue2 中如何抽离组件公共逻辑?因为 vue3 中已经有了 composition API 来帮助我们抽离组件公共逻辑。
方案
- 官方提供:mixin
- react 社区创意:高阶组件(HOC)
- react 社区创意:render props
- 基于 hook 是一个没有 UI 的组件这一思想
例子
以下统一使用 vue 组合式 API 征求意见稿 中记录鼠标位置的功能来演示(react hook 中也是用了这个例子)。
在这个例子中,我们需要抽离重用一组信息和两组逻辑:鼠标位置信息、组件挂载后监听鼠标移动事件、组件销毁前销毁鼠标移动事件。
mixin
定义
这边是通过一个函数返回一个 mixin,你也可以直接返回一个 mixin 对象,但不推荐。
// mouseMixin.js
export default function () {
return {
data() {
return {
mouse: {
x: 0,
y: 0,
},
}
},
methods: {
update(e) {
this.mouse.x = e.clientX
this.mouse.y = e.clientY
}
},
mounted: function() {
window.addEventListener('mousemove', this.update)
},
beforeDestroy() {
window.removeEventListener('mousemove', this.update)
},
}
}
使用
<template>
<!-- 这里的 mouse 对象就是从 mixin 中注入的 -->
<h2>x: {{ mouse.x }}, y: {{ mouse.y }}</h2>
</template>
<script>
import mouseMixin from './mouse.js'
export default {
name: 'MouseMixinDemo',
mixins: [mouseMixin()],
}
</script>
高阶组件(HOC)
定义一个函数,接收一个一个组件,返回新的组件(新组件透传 props 以及定义自己的逻辑传给原组件)就是一个高阶组件。下面看看 vue2 中如何实现?
定义
export default function mouse(WrappedComponent) {
return {
data() {
return {
mouse: {
x: 0,
y: 0
}
}
},
methods: {
update(e) {
this.mouse.x = e.clientX
this.mouse.y = e.clientY
}
},
mounted: function() {
window.addEventListener('mousemove', this.update)
},
beforeDestroy() {
window.removeEventListener('mousemove', this.update)
},
render(h) {
return h(WrappedComponent, {
props: {
// 注入 mouse
mouse: this.mouse,
// 透传 prop
...this.$props,
},
// 透传属性
attrs: {
...this.$attrs,
},
// 透传事件
on: {
...this.$listeners,
},
})
},
}
}
使用
- 这边使用的 demo 写得稍微复杂一些,因为 HOC 是包装了一层组件,那么中间这层组件不该对用户的使用产生任何干扰,所以需要透传 props、attrs、绑定事件。
<template>
<div>
<h1>鼠标高阶组件演示</h1>
<h3>{{ this.text }}</h3>
<button v-bind="$attrs" @click="$emit('vic')">触发自定义事件</button>
<h2>x: {{ mouse.x }}, y: {{ mouse.y }}</h2>
</div>
</template>
<script>
export default {
name: 'MouseHOCDemo',
props: {
mouse: Object,
text: String,
},
}
</script>
- 使用高阶函数来包装目标组件
<template>
<MouseHOCDemo
text="传入的 prop 文本"
:disabled="false"
@vic="log"
/>
</template>
<script>
import MouseHOCDemo from './HOC/MouseHOCDemo.vue'
import mouse from './HOC/mouse'
export default {
components: {
// 主意这里注册的组件是被包装过的目标组件
MouseHOCDemo: mouse(MouseHOCDemo),
},
methods: {
// 自定义事件
log() {
console.log('自定义事件 vic 被触发')
},
},
}
</script>
render props
所谓 render props 就是:定义一个组件,这个组件做两件事:
- 定义自己的逻辑
- 调用 props 中的 render 方法,将相关信息传入
关键在于调用 props 中的 render 方法来渲染出目标组件,而在 vue2 模版中,我们是没有办法通过调用一个方法来渲染出一段 vnode 的。但是没有关系,vue 的作用域插槽给我们在 vue2 中实践 render props 开了一道口子。下面看具体的代码演示。
定义
export default {
name: 'RenderPropsMouse',
data() {
return {
mouse: {
x: 0,
y: 0
}
}
},
methods: {
update(e) {
this.mouse.x = e.clientX
this.mouse.y = e.clientY
}
},
mounted: function() {
window.addEventListener('mousemove', this.update)
},
beforeDestroy() {
window.removeEventListener('mousemove', this.update)
},
render(h) {
return h('div', this.$scopedSlots.default({ mouse: this.mouse }))
}
}
或者使用模版
<template>
<div>
<slot :mouse="mouse">
</slot>
</div>
</template>
<script>
export default {
name: 'RenderPropsMouse',
data() {
return {
mouse: {
x: 0,
y: 0
}
}
},
methods: {
update(e) {
this.mouse.x = e.clientX
this.mouse.y = e.clientY
}
},
mounted: function() {
window.addEventListener('mousemove', this.update)
},
beforeDestroy() {
window.removeEventListener('mousemove', this.update)
},
}
</script>
使用
<template>
<Mouse>
<!-- 这里是关键,数据和方法都可以这般注入 -->
<template v-slot="{ mouse }">
<h2>x: {{ mouse.x }}, y: {{ mouse.y }}</h2>
</template>
</Mouse>
</template>
<script>
import Mouse from './Mouse.vue'
export default {
components: {
Mouse
}
}
</script>
hook 是一个没有 UI 的组件
定义
export default {
props: {
mouse: {
type: Object,
}
},
methods: {
update(e) {
this.$emit('update:mouse', {
x: e.clientX,
y: e.clientY
})
}
},
mounted() {
window.addEventListener('mousemove', this.update)
},
beforeDestroy() {
window.removeEventListener('mousemove', this.update)
},
render() {},
}
使用
<template>
<div>
<h2>x: {{ mouse.x }}, y: {{ mouse.y }}</h2>
<Mouse :mouse.sync="mouse" />
</div>
</template>
<script>
import Mouse from './mouse.js'
export default {
components: {
Mouse
},
name: 'MouseRenderPropsDemo',
data() {
return {
mouse: {
x: 0,
y: 0
}
}
},
}
</script>