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

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

相关推荐
华仔啊12 小时前
图片标签用 img 还是 picture?很多人彻底弄混了!
前端·html
lichong95112 小时前
XLog debug 开启打印日志,release 关闭打印日志
android·java·前端
南山安12 小时前
栈(Stack):从“弹夹”到算法面试题的进阶之路
javascript·算法·面试
烟袅13 小时前
作用域链 × 闭包:三段代码,看懂 JavaScript 的套娃人生
前端·javascript
San30.13 小时前
深入理解 JavaScript 异步编程:从 Ajax 到 Promise
开发语言·javascript·ajax·promise
风止何安啊13 小时前
收到字节的短信:Trae SOLO上线了?尝尝鲜,浅浅做个音乐播放器
前端·html·trae
抱琴_13 小时前
大屏性能优化终极方案:请求合并+智能缓存双剑合璧
前端·javascript
用户4639897543213 小时前
Harmony os——长时任务(Continuous Task,ArkTS)
前端
fruge13 小时前
低版本浏览器兼容方案:IE11 适配 ES6 语法与 CSS 新特性
前端·css·es6
颜酱13 小时前
开发工具链-构建、测试、代码质量校验常用包的比较
前端·javascript·node.js