JSX在基于Vue的项目中,到底有什么用?

784 阅读5分钟
原文链接: nixon.leanote.com

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. 如果是类就更好了

  • vuejs/vue-class-component 介绍

    注意: 不支持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.

    引自React技术文档

  • 试试高阶组件的做法

    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

引自decorator

所以我们修改一下代码

@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…