Vue中代码复用最佳实践的探索历程

写在前面

在编程世界中,代码复用一直是我们追求的目标之一,因为它可以帮助我们提高开发效率,减少重复劳动,同时也使得代码更加清晰和易于维护。而在 Vue.js 这个优秀的前端框架中,如何实现代码复用,又如何找到最佳实践,是我一直在探索的问题。

今天,我将以一个简单功能的实现为例,和大家分享一下我的探索历程,希望能够为大家提供一些有益的启示和参考。

基于 Options API 实现

<script>
  const { createApp } = Vue;

  const App = {
    template: `{{x}}, {{y}}`,
    data() {
      return { x: 0, y: 0 };
    },
    mounted() {
      window.addEventListener('mousemove', this.update);
    },
    beforeDestory() {
      window.removeEventListener('mousemove', this.update);
    },
    methods: {
      update({ pageX: x, pageY: y }) {
        this.x = x;
        this.y = y;
      }
    }
  };

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

这个功能会在页面上展示当前鼠标位置的坐标信息。
接下来,我将通过不同的方式实现代码复用,将此功能代码抽离出来。

Mixins

<script>
  const { createApp } = Vue;

  const mouseMixin = {
    data() {
      return { x: 0, y: 0 };
    },
    mounted() {
      window.addEventListener('mousemove', this.update);
    },
    beforeDestory() {
      window.removeEventListener('mousemove', this.update);
    },
    methods: {
      update({ pageX: x, pageY: y }) {
        this.x = x;
        this.y = y;
      }
    }
  };

  const App = {
    mixins: [mouseMixin],
    template: `{{x}}, {{y}}, {{selfZ}}`,
    data() {
      return { selfZ: 'self' };
    }
  };

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

mixin已经实现了逻辑的复用,但是它有以下缺点:

  1. 没有自己的命名空间,容易造成命名冲突。
<script>
  const { createApp } = Vue;

  const mouseMixin = {
    data() {
      return { x: 0, y: 0 };
    },
    methods: {
      update({ pageX: x, pageY: y }) {
        this.x = x;
        this.y = y;
      }
    }
  };

  const App = {
    mixins: [mouseMixin],
    template: `{{x}}, {{y}}`,
    methods: {
      // 来自mouseMixin的update和组件自身的update冲突
      update(){}
    }
  };

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

我们其实只需要 x y 这两个 props,其他的不需要暴露出来。

  1. 当有多个 mixins 应用到同一个组件时,我们无法直观的看到哪一个数据来自于哪一个 mixin 或者组件本身。
<script>
  const { createApp } = Vue;

  const mouseMixin = {
    data() {
      return { x: 0, y: 0 };
    }
  };
  
  const anotherMixin = {
    data() {
      return { z: 0 };
    }
  };

  const App = {
    mixins: [mouseMixin, anotherMixin],
    // 无法清晰知道哪些props来自于哪些mixin或者组件本身,不利于团队协作开发和维护。
    template: `{{x}}, {{y}}, {{z}}`,
    data() {
      return {  };
    }
  };

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

使用 React 的高阶组件概念来实现

<script>
  const { createApp, h } = Vue;

  const useMouse = function (Inner) {
    return {
      data() {
        return { x: 0, y: 0 };
      },
      mounted() {
        window.addEventListener('mousemove', this.update);
      },
      beforeDestory() {
        window.removeEventListener('mousemove', this.update);
      },
      methods: {
        update({ pageX: x, pageY: y }) {
          this.x = x;
          this.y = y;
        }
      },
      render() {
        return h(Inner, {
          x: this.x,
          y: this.y
        });
      }
    };
  };

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

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

高阶组件内部有自己的命名空间,所以内部没有暴露出来的数据不存在命名冲突。不过假如有多个高阶组件同时应用的话,还是会存在下面的问题:

  1. 暴露出来的 props 还是会有命名冲突的风险
  2. 依然存在上面的问题,无法直观地看到数据来源
<script>
  const { createApp, h } = Vue;

  const useMouse = Inner => {
    return {
      data() {
        return { x: 0, y: 0 };
      },
      mounted() {
        window.addEventListener('mousemove', this.update);
      },
      beforeDestory() {
        window.removeEventListener('mousemove', this.update);
      },
      methods: {
        update({ pageX: x, pageY: y }) {
          this.x = x;
          this.y = y;
        }
      },
      render() {
        return h(Inner, {
          ...this.props,
          x: this.x,
          y: this.y
        });
      }
    };
  };

  const useOther = Inner => {
    return {
      data() {
        return { x: 'x' };
      },
      render() {
        return h(Inner, {
          ...this.props,
          x: this.x
        });
      }
    };
  };

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

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

如上, props 中的 x 被覆盖,且无法确定数据来源。

使用slot来实现

<script>
  const { createApp, h } = Vue;

  const Mouse = {
    data() {
      return { x: 0, y: 0 };
    },
    mounted() {
      window.addEventListener('mousemove', this.update);
    },
    beforeDestory() {
      window.removeEventListener('mousemove', this.update);
    },
    methods: {
      update({ pageX: x, pageY: y }) {
        this.x = x;
        this.y = y;
      }
    },
    render() {
      return this.$slots.default ? this.$slots.default({ x: this.x, y: this.y }) : '';
    }
  };

  const Other = {
    data() {
      return { x: 'x' };
    },
    render() {
      return this.$slots.default ? this.$slots.default({ x: this.x }) : '';
    }
  };

  const App = {
    template: `<Mouse v-slot="{x, y}">
                    <Other v-slot="{x: otherX}">
                        {{x}}, {{y}}, {{otherX}}
                    </Other>
                </Mouse>`,
    components: { Mouse, Other }
  };

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

到此为止,我们已经解决了上面暴露的所有问题。使用 slot 我们可以清晰的看到数据来自于哪一个组件,并且加入有命名冲突,我们可以向上述的处理一样使用别名。

但是,这里又有了新的思考,我们这里为了数据的复用,创建了很多组件,性能方面是否会变差?

毕竟,维护一个组件也是需要很多性能开销的。

使用 Composition API 来实现

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

  const useMouse = () => {
    const x = ref(0);
    const y = ref(0);

    const update = e => {
      x.value = e.pageX;
      y.value = e.pageY;
    };

    onMounted(() => {
      window.addEventListener('mousemove', update);
    });

    onUnmounted(() => {
      window.removeEventListener('mousemove', update);
    });

    return {
      x,
      y
    };
  };

  const useOther = () => {
    const x = ref('otherX');

    return {
      x
    };
  };

  const App = {
    setup() {
      const { x, y } = useMouse();
      const { x: otherX } = useOther();

      return { x, y, otherX };
    },
    template: `{{x}}, {{y}}, {{otherX}}`
  };

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

我们好像又回到了js最初的模样。
使用 Composition API 使得我们不再需要考虑 mixins 中的命名空间等问题,也不需要考虑使用 scope slot 方案时造成的额外组件实例维护开销。我们可以轻松地将这些逻辑应用在任何我们想用的地方。

还有一个额外的好处就是,如果我们使用 TS 开发时,我们不需要再为类型推导头疼,在 mixin 中我们不太清楚我们合并过来的到底是什么格式,但是在这里我们可以很容易做到这些。

这些心得主要来自于尤大的讲解视频,从中受益良多,整理出来分享给大家,欢迎大家一起讨论,一起进步。

原文链接:https://juejin.cn/post/7322671519610912779 作者:an3wer

(0)
上一篇 2024年1月12日 上午10:26
下一篇 2024年1月12日 上午10:36

相关推荐

发表回复

登录后才能评论