0. 如何优雅的编写高可扩展组件
面临的问题:
- 复用外观组件
- 组件之间共同行为的封装
现有的解决方案:
- 组件包裹组件
- mixin
有没有更好的解决方案?
1. 组件的经典写法
创建一个组件
<style lang="sass">
</style>
<template>
<div class="classic">Hello Classic {{ text }}</div>
</template>
<script>
// src/classic.vue
export default {
name: 'classic',
props: {
text: String,
}
}
</script>
当我们想扩展一个组件的外观, 比如加一个边框. 想在不改变原组件的情况下添加边框这个外观, 只能通过一个组件包裹另一个组件的方式来实现了
<style lang="sass">
</style>
<template>
<div class="border">
<inner-component :text="text" />
</div>
</template>
<script>
import InnerComponent from './classic.vue';
export default {
name: 'border',
props: {
text: String,
},
components: {
'inner-component': InnerComponent,
},
}
</script>
很不幸, 这个提供边框外观的组件, 无法通用. 主要问题在代理属性的设置上. 因为Vue组件需要事先声明所有的属性, 所以BorderComponent需要完全代理内部组件属性, 这样一来, 不同的内部组件需要不同的Border. 这(模板语法)无法满足我们的预期.
再看另外一个问题, 通过混入一些Methods, 以此来扩展组件的行为.
<style lang="sass">
</style>
<template>
<div class="mixin" @click="sayHello">Hello Mixins</div>
</template>
<script>
const someFeature = {
methods: {
sayHello() {
alert('Yes, mixin successfully');
},
},
mounted() {
console.info('mounted in mixins')
}
};
export default {
name: 'mixin',
mixins: [ someFeature ],
/*
methods: {
sayHello() {
}
},
//*/
mounted() {
console.info('mounted in component self');
}
}
</script>
是的, 我们成功的加入了sayHello方法. 组件也运行正常. 但是有两个情况值得注意:
- 如果组件中本来就存在methods, 则混入的methods就会被忽略
- 组件本来的mounted与mixins带来的mounted组成了一个队列, 且先执行混入的mounted, 后执行本来的mounted, 相邻两次mounted的顺序, 不可在运行时更改.
在完成预期的情况下, 带了什么问题?
- mixin对象直接与组件对象产生耦合了, 组件对象需要持有mixin对象的实例
- 组件对象明显的感知到了mixin对象, 甚至还需要做出配合.
- 如果mixin对象很多的话, 维护成本会大幅度升高
2. 我们先暂停以上问题的思考, 看看Vue官网提到的JSX怎么写?
!!先决条件!!
export default {
name: 'demo0',
render(h) { // h is important!
return (<div class="demo0">Hello Demo0</div>);
}
}
3. 如果是类就更好了
-
注意: 不支持constructor 和super
-
引入
vue-class-component
import Component from 'vue-class-component'; @Component export default class Demo1 { render() { return ( <div class="demo1">Hello Demo1</div> ); } }
-
尝试一下类的继承和方法重写
import Vue from 'vue'; import Component from 'vue-class-component'; @Component({ props: { text: String, }, }) class Demo2 extends Vue { name = "Demo2"; doSomeThing() { return `Hello ${ this.name }`; } render() { return ( <div class="demo2">{ this.text }{ ` And ${ this.doSomeThing() }` }</div> ) } } @Component class Demo2Extend extends Demo2 { // @override doSomeThing() { const superValue = Demo2.options.methods.doSomeThing.call(this); return `${ superValue } was extended`; } // @override render() { // super is not supported const view = Demo2.options.render.call(this, this.$createElement); return ( <div class="border" style={{ border: 'solid 2px red' }}> { view } </div> ); } } export { Demo2, Demo2Extend };
-
仔细阅读$createElement API的接口文档
文档地址
4. 组合优于继承
-
什么是组件?
组件(Component)是对数据和方法的简单封装。
引自百度百科
-
什么是高阶组件?
A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the React API, per se. They are a pattern that emerges from React’s compositional nature.
Concretely, a higher-order component is a function that takes a component and returns a new component. -
试试高阶组件的做法
import Component from 'vue-class-component'; @Component({ props: { text: String, } }) class Demo3 { render() { return ( <div class="demo3">{ this.text }</div> ); } } const Border = (color) => (WrapComponent) => @Component class BorderDemo3 extends WrapComponent { render() { const view = WrapComponent.options.render.call(this, this.$createElement); return ( <div style={{ border: `solid 5px ${color}` }}>{ view }</div> ); } } // 渲染劫持 export default Border('red')(Demo3); // use HOC
5. 巧了, ES的装饰器也是这么定义的
A decorator is:
- an expression
- that evaluates to a function
- that takes the target, name, and decorator descriptor as arguments
- and optionally returns a decorator descriptor to install on the target object
所以我们修改一下代码
@Border('red') // use HOC
@Component({
props: {
text: String,
}
})
class Demo3 {
render() {
return (
<div class="demo3">{ this.text }</div>
);
}
}
export default Demo3; // Note here!
实际上还可以试试compose函数, 走一波函数式编程, 像这样写:
// 组合函子
const componse = (...fn) => fn.reduce((a, b) => (...args) => a(b(...args));
// 函子饲养
const Borders = compose(Border('red'), Border('yellow'), Border('blue'));
@Borders // use HOC
@Component
export default class Demo3 {
// ...
}
// or
@Component
class Demo3 {
// ...
}
export default Borders(Demo3); // use HOC
6. 几个示例
-
反向继承实现错误处理
const CatchError = (errorMessage) => (WrapComponent) => @Component class CatchError extends WrapComponent { render() { try { return WrapComponent.options.render.call(this, this.$createElement); } catch (e) { return <div style={{ color: 'red' }}>{ errorMessage }: { e.message }</div> } } } @CatchError('An error is detected') @Component({ props: { text: String, } }) class DemoError { render() { throw new Error('an error in render'); return ( <div class="demo3">{ this.text }</div> ); } } export default DemoError;
-
属性代理实现操作属性
import Component from 'vue-class-component'; // Note: // - 属性声明不能复用 // - Vue组件特性: 属性必须事先申明 const properties = { props: { text: String, } }; // 属性代理 - 操作Props const ProxyProperties = (prefix) => (WrappedComponent) => @Component({ ...properties }) class PrefixText { render() { const newProps = { props: { text: prefix + this.text, } }; return ( <WrappedComponent { ...newProps } /> ); } } @ProxyProperties('Prefix is HAHA ') @Component({ ...properties }) class Demo4 { render() { return ( <div class="demo4">{ this.text }</div> ); } } export default Demo4;
-
属性代理实现事件劫持
import Component from 'vue-class-component'; const properties = { props: { name: String, click: Function, } }; // 属性代理 - 事件劫持 const ProxyClick = (WrappedComponent) => @Component({ ...properties }) class NewDemo5 { handleClick(...args) { // 一个AOP拦截的栗子 alert('before handle') // handle click const { click } = this; click && click(...args); alert('after handle') } render() { const oldProps = this.$props; const newProps = { props: { ...oldProps, click: this.handleClick, } }; return <WrappedComponent { ...newProps } />; } } @ProxyClick @Component({ ...properties }) class Demo5 { render() { return ( <div class="demo5" onClick={ this.click }>Click { this.name }</div> ) } } export default Demo5;
7. 回答开头提出的问题
美中不足的地方仍然存在, 这是因为Vue组件开发过程中属性必须事先申明. 所以在属性代理中无法获得更好的开发体验, 需要将属性声明作为高阶组件的参数传进去. 不过基于装饰器模式的组合方式, 可以更彻底的解耦组件在外观上的抽象问题和在行为共享的问题.
另外这么做会也会带来一些弊端, 如果你使用了JSX的方式, 那么你就得放弃全部的vue指令, 虽然它们中有些特别好用. 而这些指令的特性都必须自己来实现.
对于mixin和HOC的比较, 我想借用《深入React技术栈》一书中的图
高阶组件属于函数式编程(functional programming)思想,对于被包裹的组件时不会感知到高阶组件的存在,而高阶组件返回的组件会在原来的组件之上具有功能增强的效果。而Mixin这种混入的模式,会给组件不断增加新的方法和属性,组件本身不仅可以感知,甚至需要做相关的处理(例如命名冲突、状态维护),一旦混入的模块变多时,整个组件就变的难以维护…
参考资料: juejin.cn/post/684490…