欢迎光临
我们一直在努力

通用消息组件 bug 修复及更好的实现是使用函数调用组件(优化:弃 createApp 改用 h 函数 + render )

Message组件只显示一次的原因分析


Message组件在多次触发错误时仅显示一次,主要原因是组件内部状态isVisible未重置。首次错误时,isVisible设为true显示消息,用户关闭后变为false。若父组件未销毁Message(如error.status未重置),后续错误因isVisible仍为false而无法显示。


解决方案:

  • 监听props变化:在Message组件中通过watch监听message或type,新错误传入时重置isVisible为true。
  • 父组件重置状态:在App.vue中监听closeMessage事件,主动重置error.status为false,触发组件销毁重建。

  • 优化建议:改用函数式调用(如createMessage)动态创建/销毁实例,避免状态残留问题。


    使用虚拟 DOM 优化消息函数实现


    通用消息组件 bug 修复


    多次点击登录,多次发生错误,为什么只显示一次Message组件


    Message组件只显示一次的原因分析

    基于提供的 App.vue 代码,Message 组件通过 v-if="error.status" 控制显示。当 error.status 为 true 时,组件会渲染并显示错误消息。以下是可能导致“只显示一次”的原因:

  • 状态重置机制缺失:

    • error 是从 Vuex store 的 state.error 计算得来的。如果每次登录错误后,store 中的 error.status 没有被重置为 false,组件会持续显示(而不是只显示一次)。但如果有自动隐藏逻辑(例如在 Message 组件内部使用 setTimeout 清除错误状态),它可能会在显示后自动消失,导致后续错误不再触发显示。
    • 检查你的 Vuex store(例如 store/modules/error.js 或类似文件)中是否有类似 setTimeout(() => { commit('clearError') }, 3000) 的逻辑。如果有,这就是原因。
  • 组件响应性问题:

    • 如果 error.status 在第一次错误后被设置为 true,但后续错误没有改变其值(例如,store action 只在状态为 false 时更新),Vue 的响应系统可能不会重新渲染组件。
    • 确保每次错误都触发 store.commit('setError', { status: true, message: '新错误' }),并在显示后重置。
  • Message 组件内部逻辑:

    • Message 组件可能有内置的“只显示一次”机制,例如使用 v-show 或动画库(如 Vue Transition)来控制可见性,但只在首次触发时生效。检查 Message.vue 文件的代码。
  • 建议修复

    要确保每次错误都显示 Message,可以在 store 中添加重置逻辑,或修改组件为每次错误都重新渲染。


    Message组件只显示一次的原因

    基于 Message.vue 的代码,问题在于组件的内部状态 isVisible 没有在每次新错误时重置。以下是详细分析:

  • 组件行为:

    • isVisible 初始化为 true,控制组件的显示。
    • 当用户点击关闭按钮时,hide() 将 isVisible 设为 false,组件隐藏,并触发 emit('close-message')(但 App.vue 中未监听此事件)。
    • 在 App.vue 中,v-if="error.status" 控制 Message 组件的存在。如果 error.status 保持 true(未重置),组件不会被销毁和重新创建,因此 isVisible 保持 false,导致后续错误时组件不再显示。
  • 为什么只显示一次:

    • 第一次错误:error.status 为 true,组件创建,isVisible = true,显示消息。
    • 用户关闭后:isVisible = false,组件隐藏。
    • 后续错误:error.status 仍为 true,组件未重新创建,isVisible 仍为 false,故不显示。
    • 这解释了“多次点击登录,多次发生错误,但只显示一次”的现象。
  • 修复建议

    要确保每次新错误都显示消息,需要在 Message.vue 中监听 props 变化,重置 isVisible。使用 watch 监听 message 或 type 的变化。


    • 解释:watch 监听 props.message(或 type),当新错误消息传入时,重置 isVisible 为 true,确保每次错误都显示。
    • 注意:如果 error.status 在显示后被重置为 false,组件会销毁,下次创建时 isVisible 自动为 true。但如果未重置,则上述修复必要。

    实际应用

    Message.vue

    <template>
    <teleport to="#message">
    <div class="alert message-info fixed-top w-50 mx-auto d-flex justify-content-between mt-2" :class="classObject" v-if="isVisible">
    <span>{{ message }}</span>
    <button type="button" class="btn-close" aria-label="Close" @click.prevent="hide">
    </button>
    </div>
    </teleport>
    </template>

    <script lang="ts" setup>
    import { ref, watch } from 'vue'
    import useDOMCreate from '../hooks/useDOMCreate'
    export type MessageType = 'success' | 'error' | 'default'

    const props = defineProps<{
    message: string,
    type: MessageType
    }>()

    //const emit = defineEmits<['closeMessage']>()
    //void 表示无返回值
    const emit = defineEmits<{ (e: 'closeMessage'): void }>()

    //创建一个dom节点并在页面中显示
    useDOMCreate('message')

    //Message组件只显示一次的原因

    //是否显示
    const isVisible = ref(true)

    const classObject = {
    'alert-success': props.type === 'success',
    'alert-danger': props.type === 'error',
    'alert-primary': props.type === 'default'
    }

    const hide = () => {
    //隐藏
    isVisible.value = false

    //isVisible 初始化为 true,控制组件的显示。
    //emit('closeMessage', true)
    //当用户点击关闭按钮时,hide() 将 isVisible 设为 false,组件隐藏,并触发 emit('closeMessage')(但 App.vue 中未监听此事件)。
    //在 App.vue 中,v-if="error.status" 控制 Message 组件的存在。如果 error.status 保持 true(未重置),组件不会被销毁和重新创建,因此 isVisible 保持 false,导致后续错误时组件不再显示。

    //通知父组件
    emit('closeMessage')
    }

    // 为什么只显示一次:
    // 第一次错误:error.status 为 true,组件创建,isVisible = true,显示消息。
    // 用户关闭后:isVisible = false,组件隐藏。
    // 后续错误:error.status 仍为 true,组件未重新创建,isVisible 仍为 false,故不显示。
    // 这解释了“多次点击登录,多次发生错误,但只显示一次”的现象。

    //修复建议
    //要确保每次新错误都显示消息,需要在 Message.vue 中监听 props 变化,重置 isVisible。使用 watch 监听 message 或 type 的变化。
    // 监听 props.message 变化,重置 isVisible

    // watch(() => props.message, (newMessage) => {
    // if (newMessage) {
    // isVisible.value = true
    // }
    // })

    // // 可选:也监听 type 变化
    // watch(() => props.type, () => {
    // isVisible.value = true
    // })

    // 解释:watch 监听 props.message(或 type),当新错误消息传入时,重置 isVisible 为 true,确保每次错误都显示。
    // 注意:如果 error.status 在显示后被重置为 false,组件会销毁,下次创建时 isVisible 自动为 true。但如果未重置,则上述修复必要。
    </script>

    useDOMCreate.ts

    import { onUnmounted } from "vue";
    function useDomCreate(nodeId:string) {
    const node = document.createElement('div')
    node.id = nodeId
    document.body.appendChild(node)
    onUnmounted(() => {
    document.body.removeChild(node)
    })
    }
    export default useDomCreate

    App.vue

    <template>
    <div class="container">
    <global-header :user="currentUser"></global-header>
    <!– 加载组件 –>
    <Loader v-if="isLoading" text="正在加载😊" background="rgba(0,0,0,0.8)"></Loader>
    <!– 全局错误信息 –>
    <Message type="error" v-if="error.status" :message="error.message" @close-message="closeMessage"></Message>
    <router-view></router-view>
    <footer class="text-center py-4 text-secondary bg-light mt-6">
    ……
    </footer>
    </div>
    </template>

    <script lang="ts" setup>
    import 'bootstrap/dist/css/bootstrap.min.css'
    import GlobalHeader from './components/GlobalHeader.vue'
    import { computed, onMounted } from 'vue'
    import { useStore } from 'vuex'
    import Loader from './components/Loader.vue'
    import axios from 'axios'
    //导入消息组件
    import Message from './components/Message.vue'

    const store = useStore()
    const currentUser = computed(() => {
    return store.state.user
    })
    const isLoading = computed(() => {
    return store.state.loading
    })
    //获取store中的token
    const token = computed(() => {
    return store.state.token
    })

    //获取全局错误信息
    const error = computed(() => {
    return store.state.error
    })

    //监听消息组件的closeMessage事件
    function closeMessage() {
    console.log("closeMessage 重置全局错误信息");
    //重置全局错误信息
    store.commit('setError', {
    status: false,
    message: ''
    });
    }

    onMounted(() => {
    //持久化登录状态
    //判断用户是否登录
    if (!currentUser.value.isLogin && token.value) {
    //如果没登录,但是有token
    //设置axios的通用头
    axios.defaults.headers.common['Authorization'] = `Bearer ${token.value}`
    //获取用户信息
    store.dispatch('fetchCurrentUser')
    }
    })
    </script>

    main.ts

    在这里重置消息,解决消息组件只显示一次的问题。

    //axios 是默认导出,不是命名导出
    //import { axios } from 'axios';
    import axios from 'axios'
    import { createApp } from 'vue'
    import { createPinia } from 'pinia'
    //导入store
    import store from './store'
    //测试
    //console.log(store.state.user)
    //导入Vuex
    //import Vuex from 'vuex'
    //import { createStore } from 'vuex'//正确的写法
    //import createStore from 'vuex'
    //无法找到模块“vuex”的声明文件。
    //TypeScript无法正确识别Vuex模块的类型声明文件,由于package.json的exports配置与类型声明文件路径不匹配导致。
    // 修复建议
    // 安装Vuex的类型定义包 @types/vuex
    // 检查并更新Vuex版本兼容性
    // 配置tsconfig.json允许未解析的模块
    //模块 ""vuex"" 没有导出的成员 "createStore"。你是想改用 "import createStore from "vuex"" 吗? ts(2614)

    import App from './App.vue'
    import router from './router'

    const app = createApp(App)

    //设置baseURL
    //使用慕课网提供的独家API
    axios.defaults.baseURL = 'http://apis.imooc.com/api/'
    //设置拦截器
    axios.interceptors.request.use(function (request) {
    // 在发送请求之前做些什么
    // 显示加载中
    store.commit('setLoading', true)
    //重置消息提示
    store.commit('setError', {
    message: "",
    status: false
    })
    // 增加icode参数验证
    // get 请求,添加到 url 中
    request.params = { …request.params, icode: '47B26FAEDF96DB7B' }
    // 其他请求,添加到 body 中
    // 如果是上传文件,添加到 FormData 中
    if (request.data instanceof FormData) {
    request.data.append('icode', '47B26FAEDF96DB7B')
    } else {
    // 普通的 body 对象,添加到 data 中
    request.data = { …request.data, icode: '47B26FAEDF96DB7B' }
    }
    return request;
    });

    axios.interceptors.response.use(function (response) {
    // 成功响应
    // 移除加载中
    store.commit('setLoading', false)
    return response;
    },e => {
    //console.log("全局错误拦截");
    const {error}=e.response.data
    store.commit('setError', {
    message: error,
    status: true
    })
    store.commit('setLoading', false)
    return Promise.reject(error)
    });

    app.use(createPinia())
    //main.ts:[Vue Router warn]: No match found for location with path "/"
    //这个问题是因为 Vue Router 没有找到与根路径 / 匹配的路由配置导致的。
    app.use(router)

    // 方法 A:导入编译好的 Bootstrap CSS(简单)
    //import 'bootstrap/dist/css/bootstrap.min.css'
    //import 'bootstrap'

    // 方法 B:导入自定义的 SCSS 文件(灵活)
    // import '@/styles/index.scss'

    // 创建 Vuex store
    // const store = createStore({
    // state: {
    // // 定义你的状态
    // count: 0
    // },
    // mutations: {//状态变更方法
    // // 定义你的变更方法
    // increment(state) {
    // state.count++
    // }
    // }
    // })
    //测试
    // console.log(store.state.count) // 输出初始状态值 0
    // // 提交一个变更
    // store.commit('increment')
    // console.log(store.state.count) // 输出变更后的状态值 1
    // 使用 Vuex store
    app.use(store)
    app.mount('#app')


    Message 组件改进为函数调用的形式

    createMessage.ts

    //导入 createApp 创建一个组件实例
    import { createApp } from 'vue'
    import Message from './Message.vue'
    export type MessageType = 'success' | 'error' | 'default'

    const createMessage = (message: string, type: MessageType,timeout=2000) => {
    //用函数的形式创建一个组件
    //createApp() 第一个参数是对象组件,第二个参数是组件的属性
    const messageInstance = createApp(Message, {//给组件传入属性
    message,
    type
    })
    //新建一个 div 元素,挂载组件
    const mountNode = document.createElement('div')
    //给 mountNode 添加一个测试的类名 testMessage
    //mountNode.className = 'testMessage'
    //document.body.appendChild(mountNode)
    //这里不写组件也能正常显示的原因是 Message 组件内部使用了 <teleport> 组件
    // <teleport> 的作用是将组件的内容"传送"到指定的 DOM 元素中。在这个例子中:
    // useDOMCreate('message'):这个钩子函数(假设)创建了一个 id 为 "message" 的 DOM 元素,并添加到 document.body 中
    // <teleport to="#message">:组件内容会被渲染到这个 #message 元素中
    // 自动挂载:即使 mountNode 没有被添加到 DOM,但 <teleport> 会将内容传送到已存在的 #message 元素中

    //挂载时机:messageInstance.mount(mountNode) 只是将组件挂载到 mountNode 上,但 mountNode 本身并没有被添加到 DOM 中,所以组件实际上没有显示。
    messageInstance.mount(mountNode)//挂载组件
    //设置延时,卸载组件
    setTimeout(() => {
    messageInstance.unmount()
    //createMessage.ts:20 Uncaught NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
    //这个错误表明在尝试从 document.body 中移除 mountNode 时,该节点并不是 document.body 的子节点。这通常是因为 mountNode 没有被正确地添加到 DOM 中,或者已经被移除了。
    //document.body.removeChild(mountNode)
    }, timeout);
    }

    export default createMessage

    App.vue

    <template>
    <div class="container">
    <global-header :user="currentUser"></global-header>
    <!– 加载组件 –>
    <Loader v-if="isLoading" text="正在加载😊" background="rgba(0,0,0,0.8)"></Loader>
    <!– 全局错误信息 –>
    <!– <Message type="error" v-if="error.status" :message="error.message" @close-message="closeMessage"></Message> –>
    <router-view></router-view>
    <footer class="text-center py-4 text-secondary bg-light mt-6">
    ……
    </footer>
    </div>
    </template>

    <script lang="ts" setup>
    import 'bootstrap/dist/css/bootstrap.min.css'
    import GlobalHeader from './components/GlobalHeader.vue'
    import { computed, onMounted, watch } from 'vue'
    import { useStore } from 'vuex'
    import Loader from './components/Loader.vue'
    import axios from 'axios'
    //导入消息组件
    //import Message from './components/Message.vue'
    import createMessage from './components/createMessage'

    const store = useStore()
    const currentUser = computed(() => {
    return store.state.user
    })
    const isLoading = computed(() => {
    return store.state.loading
    })
    //获取store中的token
    const token = computed(() => {
    return store.state.token
    })

    //获取全局错误信息
    const error = computed(() => {
    return store.state.error
    })

    // watch(error, (newValue) => {
    // //TypeError: Cannot destructure property 'status' of 'newValue.value' as it is undefined.
    // const { status, message } = newValue.value
    // if (status && message) {
    // //创建消息
    // createMessage(message, 'error', 2000)
    // }
    // })
    watch(() => error.value.status, () => {
    //TypeError: Cannot destructure property 'status' of 'newValue.value' as it is undefined.
    const { status, message } = error.value
    if (status && message) {
    //创建消息
    createMessage(message, 'error', 2000)
    }
    })

    //监听消息组件的closeMessage事件
    // function closeMessage() {
    // console.log("closeMessage 重置全局错误信息");
    // //重置全局错误信息
    // store.commit('setError', {
    // status: false,
    // message: ''
    // });
    // }

    onMounted(() => {
    //持久化登录状态
    //判断用户是否登录
    if (!currentUser.value.isLogin && token.value) {
    //如果没登录,但是有token
    //设置axios的通用头
    axios.defaults.headers.common['Authorization'] = `Bearer ${token.value}`
    //获取用户信息
    store.dispatch('fetchCurrentUser')
    }
    })
    </script>


    createMessage.ts中不使用(document.body.appendChild(mountNode))也能正常显示的原因是 Message 组件内部使用了 <teleport> 组件。


    <teleport> 的作用是将组件的内容"传送"到指定的 DOM 元素中。在这个例子中:

  • useDOMCreate('message'):这个钩子函数(假设)创建了一个 id 为 "message" 的 DOM 元素,并添加到 document.body 中

  • <teleport to="#message">:组件内容会被渲染到这个 #message 元素中

  • 自动挂载:即使 mountNode 没有被添加到 DOM,但 <teleport> 会将内容传送到已存在的 #message 元素中


  • 页面显示

    审查元素,发现区别


    优化:弃 createApp 改用 h 函数 + render


    因为使用 createApp 有点大材小用,这里使用虚拟DOM实现更优。


    createMessage.ts

    import { h,render } from 'vue'
    import Message from './Message.vue'
    export type MessageType = 'success' | 'error' | 'default'

    const createMessage = (message: string, type: MessageType,timeout?: number) => {
    //h函数可以创建虚拟节点,第一个参数是标签名,第二个参数是标签的属性,第三个参数是标签的子节点。
    const messageVnode = h(Message, {
    message,
    type
    })
    //新建一个 div 元素,挂载组件
    const mountNode = document.createElement('div')
    //给 mountNode 添加一个测试的类名 testMessage
    //mountNode.className = 'testMessage'
    //document.body.appendChild(mountNode)

    //使用Vue内部的render函数:将一个vnode渲染到一个特定的DOM上
    render(messageVnode, mountNode)

    const destroy = () => {
    //销毁组件
    render(null, mountNode)
    }

    if(timeout){
    setTimeout(() => {
    destroy()
    }, timeout);
    }
    return {
    //如果没导出会报错
    //App.vue:92 Uncaught TypeError: Cannot read properties of undefined (reading 'destroy') at App.vue:92:17
    destroy
    }
    }

    export default createMessage

    虚拟 DOM 相关文档

    https://vuejs.org/guide/extras/rendering-mechanism.html#virtual-dom

    App.vue

    onMounted(() => {
    //测试消息函数
    const msgInstance = createMessage('测试消息', 'success')
    //定时关闭消息
    setTimeout(() => {
    msgInstance.destroy()
    }, 2000)
    })

    赞(0)
    未经允许不得转载:171主机测评 » 通用消息组件 bug 修复及更好的实现是使用函数调用组件(优化:弃 createApp 改用 h 函数 + render )
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址