createApp 创建函数式组件

165 阅读5分钟

单文件组件

平时我们使用的组件方式大多是单文件组件

一个 Vue 单文件组件 (SFC),通常使用 *.vue 作为文件扩展名,它是一种使用了类似 HTML 语法的自定义文件格式,用于定义 Vue 组件。一个 Vue 单文件组件在语法上是兼容 HTML 的。

每一个 *.vue 文件都由三种顶层语言块构成:<template><script> 和 <style>,以及一些其他的自定义块:

  • Child.vue
<template>
    <div @click="emit('child-emit', 'emit回调参数')">Child: {{ content }}</div>
</template>
<script setup>
    defineProps({
        content: String
    })
    
    defineEmits('child-emit')
    
</script>
  • Parent.vue
<template>
    <Child content="这是一个单文件组件" @childEmit="childEmitHandle" />
</template>
<script setup>
    import Child from 'Child.vue'
    
    const childEmitHandle = (str) => {
        console.log(str)
    }
</script>

函数式组件

函数式组件可以使用函数的方式来调用组件,vue3中用createApp来创建

  • createApp

创建一个应用实例

  • 类型:

function createApp(rootComponent: Component, rootProps?: object): App

  • 参数

    • 第一个参数是根组件。第二个参数可选,它是要传递给根组件的 props。
  • 示例:

import { createApp } from 'vue'
import Child from 'Child.vue'

const app = createApp(Child, {
    content: '这是一个单文件组件',
    onChildEmit: (str) => childEmitHandle(str)
})

const childEmitHandle = (str) => {
    console.log(str)
}
将实例创建到#app元素上
app.mount('#app')
  • 函数式声明组件接收emit回调是在第二个参数里,用驼峰式命名前面加上on。例如子组件导出的emit('call-envent'), 函数式组件里面就得用onCallEvent接收

在 Vue 3 中,函数式声明组件具有以下优点:

  • 性能优化:不需要实例化,不需要响应式系统进行数据变更的检测,因此渲染性能相对较好,在简单,无状态的组件中性能优势尤为明显
  • 代码结构清晰: 结构比较简单,更符合函数式编程的思想,容易进行组件的抽象和复用,可减少复用代码的编写,提高代码的可维护性和可复用性。
  • 易于理解和测试:由于是无状态的,其行为更加可预测,没有状态需要跟踪,更容易被理解,维护和测试

用函数式调用组件的方式创建一个MessageBox组件

  • /hxMessageBox/module.vue
<template>
    <transition name="hx-message__box" @after-leave="onAfterLeave">
        <div class="hx-message__box" v-if="isVisible"  :style="{width: width}">
            <div class="hx-message__box-content">
                <div class="hx-message__content-header">
                    <p class="hx-message__header-title">{{ headerText }}</p>
                    <p class="hx-message__header-close" v-if="showCancel" @click="cancelHandle"></p>
                </div>
                <div class="hx-message__content-body">
                    <p class="hx-message__body-text">{{ content }}</p>
                </div>
                <div class="hx-message__content-footer">
                    <hx-button v-if="type !== 'comfirm'" @click="cancelHandle">cancelText</hx-button>
                    <hx-button class="hx-message__footer-btn" level="primary" @click="successHandle">successText</hx-button>
                </div>
            </div>
        </div>
    </transition>
</template>

<script setup lang="ts">
import { ref, computed, nextTick } from 'vue';
import HxButton from '../HxButton.vue';

const props = defineProps({
    width: {
        default: '400px'
    },
    successText: {
        type: String,
        default: '确定'
    },
    cancelText: {
        type: String,
        default: '取消'
    },
    headerText: {
        type: String,
        default: '提示'
    },
    content: {
        type: String,
        default: ''
    },
    // 成功回调函数
    successPromise: {
        type: Function
    },
    // 失败回调函数
    cancelPromise: {
        type: Function
    },
    hide: {
        type: Function
    },
    // 是否需要右上角关闭按钮
    showCancel: {
        type: Boolean,
        default: true
    },
    // 弹窗类型: default 默认显示确认,取消  /  confirm 仅显示确认
    type: {
        type: String,
        default: 'default'
    },
    // 默认点击遮罩层不关闭
    closeByOverlay: {
        type: Boolean,
        default: false
    },
    // 传入调用方法
    holdOnFn: {
        type: Function
    },
    overlayNode: {
        type: Object
    }
})

// 控制弹窗
const isVisible = ref(false)

/**
 * @description: 遮罩层需要消失动画,两个动画效果必须同时触发,所以关闭时直接调用hide方法
 * @return {*}
 */
const onAfterLeave = () => {
    props.hide && props.hide()
}

/**
 * @description: 显示弹窗
 * @return {*}
 */
 const show = () => {
    isVisible.value = true;
    if (props.closeByOverlay) {
        props.overlayNode.addEventListener('click', hidden);
    }
};

/**
 * @description: 隐藏弹窗
 * @return {*}
 */
const hidden = () => {
    isVisible.value = false;
    onAfterLeave()
    if (props.closeByOverlay) {
        props.overlayNode.removeEventListener('click', hidden);
    }
};

const successHandle = () => {
    if (!props.holdOnFn) {
        props.successPromise && props.successPromise()
        nextTick(() => {
            hidden()
        })
    } else {
        props.holdOnFn()
        nextTick(() => {
            hidden()
        })
    }
}

const cancelHandle = () => {
    props.cancelPromise && props.cancelPromise();
    nextTick(() => {
        hidden();
    });
}

/**
 * @description: 将方法暴露出去
 * @return {*}
 */
 defineExpose({
    show
});

</script>

