0%

浅谈Vue高阶组件

说到高阶组件,我们是否立刻就能够想起高阶函数,那什么是高阶函数呢?高阶函数就是接受一个函数作为参数或者以一个函数作为返回值的函数,高阶组件的定义跟高阶函数很类似,高阶组件即是接受一个组件作为参数,并返回一个新的组件的组件。这篇博客我想探索一下Vue高阶组件,但是由于Vue高阶组件并不常用,高阶组件在React生态里使用比较广泛,所以在这里我先回顾一下React高阶组件。

React 与 mixins

在使用ES6写React之前,我们有可能需要在多个不同的组件之间复用一些代码,这时候我们的做法通常是使用React的mixins属性。举个简单的例子,假如我们需要给多个不同组件的componentDidMount生命周期,也就是在组件挂载完后打印一段日志,我们通常会使用以下的写法:

const myMixins = {
    componentDidMount: function() { console.log("component componentDidMount.") }
}

const HelloWorld = React.createClass({
    mixins:[myMixins],
    componentDidMount: function() {
      console.log("HelloWorld componentDidMount.")
    },
    render: function() {
      return <h1>Hello World!</h1>;
    }
});

ReactDOM.render(<HelloWorld />, document.getElementById("example"));

上面的代码在执行后,会先输出“component componentDidMount.”,然后再输出”HelloWorld componentDidMount.”,由此也可以发现,minxins里面的方法不会覆盖组件生命周期里的方法,并且minxins里的方法优先于组件生命周期里的方法。另外如果组件需要引入多个mixins的话,只需要将要引入的mixinx逐个加入到组件的mixins数组里面,这些mixins执行的顺序就是它们在mixins数组里面的加入顺序。

当我们使用ES6写React组件的时候,我们就不能使用mixins引入公用代码了,一般是借助高阶组件的写法来实现代码的复用,而且官方也不再推荐使用mixins了,详情可见:https://reactjs.org/blog/2016/07/13/mixins-considered-harmful.html。

React 高阶组件

同样借助上面的例子,我们可能会使用以下的写法封装一个高阶组件:

export const HigherOrderComponent = (WrappedComponent) => {
  return class extends Component {
        componentDidMount() {
            console.log("component componentDidMount.")
        }
        render() {
          return (<div>
            <WrappedComponent {...this.props}/>
          </div>)
    }
  }
}

@HigherOrderComponent
class HelloWorld extends Component {
    componentDidMount() {
      console.log("HelloWorld componentDidMount.")
    },
    render() {
      return <h1>Hello World!</h1>;
    }
}

ReactDOM.render(<HelloWorld />, document.getElementById("example"));

另外需要说一下的是:在上面的代码中使用到了ES7的装饰器语法@HigherOrderComponent,所以在webpack的配置文件进行相关的修改,添加插件:

[ "@babel/plugin-proposal-decorators", { "legacy": true } ]

如果是通过 create-react-app 生成的项目,可以在webpack.config.dev.js和webpack.config.pro.js进行如下的修改:

// Process application JS with Babel.
// The preset includes JSX, Flow, and some ESnext features.
{
  test: /\.(js|mjs|jsx|ts|tsx)$/,
  include: paths.appSrc,
  loader: require.resolve('babel-loader'),
  options: {
    customize: require.resolve(
      'babel-preset-react-app/webpack-overrides'
    ),

    plugins: [
      [
        require.resolve('babel-plugin-named-asset-import'),
        {
          loaderMap: {
            svg: {
              ReactComponent: '@svgr/webpack?-prettier,-svgo![path]',
            },
          },
        },
      ],
      [ "@babel/plugin-proposal-decorators", { "legacy": true } ] // 添加这一行配置
    ],
    // This is a feature of `babel-loader` for webpack (not Babel itself).
    // It enables caching results in ./node_modules/.cache/babel-loader/
    // directory for faster rebuilds.
    cacheDirectory: true,
    // Don't waste time on Gzipping the cache
    cacheCompression: false,
  },
},

前面的示例代码执行后,我们可以发现控制台会先输出”HelloWorld componentDidMount.”,然后再输出”component componentDidMount.”,可见跟使用mixins的写法的执行结果不一样,如果我们使用了高阶组件的写法,高阶组件里面的生命周期函数会晚于组件生命周期函数的执行,通过高阶组件,我们可以实现对组件的个性化封装以及对通用逻辑的抽离,以让这些通用的逻辑更好地在各个组件之间复用,关于React高阶组件更加详细的介绍,可以看看这篇官方的文档:https://reactjs.org/docs/higher-order-components.html

Vue 高阶组件

通过前面的回顾,我们大体了解到了什么是React高阶组件,以及React高阶组件能给我们的项目带来什么价值,但由于Vue跟React底层设计的差别,Vue并不推荐使用高阶组件,而是使用mixins的形式实现功能的复用,大概也是因为使用Vue高阶组件带给我们的收益跟直接使用mixins带来的收益相差不多。关于Vue中mixins的用法可以看看下面这个例子:

