IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    脚本执行顺序引发的惨案

    记得要微笑发表于 2023-08-14 16:22:38
    love 0
    岁月磨我少年志,时光凉我善良心。人间总有一丝情,抵我心中意难平。

    在某个工作日,有线上用户向客服专员反馈称无法正常访问"查看报价页面",页面内容无法呈现,且问题表现时好时坏。客服专员收到反馈后,将问题转交给SRE(系统可靠性工程师)进行处理。令人困惑的是,SRE在访问生产环境的"查看报价页面"时发现一切正常。为了进一步分析和定位问题,SRE向用户申请了远程操作权限。在远程操作期间,并没有发现浏览器控制台中存在错误日志,所有页面所依赖的JavaScript和CSS脚本都能够正常加载,但加载完成后并没有执行挂载页面模块的操作。因此,初步猜测问题可能出现在页面路由加载方面,导致页面无法正常挂载。

    image.png

    下面是路由加载部分代码:

    // 路由加载部分代码
    component: Loadable({
        loader: () =>
        import('@/pages/inquiry-detail-by-brand').then((comp) => {
            /** 报价页面访问PV、UV */
            if ('cassSensorsTrack' in window) {
                (window as any).cassSensorsTrack({
                    eventName: BURY_EVENT_NAME.QUOTATION_RESULT_PAGE_VIEW_CLICK,
                    desc: '页面浏览',
                    eventType: 'click',
                    eventData: '',
                });
            }
            return comp;
        }),
        loading: Loading,
    })
    
    // bury.js
    window.cassSensorsTrack = function (options) {
        options.collect_uuid = Math.random().toString(36).slice(2);
        if (options.canFragment === 'true') {
          var ret = null;
          try {
            ret = JSON.parse(options.eventData);
          } catch (error) {
            ret = options.eventData;
          }
          if (ret instanceof Array) {
            ret.forEach(function (part) {
              options.eventData = JSON.stringify(part); // 神策上传字段不能接收Object类型,需要转化成JSON串
              console.log('上报数据:', options.eventName, createCollectWrap(options));
              cassSensors.track(options.eventName, createCollectWrap(options));
            });
          }
        } else {
          console.log('上报数据:', options.eventName, createCollectWrap(options));
          cassSensors.track(options.eventName, createCollectWrap(options));
        }
    };

    在对该部分代码进行断点调试时发现,在页面访问异常的情况下,无法获取全局变量cassSensors,导致后续代码执行时抛出异常。具体表现为在页面加载时,Loadable组件内部捕获了该异常,并且渲染结果为空内容。

    image.png

    // 加载函数
    function load(loader) {
      var promise = loader();
    
      var state = {
        loading: true,
        loaded: null,
        error: null
      };
    
      state.promise = promise.then(function (loaded) {
        state.loading = false;
        state.loaded = loaded;
        return loaded;
      }).catch(function (err) {
        // 捕获异常
        state.loading = false;
        state.error = err;
        throw err;
      });
    
      return state;
    }
    
    // render
    LoadableComponent.prototype.render = function render() {
        console.log('this.state', this.state);
        if (this.state.loading || this.state.error) {
            console.log("页面组件还未加载完成,显示loading...");
            return React.createElement(opts.loading, {
                isLoading: this.state.loading,
                pastDelay: this.state.pastDelay,
                timedOut: this.state.timedOut,
                error: this.state.error,
                retry: this.retry
            });
        } else if (this.state.loaded) {
            console.log('页面组件加载完成,最终渲染...');
            return opts.render(this.state.loaded, this.props);
        } else {
            return null;
        }
    };

    原因是在加载cassSensors.js时,它内部通过动态script标签异步加载了两个SDK,并在加载完成后通过回调将cassSensors变量注入到全局作用域中。但是,当页面的脚本在加载cassSensors.js之前执行时,就无法获取到该全局变量,从而导致异常。

    image.png

    这种时而访问正常时而访问不正常的现象是因为加载cassSensors.js和相关SDK的过程是异步的,而页面脚本执行的顺序是同步的,所以在某些时候,脚本可能在cassSensors.js加载完成之前就开始执行了。

    要确保页面脚本能够正确获取到全局变量cassSensors,可以采用以下优化方案。在异步加载完成之前,先给出一个结构变量定义,让应用方调用不会报错。同时,可以在页面脚本中创建一个变量来缓存需要采集的数据,等待脚本加载完成后再进行上报操作。

    // cassSensors.js
    var dataCache = [];
    
    // 在异步脚本脚本完之前先给出结构变量定义,让调用方不会执行报错
    if (!"cassSensors" in window) {
        window.cassSensors = {
            track: function() {},
            // ...
        }
    }
    
    // 异步脚本加载完成后,重新抛出变量,并且上报缓存中的数据
    script.onload = fucntion() {
        window.cassSensors = window['sensorsDataAnalytic201505'];
        window.cassSensors.init();
        // ...
        if (dataCache.length) {
            dataCache.forEach(function(dc) {
                window.cassSensors.track(dc); // 上报
            });
            dataCache = [];
        }
    }

    通过这种优化方式,即使在异步加载未完成之前,给出了一个结构变量定义,应用方可以正常调用cassSensors,避免了报错。同时,通过缓存数据并在加载完成后进行上报,确保页面脚本能够正确获取到cassSensors变量,并且不会因为加载顺序问题而导致异常。

    在封装脚本时,向全局作用域注入变量并不是最优的方式。这种方式存在一些问题,如变量命名冲突、脚本执行依赖顺序导致获取不到变量、重复引入脚本导致重复注入(单例模式)等。为了更加优雅地封装脚本并解决这些问题,推荐使用npm包的形式进行发版、按需引入和构建打包到业务代码中。



沪ICP备19023445号-2号
友情链接