<style lang="scss">
.hx-message__dialog {
    position: fixed;
    z-index: 999;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
}
.hx-message__overlay {
    background: rgba(0,0,0,.5);
    opacity: 0;
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 100;
}
.hx-message__box-overlay-show-to {
    animation: opacity-show .3s linear forwards;
}
@keyframes opacity-show {
    0% {
        opacity: 0;
    }
    100% {
        opacity: 1;
    }
}
.hx-message__box-overlay-leave-to {
    animation: opacity-leave .3s linear forwards;
}
@keyframes opacity-leave {
    0% {
        opacity: 1;
    }
    100% {
        opacity: 0;
    }
}
.hx-message__box {
    position: fixed;
    top: 45%;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: 99;
    max-width: 92%;
    overflow: hidden;
    &-content {
        width: 100%;
        padding: 24px;
        border-radius: 10px;
        background-color: #fff;
        .hx-message__content-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            .hx-message__header-title {
                flex: 1;
                font-size: 18px;
                color: #1A1A1A;
                line-height: 24px;
            }
            .hx-message__header-close {
                flex-shrink: 0;
                margin: 0 0 0 24px;
                position: relative;
                display: inline-block;
                width: 16px;
                height: 16px;
                cursor: pointer;

                &::before,
                &::after {
                    content: '';
                    position: absolute;
                    height: 1px;
                    background: black;
                    width: 100%;
                    top: 50%;
                    left: 50%;
                }

                &::before {
                    transform: translate(-50%, -50%) rotate(-45deg);
                }

                &::after {
                    transform: translate(-50%, -50%) rotate(45deg);
                }
            }
        }

        .hx-message__content-body {
            display: flex;
            justify-content: center;
            width: 100%;
            padding: 24px 0 34px;
            .hx-message__body-text{
                max-width: 100%;
                width: 100%;
                text-align: left;
                font-size: 14px;
                color: #1A1A1A;
                line-height: 18px;
            }
        }

        .hx-message__content-footer {
            display: flex;
            justify-content: flex-end;

            .hx-message__footer-btn {
                margin-left: 15px
            }
        }
    }
}
</style>
  • hxMessageBox/index.ts
import { createApp } from "vue";
import hxMessageBox from "./module.vue"

const hxMessageBoxDom = (options = {}) => {
    

    // 创建弹窗元素节点
    const rootNode:HTMLElement = document.createElement('div')
    rootNode.className = `hx-message__dialog`
    document.body.appendChild(rootNode)

    // 创建遮罩层
    let overlayNode = createOverlay(rootNode);
    function createOverlay(rootNode) {
        const overlayNode:HTMLElement = document.createElement('div')
        overlayNode.classList.add('hx-message__overlay')
        rootNode.appendChild(overlayNode)
        setTimeout(() => {
            overlayNode.classList.add('hx-message__box-overlay-show-to')
        }, 100)
        return overlayNode
    }

    const hide = function() {
        // 显示移出动画
        overlayNode.classList.add('hx-message__box-overlay-leave-to');

         // 卸载已挂载的应用实例
         setTimeout(() => {
            app.unmount();
            document.body.contains(rootNode) && document.body.removeChild(rootNode);
        }, 500);
    }

    // 创建应用实例(第一个参数是根组件。第二个参数可选,它是要传递给根组件的 props)
    const app = createApp(hxMessageBox, {
        ...options,
        hide,
        overlayNode
    });

    // 将应用实例挂载到创建的 DOM 元素上
    return app.mount(overlayNode);
}

/**
 * @description: 显示弹窗
 * @return {*}
 * @param {*} options
 */
const showHxMessageBoxDom = (options = {}) => {
    return new Promise<void>((resolve, reject) => {
        options['successPromise'] = () => {
            resolve();
        };
        options['cancelPromise'] = () => {
            reject();
        };
        const popres: any = hxMessageBoxDom(options);
        popres.show();
    });
};

// 注册插件app.use()会自动执行install函数
hxMessageBoxDom.install = app => {
    // 注册全局属性,类似于 Vue2 的 Vue.prototype
    app.config.globalProperties.$hxMessageBox = options => showHxMessageBoxDom(options);
};
// 定义confirm方法用于直接调用
hxMessageBoxDom.confirm = options => showHxMessageBoxDom(options);

export default hxMessageBoxDom;
  • main.ts
    • 在main.ts中全局注册一下
import { createApp } from 'vue'
import hxMessageBox from './lib/hxMessageBox/index'

const app = createApp(App)

app.use(hxMessageBox)

  • 调用
<demo>常规用法</demo>
<template>
    <div>
        <hx-button @click="open">点击</hx-button>
         <hx-button @click="open2">只有确定框</hx-button>
         <hx-button @click="open3">点击遮罩关闭弹框</hx-button>
    </div>
</template>

<script setup lang="ts">
import { HxButton, HxMessageBox } from 'hx-gulu-ui';
const open = () => {
    HxMessageBox.confirm({
        content: '消息内容',
        successText: '确定',
        cancelText: '取消',
        headerText: '头部标题',
    })
    .then(() => {
      console.log('success')
    })
    .catch(() => {
      console.log('cancel')
    })
}

const open2 = () => {
     hxMessageBox.confirm({
        content: 'confirm 弹框',
        successText: '确定',
        headerText: '头部标题',
        type: 'comfirm',
    })
    .then(() => {
        console.log('success')
    })
}

const open3 = () => {
     hxMessageBox.confirm({
        content: '点击遮罩层可以关闭',
        successText: '确定',
        cancelText: '取消',
        headerText: '头部标题',
        closeByOverlay: true,
    })
    .then(() => {
        console.log('success')
    })
    .catch(() => {
        console.log('cancel')
        })
}
</script>