var myMixin = {
  created: function () {
    this.hello()
  },
  methods: {
    hello: function () {
      console.log('mixin hello!')
    }
  }
}

var Component = Vue.extend({
  mixins: [myMixin],
  created: function() {
      console.log('component hello!')
  }
})

var component = new Component()

上面的代码执行后,会先输出”mixin hello!”,然后再输出”component helllo!”,从运行结果也可以看出,在Vue中,mixins里面的生命周期钩子函数也不会覆盖组件的生命周期钩子函数,而且mixins里面的钩子会优先于组件的钩子先执行。当然,mixins还有许多其他的特性,可以通过官方文档去了解:https://cn.vuejs.org/v2/guide/mixins.html 。但是通过mixins复用功能的写法是侵入式的,只是通过浅合并的形式改变原有的组件对象,所以同时引入了mixin的所有组件都会依赖它,而我们使用高阶组件的话就可以避免这种副作用,接下来就简单探索一下在Vue中该如何写高阶组件。

我们可以这样认为,组件的本质就是一个函数,所以高阶组件也可以认为是一个高阶函数。但是我们平时写Vue单文件组件的时候,通常是按照如下格式进行编写:

export default {
  name: 'MyComponent',
  props: {...},
  mixins: [...]
  methods: {...}
}

从写法来看,Vue组件只是一个普通的对象,但为什么又说组件的本质是一个函数呢?原因就是,Vue在底层会以这个普通对象作为参数创建一个构造函数,然后这个构造函数就是我们用来实例化组件的构造函数,所以说,Vue组件的本质也是一个函数。既然这样,我们可以认为Vue高阶组件是这样的一种组件,就是传入一个纯对象,然后返回一个新的纯对象的函数。如下:

export default function HigherOrderComponent (WrappedComponent) {
  return {
    template: '<wrapped v-on="$listeners" v-bind="$attrs"/>',
    components: {
      wrapped: WrappedComponent
    },
    created: function () {
        this.hello()
    },
    methods: {
      hello: function () {
        console.log('hoc hello!')
      }
    }
  }
}

这个HigherOrderComponent组件就是高阶组件,它接受WrappedComponent作为参数,并返回了一个新的组件。这样,我们就使用高阶组件完成了前面mixins实现的功能,但是我们没有对原组件进行修改,所以是非侵入式的。但是需要注意的是下面这一行代码里面的$listeners和$attrs:

<wrapped v-on="$listeners" v-bind="$attrs"/>

这一行代码的作用类似于我们在React写高阶组件时的:

<WrappedComponent {...this.props}/>

也就是将props和事件传递到wrapped组件。

另外,在运行时版本的Vue中,我们是使用不了template选项的,所以我们需要将template选项替换成render函数,如下:

export default function HigherOrderComponent (WrappedComponent) {
  return {
    components: {
      wrapped: WrappedComponent
    },
    created: function () {
        this.hello()
    },
    methods: {
      hello: function () {
        console.log('hoc hello!')
      }
    },
    render: function(h) {
        return h(WrappedComponent, {
            on: this.$listeners,
            attrs: this.$attrs,
        })
    }
  }
}

由于$attrs只代表的是那些没有被声明为props的属性,所以上面的render函数中,还需要添加props:

export default function HigherOrderComponent (WrappedComponent) {
  return {
    components: {
      wrapped: WrappedComponent
    },
    created: function () {
        this.hello()
    },
    methods: {
      hello: function () {
        console.log('hoc hello!')
      }
    },
    render: function(h) {
        return h(WrappedComponent, {
            on: this.$listeners,
            attrs: this.$attrs,
            props: this.$props
        })
    }
  }
}

上面这样添加props后,但取到的值仍然是一个空对象,这是因为this.$props代表的是高阶组件接收到的props,然而高阶组件没有声明任何的props,所以this.$props就是一个空对象,解决办法就是我们让高阶组件的props指向传入组件的props:

export default function HigherOrderComponent (WrappedComponent) {
  return {
    components: {
      wrapped: WrappedComponent
    },
    created: function () {
        this.hello()
    },
    methods: {
      hello: function () {
        console.log('hoc hello!')
      }
    },
    props: WrappedComponent.props,
    render: function(h) {
        return h(WrappedComponent, {
            on: this.$listeners,
            attrs: this.$attrs,
            props: this.$props
        })
    }
  }
}

到了这一步,我们的Vue高阶组件可以实现props、事件以及没有被声明为props的$attrs的传递了。但是Vue高阶组件还有可以完善的空间,当我有空的时候再继续研究,现在的重点工作是找工作,因为遇到坑爹的汇桔网,让我失业了。