4种方案带你探索 Vue.js 代码复用的前世今生

1,442 阅读6分钟

前言

在我们平时开发中,不论你使用什么语言,当遇到了大量的重复代码,我们可能会去将重复代码提取出来,独立一个模块,在多个地方引用,这是一个好习惯,是值得推荐的!当然也有些同学不感冒,使用到了直接CV,撇开代码规范设计模式这些不谈,往往CV会给你带来更大的工作量(比如用了很多地方,你要去CV很多地方,如果后续有变动,你又要重复CV到很多地方......,当然不推荐CV)。我们所熟知的Vue.js也在如何提取公共代码复用方面也一直在探索优化,本文笔者就来和各位聊聊Vue.js代码复用的前世今生

在Vue.js中我们可通过以下4种方案来实现代码逻辑复用

  • mixin
  • 高阶组件
  • 作用域插槽(scoped slots)
  • Composition API 组合式函数

可能各位常用的是mixin,没关系,其他几种也很好理解。笔者会通过一个实际的案例分别使用以上的方案实现,并分析各种方案的优缺点来带各位掘友体会Vue.js代码逻辑复用方面的优化历程。

案例:就以大家所熟知的 鼠标位置 来吧

Vue.js 代码逻辑复用

我们先不考虑复用,先来看看如何实现鼠标位置这个功能,功能十分简单,大家肯定都会,笔者就不废话了,直接看下代码吧:

基础实现

<script src="https://unpkg.com/vue@next"></script>

<div id="app"></div>

<script>
  const { createApp } = Vue
  const App = {
    template: `{{x}}  {{y}}`,
    data() {
      return {
        x: 0,
        y: 0
      }
    },
    methods: {
      handleMouseMove(e) {
        this.x = e.pageX
        this.y = e.pageY
      }
    },
    mounted() {
      window.addEventListener('mousemove', this.handleMouseMove)
    },
    unmounted() {
      window.removeEventListener('mousemove', this.handleMouseMove)
    }
  }

  createApp(App).mount('#app')
</script>

效果: 1.gif

接下来,我们尝试将这个功能提取以达到复用的目的,先来看看 mixin 这个方案。

mixin

简单来说,mixin允许我们提供一个或多个像普通实例对象一样包含实例选项的对象,Vue.js会以一定的逻辑自动合并这些对象里面的选项和组件的选项。举例来说,如果你的 mixin 包含了一个 created 钩子,而组件自身也有一个,那么这两个函数都会被调用。本文不再赘述,请参考Vue.js——mixins。以下就是通过mixin实现复用MouseMove的逻辑:

<script>
  const { createApp } = Vue

  const MouseMoveMixin = {
    data() {
      return {
        x: 0,
        y: 0
      }
    },
    methods: {
      handleMouseMove(e) {
        this.x = e.pageX
        this.y = e.pageY
      }
    },
    mounted() {
      window.addEventListener('mousemove', this.handleMouseMove)
    },
    unmounted() {
      window.removeEventListener('mousemove', this.handleMouseMove)
    }
  }

  const App = {
    template: `{{x}}  {{y}}`,
    mixins: [ MouseMoveMixin ]
  }

  createApp(App).mount('#app')
</script>

效果与之前的一致。

我们来分析下mixin的缺点:

  1. 当我们的组件有多个mixin,比如:mixins: [ MouseMoveMixin, anthorMixin, fooMixin ],我们就会分不清哪些变量是从MouseMoveMixin来的?哪些变量是从anthorMixin来的?那就出现了第一个缺点:变量来源不清
  2. 同样的,当我们的组件有多个mixin,我们不得不去考虑他们注入的变量名会不会存在冲突。那就出现了第二个缺点:命名冲突

高阶组件

所谓高阶组件,就是通过实现一个包装函数,这个包装函数返回像普通实例对象一样包含实例选项的对象,该对象内包含render选项,render用于渲染内部的组件,并将属性通过props注入到内部组件。比如我们可以像下面这样通过高阶组件复用这个鼠标位置的逻辑。

<script>
  const { createApp, h } = Vue
  // 包装函数
  function withMouse(inner) {
    return {
      data() {
        return {
          x: 0,
          y: 0
        }
      },
      methods: {
        handleMouseMove(e) {
          this.x = e.pageX
          this.y = e.pageY
        }
      },
      mounted() {
        window.addEventListener('mousemove', this.handleMouseMove)
      },
      unmounted() {
        window.removeEventListener('mousemove', this.handleMouseMove)
      },
      render() {
        // 注入 x, y
        return h(inner, { x: this.x, y: this.y })
      }
    }
  }

  const App = withMouse({
    template: `{{x}}  {{y}}`,
    props: ['x', 'y']
  })

  createApp(App).mount('#app')
</script>

我们再来分析下,用高阶组件来实现逻辑复用,是不是就没有缺点呢?

同样的,我们还是假设我有还多块逻辑要复用,比如把mixins: [ MouseMoveMixin, anthorMixin, fooMixin ]改写成高阶组件,那将变成以下代码:

  function withMouse(inner) {
    // 此处省略
  }
  function withFoo(inner) {
    // 此处省略
  }
  function withAnthor(inner) {
    // 此处省略
  }

  const App = withAnthor(withFoo(withMouse({
    template: `{{x}}  {{y}}`,
    props: ['x', 'y', 'foo', 'anthor']
  })))

  createApp(App).mount('#app')

