vuejs 封装技巧

212 阅读4分钟

vuejs 在运行时采用vdom方案的框架,在写vue sfc的时候其实我们还是在写js。在开发vue应用过程中通常需要对逻辑进行拆分,我在使用vue的过程中总结出几种封装技巧。

下面列子都以请求用户信息为例进行封装说明。

mixins

类似 Object.assign 将 vue 组件配置组合在一起。

使用

sfc playground

封装

import { defineComponent } from "vue";

export default defineComponent({
  methods: {
    fetchUserInfo(params) {
      return Promise.resolve({
        params,
        name: "hello",
        phone: "11111111111",
      });
    },
  },
  data() {
    return {
      userInfo: null,
    };
  },
  async mounted() {
    this.userInfo = await this.fetchUserInfo({
      token: "9527",
      uid: "9527",
    });
  },
});

使用

<template>
  usecase:
  <pre>
    {{ userInfo }}
  </pre>
</template>
<script>
  import { defineComponent } from "vue";
  import FetchUserInfo from "./fetchUserInfo.ts";

  export default defineComponent({
    mixins: [FetchUserInfo],
  });
</script>

优点

  • 使用简单,封装简单。

缺点

  • 组合后的 vue 配置将丢失类型,使用者只能进去 minix 文件里面找到方法定义才可以使用,vscode 没有办法提示也没有办法跳转。
  • 在 mixins 里可以加任何代码,props、data、methods、甚至多个 minix 可以互相调用代码,就导致如果不了解 mixins 封装的代码的话很难维护。

无界面组件

将逻辑封装进组件,但是这个组件不产生视图,只执行逻辑和产生数据。

使用

sfc playground

封装

import { defineComponent, onMounted, ref } from "vue";

export default defineComponent({
  name: "FetchUserInfo",
  props: {
    params: Object,
  },
  emits: ["change"],
  setup(props, { emit, slots }) {
    const { params } = props;
    const userInfo = ref(null);

    onMounted(async () => {
      userInfo.value = await api(params);
      emit("change", userInfo.value);
    });
    return () => slots.default?.(userInfo.value);
  },
});

使用

  1. 使用时需要缓存 userInfo
<template>
  case1: <br />
  <FetchUserInfo :params="{ token: '9527'  }" @change="handleUserInfo" />
  <pre>
    {{ state.userInfo }}
  </pre>
</template>
<script lang="ts">
  import { defineComponent, reactive } from "vue";
  import FetchUserInfo from "./fetchUserInfo.ts";
  export default defineComponent({
    components: { FetchUserInfo },
    setup() {
      const state = reactive({
        userInfo: null,
      });

      return {
        state,
        handleUserInfo(userInfo) {
          state.userInfo = userInfo;
        },
      };
    },
  });
</script>
  1. 使用时不需要缓存 userInfo
<template>
  case2: <br />
  <FetchUserInfo :params="{ token: '9527'  }">
    <template #default="userInfo">
      <pre>
        {{ userInfo }}
      </pre>
    </template>
  </FetchUserInfo>
</template>
<script lang="ts">
  import { defineComponent } from "vue";
  import FetchUserInfo from "./fetchUserInfo.ts";
  export default defineComponent({
    components: { FetchUserInfo },
  });
</script>

优点

  • 封装代码的感受和 minix 相当,多了组件的限制,限制了 minix 的灵活性,将每个 minix 都独立,不会相互影响,而需要相互影响的代码只会在最上层应用中体现。
  • 符合组件化的设计逻辑,没有学习成本。在不提供任何用户界面的情况下,提供了复杂的逻辑,而且这些逻辑都是可以加上界面做定制化的 ui。

缺点

  • 使用起来相对复杂一点,因为缓存数据是异步的需要额外处理(比如使用 Promise 封装 defer)来处理。
  • 如果使用多个组件不缓存数据 template 的层级容易变深。
<FetchUserInfo :params="{ token: '9527'  }">
  <template #default="userInfo">
    <FetchGroupInfo :params="{ groupId: userInfo.groupId  }">
      <template #default="groupInfo">
        <FetchTags :params="{ group: groupInfo.group }">
          <template #default="tags">
            {{ userInfo }} {{ groupInfo }} {{ tags }}
          </template>
        </FetchTags>
      </template>
    </FetchGroupInfo>
  </template>
