vuejs 在运行时采用vdom方案的框架,在写vue sfc的时候其实我们还是在写js。在开发vue应用过程中通常需要对逻辑进行拆分,我在使用vue的过程中总结出几种封装技巧。
下面列子都以请求用户信息为例进行封装说明。
mixins
类似 Object.assign
将 vue 组件配置组合在一起。
使用
封装
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 封装的代码的话很难维护。
无界面组件
将逻辑封装进组件,但是这个组件不产生视图,只执行逻辑和产生数据。
使用
封装
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);
},
});
使用
- 使用时需要缓存 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>
- 使用时不需要缓存 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
为例。
封装
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 方法,依赖的实例通过参数传递,数据的变化通过回调执行。
使用
封装
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
与框架强绑定的逻辑封装,框架帮我们把回调函数做了抽象,将变化直接更新视图,封装逻辑有点像无界面组件的缓存数据部分。
使用
封装
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 之间如果依赖同一份数据,副作用会变得很难管理。