通用消息组件 bug 修复及更好的实现是使用函数调用组件

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


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

解决方案

  1. 监听props变化 :在Message组件中通过watch监听messagetype,新错误传入时重置isVisibletrue
  2. 父组件重置状态 :在App.vue中监听closeMessage事件,主动重置error.statusfalse,触发组件销毁重建。

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


通用消息组件 bug 修复


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


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

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

  1. 状态重置机制缺失

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

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

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

建议修复

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


Message组件只显示一次的原因

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

  1. 组件行为

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

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

修复建议

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


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

实际应用

Message.vue

html 复制代码
<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

TypeScript 复制代码
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

html 复制代码
<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

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

TypeScript 复制代码
//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

TypeScript 复制代码
//导入 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

TypeScript 复制代码
<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 元素中。在这个例子中:

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

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

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


页面显示

审查元素,发现区别

相关推荐
ANnianStriver1 天前
PetLumina-AI 驱动的宠物生活管理平台
java·生活·vue3·springboot·ai编程·宠物·全栈开发
雨季mo浅忆2 天前
记录Vue3项目中的各类问题
前端·bug·vue3
八目蛛5 天前
八目蛛网络(免费工具网站导航)
css·vue.js·开源·vue3·html5·ai编程
颂love5 天前
Vue3基础入门
前端·学习·vue3
海市公约6 天前
Vue3组合式API中watch传值生命周期与自定义Hook实战
vue3·生命周期·watch·props·组件通信·defineexpose·自定义hook
海市公约7 天前
Vue3组合式API与响应式系统核心机制详解
vue3·computed·reactive·ref·响应式系统·composition api·script setup
小茴香3538 天前
Vue3路由权限动态管理
前端·前端框架·vue3
暗冰ཏོ12 天前
《2026 Vue2 + Vue3 完整学习指南:基础语法、路由缓存、登录拦截、项目实战与面试题》
前端·vue.js·vue·vue3·vue2
曲幽13 天前
写页面时别再把 Element Plus 整个搬进来啦!Vue3按需加载的坑我帮你踩平了
vue3·web·vite·icon·element plus·vs code·import·unplugin
小云小白14 天前
若依-vue3 把深色版本改成天蓝色-含登录页
vue3·若依·天蓝色