VuePressVuePress
首页
  • 基础
  • UI
  • JavaScript
  • CSS
  • postcss
  • Vue3
  • Vue的设计与实现
  • 前端常用插件
  • PHP
  • Laravel
  • Linux
  • 线性代数
Category
AI
jiyun
Timeline
首页
  • 基础
  • UI
  • JavaScript
  • CSS
  • postcss
  • Vue3
  • Vue的设计与实现
  • 前端常用插件
  • PHP
  • Laravel
  • Linux
  • 线性代数
Category
AI
jiyun
Timeline
  • 基础知识

    • ref&reactive
    • watchEffect
    • 响应式: 工具函数
    • 组合式函数
    • 防止ref不会自动解包
    • 注入全局属性
  • 虚拟节点

    • 虚拟节点介绍
    • component 属性
    • component.proxy
    • appContext
  • 组件

    • 函数式组件
      • 封装一个 Message 组件
        • 1. 创建 Message 组件
        • 2. 创建函数式 Message 调用
        • 3. 调用示例
  • 过渡和动画

    • 函数式组件
  • vite

    • import.meta.glob
    • import.meta.url
  • demo

    • 打印面单-雏形
    • 打印面单-多模板

封装一个 Message 组件

封装函数式组件的核心点主要是:

  1. 学会熟练使用 createVNode 函数创建 vNode
  2. 以及使用 render函数将 vNode 渲染成真实DOM。

以下是一个基本的实现步骤,包括创建组件和通过插件安装全局调用的方法。

1. 创建 Message 组件

首先,创建一个 Message.vue 文件,用于显示消息内容:

<template>
    <div v-if="visible" ref="comRef"
         class="fixed left-1/2 -translate-x-1/2
         w-full sm:max-w-[300px]
         z-message
         border-box
         data-[border]:border
         px-4 py-2 rounded-lg overflow-hidden shadow-2xl
         text-sm font-medium text-center
         transition-all duration-200 ease-in-out"
         :class="{
             'text-emerald-500 bg-emerald-50 border-emerald-400':type==='success',
             'text-amber-500 bg-amber-50 border-amber-400':type==='warning',
             'text-red-500 bg-red-50 border-red-400':type==='error',
             'text-gray-500 bg-gray-50 border-gray-400':type==='info',
         }"
         :style="{top:`${topOffset}px`}">
        <div class="flex-auto">
            <slot>
                {{ message }}
            </slot>
        </div>
        <span class="flex-none" v-if="showClose" @handleClick="close">
            <svg-icon name="close" class="w-4 h-4"/>
        </span>
    </div>
</template>
<script setup>
import {ref, computed,onMounted} from 'vue';
import {useElementSize} from '@vueuse/core';
import {isFinite} from "lodash";
import SvgIcon from "~m/svgIcons/SvgIcon.vue";

const props = defineProps({
    message: {
        type: [String, Object],
        default: null
    },
    type: {
        type: String,
        default: 'success',
        validator: (value) => {
            return ['success', 'warning', 'error', 'info'].includes(value)
        }
    },
    showClose: {
        type: Boolean,
        default: false
    },
    duration: {
        type: Number,
        default: 3000
    }
})



const comRef = ref(null);
const comIndex = ref(0);
const {height} = useElementSize(comRef);

const topOffset = computed(() => {
    if (isFinite(comIndex.value) && isFinite(height.value)) {
        return comIndex.value * (height.value + 25) + 10;
    }
    return 10;
});



//#region 关闭操作

const visible = ref(true);

const emit = defineEmits(['close']);

onMounted(() => {
    startTimer()
})

let timer = null;
const startTimer = () => {
    timer = setTimeout(() => {
        close()
    }, props.duration);
}
const clearTimer = () => {
    if (timer) {
        clearTimeout(timer);
        timer = null;
    }
}
const close = () => {
    clearTimer();
    visible.value = false;
    emit('close');
}
//#endregion

defineExpose({
    comIndex,
    visible
})
</script>

2. 创建函数式 Message 调用

