1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        React,優(yōu)雅的捕獲異常進階篇, 含Hooks方案

        共 28780字,需瀏覽 58分鐘

         ·

        2021-08-06 11:36

        點擊上方 全棧前端精選,關注公眾號

        回復【1】,加入前端群


        在React項目中,因為事件處理程序總是需要寫 try/catch,不勝其煩。

        雖然可以丟給window.onerror或者 window.addEventListener("error")去處理,但是對錯誤細節(jié)的捕獲以及錯誤的補償是極其不友好的。

        于是基于ES標準的裝飾器,出了一個事件處理程序的捕獲方案,詳情參見前篇 React,優(yōu)雅的捕獲異常 。

        評論區(qū)有掘友吐槽,都啥年代,還寫Class?, Hooks 666啊。

        掘友說的對,我要跟上時代的步伐, 要支持Hooks, getter等等。

        補充一下

        最初僅僅是為了捕獲和處理事件程序的異常,實際上是可以用于任何Class的方法上的。

        問題回顧

        React,優(yōu)雅的捕獲異常 方案存在的問題:

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

        我們來一一解決。

        一覽風姿

        我們捕獲的范圍:

        1. Class的靜態(tài)同步方法
        2. Class的靜態(tài)異步方法
        3. Class的同步方法
        4. Class的異步方法
        5. Class的同步屬性賦值方法
        6. Class的異步屬性賦值方法
        7. Class的getter方法
        8. Hooks方法

        getter這里是不是很類似 vue的 計算值,所以以后別說我大React沒有計算屬性,哈哈。

        來來來,一覽其風采:

        先看看Class組件的


        interface State {
            price: number;
            count: number;
        }

        export default class ClassT extends BaseComponent<{}, State> {
            constructor(props) {
                super(props);
                this.state = {
                    price: 100,
                    count: 1
                }
                this.onIncrease = this.onIncrease.bind(this);
                this.onDecrease = this.onDecrease.bind(this);
            }

            componentDidMount() {
                ClassT.printSomething();
                ClassT.asyncPrintSomething();

                this.doSomethings();
                this.asyncDoSomethings();
            }

            @catchMethod({ message: "printSomething error", toast: true })
            static printSomething() {
                throw new CatchError("printSomething error: 主動拋出");
                console.log("printSomething:"Date.now());
            }

            @catchMethod({ message: "asyncPrintSomething error", toast: true })
            static async asyncPrintSomething() {
                const { run } = delay(1000);
                await run();
                throw new CatchError("asyncPrintSomething error: 主動拋出");
                console.log("asyncPrintSomething:"Date.now());
            }

            @catchGetter({ message: "計算價格失敗", toast: true })
            get totalPrice() {
                const { price, count } = this.state;
                // throw new Error("A");
                return price * count;
            }

            @catchMethod("增加數(shù)量失敗")
            async onIncrease() {

                const { run } = delay(1000);
                await run();

                this.setState({
                    count: this.state.count + 1
                })
            }

            @catchMethod("減少數(shù)量失敗")
            onDecrease() {
                this.setState({
                    count: this.state.count - 1
                })
            }

            @catchInitializer({ message: "catchInitializer error", toast: true })
            doSomethings = () => {
                console.log("do some things");
                throw new CatchError("catchInitializer error: 主動拋出");
            }

            @catchInitializer({ message: "catchInitializer async error", toast: true })
            asyncDoSomethings = async () => {
                const { run } = delay(1000);
                await run();
                throw new CatchError("catchInitializer async error: 主動拋出");
            }

            render() {
                const { onIncrease, onDecrease } = this;
                const totalPrice = this.totalPrice;

                return <div style={{
                    padding: "150px",
                    lineHeight: "30px",
                    fontSize: "20px"
                }}>
                    <div>價格:{this.state.price}</div>
                    <div>數(shù)量:1</
        div>
                    <div>
                        <button onClick={onIncrease}>增加數(shù)量</button>
                        <button onClick={onDecrease}>減少數(shù)量</
        button>
                    </div>
                    <div>{totalPrice}</
        div>
                </div>
            }

        }
        復制代碼

        再看看函數(shù)式組件,就是大家關注的Hooks,包裝出useCatch,底層是基于useMemo

        const HooksTestView: 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);
                (d as any).xxx.xxx.x.x.x.x.x.x.x.x.x.x.x
                // 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(HooksTestView);
        復制代碼

        我們一覽風采之后,先看看我們做了哪些優(yōu)化,為什么要說優(yōu)化呢。因為優(yōu)化之前的代碼之后,代碼的可讀性,復用性,可擴展性大幅增強。

        優(yōu)化

        封裝getOptions方法

        // options類型白名單
        const W_TYPES = ["string""object"];

        export function getOptions(options: string | CatchOptions{
            const type = typeof options;
            let opt: CatchOptions;
            
            if (options == null || !W_TYPES.includes(type)) { // null 或者 不是字符串或者對象
                opt = DEFAULT_ERRPR_CATCH_OPTIONS;
            } else if (typeof options === "string") {  // 字符串
                opt = {
                    ...DEFAULT_ERRPR_CATCH_OPTIONS,
                    message: options || DEFAULT_ERRPR_CATCH_OPTIONS.message,
                }
            } else { // 有效的對象
                opt = { ...DEFAULT_ERRPR_CATCH_OPTIONS, ...options }
            }

            return opt;
        }
        復制代碼

        定義默認處理函數(shù)

        /**
         * 
         * @param err 默認的錯誤處理函數(shù)
         * @param options 
         */

        function defaultErrorHanlder(err: any, options: CatchOptions{
            const message = err.message || options.message;
            console.error("defaultErrorHanlder:", message, err);
        }

        復制代碼

        區(qū)分同步方法和異步方法

        export function observerHandler(fn: AnyFunction, context: any, callback: ErrorHandler{
            return async function (...args: any[]{
                try {
                    const r = await fn.call(context || this, ...args);
                    return r;
                } catch (err) {
                    callback(err);
                }
            };
        }

        export function observerSyncHandler(fn: AnyFunction, context: any, callback: ErrorHandler{
            return function (...args: any[]{
                try {
                    const r = fn.call(context || this, ...args);
                    return r;
                } catch (err) {
                    callback(err);
                }
            };
        }
        復制代碼

        具備多級選項定義能力

        export default function createErrorCatch(handler: ErrorHandlerWithOptions, 
        baseOptions: CatchOptions = DEFAULT_ERRPR_CATCH_OPTIONS
        {

            return {
                catchMethod(options: CatchOptions | string = DEFAULT_ERRPR_CATCH_OPTIONS) {
                    return catchMethod({ ...baseOptions, ...getOptions(options) }, handler)
                }   
            }
        }
        復制代碼

        自定義錯誤處理函數(shù)

        export function commonErrorHandler(error: any, options: CatchOptions{    
            try{
                let message: string;
                if (error.__type__ == "__CATCH_ERROR__") {
                    error = error as CatchError;
                    const mOpt = { ...options, ...(error.options || {}) };

                    message = error.message || mOpt.message ;
                    if (mOpt.log) {
                        console.error("asyncMethodCatch:", message , error);
                    }

                    if (mOpt.report) {
                        // TODO::
                    }

                    if (mOpt.toast) {
                        Toast.error(message);
                    }

                } else {

                    message = options.message ||  error.message;
                    console.error("asyncMethodCatch:", message, error);

                    if (options.toast) {
                        Toast.error(message);
                    }
                }
            }catch(err){
                console.error("commonErrorHandler error:", err);
            }
        }


        const errorCatchInstance = createErrorCatch(commonErrorHandler);

        export const catchMethod = errorCatchInstance.catchMethod; 
        復制代碼

        增強

        支持getter

        先看一下catchGetter的使用

        class Test {

            constructor(props) {
                super(props);
                this.state = {
                    price: 100,
                    count: 1
                }

                this.onClick = this.onClick.bind(this);
            }

            @catchGetter({ message: "計算價格失敗", toast: true })
            get totalPrice() {
                const { price, count } = this.state;
                throw new Error("A");
                return price * count;
            }
            
              render() {   
                const totalPrice = this.totalPrice;

                return <div>
                    <div>價格:{this.state.price}</div>
                    <div>數(shù)量:1</
        div>
                    <div>{totalPrice}</div>
                </
        div>
            }
            
        }
        復制代碼

        實現(xiàn)

        /**
         * class {  get method(){} }
         * @param options 
         * @param hanlder 
         * @returns 
         */

        export function catchGetter(options: string | CatchOptions = DEFAULT_ERRPR_CATCH_OPTIONS, 
        hanlder: ErrorHandlerWithOptions = defaultErrorHanlder
        {

            let opt: CatchOptions = getOptions(options);

            return function (_target: any, _name: string, descriptor: PropertyDescriptor{
                const { constructor } = _target;
                const { get: oldFn } = descriptor;

                defineProperty(descriptor, "get", {
                    value: function () {
                        // 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 oldFn();
                        }
                        // 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 oldFn();
                        }
                        const boundFn = observerSyncHandler(oldFn, thisfunction (error: Error) {
                            hanlder(error, opt)
                        }
        );
                        (boundFn as any)._bound = true;
                    
                        return boundFn();
                    }
                });

                return descriptor;
            }

        }
        復制代碼

        支持屬性定義和賦值

        這個需要babel的支持,詳情可以參見babel-plugin-proposal-class-properties

        demo可以參見class-error-catch

        class Test{
            @catchInitializer("nono")
            doSomethings = ()=> {
                console.log("do some things");
            }
        }
        復制代碼

        實現(xiàn)

        export function catchInitializer(options: string | CatchOptions = DEFAULT_ERRPR_CATCH_OPTIONS, hanlder: ErrorHandlerWithOptions = defaultErrorHanlder){

            const opt: CatchOptions = getOptions(options);

             return function (_target: any, _name: string, descriptor: any{

                console.log("debug....");
                const initValue = descriptor.initializer();
                if (typeof initValue !== "function") {
                    return descriptor;
                }

                descriptor.initializer = function() {
                    initValue.bound = true;
                    return observerSyncHandler(initValue, thisfunction (error: Error{
                        hanlder(error, opt)
                    });
                };
                return descriptor;
            }
        }
        復制代碼

        支持Hooks

        使用


        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);

        復制代碼

        實現(xiàn): 其基本原理就是利用 useMemo和之前封裝的observerHandler,寥寥幾行代碼就實現(xiàn)了。

        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;
        }
        復制代碼

        這里你可能會問啥,你只是實現(xiàn)了方法的異常捕獲啊,我的useEffect, useCallbak, useLayout等等,你就不管呢?

        其實到這里,基本兩條思路

        1. 基于useCatch分離定義的方法
        2. 針對每一個Hook再寫一個useXXX

        到這里,我想,已經(jīng)難不倒各位了。

        我這里只是提供了一種思路,一種看起來不復雜,可行的思路。

        關于源碼

        因為目前代碼是直接跑在我們的實際項目上的,還沒時間去獨立的把代碼分離到一個獨立的項目。想要全部源碼的同學可以聯(lián)系我。

        之后會把全部源碼,示例獨立出來。

        后續(xù)

        我想肯定有人會問,你用Object.defineProperty,out了,你看vue都用Proxy來實現(xiàn)了。

        是的,Proxy固然強大,但是要具體情況具體對待,這里我想到有兩點Proxy還真不如 Object.defineProperty 和 裝飾器。

        1.兼容性
        2.靈活度

        后續(xù):

        1. 支持直接捕獲整個Class
        2. 通過實用修復相關的問題
        3. 獨立代碼和示例,封裝為庫
        4. 嘗試使用Proxy實現(xiàn)

        具備類似功能的庫

        • catch-decorator

        僅僅捕獲方法,處理比較初級

        • catch-decorator-ts

        同上

        • catch-error-decorator

        通過 AsyncFunction判斷,提供失敗后的默認返回值。

        • auto-inject-async-catch-loader

        主要捕獲異步方法,原理是webpack loader, 遍歷AST. 其他async-catch-loader,babel-plugin-promise-catcher等原理類似。

        寫在最后

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

        babel-plugin-proposal-class-properties)
        setpublicclassfields

        關于本文

        來源:云的世界

        https://juejin.cn/post/6976414994107727909

        瀏覽 31
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

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

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            安娜大尺度做爰未删减视频 | 国产乱子伦精品久久 | 亚洲精品中文字幕乱码三区91 | 最近中文字幕第三页 | 少妇张开双腿迎合小坏蛋粗大 | 亚洲入口 | 青青精品视频在线观看 | 亚洲综合大香蕉 | 自拍第五页 | 免费看成人毛片A片用皮丝 |