俗话说懒是程序员的美德. 在越来越注重前端工程化的今天,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 模式给予组件公共抽象与复用能力, 但另一方面也具有大量的局限性 https://reactjs.org/blog/2016/07/13/mixins-considered-harmful.html . 由于 Mixin 是侵入式的, 因此修改了 Mixin 相当于修改了原组件. 其次, 在混入过程中, 对于相同键值对象与函数的相互覆盖与合并, 容易导致各种意外产生. 因此使用过程中必须对 Mixin 内部实现有一定了解. 强大的灵活性导致了在大型项目中 Mixin 的难维护.
高阶组件
高阶组件 (High Order Component) 这个概念最早是 React 社区提出, 借鉴函数式中的高阶函数, 提出通过传入一个组件, 操作后返回一个新组件的方式进行复用.
在 React 中的使用非常便捷, 官方博客中就有相关介绍 https://reactjs.org/docs/higher-order-components.html :
- 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, 因而能够获取其 state, render 等各种组件数据, 从而做到对组件的渲染和 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 属性值时, 就会根据组件的写法产生不同的覆盖方式而导致错误.
由于篇幅限制, 这里只粗略地介绍了一些基础使用, 要更深入理解可以参考这篇文章 http://hcysun.me/2018/01/05/探索Vue高阶组件/ .
渲染属性 / 函数子组件
为了解决高阶组件存在的问题, 一种新的Render Props https://reactjs.org/docs/render-props.html 的方案被提出. 该方案提供了一个叫做 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 https://reactjs.org/docs/context.html 新 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+CCtrl+V程序员, 才是打造可复用可维护的良好组件, 项目的最优选择.
来源: https://juejin.im/entry/5b32fea7e51d4558a57fef0e