错误处理是框架开发过程中非常重要的环节。框架错误处理机制的好坏直接决定了用户应用程序的健壮性,还决定了用户开发时处理错误的心智负担。
为了让大家更加直观地感受错误处理的重要性,我们从一个小例子说起。假设我们开发了一个工具模块,代码如下:
export default {
foo(fn) {
fn && fn()
}
}
该模块导出一个对象,其中 foo 属性是一个函数,接收一个回调函数作为参数,调用 foo 函数时会执行该回调函数,在用户侧使用时:
import utils from 'utils.js'
utils.foo(() => {
// ...
})
大家思考一下,如果用户提供的回调函数在执行的时候出错了,怎么办?此时有两个办法,第一个办法是让用户自行处理,这需要用户自己执行 try...catch:
import utils from 'utils.js'
utils.foo(() => {
try {
// ...
} catch (e) {
// ...
}
})
但是这会增加用户的负担。试想一下,如果 utils.js 不是仅仅提供了一个 foo 函数,而是提供了几十上百个类似的函数,那么用户在使用的时候就需要逐一添加错误处理程序。
第二个办法是我们代替用户统一处理错误,如以下代码所示:
export default {
foo(fn) {
try {
fn && fn()
} catch(e) {/* ... */}
},
bar(fn) {
try {
fn && fn()
} catch(e) {/* ... */}
},
}
在每个函数内都增加 try...catch 代码块,实际上,我们可以进一步将错误处理程序封装为一个函数,假设叫它 callWithErrorHandling:
export default {
foo(fn) {
callWithErrorHandling(fn)
},
bar(fn) {
callWithErrorHandling(fn)
},
}
function callWithErrorHandling(fn) {
try {
fn && fn()
} catch (e) {
console.log(e)
}
}
可以看到,代码变得简洁多了。但简洁不是目的,这么做真正的好处是,为开发者提供统一的错误处理接口,如以下代码所示:
let handleError = null
export default {
foo(fn) {
callWithErrorHandling(fn)
},
// 开发者可以调用该函数注册统一的错误处理函数
registerErrorHandler(fn) {
handleError = fn
}
}
function callWithErrorHandling(fn) {
try {
fn && fn()
} catch (e) {
// 将捕获到的错误传递给开发者的错误处理程序
handleError(e)
}
}
我们提供了 registerErrorHandler 函数,用户可以使用它注册错误处理程序,然后在 callWithErrorHandling 函数内部捕获错误后,把错误传递给用户注册的错误处理程序。
这样用户侧的代码就会非常简洁且健壮:
import utils from 'utils.js'
// 注册错误处理程序
utils.registerErrorHandler((e) => {
console.log(e)
})
utils.foo(() => {/* ... */})
utils.bar(() => {/* ... */})
这时错误处理的能力完全由用户控制,用户既可以选择忽略错误,也可以调用上报程序将错误上报给监控系统。
实际上,这就是 Vue.js 错误处理的原理,你可以在源码中搜索到 callWithErrorHandling 函数。另外,在 Vue.js 中,我们也可以注册统一的错误处理函数:
import App from 'App.vue'
const app = createApp(App)
app.config.errorHandler = () => {
// 错误处理程序
}