通用消息组件 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 元素中


页面显示

审查元素,发现区别

相关推荐
Irene19911 天前
Vuex4:专为 Vue 3 设计,提供完整 TypeScript 支持
vue3·vuex4
无法长大1 天前
如何判断项目需不需要用、能不能用Tailwind CSS
前端·css·vue.js·elementui·vue3·tailwind css
cui_win2 天前
企业级中后台开源解决方案汇总
开源·vue3·ts
Sapphire~3 天前
Vue3-19 hooks 前端数据和方法的封装
前端·vue3
記億揺晃着的那天3 天前
Vue3 动态路由在生产环境才出现白屏的排查与解决(keep-alive 踩坑实录)
vue3·vue router·动态路由·生产环境报错
kong79069287 天前
Vue3快速入门
前端·vue3
无法长大8 天前
Mac M1 环境下使用 Rust Tauri 将 Vue3 项目打包成 APK 完整指南
android·前端·macos·rust·vue3·tauri·打包apk
淡笑沐白9 天前
Vue3使用ElementPlus实现菜单的无限递归
javascript·vue3·elementplus
Sapphire~9 天前
Vue3-18 生命周期(vue2+vue3)
vue3