React中如何優雅的捕捉事件錯誤
前話
人無完人,所以代碼總會出錯,出錯並不可怕,關鍵是怎么處理。
我就想問問大家react的錯誤怎么捕捉呢? 這個時候:
- 小白:怎么處理?
- 小白+: ErrorBoundary
- 小白++: ErrorBoundary, try catch
- 小白#: ErrorBoundary, try catch, window.onerror
- 小白##: 這個是個嚴肅的問題,我知道*種處理方式,你有什么好的方案?
正題
小白#回答的基本就是解決思路。我們來一個一個簡單說說。
1. EerrorBoundary
EerrorBoundary是16版本出來的,有人問那我的15版本呢,我不聽我不聽,反正我用16,當然15有unstable_handleError。
關於EerrorBoundary官網介紹比較詳細,這個不是重點,重點是他能捕捉哪些異常。
Error boundaries在rendering,lifeCyclemethod或處於他們樹層級之下的構造函數中捕獲錯誤
哦,原來如此。 怎么用
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error, info) {
// Display fallback UI
this.setState({ hasError: true });
// You can also log the error to an error reporting service
logErrorToMyService(error, info);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
重點:error boundaries並不會捕捉這些錯誤:
- 事件處理器
- 異步代碼
- 服務端的渲染代碼
- 在error boundaries區域內的錯誤
2. try catch
簡單有效的捕捉
handleClick = () => {
try {
// Do something that could throw
} catch (error) {
this.setState({ error });
}
}
3. window.onerror
超級奧特曼,只是錯誤信息比較不好分析。
4.其他
- http請求
封裝后,專門處理 - 狀態管理redux,mobx等
封裝攔截 ,我們在項目的應用mobx-state-tree基本都是在model里面攔截的 - 其他
自己看着辦啊,都找我,我很忙的。
問題
啊?這么多事件處理和方法都要加try catch啊。 你笨啊window.onerror啊。
onerror是非常好,但是有個問題,錯誤細節不好分析,有大神說,正則解析。
我不扶牆扶你。
解決
decorator特性,裝飾器。 create-react-app創建的app默認是不知此的裝飾器的。
不要和我爭,github地址上人家寫着呢can-i-use-decorators?
那問題又來了,如何支持裝飾器。
- 場景一:自己構建的項目
那還不簡單的飛起 - 場景二: create-react-app腳手架創建的項目
const {injectBabelPlugin} = require('react-app-rewired');
/* config-overrides.js */
module.exports = {
webpack: function override(config, env) {
// babel 7
config = injectBabelPlugin('transform-decorators-legacy',config)
// babel 6
config = injectBabelPlugin('transform-decorators',config)
return config;
}
}
關於裝飾器這里不做過多的說明,修改類的行為。
這里又有幾個點
- 裝飾方法 裝飾類 裝飾getter, setter都可以,我們選在裝飾方法和類
- 裝飾類,如何排除系統內置方法和繼承的方法
- 裝飾的時候有參和無參數怎么處理
我們先寫一個來檢查內置方法的方法, 不夠自己補全
const PREFIX = ['component', 'unsafe_']
const BUILTIN_METHODS = [
'constructor',
'render',
'replaceState',
'setState',
'isMounted',
'replaceState'
]
// 檢查是不是內置方法
function isBuiltinMethods(name) {
if (typeof name !== 'string' || name.trim() === '') {
return false
}
// 以component或者unsafe_開頭
if (PREFIX.some(prefix => name.startsWith(prefix)))) {
return true
}
// 其他內置方法
if (BUILTIN_METHODS.includes(name)) {
return true
}
return false
}
再弄一個裝飾方法的方法, 這個方法參考了autobind.js
handleError是自己的錯誤處理函數,這里沒有寫出來
// 監聽方法
function createDefaultSetter(key) {
return function set(newValue) {
Object.defineProperty(this, key, {
configurable: true,
writable: true,
// IS enumerable when reassigned by the outside word
enumerable: true,
value: newValue
});
return newValue;
};
}
function observerHandler(fn, callback) {
return (...args) => {
try {
fn(...args)
} catch (err) {
callback(err)
}
}
}
//方法的裝飾器, params是額外的參數
function catchMethod(target, key, descriptor, ...params) {
if (typeof descriptor.value !== 'function') {
return descriptor
}
const { configurable, enumerable, value: fn } = descriptor
return {
configurable,
enumerable,
get() {
// Class.prototype.key lookup
// Someone accesses the property directly on the prototype on which it is
// actually defined on, i.e. Class.prototype.hasOwnProperty(key)
if (this === target) {
return fn;
}
// Class.prototype.key lookup
// Someone accesses the property directly on a prototype but it was found
// up the chain, not defined directly on it
// i.e. Class.prototype.hasOwnProperty(key) == false && key in Class.prototype
if (this.constructor !== constructor && getPrototypeOf(this).constructor === constructor) {
return fn;
}
const boundFn = observerHandler(fn.bind(this), err => {
handleError(err, target, key, ...params)
})
defineProperty(this, key, {
configurable: true,
writable: true,
// NOT enumerable when it's a bound method
enumerable: false,
value: boundFn
});
boundFn.bound = true
return boundFn;
},
set: createDefaultSetter(key)
};
}
再來一個裝飾類的
/**
* 檢查是不是需要代理
* @param {*} method
* @param {*} descriptor
*/
function shouldProxy(method, descriptor) {
return typeof descriptor.value === 'function'
&& !isBuiltinMethods(method)
&& descriptor.configurable
&& descriptor.writable
&& !descriptor.value.bound
}
function catchClass(targetArg, ...params) {
// 獲得所有自定義方法,未處理Symbols
const target = targetArg.prototype || targetArg
let descriptors = getOwnPropertyDescriptors(target)
for (let [method, descriptor] of Object.entries(descriptors)) {
if (shouldProxy(method, descriptor)) {
defineProperty(target, method, catchMethod(target, method, descriptors[method], ...params))
}
}
}
最后暴露一個自動識別方法和類的方法
/**
*
* 未攔截getter和setter
* 未攔截Symbols屬性
*/
export default function catchError(...args) {
const lastArg = args[args.length - 1]
// 無參數方法
if (isDescriptor(lastArg)) {
return catchMethod(...args)
} else {
// 無參數class?? 需要改進
if (args.length === 1 && typeof args[0] !== 'string') {
return catchClass(...args)
}
// 有參
return (...argsList) => {
// 有參數方法
if (isDescriptor(argsList[argsList.length - 1])) {
return catchMethod(...[...argsList, ...args])
}
// 有參數class
return catchClass(...[...argsList, ...args])
}
}
}
基本成型。
怎么調用
裝飾類
@catchError('HeHe')
class HeHe extends React.Component {
state = {
clicked: false
}
onClick(){
this.setState({
clicked:true
})
this.x.y.z.xxx
}
render(){
return (
<input type="button" value="點擊我" onClick={this.onClick}/>
)
}
}
裝飾方法
class HeHe extends React.Component {
state = {
clicked: false
}
@catchError('HeHe onClick')
onClick(){
this.setState({
clicked:true
})
this.x.y.z.xxx
}
render(){
return (
<input type="button" value="點擊我" onClick={this.onClick}/>
)
}
}
當然你還可以既裝飾類又裝飾方法, 這個時候方法的裝飾優先於類的裝飾,不會重復裝飾
@catchError('HeHe')
class HeHe extends React.Component {
state = {
clicked: false
}
@catchError('HeHe onClick')
onClick(){
this.setState({
clicked:true
})
this.x.y.z.xxx
}
onClick2(){
}
render(){
return (
<React.Fragment>
<input type="button" value="點擊我" onClick={this.onClick}/>
<input type="button" value="點擊我2" onClick={this.onClick2}/>
</React.Fragment>
)
}
}
如上,細心的人可以發現, 沒有 onClick.bind(this), 是的, catchError會自動完成bind,是不是很cool。
如上,現在的所有的事件處理都會被catchError里面定義的handleError處理,怎么處理就看你自己了。
有人就問了,我要是想捕捉后還要有額外處理的了,比如來個提示框之類的。
這個就取決你的需求和怎么處理,你依舊可以在你的事件處理器try catch。
二是,你沒看到@catchError里面可以傳遞參數么,可以提供額外的錯誤信息,比如場景,是不是致命錯誤等等信息。
她解決了你未顯示處理的事件處理錯誤,有沒有很優雅,有沒有。
你們都說沒有的話, 我就放棄前端了,可是我還有老婆孩子要養,所以你們一定要有人說有。
error-boundaries
React異常處理
catching-react-errors
react進階之異常處理機制-error Boundaries
decorator
core-decorators
autobind.js