接下来,创建一个 JavaScript 文件,例如 message.js,用于封装函数式调用方法:


import { shallowReactive,createVNode, isVNode, render,watch } from 'vue';
import {isElement,isString, isFunction, hasOwn, isObject,isClient} from "~m/utils/types.js";

import MessageComponent from './message.vue';

const InstanceMap = shallowReactive(new Map());

watch(InstanceMap, () => {
    let comIndex = 0;
    InstanceMap.forEach((_, com) => {
        console.log('com~~~~:',com)
        com.exposed.comIndex.value = comIndex++;
    })
});

const getContainer = () => {
    return document.createElement('div');
};

const getAppendToElement = (props) => {

    // 默认将元素附加到 body 上
    let appendTo = document.body;

    if (!props.appendTo) return appendTo;

    // 如果 props 中有 appendTo 属性,则根据其类型来确定附加位置
    if (isString(props.appendTo)) {
        return document.querySelector(props.appendTo) || appendTo;
    }else if (isFunction(props.appendTo)) {
        return props.appendTo() || appendTo;
    } else if (isElement(props.appendTo)) {
        return props.appendTo;
    } else {
        return appendTo;
    }
}

const initInstance = (props, container, appContext = null) => {
    // createVNode 的函数签名中, 第一个参数可以是动态组件,也可以是类组件,亦或是标签名字符串 第2个参数是props 第3个参数是children
    const vNode = createVNode(MessageComponent, props,
        isFunction(props.message) || isVNode(props.message) ?
            {default: isFunction(props.message)? props.message: () => props.message}
            : null);

    // 如果提供了 appContext,则将其设置为虚拟节点的 appContext
    vNode.appContext = appContext

    // 使用 render 函数将虚拟节点渲染到挂载容器中
    render(vNode, container);

    const com = vNode.component;

    if (!container.firstElementChild) {
        com.exposed.visible.value = true;
    }

    if (!container.firstElementChild) {
        console.error('无法呈现消息组件。容器中未找到子元素。');
        return null;
    }

    // 获取最终确定的附加位置,并将挂载容器的第一个子元素附加到该位置
    const appendToElement = getAppendToElement(props);

    appendToElement.appendChild(container.firstElementChild)

    return com;
}

const showMessage = (options, appContext = null) => {

    const container = getContainer();

    options.onClose = () => {
        const currentInstance = InstanceMap.get(instance);
        render(null, container);
        InstanceMap.delete(instance);
        currentInstance.resolve('close');
    };

    const instance = initInstance(options, container, appContext);

    if (!instance) return;

    const vm = instance.proxy;
    for (const prop in options) {
        if(hasOwn(options,prop) && !hasOwn(vm.$props,prop)) {
            vm[prop] = options[prop];
        }
    }
    return instance;
}

const Message = (options, appContext = null) => {
    if(!isClient) return Promise.reject('Message is not supported in the current environment');
    let callback = null;
    if (isString(options) || isVNode(options)) {
        options = {
            message: options
        };
    } else {
        callback = options.callback;
    }
    return new Promise((resolve, reject) => {
        const com = showMessage(options, appContext != null ? appContext : Message._context);
        if (!com) return reject('无法呈现消息组件。容器中未找到组件子元素。');
        InstanceMap.set(com, {
            options,
            callback,
            resolve,
            reject
        });
    });
}

const MESSAGE_TYPE_VARIANTS = ['info', 'success', 'warning', 'error'];
MESSAGE_TYPE_VARIANTS.forEach(type=> {
    Message[type] = (message, options = {}) => {
        return Message(Object.assign( options,{
            message,
            type,
        }), appContext);
    };
})

Message._context = null;

export {Message as default};

3. 调用示例

Message('请先添加货物类型').then(res=>{
console.log('请先添加货物类型~~~~:',res)
})
Message({
message: 'hello world',
'data-border':true,
type: 'error',
duration:10000
}).then(res=>{
console.log('hello world~~~~:',res)
})
Last Updated:
Contributors: BaronYan