React ,優雅的捕獲異常
前言
人無完人,所以代碼總會出錯,出錯並不可怕,關鍵是怎麼處理。
我就想問問大家 react 的應用的錯誤怎麼捕捉呢?這個時候:
-
小白 +++:怎麼處理?
-
小白 ++:ErrorBoundary
-
小白 +:ErrorBoundary, try catch
-
小黑 #: ErrorBoundary, try catch, window.onerror
-
小黑 ##: 這個是個嚴肅的問題,我知道 N 種處理方式,你有什麼更好的方案?
ErrorBoundary
EerrorBoundary 是 16 版本出來的,有人問那我的 15 版本呢,我不聽我不聽,反正我用 16,當然 15 有unstable_handleError
。
關於 ErrorBoundary 官網介紹比較詳細,這個不是重點,重點是他能捕捉哪些異常。
-
子組件的渲染
-
生命週期函數
-
構造函數
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>
開源世界就是好,早有大神封裝了 react-error-boundary[1] 這種優秀的庫。
你只需要關心出現錯誤後需要關心什麼,還以來個 Reset
, 完美。
import {ErrorBoundary} from 'react-error-boundary'
function ErrorFallback({error, resetErrorBoundary}) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)
}
const ui = (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// reset the state of your app so the error doesn't happen again
}}
>
<ComponentThatMayError />
</ErrorBoundary>
)
遺憾的是,error boundaries 並不會捕捉這些錯誤:
-
事件處理程序
-
異步代碼 (e.g. setTimeout or requestAnimationFrame callbacks)
-
服務端的渲染代碼
-
error boundaries 自己拋出的錯誤
原文可見參見官網 introducing-error-boundaries[2]
本文要捕獲的就是 事件處理程序的錯誤。
官方其實也是有方案的 how-about-event-handlers[3], 就是 try catch.
但是,那麼多事件處理程序,我的天,得寫多少,。。。。。。。。。。。。。。。。。。。。
handleClick() {
try {
// Do something that could throw
} catch (error) {
this.setState({ error });
}
}
Error Boundary 之外
我們先看看一張表格,羅列了我們能捕獲異常的手段和範圍。
try/catch
可以捕獲同步和 async/await 的異常。
window.onerror , error 事件
window.addEventListener('error', this.onError, true);
window.onerror = this.onError
window.addEventListener('error')
這種可以比 window.onerror
多捕獲資源記載異常. 請注意最後一個參數是 true
, false
的話可能就不如你期望。
當然你如果問題這第三個參數的含義,我就有點不想理你了。拜。
unhandledrejection
請注意最後一個參數是 true
。
window.removeEventListener('unhandledrejection', this.onReject, true)
其捕獲未被捕獲的 Promise 的異常。
XMLHttpRequest 與 fetch
XMLHttpRequest
很好處理,自己有 onerror 事件。當然你 99.99% 也不會自己基於XMLHttpRequest
封裝一個庫, axios
真香,有這完畢的錯誤處理機制。
至於fetch
, 自己帶着 catch 跑,不處理就是你自己的問題了。
這麼多,太難了。
還好,其實有一個庫 react-error-catch[4] 是基於 ErrorBoudary,error 與 unhandledrejection 封裝的一個組件。
其核心如下
ErrorBoundary.prototype.componentDidMount = function () {
// event catch
window.addEventListener('error', this.catchError, true);
// async code
window.addEventListener('unhandledrejection', this.catchRejectEvent, true);
};
使用:
import ErrorCatch from 'react-error-catch'
const App = () => {
return (
<ErrorCatch
app="react-catch"
user="cxyuns"
delay={5000}
max={1}
filters={[]}
onCatch={(errors) => {
console.log('報錯咯');
// 上報異常信息到後端,動態創建標籤方式
new Image().src = `http://localhost:3000/log/report?info=${JSON.stringify(errors)}`
}}
>
<Main />
</ErrorCatch>)
}
export default
鼓掌,鼓掌。
其實不然:利用 error 捕獲的錯誤,其最主要的是提供了錯誤堆棧信息,對於分析錯誤相當不友好,尤其打包之後。
錯誤那麼多,我就先好好處理 React 裏面的事件處理程序。
至於其他,待續。
事件處理程序的異常捕獲
示例
我的思路原理很簡單,使用 decorator[5] 來重寫原來的方法。
先看一下使用:
@methodCatch({ message: "創建訂單失敗", toast: true, report:true, log:true })
async createOrder() {
const data = {...};
const res = await createOrder();
if (!res || res.errCode !== 0) {
return Toast.error("創建訂單失敗");
}
.......
其他可能產生異常的代碼
.......
Toast.success("創建訂單成功");
}
注意四個參數:
-
message:出現錯誤時,打印的錯誤
-
toast:出現錯誤,是否 Toast
-
report: 出現錯誤,是否上報
-
log: 使用使用 console.error 打印
可能你說,這這,消息定死,不合理啊。我要是有其他消息呢。
此時我微微一笑別急, 再看一段代碼
@methodCatch({ message: "創建訂單失敗", toast: true, report:true, log:true })
async createOrder() {
const data = {...};
const res = await createOrder();
if (!res || res.errCode !== 0) {
return Toast.error("創建訂單失敗");
}
.......
其他可能產生異常的代碼
.......
throw new CatchError("創建訂單失敗了,請聯繫管理員", {
toast: true,
report: true,
log: false
})
Toast.success("創建訂單成功");
}
是都,沒錯,你可以通過拋出 自定義的CatchError
來覆蓋之前的默認選項。
這個methodCatch
可以捕獲,同步和異步的錯誤,我們來一起看看全部的代碼。
類型定義
export interface CatchOptions {
report?: boolean;
message?: string;
log?: boolean;
toast?: boolean;
}
// 這裏寫到 const.ts更合理
export const DEFAULT_ERROR_CATCH_OPTIONS: CatchOptions = {
report: true,
message: "未知異常",
log: true,
toast: false
}
自定義的 CatchError
import { CatchOptions, DEFAULT_ERROR_CATCH_OPTIONS } from "@typess/errorCatch";
export class CatchError extends Error {
public __type__ = "__CATCH_ERROR__";
/**
* 捕捉到的錯誤
* @param message 消息
* @options 其他參數
*/
constructor(message: string, public options: CatchOptions = DEFAULT_ERROR_CATCH_OPTIONS) {
super(message);
}
}
裝飾器
import Toast from "@components/Toast";
import { CatchOptions, DEFAULT_ERROR_CATCH_OPTIONS } from "@typess/errorCatch";
import { CatchError } from "@util/error/CatchError";
const W_TYPES = ["string", "object"];
export function methodCatch(options: string | CatchOptions = DEFAULT_ERROR_CATCH_OPTIONS) {
const type = typeof options;
let opt: CatchOptions;
if (options == null || !W_TYPES.includes(type)) { // null 或者 不是字符串或者對象
opt = DEFAULT_ERROR_CATCH_OPTIONS;
} else if (typeof options === "string") { // 字符串
opt = {
...DEFAULT_ERROR_CATCH_OPTIONS,
message: options || DEFAULT_ERROR_CATCH_OPTIONS.message,
}
} else { // 有效的對象
opt = { ...DEFAULT_ERROR_CATCH_OPTIONS, ...options }
}
return function (_target: any, _name: string, descriptor: PropertyDescriptor): any {
const oldFn = descriptor.value;
Object.defineProperty(descriptor, "value", {
get() {
async function proxy(...args: any[]) {
try {
const res = await oldFn.apply(this, args);
return res;
} catch (err) {
// if (err instanceof CatchError) {
if(err.__type__ == "__CATCH_ERROR__"){
err = err as CatchError;
const mOpt = { ...opt, ...(err.options || {}) };
if (mOpt.log) {
console.error("asyncMethodCatch:", mOpt.message || err.message , err);
}
if (mOpt.report) {
// TODO::
}
if (mOpt.toast) {
Toast.error(mOpt.message);
}
} else {
const message = err.message || opt.message;
console.error("asyncMethodCatch:", message, err);
if (opt.toast) {
Toast.error(message);
}
}
}
}
proxy._bound = true;
return proxy;
}
})
return descriptor;
}
}
總結一下
-
利用裝飾器重寫原方法,達到捕獲錯誤的目的
-
自定義錯誤類,拋出它,就能達到覆蓋默認選項的目的。增加了靈活性。
@methodCatch({ message: "創建訂單失敗", toast: true, report:true, log:true })
async createOrder() {
const data = {...};
const res = await createOrder();
if (!res || res.errCode !== 0) {
return Toast.error("創建訂單失敗");
}
Toast.success("創建訂單成功");
.......
其他可能產生異常的代碼
.......
throw new CatchError("創建訂單失敗了,請聯繫管理員", {
toast: true,
report: true,
log: false
})
}
下一步
啥下一步,走一步看一步啦。
不,接下來的路,還很長。這纔是一個基礎版本。
-
擴大成果,支持更多類型,以及hooks版本。
@XXXCatch
classs AAA{
@YYYCatch
method = ()=> {
}
}
- 抽象,再抽象,再抽象
玩笑開完了,嚴肅一下:
當前方案存在的問題:
-
功能侷限
-
抽象不夠
獲取選項, 代理函數, 錯誤處理函數完全可以分離,變成通用方法。 -
同步方法經過轉換後會變爲異步方法。
所以理論上,要區分同步和異步方案。 -
錯誤處理函數再異常怎麼辦
之後,我們會圍繞着這些問題,繼續展開。
Hooks 版本
有掘友說,這個年代了,誰還不用 Hooks。
是的,大佬們說得對,我們得與時俱進。
Hooks 的基礎版本已經有了,先分享使用,後續的文章跟上。
Hook 的名字就叫 useCatch
const TestView: React.FC<Props> = function (props) {
const [count, setCount] = useState(0);
const doSomething = useCatch(async function(){
console.log("doSomething: begin");
throw new CatchError("doSomething error")
console.log("doSomething: end");
}, [], {
toast: true
})
const onClick = useCatch(async (ev) => {
console.log(ev.target);
setCount(count + 1);
doSomething();
const d = delay(3000, () => {
setCount(count => count + 1);
console.log()
});
console.log("delay begin:", Date.now())
await d.run();
console.log("delay end:", Date.now())
console.log("TestView", this)
throw new CatchError("自定義的異常,你知道不")
},
[count],
{
message: "I am so sorry",
toast: true
});
return <div>
<div><button onClick={onClick}>點我</button></div>
<div>{count}</div>
</div>
}
export default React.memo(TestView);
至於思路,基於useMemo
, 可以先看一下代碼:
export function useCatch<T extends (...args: any[]) => any>(callback: T, deps: DependencyList, options: CatchOptions =DEFAULT_ERRPR_CATCH_OPTIONS): T {
const opt = useMemo( ()=> getOptions(options), [options]);
const fn = useMemo((..._args: any[]) => {
const proxy = observerHandler(callback, undefined, function (error: Error) {
commonErrorHandler(error, opt)
});
return proxy;
}, [callback, deps, opt]) as T;
return fn;
}
寫在最後
寫作不易,如果覺得還不錯, 一讚一評,就是我最大的動力。
error-boundaries[6]
React 異常處理 [7]
catching-react-errors[8]
react 進階之異常處理機制 - error Boundaries[9]
decorator[10]
core-decorators[11]
autobind.js[12]
參考資料
[1] https://www.npmjs.com/package/react-error-boundary: https://link.juejin.cn?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Freact-error-boundary
[2] https://reactjs.org/docs/error-boundaries.html#introducing-error-boundaries: https://link.juejin.cn?target=https%3A%2F%2Freactjs.org%2Fdocs%2Ferror-boundaries.html%23introducing-error-boundaries
[3] https://reactjs.org/docs/error-boundaries.html#how-about-event-handlers: https://link.juejin.cn?target=https%3A%2F%2Freactjs.org%2Fdocs%2Ferror-boundaries.html%23how-about-event-handlers
[4] https://www.npmjs.com/package/react-error-catch: https://link.juejin.cn?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Freact-error-catch
[5] http://es6.ruanyifeng.com/#docs/decorator: https://link.juejin.cn?target=http%3A%2F%2Fes6.ruanyifeng.com%2F%23docs%2Fdecorator
[6] https://reactjs.org/docs/error-boundaries.html: https://link.juejin.cn?target=https%3A%2F%2Freactjs.org%2Fdocs%2Ferror-boundaries.html
[7] https://www.colabug.com/1867349.html: https://link.juejin.cn?target=https%3A%2F%2Fwww.colabug.com%2F1867349.html
[8] https://engineering.classdojo.com/blog/2016/12/10/catching-react-errors/: https://link.juejin.cn?target=https%3A%2F%2Fengineering.classdojo.com%2Fblog%2F2016%2F12%2F10%2Fcatching-react-errors%2F
[9] https://blog.csdn.net/a986597353/article/details/78469979: https://link.juejin.cn?target=https%3A%2F%2Fblog.csdn.net%2Fa986597353%2Farticle%2Fdetails%2F78469979
[10] http://es6.ruanyifeng.com/#docs/decorator: https://link.juejin.cn?target=http%3A%2F%2Fes6.ruanyifeng.com%2F%23docs%2Fdecorator
[11] https://github.com/jayphelps/core-decorators: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fjayphelps%2Fcore-decorators
[12] https://github.com/jayphelps/core-decorators/blob/master/src/autobind.js: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fjayphelps%2Fcore-decorators%2Fblob%2Fmaster%2Fsrc%2Fautobind.js
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/PT9w-vv4XlwsmO-9h_SNSg