美文网首页
DIY一个Web前端异常收集器

DIY一个Web前端异常收集器

作者: _茂 | 来源:发表于2020-02-17 15:47 被阅读0次

    一、背景

    最近加入了一个刻意练习小组,自选了一个课题。
    题目:《实现一个前端异常收集器》
    目标:收集前端的各类错误,包括收集时间、容错等。

    先介绍一下思路:


    思路

    二、github源码

    安装:
    yarn add web-error-tracker

    https://github.com/evilrescuer/web-error-tracker

    测试项目:showcase

    三、常见前端异常类型

    此处为示例片段代码,具体请查看github源码

    1.JavaScript语法异常

    Uncaught ReferenceError: t is not defined

    function testJavaScriptSyntaxError() {
        t();
    }
    
    testJavaScriptSyntaxError();
    

    2.加载图片资源异常

    加载图片错误

    function testImgError() {
        const img = document.createElement('IMG');
        img.src = './test.png';
        document.body.append(img);
    }
    
    testImgError();
    

    3.未捕获的Promise错误

    没有捕获 unhandledrejection错误

    function testPromiseError() {
        new Promise((resolve, reject) => {
            t();
        });
    }
    
    testPromiseError();
    

    4.api返回错误

    // Node.js启动一个server,模拟接口返回500
    if (ctx.req.url === '/test500') {
        ctx.body = 'Internal Server Error';
        ctx.status = 500;
    }
    
    // 测试
    // 事先引入axios库
    // ...
    function testCallApiError() {
        axios.get('http://localhost:3000/test500');
    }
    
    testCallApiError();
    

    5.跨域异常

    // html
    <button id="btn-cors-error">跨域异常</button>
    <script src="http://localhost:3000/file.js" crossorigin></script>
    
    // Node.js服务模拟file.js返回
    if (ctx.req.url === '/file.js') {
        ctx.set('Content-Type', 'text/javascript');
        ctx.body = `
            const btn = document.querySelector('#btn-cors-error');
            btn.addEventListener('click', () => {
                // b is not defined
                var a = b;
            });
        `;
    }
    

    注:必须加上crossorigin,否则捕获的错误不够详细,而是Script Error

    6.动态创建的有错误的脚本

    function testCreateAWrongScriptError() {
        const script = document.createElement('script');
        // testCreateAWrongScriptErrorValiable is not defined
        script.innerHTML = ` 
            var a = testCreateAWrongScriptErrorValiable;
        `;
        document.body.append(script);
    }
    
    testCreateAWrongScriptError()
    

    7.iframe内部异常

    // html
    <iframe src="./iframe.html" frameborder="0"></iframe>
    
    // iframe
    <script>
        setTimeout(() => {
            // b is not defined
            var a = b;
        }, 1000)
    </script>
    

    四、源码解析

    // 修改原生EventTarget对象
    function modifyEventTarget (destWindow) {
        // 跨域异常-crossOrigin
        const originAddEventListener = destWindow.EventTarget.prototype.addEventListener;
        destWindow.EventTarget.prototype.addEventListener = function (type, listener, options) {
            const wrappedListener = function (...args) {
                try {
                    return listener.apply(this, args);
                }
                catch (err) {
                    throw err;
                }
            };
            return originAddEventListener.call(destWindow, type, wrappedListener, options);
        };
    }
    
    // 修改原生XMLHttpRequest
    function hookAjax (proxy, destWindow = window) {
        const realXhr = "RealXMLHttpRequest";
        destWindow[realXhr] = destWindow[realXhr] || destWindow.XMLHttpRequest;
    
        destWindow.XMLHttpRequest = function () {
            const xhr = new destWindow[realXhr];
            for (const attr in xhr) {
                let type = "";
                try {
                    type = typeof xhr[attr];
                } catch (e) {
                }
                if (type === "function") {
                    this[attr] = hookFunction(attr);
                } else {
                    Object.defineProperty(this, attr, {
                        get: getterFactory(attr),
                        set: setterFactory(attr),
                        enumerable: true
                    });
                }
            }
            this.xhr = xhr;
    
        };
    
        function getterFactory(attr) {
            return function () {
                const v = this.hasOwnProperty(attr + "_") ? this[attr + "_"] : this.xhr[attr];
                const attrGetterHook = (proxy[attr] || {})["getter"];
                return attrGetterHook && attrGetterHook(v, this) || v
            }
        }
    
        function setterFactory(attr) {
            return function (v) {
                const xhr = this.xhr;
                const that = this;
                const hook = proxy[attr];
                if (typeof hook === "function") {
                    xhr[attr] = function () {
                        proxy[attr](that) || v.apply(xhr, arguments);
                    }
                } else {
                    const attrSetterHook = (hook || {})["setter"];
                    v = attrSetterHook && attrSetterHook(v, that) || v
                    try {
                        xhr[attr] = v;
                    } catch (e) {
                        this[attr + "_"] = v;
                    }
                }
            }
        }
    
        function hookFunction(fun) {
            return function () {
                const args = [].slice.call(arguments);
                if (proxy[fun] && proxy[fun].call(this, args, this.xhr)) {
                    return;
                }
                return this.xhr[fun].apply(this.xhr, args);
            }
        }
    
        return destWindow[realXhr];
    }
    
    class ErrorTracker {
        constructor() {
            this.errorBox = [];
        }
    
        init() {
            this.handleWindow(window);
        }
    
        getErrors() {
            return this.errorBox;
        }
    
        handleWindow(destWindow) {
            const _instance = this;
            modifyEventTarget(destWindow);
    
            // XHR错误(利用http status code判断)
            hookAjax({
                onreadystatechange: xhr => {
                if (xhr.readyState === 4) {
                    if (xhr.status >= 400 || xhr.status <= 599) {
                        console.log('xhr错误:', xhr);
                        const error = xhr.xhr;
                        _instance.errorBox.push(new FEError(`api response ${error.status}`, error.responseURL, null, null, error.responseText));
                    }
                }
            }
            }, destWindow);
    
            // 全局JS异常-window.onerror / 全局静态资源异常-window.addEventListener
            destWindow.addEventListener('error', event => {
                event.preventDefault();
                console.log('errorEvent错误:', event);
                if (event instanceof destWindow.ErrorEvent) {
                    _instance.errorBox.push(new FEError(event.message, event.filename, event.lineno, event.colno, event.error));
                }
                else if (event instanceof destWindow.Event) {
                    if (event.target instanceof HTMLImageElement) {
                        _instance.errorBox.push(new FEError('load img error', event.target.src, null, null, null));
                    }
                }
                return true;
            }, true);
    
            // 没有catch等promise异常-unhandledrejection
            destWindow.addEventListener('unhandledrejection', event => {
                event.preventDefault();
                console.log('unhandledrejection错误:', event);
                _instance.errorBox.push(new FEError('unhandled rejection', null, null, null, event.reason));
                return true;
            });
    
            // 页面嵌套错误(iframe错误等等、单点登录)(注意:不能捕获iframe加载时的错误)
            destWindow.addEventListener('load', () => {
                const iframes = destWindow.document.querySelectorAll('iframe');
                iframes.forEach(iframe => {
                    _instance.handleWindow(iframe.contentWindow);
                });
            });
        }
    }
    
    class FEError {
        constructor(message, source, lineno, colno, stack) {
            this.message = message;
            this.source = source;
            this.lineno = lineno;
            this.colno = colno;
            this.stack = stack;
            this.time = new Date();
        }
    }
    
    window.errorTracker = new ErrorTracker();
    window.errorTracker.init();
    
    

    五、总结

    总体思路:通过监听error事件、修改原生xhr、修改EventTarget。
    如有iframe,需要在iframe的window环境下,递归以上处理。

    问题 方案 备注
    1.JavaScript语法异常 window.addEventListener监听error事件 回调中event对象为ErrorEvent的实例
    2.加载图片资源异常 window.addEventListener监听error事件 回调中event对象为Event的实例
    3.未捕获的Promise错误 window.addEventListener监听unhandledrejection事件
    4.api返回错误 修改原生XMLHttpRequest
    5.跨域异常 修改原生EventTarget对象、并在资源link上加上crossorigin属性
    6.动态创建的有错误的脚本 无需特殊处理
    7.iframe内部异常 先获取所有iframe的Dom节点,再递归处理

    (完)

    相关文章

      网友评论

          本文标题:DIY一个Web前端异常收集器

          本文链接:https://www.haomeiwen.com/subject/ynllfhtx.html