社区精选|脚本执行顺序引发的惨案

SegmentFault

共 6701字,需浏览 14分钟

 · 2023-09-08

今天小编为大家带来的是社区作者 记得要微笑 的文章,让我们一起来看看这场脚本执行顺序引发的惨案🐶




▏岁月磨我少年志,时光凉我善良心。人间总有一丝情,抵我心中意难平。

在某个工作日,有线上用户向客服专员反馈称无法正常访问"查看报价页面",页面内容无法呈现,且问题表现时好时坏。客服专员收到反馈后,将问题转交给SRE(系统可靠性工程师)进行处理。令人困惑的是,SRE在访问生产环境的"查看报价页面"时发现一切正常。

为了进一步分析和定位问题,SRE向用户申请了远程操作权限。在远程操作期间,并没有发现浏览器控制台中存在错误日志,所有页面所依赖的JavaScriptCSS脚本都能够正常加载,但加载完成后并没有执行挂载页面模块的操作。因此,初步猜测问题可能出现在页面路由加载方面,导致页面无法正常挂载。

以下是使用react-router模块进行路由配置的部分代码,同时在路由加载过程中进行数据采集:

component: Loadable({    loader: () => import('@/pages/inquiry-detail-by-brand').then((comp) => {        /** 页面访问 PV、UV */        debugger;        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,})


怀疑页面组件模块comp没有正确挂载到页面上,在then回调函数中添加断点来验证自己的猜想。



经过调试,发现问题是在脚本执行时,cassSensors 对象未定义,导致无法调用 track 方法。此外,外部代码没有包含异常处理的 try...catch 块。然而,在使用 Loadable 组件时,它内部捕获了该异常,并渲染了空内容。



// 加载函数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;}
// renderLoadableComponent.prototype.render = function render() { if (this.state.loading || this.state.error) { 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) { return opts.render(this.state.loaded, this.props); } else { return null; }};

为什么在加载页面组件脚本后获取不到 cassSensors 变量呢?


注:上述脚本加载顺序是模拟出来的场景,跟实际脚本加载顺序一致


不难发现问题是由于:

3.72881c2b.chunk.js 和 4.e6995327.chunk.js 在 sensorsdata1.19.4.min.js 之前加载和执行,导致在执行 .then 回调时无法访问到 cassSensors 变量。


这是因为:

cassSensors 是由 sensorsdata1.19.4.min.js 在全局作用域中注入的,而在回调执行时它尚未定义,从而导致异常。



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

// cassSensors.jsvar 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包的形式进行发版、按需引入和构建打包到业务代码中。

另外,上述线上问题是一个很小概率且难以复现的问题,遇到此类问题时,您可以考虑使用 Fiddler 反向代理来延迟加载 sensorsdata1.19.4.min.js 文件,以使页面脚本的执行在 sensorsdata1.19.4.min.js 之前。

没有 Fiddler 破解版的同学可以使用 Nginx 来模拟该场景,并通过第三方的 echo-nginx-module 模块进行配置,以实现延迟响应的反向代理。具体安装步骤可以参考:https://www.cnblogs.com/52fhy/p/10166333.html

配置反向代理示例:
http {  listen       8081;  server_name  localhost;  # 其他配置项...    server {    # 其他 server 配置项...    location /proxy/ {        rewrite ^/proxy/(.*)$ /$1 break;        proxy_pass https://mstatic.cassmall.com;        # 设置头部内容编码类型,防止返回乱码        proxy_set_header Accept-Encoding "";        proxy_set_header Accept-Language $http_accept_language;        proxy_set_header Content-Type "text/javascript; charset=utf-8";    }
location /delay { # prelight request if ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; add_header 'Access-Control-Max-Age' 1728000; add_header 'Content-Type' 'text/plain; charset=utf-8'; add_header 'Content-Length' 0; return 204; } # 设置反向代理目标服务器 # proxy_pass http://your_backend_server;      echo_location /proxy/$uri;
# 设置延迟时间为 5 秒钟 set $delay 5; # 在代理响应之前延迟 5 秒钟 echo_sleep $delay; } }}

echo_sleep 与 proxy_pass 配置会有冲突,两者同时配置,只有一个会生效,所以此处使用 echo_location。




点击左下角阅读原文,到 SegmentFault 思否社区 和文章作者展开更多互动和交流,公众号后台回复“ 入群 ”即可加入我们的技术交流群,收获更多的技术文章~

- END -



往期推荐



社区精选|“奇怪”的 Axios 拦截器


社区精选|纳尼!CSS 也能实现碰撞检测?


社区精选|【NestJS 系列】DI 依赖注入与 IOC 控制反转

浏览 324
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报