zoukankan      html  css  js  c++  java
  • 【原】从一个bug浅谈YUI3组件的资源加载

    篇前声明:为了不涉及业务细节,篇内信息统一以某游戏,某功能代替

    前不久,某游戏准备内测客户端,开发人员测试过程中发现某功能突然不灵了,之前的测试一切ok,没有发现任何异常,第一反应是,游戏内浏览器都是自己包装的,是不是做了什么改造,触发了某个盲点。

    游戏方表示浏览器还是以前包装的Chromium,不过还真有不同的,就是UA改了,而且不是在原UA后加的后缀标识,而是完全替换,使用了游戏名称做UA,问题应该就在这里了,从代码上来看,不会触发任何雷区,理论上不会有问题,如果有问题,极有可能出现在框架中,YUI3的底层逻辑。

    顺便抱怨一下,游戏内测试本身就是一个蛋痛的事情,基本都是自己封装浏览器,即使使用同样的内核,也有可能包装方式不同引出不同的坑,下载了一个游戏,使用fiddler神器代理js测试,js又一直被缓存,游戏内也不知道如何清缓存,代理不到,只能转而代理html页面,js加随机数时间戳。

    第一步:YUI组件加载完毕打log

    YUI({combine: true}).use('node', 'io', 'xml', function (Y) {
        Y.log("init");
    });

    测试结果:没有log,验证了我的想法,影响YUI组件加载,框架层级的

    第二步:增减组件测试

    测试结果:刚开始毫无规律,确实有部分组件去掉以后可以正常加载,貌似有共同点,但又看不出明确的共同点,跟其他同事沟通,貌似这些组件加载了皮肤,单独加载其它自带皮肤的组件,果然有问题,而不带皮肤就ok

    第三步:YUI组件加载打log观察

    测试结果:果然,竟然… 只加载了皮肤,没有加载js

    这个时候才想起来看抓包… 发现组件真的没加载

    第四步:YUI组件加载代码分析

    先来看YUI加载过程

    111抓包

    从这个抓包图来看,分为四步,yui种子文件—》loader组件配置文件—》组件皮肤—》组件js代码

    1. 加载yui.js

    可以理解为种子文件,它将负责后续yui的模块加载,当执行YUI.use引用模块时,开始加载loader文件(如果只引用了种子文字,但没有写js去use方法,实际上不会加载模块的)

    YUI的_init方法对loader的配置,_init:

    config.base = YUI.config.base || Y.Env.getBase(/^(.*)yui/yui([.-].*)js(?.*)?$/,  /^(.*?)(.*&)(.*)yui/yui[.-].*js(?.*)?$/);
    config.loaderPath = YUI.config.loaderPath || 'loader/loader' + (filter || '-min.') + 'js';

    use方法代码片段,开始加载loader

    handleBoot = function() {
        Y._loading = false;
        queue.running = false;
        Env.bootstrapped = true;
        if (Y._attach(['loader'])) {
            Y.use.apply(Y, args);
        }
    };
    
    if (G_ENV._bootstrapping) {
        queue.add(handleBoot);
    } else {
        G_ENV._bootstrapping = true;
        var url = config.base + config.loaderPath;
        url += (url.indexOf("?") > -1 ? "&" : "?") + "v=" + Y.Env.CACHE_VERSION;
        Y.Get.script(url, {
            onEnd: handleBoot
        });
    }

    2. 加载loader.js

    yui3为按需加载,Loader相当于所有组件的一个配置文件(yui最新版本已将loader直接放入yui.js,减少一个请求),配置内容包括所有的模块,模块的依赖项,模块的皮肤等,如果模块有皮肤,会优先加载,模块有依赖模块,也需要同时加载

    我们来简单看一下loader的代码

    "console": {
        "requires": [
            "yui-log",
            "widget",
            "substitute"
        ],
        "skinnable": true
    }

    requires代表它依赖的其它模块,skinnable则代表它需要皮肤

    模块添加时有这么一句代码 ,如果有皮肤,则添加皮肤

    if (o.skinnable) {
        this._addSkin(this.skin.defaultSkin, i, name);
    }
    
    _addSkin: function(skin, mod, parent) {
        var mdef, pkg, name,
            info = this.moduleInfo,
            sinf = this.skin,
            ext  = info[mod] && info[mod].ext;
    
        // Add a module definition for the module-specific skin css
        if (mod) {
            name = this.formatSkin(skin, mod);
            if (!info[name]) {
                mdef = info[mod];
                pkg = mdef.pkg || mod;
                this.addModule({
                    name:  name,
                    group: mdef.group,
                    type:  'css',
                    after: sinf.after,
                    after_map: YArray.hash(sinf.after),
                    path:  (parent || pkg) + '/' + sinf.base + skin + '/' + mod + '.css',
                    ext:   ext
                });
            }
        }
        return name;
    }

    看一下addskin方法,会根据配置的基准路径寻找该皮肤文件,默认是该组件所在文件夹下面的assets/skins/sam下的同名css文件

    3. Loader加载成功

    重新回到yui.Js的user方法中来看,此时,该判断就会生效,便拿到了所有loader对象,获取到所有的组件加载项args

    if (boot && len && Y.Loader) {
        Y._loading = true;
        loader = getLoader(Y);
        loader.onEnd = handleLoader;
        loader.context = Y;
        // loader.attaching = args;
        loader.data = args;
        loader.require((fetchCSS) ? missing : args);
        loader.insert(null, (fetchCSS) ? null : 'js');
    }

    一路追踪到loader.js中的loadNext,我们使用了合并的方式,该方法会将同类资源文件合并加载(js or css),然后调用Y.Get.js or Y.Get. css进行资源文件的加载,由此再追踪到yui.js中的_next方法

    4. 组件加载

    此时,开始进行组件加载,并跟踪加载流程

    if (q.type === "script") {
        n = _scriptNode(url, w, q.attributes);
    } else {
        n = _linkNode(url, w, q.attributes);
    }
    // track this node's load progress
    _track(q.type, n, id, url, w, q.url.length);

    创建script or link节点

    // add it to the head or insert it before 'insertBefore'.  Work around IE
    // bug if there is a base tag.
    insertBefore = q.insertBefore || d.getElementsByTagName('base')[0];
    
    if (insertBefore) {
        s = _get(insertBefore, id);
        if (s) {
            s.parentNode.insertBefore(n, s);
        }
    } else {
        h.appendChild(n);
    }

    添加到页面上,这里皮肤css与js并不是完全的并行加载,对于webkit与gecko内核,直接开始加载下一个资源文件,即js

    if ((ua.webkit || ua.gecko) && q.type === "css") {
        _next(id, url);
    }

    我们看一下_track方法

    if (ua.ie) {
        n.onreadystatechange = function() {
            var rs = this.readyState;
            if ("loaded" === rs || "complete" === rs) {
                n.onreadystatechange = null;
                f(id, url);
            }
        };
    // webkit prior to 3.x is no longer supported
    } else if (ua.webkit) {
        if (type === "script") {
            // Safari 3.x supports the load event for script nodes (DOM2)
            n.addEventListener("load", function() {
                f(id, url);
            });
        }
    
    // FireFox and Opera support onload (but not DOM2 in FF) handlers for
    // script nodes.  Opera, but not FF, supports the onload event for link
    // nodes.
    } else {
        n.onload = function() {
            f(id, url);
        };
        n.onerror = function(e) {
            _fail(id, e + ": " + url);
        };
    }

    ie浏览器通过readystatechange进行判断并加载后续的资源文件,其它浏览器如webkit則监听onload(即使监听失败,_next方法中可直接执行js加载)

    我们加载的模块是console,但是只加载了css,js不见了,如下图,我不是webkit,我不是gecko,我也不是IE,然后就进入了死胡同

    222

    333

    由于UA变更,没有将其判断为webkit,_next方法没有执行,加载完css后无法直接加载js,故只能依靠track方法,track方法判断其不属于IE浏览器,直接进入了else分支,但由于浏览器本身是Chromium内核,link节点的onload方法无法触发。进入了死万劫不复之地。

    解决方法也比较简单,对于非css请求,依赖_trace方法触发回调其它js加载(js加载需要严格时序保证),而css请求,不依赖_trace,直接调用_next触发其它资源加载

    该方法改动较小,风险较小,即使后续有新的UA变更或浏览器更新等,仍可以保证正常使用,缺点在于,如果加载过程中发生网络波动等异常,有可能出现样式闪动的问题。

  • 相关阅读:
    Node_JS
    读JS高级——第五章-引用类型 _记录
    读JS高级(兼容&&BOM&&私有变量&&面向对象)
    JS高级设计第七章——复习知识点
    nodeJs抓取网页
    表单脚本api_contenteditable
    泛——复习js高级第三版
    nodeJS
    Eclipse布局问题小记
    再议负载均衡算法
  • 原文地址:https://www.cnblogs.com/v-rockyli/p/3639930.html
Copyright © 2011-2022 走看看