漫谈组件复用

俗话说「懒是程序员的美德」。在越来越注重前端工程化的今天,「Ctrl+C」、「Ctrl+V」的代码,虽然用起来一时爽,一旦需要修改就如同面临火葬场。如何「懒」出效率,是值得思考的问题。减少代码的拷贝,增加封装复用能力,实现可维护、可复用的代码,无疑是我所认为的「懒」的高级境界。鉴于笔者之前使用 React 偏多,进入饿了么后也逐步使用了不少 Vue 进行开发,所以就借此机会,谈谈在 React 和 Vue 中各种基于组件的复用与实现方式。

Mixin 混入模式

最原始的一种复用方式应该就是 Mixin。通过将公用逻辑封装为一个 Mixin,通过注入的方式进行组件间的复用。「ps: 该方式不仅用于组件,也流行于各种 css 预处理器中」。

在 React 中,通过 React.createClass() 方式创建的组件可使用 Mixin 模式,而在 ES6 的伪类模式下,并不支持 Mixin 模式,官方推荐用组合或者高阶组件方式实现复用,废话不多说,使用方式如下:

//mixin
const mixinPart = {
  mixinFunc() {
    console.log('this mixin!');
    return 'this mixin!';
  }
};

const Contacts = React.createClass({
  mixins: [mixinPart],
  render() {
    return (
      <div>{this.mixinFunc()}</div>
    );
  }
});
// => 'this mixin!';

而在 Vue 中,使用逻辑类似:

//mixin
const mixinPart = {
  created() {
    console.log('this mixin!');
    return 'this mixin!';
  }
};

const Component = Vue.extend({
  mixins: [mixinPart]
});
const component = new Component(); // => "this mixin!"

Mixin 模式给予组件公共抽象与复用能力,但另一方面也具有大量的局限性。由于 Mixin 是侵入式的,因此修改了 Mixin 相当于修改了原组件。其次,在混入过程中,对于相同键值对象与函数的相互覆盖与合并,容易导致各种意外产生。因此使用过程中必须对 Mixin 内部实现有一定了解。强大的灵活性导致了在大型项目中 Mixin 的难维护。

高阶组件

高阶组件(High Order Component)这个概念最早是 React 社区提出,借鉴函数式中的高阶函数,提出通过传入一个组件,操作后返回一个新组件的方式进行复用。

在 React 中的使用非常便捷,官方博客中就有相关介绍

const HOC = (WrappedComponent) => {
  const HOC_Component = (props) => {
    return (
      <React.Fragment>
        <WrappedComponent {...props} name="WrappedComponent" />
        <div>This comes from HOC Component</div>
      </React.Fragment>
    );
  };
  HOC_Component.displayName = 'HOC_Component';
  return HOC_Component;
}
const Component = (props) => {
  return <div>This message comes from Component: {props.name}</div>;
}
const Result = HOC(Component);
ReactDOM.render(<Result />, document.getElementById('root'));
// => This message comes from Component: WrappedComponent
// => This comes from HOC Component

Vue 虽然没有官方示例,但与 React 进行类比,Vue 中的组件最终的展现形式是函数,但在过程中,实际上是一个个对象。因此,Vue 中的高阶组件,应当是传入一个对象,最后传出一个对应对象。我们可以简单实现个上例对应的 HOC 功能:

const HOC = (WrappedComponent) => {
    return {
        components: {
            'wrapped-component': WrappedComponent
        },
        template: `
          <div>
              <wrapped-component name="WrappedComponent" v-bind="$attrs" />
              <div>This comes from HOC Component</div>
            </div>
          `
    };
}
const Component = {
    props: ['name'],
    template: '<div>This message comes from Component: {{ name }}</div>'
};

new Vue(HOC(Component)).$mount('#root')
// => This message comes from Component: WrappedComponent
// => This comes from HOC Component

高阶组件用途十分广泛,主要可以分为属性代理反向继承两种。

属性代理具体为高阶组件可以直接获取外部传入的参数,根据需求完成变更后重新传给被包含的组件。如上例中就在原始 props 基础上为 WrappedComponent 增加了一个 name 属性,同时在原始渲染基础上增添了一行信息渲染。

// 最基本的反向继承
const HOC = (WrappedComponent) => {
  return class extends WrappedComponent {
    render() {
      return super.render();
    }
  }
}

