我目前正在开发一个相当简单的 Vue-3 应用程序,但遇到了一个问题,我不确定如何找到根本原因。在我的应用程序中,我有一个通过路由器呈现的视图。该视图具有一些简单的状态(用于存储某些文本字段的值的对象引用,以及用于打开和关闭模式的布尔引用),并且还使用来自 pinia 存储的单个状态对象。
第一次加载视图时,我可以轻松编辑绑定到对象引用的文本字段,但是第一次尝试通过单击按钮打开模式时,会发生两件事:文本字段被清除,并且模式无法打开。第一次单击按钮后,一切都按我预期的方式工作,我可以打开和关闭模式,而不会破坏视图的状态。
我尝试将观察程序添加到所有内部状态引用中,以便我可以查看值是否正在更改,但在单击第一个按钮后清除文本字段时,我没有看到这些观察程序被触发,我不知所措找出可能导致这种反应发生的原因。
编辑: 经过一番挖掘后,我可以看到,第一次单击按钮以显示模态 onMounted() 会在视图本身上触发。这是否可能与模态被传送到 body 标签并导致整个视图重新安装有关? /编辑
对于上下文,这里是视图的代码(TemplateEditorView.vue):
<script setup>
import TemplateSelectorComponent from '../components/TemplateSelectorComponent.vue'
import useTemplate from '../composables/useTemplate'
import { computed, ref, watch } from 'vue'
import AddSectionModal from '../components/AddSectionModal.vue'
import { useTemplateStore } from '../stores/template'
import { storeToRefs } from 'pinia'
const templateStore = useTemplateStore()
const { templates } = storeToRefs(templateStore)
const { selectedTemplate, selectedTemplateKey, updateSelectedTemplateKey } = useTemplate()
const newTemplate = ref({
id: '',
name: '',
templateText: '',
sections: []
})
const showModal = ref(false)
const handleModalSubmit = function (section) {
newTemplate.value.sections.push(section)
showModal.value = false
}
const handleSave = function () {
if (saveEnabled.value) {
templates.value[newTemplate.value.id] = newTemplate.value
clearForm()
}
}
function clearForm() {
newTemplate.value = {
id: '',
name: '',
templateText: '',
sections: []
}
}
const handleDelete = function () {
if (deleteEnabled.value) {
delete templates.value[selectedTemplateKey.value]
clearForm()
}
}
const deleteEnabled = computed(() => {
return Object.hasOwn(templates.value, selectedTemplateKey.value)
})
const saveEnabled = computed(() => {
return newTemplate.value.id && newTemplate.value.name && newTemplate.value.templateText
})
watch(selectedTemplate, async () => {
newTemplate.value = selectedTemplate.value
})
</script>
<template>
<div id="template-editor-view-content" class="row p-1 pt-4">
<div class="col">
<div class="row">
<div class="col"><h1>Template Editor</h1></div>
</div>
<div class="row">
<div id="options-column" class="col-4 primary-bordered vh-85">
<div class="row pt-3">
<div class="col"><h2 class="text-center">Template Options</h2></div>
</div>
<TemplateSelectorComponent @selected-template-changed="updateSelectedTemplateKey" />
<div class="row">
<div class="col">
<h3 class="text-center">Global Variables</h3>
</div>
</div>
<div class="row">
<div class="col">
<div id="globalVarsAccordion" class="accordion">
<div class="accordion-item">
<h2 id="companyNameAccordionHeader" class="accordion-header">
<button
class="accordion-button"
type="button"
data-bs-toggle="collapse"
data-bs-target="#companyNameAccordionBody"
aria-expanded="false"
aria-controls="companyNameAccordionBody"
>
Company Name
</button>
</h2>
<div
id="companyNameAccordionBody"
class="accordion-collapse collapse"
aria-labelledby="companyNameAccordionHeader"
data-bs-parent="#globalVarsAccordion"
>
<div class="accordion-body">
<ul class="list-group list-group-horizontal">
<li class="list-group-item">isSelected</li>
<li v-pre class="list-group-item">{{ companyName.isSelected }}</li>
</ul>
<ul class="pt-2 list-group list-group-horizontal">
<li class="list-group-item">value</li>
<li v-pre class="list-group-item">{{ companyName.value }}</li>
</ul>
</div>
</div>
</div>
<div class="accordion-item">
<h2 id="jobTitleAccordionHeader" class="accordion-header">
<button
class="accordion-button"
type="button"
data-bs-toggle="collapse"
data-bs-target="#jobTitleAccordionBody"
aria-expanded="false"
aria-controls="jobTitleAccordionBody"
>
Job Title
</button>
</h2>
<div
id="jobTitleAccordionBody"
class="accordion-collapse collapse"
aria-labelledby="jobTitleAccordionHeader"
data-bs-parent="#globalVarsAccordion"
>
<div class="accordion-body">
<ul class="list-group list-group-horizontal">
<li class="list-group-item">isSelected</li>
<li v-pre class="list-group-item">{{ jobTitle.isSelected }}</li>
</ul>
<ul class="pt-2 list-group list-group-horizontal">
<li class="list-group-item">value</li>
<li v-pre class="list-group-item">{{ jobTitle.value }}</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<template v-if="newTemplate.sections">
<div class="row">
<div class="col">
<h3 class="text-center">Template Variables</h3>
</div>
</div>
<div class="row">
<div class="col">
<div id="templateVarsAccordion" class="accordion">
<div
v-for="section in newTemplate.sections"
:key="section.key"
class="accordion-item"
>
<h2 id="`${section.key}AccordionHeader`" class="accordion-header">
<button
class="accordion-button"
type="button"
data-bs-toggle="collapse"
data-bs-target="#`${section.key}AccordionBody`"
aria-expanded="false"
aria-controls="`${section.key}AccordionBody`"
>
{{ section.label }}
</button>
</h2>
<div
id="`${section.key}AccordionBody`"
class="accordion-collapse collapse"
aria-labelledby="`${section.key}AccordionHeader`"
data-bs-parent="#templateVarsAccordion"
>
<div class="accordion-body">
<ul class="list-group list-group-horizontal">
<li class="list-group-item">isSelected</li>
<li class="list-group-item">
<span v-pre>{{</span> {{ section.key }}.isSelected <span v-pre>}}</span>
</li>
</ul>
<ul class="pt-2 list-group list-group-horizontal">
<li class="list-group-item">value</li>
<li class="list-group-item">
<span v-pre>{{</span> {{ section.key }}.value <span v-pre>}}</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
<div id="working-area" class="col vh-85">
<form class="row gx-3 align-items-center pb-3 pt-2">
<div class="col-auto">
<label for="templateKeyInput" class="col-form-label-lg">Template ID</label>
</div>
<div class="col">
<input
id="templateKeyInput"
v-model="newTemplate.id"
type="text"
class="form-control form-control-lg"
/>
</div>
<div class="col-auto">
<label for="templateNameInput" class="col-form-label-lg">Template Name</label>
</div>
<div class="col">
<input
id="templateNameInput"
v-model="newTemplate.name"
type="text"
class="form-control form-control-lg"
/>
</div>
</form>
<form class="row text-center justify-content-center">
<div class="col-2">
<button class="btn btn-secondary" @click="showModal = true">Add Section</button>
</div>
<div class="col-2">
<button class="btn btn-success" :disabled="!saveEnabled" @click="handleSave">
Save Template
</button>
</div>
<div class="col-2">
<button class="btn btn-danger" :disabled="!deleteEnabled" @click="handleDelete">
Delete Template
</button>
</div>
</form>
<div class="row pt-3">
<div class="col">
<textarea
id="templateTextArea"
v-model="newTemplate.templateText"
class="form-control"
></textarea>
</div>
</div>
</div>
</div>
</div>
<AddSectionModal :show="showModal" @close="showModal = false" @submit="handleModalSubmit" />
</div>
</template>
<style scoped>
// Some css
</style>
正在创建的模态定义如下(AddSectionModal.vue):
<script setup>
import GenericModal from './GenericModal.vue'
import { computed, ref } from 'vue'
defineProps({
show: Boolean
})
const emit = defineEmits(['close', 'submit'])
const section = ref({
key: '',
label: '',
text: '',
isSelected: false
})
function clearState() {
section.value = {
key: '',
label: '',
text: '',
isSelected: false
}
}
const submitEnabled = computed(() => {
return section.value.key && section.value.text && section.value.label
})
const handleClose = function () {
clearState()
emit('close')
}
const handleSubmit = function () {
if (submitEnabled.value) {
const value = section.value
clearState()
emit('submit', value)
}
}
</script>
<template>
<Teleport to="body">
<GenericModal :show="show">
<template #header>
<h5 class="mx-auto">Add New Section</h5>
</template>
<template #body>
<div class="container">
<div class="row gx-3 align-items-center">
<div class="col-3">
<label for="sectionKey" class="col-form-label-lg">Section Key</label>
</div>
<div class="col">
<input
v-model="section.key"
name="sectionKey"
type="text"
class="form-control form-control-lg"
/>
</div>
</div>
<div class="row pt-2 gx-3 align-items-center">
<div class="col-3">
<label for="sectionLabel" class="col-form-label-lg">Section Label</label>
</div>
<div class="col">
<input
v-model="section.label"
name="sectionLabel"
type="text"
class="form-control form-control-lg"
/>
</div>
</div>
<div class="row pt-2 gx-3 align-items-center">
<div class="col-3">
<label for="sectionText" class="col-form-label-lg">Section Text</label>
</div>
<div class="col">
<input
v-model="section.text"
name="sectionText"
type="text"
class="form-control form-control-lg"
/>
</div>
</div>
</div>
</template>
<template #footer>
<div class="row gx-1 float-end">
<div class="col">
<button class="btn btn-danger" @click="handleClose">Cancel</button>
</div>
<div class="col">
<button class="btn btn-success" :disabled="!submitEnabled" @click="handleSubmit">
Submit
</button>
</div>
</div>
</template>
</GenericModal>
</Teleport>
</template>
GenericModal 实现如下(GenericModal.vue):
<template>
<Transition name="modal">
<div v-if="show" class="modal-mask">
<div class="modal-container">
<div class="modal-header">
<slot name="header">default header</slot>
</div>
<div class="modal-body">
<slot name="body">default body</slot>
</div>
<div class="modal-footer">
<slot name="footer">
default footer
<button class="modal-default-button" @click="$emit('close')">OK</button>
</slot>
</div>
</div>
</div>
</Transition>
</template>
<style>
// Some CSS
</style>
我可以使用什么策略来追踪意外反应的根源?
由于这是 Electron 项目,因此可能需要采取额外的步骤来启用 Vue 开发工具,但它们不是必需的,因为这是允许修改源代码以进行调试的一般任务。
这里的问题是,有些状态修改是直接在模板中进行的。应将它们移至函数中,以便更容易设置断点。
在自己的代码中调试反应性的一个简单方法是添加一个带有调试钩子的观察器,例如:
watchEffect(
() => {
showModal.value;
debugger;
},
{
flush: 'post',
onTrack(e) {
// debugger
},
onTrigger(e) {
debugger
}
}
)
如果模式因
showModal
状态中的意外变化而切换,则可以对其进行跟踪,并且可以在调用堆栈中看到发生这种情况的确切位置,这包括模板中的内联事件处理程序。在这种情况下这不会有帮助,因为很明显 showModal
以有限的方式更改,并且不会意外触发两次。
原因可以在控制台中看到(启用“保留日志”选项并禁用过滤器):
页面最初加载到
http://localhost:5173/
加上哈希部分进行路由。然后,当单击按钮时会重定向到 http://localhost:5173/?
,并且页面会重新加载。这与模板中使用的 HTML 有关,与模态组件的实现无关。
<form ...>
<div class="col-2">
<button ...>Add Section</button>
</div>
没有 button
的
type
默认提交表单,没有 action
、
method
和字段的表单提交给
?
,因此会发生
http://localhost:5173/?
重定向。它在后续提交中保留在同一页面上,因此第一个和其余模态打开操作之间存在差异。解决方案是,如果您不处理
<form>
事件,则根本不使用
submit
。如果您仅出于语义原因使用它,请改用
<form @submit.prevent>
。