</FetchUserInfo>

hoc

高阶组件是 react 提出的概念。HOC 最大的特点就是:接受一个组件作为参数,返回一个新的组件。比如给组件挂载的时候做上报,点击事件做上报都特别方便。

zh-hans.reactjs.org/docs/higher…

使用

logProps 为例。

sfc playground

封装

import { defineComponent, getCurrentInstance, h } from "vue"

export default function logProps(component) {
  return defineComponent({
    setup () {
      const instance = getCurrentInstance()!
      const { ref, props, children } = instance.vnode
      console.log(props)
      return () => {
        const vnode = h(component, props, children)
        // ensure inner component inherits the async wrapper's ref owner
        vnode.ref = ref
        return vnode
      }
    }
  })
}

使用

<template>
  <Comp msg="hello world" />
</template>
<script>
import { defineComponent, ref } from "vue"
import logProps from "./logProps.ts"
import Comp from "./Comp.vue"

export default defineComponent({
  components: {
    Comp: logProps(Comp)
  }
})
</script>

优点

  • 支持 ES6,比 mixins 优胜。
  • 复用性强,HOC 是纯函数且返回值仍为组件,在使用时可以多层嵌套,在不同情境下使用特定的 HOC 组合也方便调试。
  • 同样由于 HOC 是纯函数,支持传入多个参数,增强了其适用范围。

缺点

  • 当有多个 HOC 一同使用时,无法直接判断子组件的 props 是哪个 HOC 负责传递的。
  • Vue 中的 HOC 类型很难覆盖重写。
  • HOC 产生了许多无用的组件,加深了组件层级。

IOC

将代码封装成 IOC 方法,依赖的实例通过参数传递,数据的变化通过回调执行。

使用

sfc playground

封装

export default function userInfoService(http, cb) {

  return {
    async get(params) {
      const res = await http.post('/userinfo', params)
      cb(res)
    }
  }
}

使用

<template>
  usecase:
  <pre>
    {{ userInfoState }}
  </pre>
</template>
<script>
  import { defineComponent, ref } from "vue";
  import createUserInfoService from "./fetchUserInfo.ts";

  export default defineComponent({
    setup() {
      const userInfoState = ref(null);
      const userInfoService = createUserInfoService(null, (userInfo) => {
        userInfoState.value = userInfo;
      });

      userInfoService.get({
        token: "9527",
        uid: "9527",
      });

      return {
        userInfoState,
      };
    },
  });
</script>

优点

  • 执行服务提供的方法来更改数据,当数据变化的时候调用参数中的回调,在回调中再触发框架的视图变化,解耦框架视图变化逻辑,纯 js 操作更新数据结构,封装出来的服务甚至可以跨框架使用。
  • 降低了使用资源双方的依赖程度,资源集中管理,资源容易配置和管理。

缺点

  • 封装难度变大,在封装过程中不能使用框架提供的方法提高开发效率。

hooks

与框架强绑定的逻辑封装,框架帮我们把回调函数做了抽象,将变化直接更新视图,封装逻辑有点像无界面组件的缓存数据部分。

sfc playground

使用

封装

import { ref } from "vue";

export default function useUserInfo(params) {
  const userInfo = ref(null);

  api(params).then((res) => {
    userInfo.value = res;
  });

  return {
    userInfo,
  };
}

使用

<template>
  usecase:
  <pre>
    {{ userInfoState }}
  </pre>
</template>
<script>
  import { defineComponent, ref } from "vue";
  import useUserInfo from "./fetchUserInfo.ts";

  export default defineComponent({
    setup() {
      const userInfoState = useUserInfo({
        token: "9527",
        uid: "9527",
      });

      return {
        userInfoState,
      };
    },
  });
</script>

优点

  • 可以用框架提供的副作用方法,不需要关注数据和视图之间的桥接,让封装变简单。

缺点

  • 封装的方法和框架强耦合,不能跨框架使用。
  • 框架提供的方法都会有让视图变化的副作用,hook 之间如果依赖同一份数据,副作用会变得很难管理。