Vue3 Dialog 组件封装实践(包含与 react 的比较)

2,916 阅读2分钟

前面我写了一篇 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>

最后,来看下具体效果:

GIF 2021-8-3 16-07-33.gif


欢迎各位在评论区交流。