反向继承因为继承于 WrappedComponent,因而能够获取其 staterender 等各种组件数据,从而做到对组件的渲染和 state 状态等的干预。反向继承虽然在日常使用中遇到情况较少,但无疑是高阶组件中笔者认为的一个闪光点 ( 貌似其它方式中暂时没有可以替代的方案 )。例如,在 Vue 中有着 keep-alive 作为组件缓存,而在 React 中官方暂无类似功能,应用 data => view 的原则,一个常用的替代实现是进行状态保存,然后在需要的时候进行状态还原,在这种情况下,反向继承就是一个很好的工具。

const withStateCached = (WrappedComponent) => {
  return class extends WrappedComponent {
    static getDerivedStateFromProps(nextProps, state) {
      // 进行数据的存储等
    }

    componentDidMount() {
      // 进行缓存数据的读取
    }

    render() {
      return super.render();
    }
  }
}

在笔者的实际项目中,更多的把高阶组件看作是一个组件工厂或者装饰者模式的应用,例如对一个基础表格元素进行多次的高阶组件的包装,添加分页、工具栏等功能,形成一个个更符合具体业务需求的新组件,达到组件复用的目的。当然,高阶组件也不是全能的,首先其对于业务耦合度较高,更适合封装一些日常业务中常用的组件。其次最重要的弊端是因为内部产生的的 Props 值固定,容易被外部传入值覆盖。如例子中,当外部也传入了一个 name 属性值时,就会根据组件的写法产生不同的覆盖方式而导致错误。

由于篇幅限制,这里只粗略地介绍了一些基础使用,要更深入理解可以参考这篇文章

渲染属性/函数子组件

为了解决高阶组件存在的问题,一种新的「Render Props」的方案被提出。该方案提供了一个叫做 render 的函数作为 Props 参数传入,在内部处理完毕后,将所需的组件信息,数据作为 render 的参数传出,从而实现更加灵活的复用逻辑。

const RenderProps = ({ render, ...props }) => render(props, 'RenderPropComponent');
const Component = () => (
    <RenderProps
        render={(originProps, componentName) => (<div>From {componentName}</div>)}
    />
);

ReactDOM.render(<Component />, document.getElementById('root'));
// => From RenderPropComponent

在该例中,我们通过 render 函数传入了原 Props 和一个新的 name 属性,在实际使用中,重新命名 name 为 componentName,由此避开了高阶组件的弊端。

由此理念,在 React 中,延伸出函数子组件的概念,将 children 作为函数使用,更加贯彻了一切皆为组件的概念。同时在 React@16.3 版本中,FB 官方的 Context 新 API 的实现也采用了函数子组件的方式。

const RenderProps = ({ children, ...props }) => children(props, name = 'RenderPropComponent');
const Component = () => (<RenderProps>
    {(originProps, componentName) => (<div>From {componentName}</div>)}
</RenderProps>);

ReactDOM.render(<Component />, document.getElementById('root'));
// => From RenderPropComponent

而在 Vue@2.5 后的版本中,slot-scope 的概念也有点渲染属性的影子。

const RenderProps = {
    template: `<div><slot v-bind="{ name: 'RenderPropComponent' }"></slot></div>`
};

const vm = new Vue({
    el: '#root',
    components: { 'render-props': RenderProps },
    template: `
      <render-props>
        <template slot-scope="{ name }">
          <div>From Component</div>
          From {{ name }}
        </template>
      </render-props>
    `
});
// => From Component
// => From RenderPropComponent

组件注入

组件注入(Component Injection)的概念有些类似渲染属性,都是传递一个类似 render 的函数属性,区别在于组件注入将该函数作为 React 中的无状态组件使用,而非原始的函数。

const RenderProps = ({ Render, ...props }) => <Render {...props} name='RenderPropComponent' />;
const Component = () => (<RenderProps Render={({ name }) => (<div>From {name}</div>)} />);

ReactDOM.render(<Component />, document.getElementById('root'));
// => From RenderPropComponent

与渲染属性相比,组件注入能在 devTool 的组件树上直观的展现出内嵌的组件结构。但在另一方面,由于所有属性都被打包成了 props 传出,反而失去了渲染属性的多参数的灵活性。

总结

组件复用的方式如今可谓是多种多样,笔者认为也并不存在什么所谓的最佳实践,结合具体场景和个人喜好的使用不同方案,远离「Ctrl+C」「Ctrl+V」程序员,才是打造可复用可维护的良好组件、项目的最优选择。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值