本文是系列文章的一部分:框架实战指南 - 基础知识
尽管我们尽了最大努力,但应用程序中还是会存在一些 bug。遗憾的是,我们无法完全忽略它们,否则用户体验会大打折扣。
以下面的代码为例:
html
<!-- App.vue --><script setup>const items = [ { id: 1, name: "Take out the trash", priority: 1 }, { id: 2, name: "Cook dinner", priority: 1 }, { id: 3, name: "Play video games", priority: 2 },];const priorityItems = items.filter((item) => item.item.priority === 1);</script><template> <h1>To-do items</h1> <ul> <li v-for="item of priorityItems" :key="item.id">{{ item.name }}</li> </ul></template>
无需运行代码,一切看起来都很好,对吗?
也许你现在已经发现了错误------太好了!记住,我们每个人都会时不时地犯一些小错误。在我们继续讨论的过程中,不要轻易忽视错误处理的重要性。
h1
但是哦不!当你运行应用程序时,它并没有像我们期望的那样显示或任何列表项。
这些项目没有显示在屏幕上的原因是出现了错误。打开以下任意一个示例的控制台,你都会看到一个错误:
错误:无法访问属性"priority",item.item 未定义
幸运的是,这个错误很容易修复,但即使我们修复了,我们的应用也难免会引入 bug。白屏会给最终用户带来非常糟糕的体验------他们很可能甚至不明白到底发生了什么,导致他们访问了这个崩溃的页面。
尽管我怀疑我们是否能够说服用户错误是一件好事 ,但我们至少可以如何改善用户体验呢?
不过,在我们这样做之前,让我们先探讨一下为什么抛出错误会导致页面渲染失败。
抛出错误会导致屏幕空白?!
如前所述,当组件的渲染步骤中抛出错误时,它将无法渲染组件模板中的任何内容。这意味着以下操作将抛出错误并阻止渲染发生:
html
<!-- ErrorThrowing.vue --><script setup>throw new Error("Error");</script><template> <p>Hello, world!</p></template>
但是,如果我们更改代码以在事件处理程序期间引发错误,则内容将正常呈现,但无法执行所述错误处理程序的逻辑:
html
<!-- ErrorThrowing.vue --><script setup>const onClick = () => { throw new Error("Error");};</script><template> <button @click="onClick()">Click me</button></template>
这种行为可能看起来很奇怪,除非你考虑 JavaScriptthrow
子句的工作原理。当 JavaScript 函数抛出错误时,它也起到了某种提前返回的作用。
js
function getRandomNumber() { // Try commenting this line and seeing the different behavior throw new Error("There was an error"); // Anything below the "throw" clause will not run console.log("Generating a random number"); // This means that values returned after a thrown error are not utilized return Math.floor(Math.random() * 10);}try { const val = getRandomNumber(); // This will never execute because the `throw` bypasses it console.log("I got the random number of:", val);} catch (e) { // This will always run instead console.log("There was an error:", e);}
此外,这些错误超出了它们的范围,这意味着它们将使执行堆栈冒泡。
这在英语中是什么意思?
从实际意义上讲,这意味着抛出的错误将超出调用它的函数的界限,并进一步沿着调用的函数列表向上移动以到达抛出的错误。
js
function getBaseNumber() { // Error occurs here, throws it upwards throw new Error("There was an error"); return 10;}function getRandomNumber() { // Error occurs here, throws it upwards return Math.floor(Math.random() * getBaseNumber());}function getRandomTodoItem() { const items = [ "Go to the gym", "Play video games", "Work on book", "Program", ]; // Error occurs here, throws it upwards const randNum = getRandomNumber(); return items[randNum % items.length];}function getDaySchedule() { let schedule = []; for (let i = 0; i < 3; i++) { schedule.push( // First execution will throw this error upwards getRandomTodoItem(), ); } return schedule;}function main() { try { console.log(getDaySchedule()); } catch (e) { // Only now will the error be stopped console.log("An error occurred:", e); }}

由于错误的这两个属性,React、Angular 和 Vue 无法从渲染周期中抛出的错误中"恢复"(在发生错误后继续渲染)。
事件处理程序中引发的错误
相反,由于事件处理程序的性质,这些框架不需要处理事件处理程序期间发生的错误。假设我们在 HTML 文件中有以下代码:
html
<!-- index.html --><button id="btn">Click me</button><script> const el = document.getElementById("btn"); el.addEventListener("click", () => { throw new Error("There was an error"); });</script>
当你点击<button>
这里时,它会抛出一个错误,但这个错误不会超出事件监听器的范围。这意味着下面的代码将无法正常工作:
js
try { const el = document.getElementById("btn"); el.addEventListener("click", () => { throw new Error("There was an error"); });} catch (error) { // This will not ever run with this code alert("We're catching an error in try/catch");}
因此,为了在事件处理程序中捕获错误,React、Angular 或 Vue 必须添加一个窗口'error'
监听器,如下所示:
js
const el = document.getElementById("btn");el.addEventListener("click", () => { throw new Error("There was an error");});window.addEventListener("error", (event) => { const error = event.error; alert("We're catching an error in another addEventListener");});
但是让我们想想添加这个window
监听器意味着什么:
-
各个框架中的代码更复杂
- 更难维护
- 更大的捆绑尺寸
-
当用户点击一个故障按钮时,整个组件都会崩溃,而不是某个组件发生故障
当我们能够try/catch
在事件处理程序中添加自己的处理程序时,这似乎不值得权衡。
毕竟,部分损坏的应用程序比完全损坏的应用程序要好!
其他 API 中引发的错误
错误处理程序中抛出的错误不会阻止渲染的这一特性也会转移到这些框架的其他方面:
当发生错误时,Vue 的一些 API(例如watchEffect
或 )会阻止渲染:computed
html
<!-- App.vue --><script setup>import { watchEffect, computed } from "vue";// This will prevent renderingwatchEffect(() => { throw new Error("New error in effect");});// This will also prevent renderingconst result = computed(() => { throw new Error("New error in computed");});// "computed" is lazy, meaning that it will not throw the error// (or compute a result) unless the value is used, like so:console.log(result.value);</script><template> <p>Hello, world!</p></template>
其他 API(例如onMounted
生命周期方法)在内部引发错误时不会阻止渲染:
html
<!-- App.vue --><script setup>import { onMounted } from "vue";onMounted(() => { // Will not prevent `Hello, world!` from showing throw new Error("New error");});</script><template> <p>Hello, world!</p></template>
尽管乍一看这可能令人困惑,但当您考虑与相比运行时 ,这是有意义的。onMounted``computed
瞧,whilecomputed
在组件设置期间运行,并在组件渲染完成后 onMounted
执行。因此,Vue 能够从渲染过程中和渲染之后抛出的错误中恢复,但无法从渲染函数中抛出的错误中恢复。**onMounted
现在我们了解了为什么这些错误会阻止您呈现内容,让我们看看当错误发生时我们如何能够改善用户体验。
记录错误
当出现错误时,提供更好的最终用户体验的第一步是减少错误的数量。
当然,这似乎很明显,但请考虑一下:如果用户的机器上发生错误并且内部没有捕获到,您怎么知道如何修复它呢?
这时,"日志"的概念就派上用场了。日志的总体思路是,您可以捕获一系列错误以及导致错误的事件信息。您需要提供一种导出这些数据的方法,以便用户将其发送给您进行调试。
虽然这种日志记录通常涉及向服务器提交数据,但现在让我们将数据保留在用户的机器本地。
Vue 使我们能够使用简单的onErrorCaptured
组合 API 来跟踪应用程序中的错误。
html
<!-- App.vue --><script setup>import { onErrorCaptured } from "vue";import Child from "./Child.vue";onErrorCaptured((err, instance, info) => { // Do something with the error console.log(err, instance, info);});</script><template> <Child /></template>
现在,当我们在子组件中抛出一个错误时,如下所示:
html
<!-- Child.vue --><script setup>throw new Error("Test");</script><template> <p>Hello, world!</p></template>
它将运行里面的函数onErrorCaptured
。
太棒了!现在我们可以跟踪应用中发生的错误了。希望这能让我们及时解决用户遇到的错误,让应用随着时间的推移更加稳定。
现在,让我们看看当用户遇到错误时我们是否能够为他们提供更好的体验。
忽略错误
一些 bug?它们简直是致命伤。一旦出现 bug,你根本无法恢复,最终只能暂停用户与页面的交互。
另一方面,其他错误可能不需要如此严厉的措施。例如,如果你可以默默地记录错误,假装什么都没发生,并允许应用程序继续正常运行,这通常会带来更好的用户体验。
让我们看看如何在我们的应用程序中实现这一点。
为了避免错误导致您的 Vue 应用程序空白,只需false
从您的onErrorCaptured
组合中返回即可。
html
<!-- App.vue --><script setup>import { onErrorCaptured } from "vue";import Child from "./Child.vue";onErrorCaptured((err, instance, info) => { console.log(err, instance, info); return false;});</script><template> <Child /></template>
这允许这样的组件:
html
<!-- Child.vue --><script setup>throw new Error("Test");</script><template> <p>Hello, world!</p></template>
在记录错误时仍然呈现其内容。
后备用户界面
虽然静默失败可以成为向用户隐藏错误的有效策略,但其他时候,您可能希望在抛出错误时显示不同的 UI。
例如,让我们构建一个屏幕,当抛出某些东西时告诉用户发生了未知错误。
因为我们仍然可以完全访问组件的状态onErrorCaptured
,所以我们可以将 aref
从更改false
为true
来跟踪是否发生了错误。
如果它尚未呈现我们的主要应用程序,否则就呈现我们的后备 UI。
html
<!-- App.vue --><script setup>import { onErrorCaptured, ref } from "vue";import Child from "./Child.vue";const hadError = ref(false);onErrorCaptured((err, instance, info) => { console.log(err, instance, info); hadError.value = true; return false;});</script><template> <p v-if="hadError">An error occurred</p> <Child v-if="!hadError" /></template>
显示错误
虽然显示后备 UI 通常对用户有利,但大多数用户希望了解出了什么问题,而不是简单地知道"某事"出了问题。
让我们向用户显示组件抛出的错误。
使用我们的ref
方法来跟踪错误,当错误发生时,我们可以在屏幕上显示错误的内容。
html
<!-- App.vue --><script setup>import { onErrorCaptured, ref } from "vue";import Child from "./Child.vue";const error = ref(null);onErrorCaptured((err, instance, info) => { console.log(err, instance, info); error.value = err; return false;});// JSON.stringify-ing an Error object provides `{}`.// This function fixes thatconst getErrorString = (err) => JSON.stringify(err, Object.getOwnPropertyNames(err));</script><template> <div v-if="error"> <h1>You got an error:</h1> <pre style="white-space: pre-wrap" ><code>{{ getErrorString(error) }}</code></pre> </div> <Child v-if="!error" /></template>
如果使用 bind
{{error}}
而不是{{error.message}}
,则会出现以下错误:
arduinoUncaught (in promise) RangeError: Maximum call stack size exceeded
Vue 的内部尝试在其反应性处理程序中包装和解开 Error 对象。
挑战
假设我们正在构建之前的代码挑战,并意外地输入了组件中变量的名称Sidebar
:
html
<!-- Sidebar.vue --><script setup>import { ref } from "vue";const emits = defineEmits(["toggle"]);const isCollapsed = ref(false);const setAndToggle = (v) => { isCollapsed.value = v; emits("toggle", v);};// ...const toggleCollapsed = () => { setAndToggle(!isCollapsed.value);};</script><template> <!--`collapsed` doesn't exist!--> <!--It's supposed to be `isCollapsed`! 😱--> <button v-if="isCollapsed" @click="collapsed()">Toggle</button> <div v-if="!isCollapsed"> <button @click="collapsed()">Toggle</button> <ul style="padding: 1rem"> <li>List item 1</li> <li>List item 2</li> <li>List item 3</li> </ul> </div></template>
单击侧边栏切换后,我们会看到一段 JavaScriptTypeError
:
js
"Uncaught TypeError: _ctx.collapsed is not a function";
虽然我们可以通过修改拼写错误来解决这个问题,但我们还需要添加一个错误处理程序来记录此类问题,以防它们在生产环境中发生。毕竟,如果一个 bug 在野外被发现却无法报告给开发人员,那它真的会被修复吗?
让我们通过以下方法解决这个问题:
- 弄清楚用户如何报告错误
- 实现错误处理程序
- 向用户显示更美观的错误屏幕
向开发人员报告错误复制链接
如果用户在使用该应用程序时发现类似的东西,我们可以为他们提供一种向我们发送电子邮件的方式。

我们可以通过在发生错误时向用户显示链接来实现这一点。mailto:
这样,只需单击鼠标即可报告错误。
此mailto:
链接可能类似于以下 HTML
html
<a href="mailto:dev@example.com&subject=Bug%20Found&body=There%20was%20an%20error" >Email Us</a>
其中subject
和的body
编码方式encodeURIComponent
如下:
js
// JavaScript pseudo-codeconst mailTo = "dev@example.com";const errorMessage = `There was some error that occurred. It's unclear why that happened.`;const header = "Bug Found";const encodedErr = encodeURIComponent(errorMessage);const encodedHeader = encodeURIComponent(header);const href = `mailto:${mailTo}&subject=${encodedHeader}&body=${encodedErr}`;// HREF can be bound via each frameworks' attribute binding syntaxconst html = `<a href="${href}">Email Us</a>`;
实现错误处理程序
制定了攻击计划后,让我们退一步,首先评估如何实现错误处理程序。
记住:先实施,再增加。
让我们从一个错误处理程序开始,它将捕获我们的错误并在发生错误时显示 UI。
我们还将确保此错误处理程序是应用程序范围的,以确保在应用程序出现任何错误时它会显示出来:
html
<!-- ErrorCatcher.vue --><script setup>import { onErrorCaptured, ref } from "vue";const error = ref(null);onErrorCaptured((err, instance, info) => { error.value = err; return false;});</script><template> <div v-if="error"> <h1>There was an error</h1> <pre><code>{{error.message}}</code></pre> </div> <slot v-if="!error" /></template>
html
<!-- App.vue --><script setup>import ErrorCatcher from "./ErrorCatcher.vue";// ...</script><template> <ErrorCatcher> <!-- The rest of the app --> </ErrorCatcher></template>
显示更美观的错误消息
现在,我们有了在错误发生时向用户显示错误的方法,接下来我们要确保能够将错误报告给开发团队。我们将通过显示用户报告错误所需的所有信息以及自动填充的 mailto:
链接来实现这一点,这样用户只需按下一个按钮即可向开发人员发送电子邮件。
html
<script setup>import { onErrorCaptured, ref, computed } from "vue";const error = ref(null);const mailTo = "dev@example.com";const header = "Bug Found";const message = computed(() => !error.value ? "" : ` There was a bug found of type: "${error.value.name}". The message was: "${error.value.message}". The stack trace is: """ ${error.value.stack} """ `.trim(),);const encodedMsg = computed(() => encodeURIComponent(message.value));const encodedHeader = encodeURIComponent(header);const href = computed( () => `mailto:${mailTo}&subject=${encodedHeader}&body=${encodedMsg.value}`,);onErrorCaptured((err, instance, info) => { error.value = err; return false;});</script><template> <div v-if="error"> <h1>{{ error.name }}</h1> <pre><code>{{error.message}}</code></pre> <a :href="href">Email us to report the bug</a> <br /> <br /> <details> <summary>Error stack</summary> <pre><code>{{error.stack}}</code></pre> </details> </div> <slot v-if="!error" /></template>