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

写在前面

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

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

基于 Options API 实现

js 复制代码
<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

js 复制代码
<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. 没有自己的命名空间,容易造成命名冲突。
js 复制代码
<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 或者组件本身。
js 复制代码
<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 的高阶组件概念来实现

js 复制代码
<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. 依然存在上面的问题,无法直观地看到数据来源
js 复制代码
<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来实现

js 复制代码
<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 来实现

js 复制代码
<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 中我们不太清楚我们合并过来的到底是什么格式,但是在这里我们可以很容易做到这些。

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

相关推荐
小雨下雨的雨9 小时前
井字棋AI机器人实现详解 - Minimax算法实战-鸿蒙PC Electron框架完成
前端·人工智能·算法·华为·electron·鸿蒙
ZC跨境爬虫13 小时前
跟着 MDN 学JavaScript day_7:数学运算与逻辑判断实战测试
开发语言·前端·javascript·学习·ecmascript
fangdengfu12313 小时前
ES分析系统各个服务日志占用量
java·前端·elasticsearch
凌云拓界13 小时前
文件管理:让AI安全操作你的电脑 ——CogitoAgent开发实战(三)
javascript·人工智能·架构·开源·node.js
凌云拓界13 小时前
联网能力:让AI看见更广阔的世界 ——CogitoAgent开发实战(四)
javascript·人工智能·架构·node.js·创业创新
JustHappy14 小时前
古法编程秘籍(六):程序到底是怎么跑起来的?从 IO 到中断,一次讲明白
前端·后端·全栈
HYCS15 小时前
用pixi.js实现fabric.js(六):从线性代数的角度理解编辑器交互
前端·javascript·canvas
卷帘依旧15 小时前
useImperativeHandle的作用
前端
卷帘依旧15 小时前
Hooks在Fiber上的存储原理
前端