前面我写了一篇 React Dialog 组件的个人实践 (juejin.cn),本人公司技术栈一直是 react ,没有足够的机会实践 vue3 。今天使用同样的思路封装 vue3 Dialog 组件,比较下两个框架的区别。本篇文章在思路方面不会像 react 那篇详细,感兴趣的可以先看 react 封装篇。
封装 useDialog Hook
在 react 中我使用了 useReducer 来封装,其有一个特性就是 Immutable ,数据一旦被创建,就无法再更改,对 Immutable 对象的任何修改添加或删除操作都会返回一个新的 Immutable 对象,但在 vue 中我们则需要直接对 reactive 对象进行更改,直接重新赋值会导致响应性特性失效,用于 Dialog 出现时隐藏滚动条使用的 useEffect 则使用 watch 来替代。
import {reactive, watch} from 'vue';
type DialogStack = Array<{
dialogFlag: string;
dialogProps?: object;
}>;
interface Action {
type: string;
dialogFlag: string;
dialogProps?: object;
}
const useDialog = () => {
const dialogStack: DialogStack = reactive([]);
const dispatch = (action: Action) => {
switch (action.type) {
case 'close':
// 不能直接重新赋值
while (dialogStack.length) {
dialogStack.pop();
}
break;
case 'open':
dialogStack.push({
dialogFlag: action.dialogFlag,
dialogProps: action.dialogProps,
});
break;
case 'back':
dialogStack.pop();
break;
}
};
watch(() => dialogStack.length, (currLen) => {
document.body.style.cssText = currLen ? 'overflow: hidden;' : '';
});
return {
dialogStack,
dispatch,
};
};
export default useDialog;
Provide/inject 实现全局引用
不引入外部库的情况下,React 中可以使用 context 来实现全局,vue 的解决方案是 Provide/inject
在 App.vue 文件中通过 provide 提供 useDialog 的数据
<script lang="ts">
import {provide} from 'vue';
import useDialog from '@/hooks/useDialog';
export default {
setup() {
const {dialogStack, dispatch} = useDialog();
provide('DialogContext', {
dialogStack,
dispatch
});
}
};
</script>
在 Demo.vue 文件中通过 inject 获取数据
<script lang="ts">
import {inject} from 'vue';
export default {
name: 'Demo',
setup() {
// @ts-ignore
const {dispatch} = inject('DialogContext')
}
};
</script>
Dialog 子组件
我们后续会通过 dispatch 一个 action (action 的类型可通过前面的 ts 标注查看)来显示对应的 Dialog 子组件,建议把 Dialog 展示需要的数据以及事件都通过 action 传过去
我创建了两个 Dialog 子组件,方便后续的演示。
-
AboutUs
<template> <div class="container"> <h1> About Us </h1> <div> <button @click="dialogProps.close"> close </button> <button @click="dialogProps.openElse" class="open-else-margin"> open else </button> </div> </div> </template> <script> export default { name: "AboutUs", props: { dialogProps: { close: Function, openElse: Function } } } </script> <style scoped> .container { overflow: hidden; width: 200px; height: 130px; text-align: center; } .open-else-margin { margin-left: 10px; } </style> -
ContactUs
<template> <div class="container"> <h1> Contact Us </h1> <div> <button @click="dialogProps.back"> back </button> </div> </div> </template> <script> export default { name: "ContactUs", props: { dialogProps: { back: Function } } } </script> <style scoped> .container { overflow: hidden; width: 300px; height: 170px; text-align: center; } </style>
创建 Dialog Component
react 中可以通过 createPortal 挂载在 body 节点上,vue 也提供了功能相似的 teleport ,但在 Dialog 子组件匹配上 v-if 显然没有原生 js 来得灵活(如果有更好的匹配方式请指教)。最后,你还需要在 App.vue 中引入 Dialog 组件,作为 Dialog 子组件显示的容器。
<template>
<!-- 挂载到 body 节点-->
<teleport to="body">
<section class="overlay" v-for="({dialogFlag, dialogProps}) in dialogStack" :key="dialogFlag">
<main class="wrapper">
<AboutUs v-if="dialogFlag === 'About_Us'" :dialog-props="dialogProps" />
<ContactUs v-if="dialogFlag === 'Contact_Us'" :dialog-props="dialogProps" />
</main>
</section>
</teleport>
</template>
<script lang="ts">
import {inject} from 'vue';
import AboutUs from '@/components/Dialog/AboutUs.vue';
import ContactUs from '@/components/Dialog/ContactUs.vue';
export default {
name: 'index',
components: {ContactUs, AboutUs},
setup() {
// @ts-ignore
const {dialogStack} = inject('DialogContext');
return {
dialogStack
};
}
};
</script>
<style scoped>
.overlay {
display: flex;
align-items: center;
justify-content: center;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, .5);
z-index: 9999;
}
.wrapper {
border-radius: 20px;
box-shadow: 0 2px 30px 0 rgba(0, 0, 0, .4);
background-color: #fff;
}
</style>
使用
使用 inject 拿到 useDialog 的数据,通过 dispatch 进行调用,以下为具体代码:
<template>
<button @click="openDialog">
dispatch dialog
</button>
<!-- 用于展示滚动条-->
<div class="block"/>
</template>
<script lang="ts">
import {inject} from 'vue';
export default {
name: 'Demo',
setup() {
// @ts-ignore
const {dispatch} = inject('DialogContext');
const close = () => {
dispatch({
type: 'close'
});
};
const back = () => {
dispatch({
type: 'back'
});
};
const openElse = () => {
dispatch({
type: 'open',
dialogFlag: 'Contact_Us',
dialogProps: {
back
}
});
};
const openDialog = () => {
dispatch({
type: 'open',
dialogFlag: 'About_Us',
dialogProps: {
close,
openElse
}
});
};
return {
openDialog
};
}
};
</script>
<style scoped>
.block {
width: 200px;
height: 1500px;
background-color: #2b2b2b;
}
</style>
最后,来看下具体效果:
欢迎各位在评论区交流。