mixin的问题它都有,props中我们依然看不清哪些属性是由哪个高阶组件注入的,也依然不得不考虑命名冲突的问题。(有些同学可能觉得,如果注入的变量名能够和包裹函数名有联系,那就能够看出来。那确实是的,但是这就需要有很严格的开发规范代码走查来约束开发人员了)显然高阶组件也不是什么”灵丹妙药“,我们接着看如何使用scoped slots来实现这个逻辑复用。

作用域插槽(scoped slots)

作用域插槽(scoped slots)这种方式和高阶组件有点像,区别在于不是通过函数来包裹,而是通过实现一个组件来包裹,我们叫它父组件,在父组件实现需要复用的逻辑,使用作用域插槽,将父组件的状态共享给子组件。代码实现如下:

<script>
  const { createApp } = Vue

  const MouseMove = {
    data() {
      return {
        x: 0,
        y: 0
      }
    },
    methods: {
      handleMouseMove(e) {
        this.x = e.pageX
        this.y = e.pageY
      }
    },
    mounted() {
      window.addEventListener('mousemove', this.handleMouseMove)
    },
    unmounted() {
      window.removeEventListener('mousemove', this.handleMouseMove)
    },
    // 等价于 template: `<slot :x="x" :y="y"></slot>`,
    render() {
      return this.$slots.default && this.$slots.default({
        x: this.x,
        y: this.y
      })
    }
  }

  const App = {
    template: `<MouseMove v-slot="{x, y}">{{x}}  {{y}}</MouseMove>`,
    components: { MouseMove }
  }
  createApp(App).mount('#app')
</script>

我们还是来分析下这种方式的优缺点,还是通过假设我们需要重用多个逻辑,把mixins: [ MouseMoveMixin, anthorMixin, fooMixin ]改写为使用作用域插槽:

  const MouseMove = {

  }
  const Foo = {

  }
  const Anthor = {

  }

  const App = {
    template: `
    <MouseMove v-slot="{ x, y }">
      <Foo v-slot="{ foo }">
        <Anthor v-slot="{ anthor }">
          {{x}} {{y}} {{foo}} {{anthor}}
        </Anthor>
      </Foo>
    </MouseMove>`,
    components: { MouseMove, Foo, Anthor }
  }
  
  createApp(App).mount('#app')

看上去是解决了上面两个问题了,我们能够很明显的看到每个属性是从哪个组件注入的,来源清晰了,即使有命名的问题,我们在解构的时候是可以重命名避免的,比如Foo注入的也叫x,那我们可以这么写<Foo v-slot="{ x: foo }">

那是不是这样就完美了呢?并没有,细心的同学可能发现了,我们为了复用逻辑导致了更多的组件实例创建,是不是有点鱼和熊掌不可兼得的感觉,我们接下来看Vue.js的终极大招——Composition API 组合式函数

Composition API 组合式函数

先简单介绍下Composition API

组合式 API (Composition API) 是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。它包含了这些API:

  • 响应式API —— ref、reactive computed、watch......
  • 生命周期钩子 —— onMounted、onUnmounted......
  • 依赖注入 —— provide、inject......

接着我们用Composition API来实现一下:

<script>
  const { createApp, ref, onMounted, onUnmounted } = Vue

  function useMouseMove() {
    const x = ref(0)
    const y = ref(0)
    const handleMouseMove = e => {
      x.value = e.pageX
      y.value = e.pageY
    }
    onMounted(() => {
      window.addEventListener('mousemove', handleMouseMove)
    })
    onUnmounted(() => {
      window.removeEventListener('mousemove', handleMouseMove)
    })
    return { x, y }
  }

  const App = {
    setup() {
      const { x, y } = useMouseMove()
      return { x, y }
    },
    template: `{{x}} {{y}}`,
  }

  createApp(App).mount('#app')
</script>

看完这个实现,首先它肯定是没有以上的各种问题的,同时Composition API也是Vue3的一个重大更新,能够让我们更轻松的组织我们的逻辑代码,更轻松的达到逻辑复用,可谓是完美方案!

可能你还有点小问题,比如setup为啥要先解构,再返回 { x, y }

能直接返回useMouseMove()吗?

  const App = {
    setup() {
      return useMouseMove()
    },
    template: `{{x}} {{y}}`,
  }

答:如果你没有其他变量需要暴露出去,你当然可以直接返回useMouseMove()。但是直接返回useMouseMove(),那又回到了之前的问题,又不能清晰地看出哪个变量是哪个组合式函数注入的。

我能不能在return的对象里解构?

  const App = {
    setup() {
      return {
        ...useMouseMove()
      }
    },
    template: `{{x}} {{y}}`,
  }

答:可以,但不推荐,这么写还是又回到了之前的问题。

最佳实践

  const App = {
    setup() {
      const { x, y } = useMouseMove()
      return { x, y }
    },
    template: `{{x}} {{y}}`,
  }

总结

本文用Vue.js四种逻辑复用的方案实现了 鼠标位置 的例子,并且分析了每种方案的优缺点。

  • mixin —— 存在 命名冲突变量来源不清
  • 高阶组件 —— 存在 命名冲突变量来源不清
  • 作用域插槽(scoped slots)—— 为了逻辑复用导致更多组件实例创建,得不偿失
  • Composition API 组合式函数 —— 完美方案

相信读完本文,你一定学到了在Vue.js搭建的应用中实现代码逻辑复用的最佳姿势!

🌹码字不易!欢迎点赞、评论、收藏、和关注哦!感谢浏览!