<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          React,優(yōu)雅的捕獲異常

          共 22993字,需瀏覽 46分鐘

           ·

          2021-07-06 16:49

          點擊上方 前端Q,關注公眾號

          回復加群,加入前端Q技術交流群

          來源:云的世界

          https://juejin.cn/post/6974383324148006926


          前言

          人無完人,所以代碼總會出錯,出錯并不可怕,關鍵是怎么處理。
          我就想問問大家react的應用的錯誤怎么捕捉呢?這個時候:

          • 小白+++:怎么處理?
          • 小白++:ErrorBoundary
          • 小白+:ErrorBoundary, try catch
          • 小黑#: ErrorBoundary, try catch, window.onerror
          • 小黑##: 這個是個嚴肅的問題,我知道N種處理方式,你有什么更好的方案?

          ErrorBoundary

          EerrorBoundary是16版本出來的,有人問那我的15版本呢,我不聽我不聽,反正我用16,當然15有unstable_handleError

          關于ErrorBoundary官網(wǎng)介紹比較詳細,這個不是重點,重點是他能捕捉哪些異常。

          • 子組件的渲染
          • 生命周期函數(shù)
          • 構造函數(shù)
          class ErrorBoundary extends React.Component {
            constructor(props) {
              super(props);
              this.state = { hasErrorfalse };
            }

            componentDidCatch(error, info) {
              // Display fallback UI
              this.setState({ hasErrortrue });
              // 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 這種優(yōu)秀的庫。
          你只需要關心出現(xiàn)錯誤后需要關心什么,還以來個 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自己拋出的錯誤

          原文可見參見官網(wǎng)introducing-error-boundaries

          本文要捕獲的就是 事件處理程序的錯誤。
          官方其實也是有方案的how-about-event-handlers, 就是 try catch.
          但是,那么多事件處理程序,我的天,得寫多少,。。。。。。。。。。。。。。。。。。。。

            handleClick() {
              try {
                // Do something that could throw
              } catch (error) {
                this.setState({ error });
              }
            }
          復制代碼

          Error Boundary 之外

          我們先看看一張表格,羅列了我們能捕獲異常的手段和范圍。

          異常類型同步方法異步方法資源加載Promiseasync/await
          try/catch


          window.onerror


          error

          unhandledrejection


          try/catch

          可以捕獲同步和async/await的異常。

          window.onerror , error事件

              window.addEventListener('error'this.onError, true);
              window.onerror = this.onError
          復制代碼

          window.addEventListener('error') 這種可以比 window.onerror 多捕獲資源記載異常. 請注意最后一個參數(shù)是 true, false的話可能就不如你期望。

          當然你如果問題這第三個參數(shù)的含義,我就有點不想理你了。拜。

          unhandledrejection

          請注意最后一個參數(shù)是 true

          window.removeEventListener('unhandledrejection'this.onReject, true)
          復制代碼

          其捕獲未被捕獲的Promise的異常。

          XMLHttpRequest 與 fetch

          XMLHttpRequest 很好處理,自己有onerror事件。當然你99.99%也不會自己基于XMLHttpRequest封裝一個庫, axios 真香,有這完畢的錯誤處理機制。

          至于fetch, 自己帶著catch跑,不處理就是你自己的問題了。

          這么多,太難了。
          還好,其實有一個庫 react-error-catch 是基于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('報錯咯');
                  // 上報異常信息到后端,動態(tài)創(chuàng)建標簽方式
                  new Image().src = `http://localhost:3000/log/report?info=${JSON.stringify(errors)}`
                }}
              >
                <Main />
              </ErrorCatch>
          )
          }

          export default 
          復制代碼

          鼓掌,鼓掌。

          其實不然:利用error捕獲的錯誤,其最主要的是提供了錯誤堆棧信息,對于分析錯誤相當不友好,尤其打包之后。

          錯誤那么多,我就先好好處理React里面的事件處理程序。
          至于其他,待續(xù)。

          事件處理程序的異常捕獲

          示例

          我的思路原理很簡單,使用decorator來重寫原來的方法。

          先看一下使用:


             @methodCatch({ message"創(chuàng)建訂單失敗"toasttruereport:truelog:true })
              async createOrder() {
                  const data = {...};
                  const res = await createOrder();
                  if (!res || res.errCode !== 0) {
                      return Toast.error("創(chuàng)建訂單失敗");
                  }
                  
                  .......
                  其他可能產(chǎn)生異常的代碼
                  .......
                  
                 Toast.success("創(chuàng)建訂單成功");
              }
          復制代碼

          注意四個參數(shù):

          • message:出現(xiàn)錯誤時,打印的錯誤
          • toast:出現(xiàn)錯誤,是否Toast
          • report: 出現(xiàn)錯誤,是否上報
          • log: 使用使用console.error打印

          可能你說,這這,消息定死,不合理啊。我要是有其他消息呢。
          此時我微微一笑別急, 再看一段代碼

            @methodCatch({ message"創(chuàng)建訂單失敗"toasttruereport:truelog:true })
              async createOrder() {
                  const data = {...};
                  const res = await createOrder();
                  if (!res || res.errCode !== 0) {
                      return Toast.error("創(chuàng)建訂單失敗");
                  }
                 
                  .......
                  其他可能產(chǎn)生異常的代碼
                  .......
                  
                 throw new CatchError("創(chuàng)建訂單失敗了,請聯(lián)系管理員", {
                     toasttrue,
                     reporttrue,
                     logfalse
                 })
                 
                 Toast.success("創(chuàng)建訂單成功");

              }
          復制代碼

          是都,沒錯,你可以通過拋出 自定義的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 其他參數(shù)
               */

              constructor(message: stringpublic 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;
              }
          }
          復制代碼

          總結一下

          1. 利用裝飾器重寫原方法,達到捕獲錯誤的目的
          2. 自定義錯誤類,拋出它,就能達到覆蓋默認選項的目的。增加了靈活性。
            @methodCatch({ message"創(chuàng)建訂單失敗"toasttruereport:truelog:true })
              async createOrder() {
                  const data = {...};
                  const res = await createOrder();
                  if (!res || res.errCode !== 0) {
                      return Toast.error("創(chuàng)建訂單失敗");
                  }
                 Toast.success("創(chuàng)建訂單成功");
                 
                  .......
                  其他可能產(chǎn)生異常的代碼
                  .......
                  
                 throw new CatchError("創(chuàng)建訂單失敗了,請聯(lián)系管理員", {
                     toasttrue,
                     reporttrue,
                     logfalse
                 })
              }
          復制代碼

          下一步

          啥下一步,走一步看一步啦。

          不,接下來的路,還很長。這才是一個基礎版本。

          1. 擴大成果,支持更多類型,以及hooks版本。

          @XXXCatch
          classs AAA{
              @YYYCatch
              method = ()=> {
              }
          }
          復制代碼
          1. 抽象,再抽象,再抽象

          玩笑開完了,嚴肅一下:

          當前方案存在的問題:

          1. 功能局限
          2. 抽象不夠
            獲取選項,代理函數(shù), 錯誤處理函數(shù)完全可以分離,變成通用方法。
          3. 同步方法經(jīng)過轉換后會變?yōu)楫惒椒椒ā?br>所以理論上,要區(qū)分同步和異步方案。
          4. 錯誤處理函數(shù)再異常怎么辦

          之后,我們會圍繞著這些問題,繼續(xù)展開。

          Hooks版本

          有掘友說,這個年代了,誰還不用Hooks。
          是的,大佬們說得對,我們得與時俱進。
          Hooks的基礎版本已經(jīng)有了,先分享使用,后續(xù)的文章跟上。

          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, undefinedfunction (error: Error{
                      commonErrorHandler(error, opt)
                  });
                  return proxy;

              }, [callback, deps, opt]) as T;

              return fn;
          }

          復制代碼

          寫在最后

          寫作不易,如果覺得還不錯, 一贊一評,就是我最大的動力。

          error-boundaries
          React異常處理
          catching-react-errors
          react進階之異常處理機制-error Boundaries
          decorator
          core-decorators
          autobind.js

          聲明:文章著作權歸作者所有,如有侵權,請聯(lián)系小編刪除。




          內(nèi)推社群


          我組建了一個氛圍特別好的騰訊內(nèi)推社群,如果你對加入騰訊感興趣的話(后續(xù)有計劃也可以),我們可以一起進行面試相關的答疑、聊聊面試的故事、并且在你準備好的時候隨時幫你內(nèi)推。下方加 winty 好友回復「面試」即可。


          瀏覽 63
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  色色影院精品 | 一本色道久草在线 | 亚洲高清无码卡一 卡二 | 国产亚洲 久一区二区草榴AV | 五十路在线视频 |