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)动态创建/销毁实例,避免状态残留问题。
通用消息组件 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
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 元素中。在这个例子中:
useDOMCreate('message'):这个钩子函数(假设)创建了一个 id 为"message"的 DOM 元素,并添加到document.body中
<teleport to="#message">:组件内容会被渲染到这个#message元素中自动挂载 :即使
mountNode没有被添加到 DOM,但<teleport>会将内容传送到已存在的#message元素中
页面显示
审查元素,发现区别

