Layui 源码浅读(模块加载原理)

前情提要

文章部分内容有修改,部分压缩代码替换成未压缩代码,由于版本问题部分变量名为猜测,但不影响阅读。
纠正之前的错误猜测,if 括号内多个非布偶语句和 return 后多个赋值语句应该是压缩后所导致的简写格式。
正常开发情况下,尽量把代码的可读性放在第一,性能问题就交给压缩工具吧。

经典开场

// Layui
;! function (win) {
    var Lay = function () {
        this.v = '2.5.5';
    };
    win.layui = new Lay();
}(window);
// Jquery
(function (global, factory) {
    "use strict";
    if (typeof module === "object" && typeof module.exports === "object") {
        module.exports = global.document ?
            factory(global, true) :
            function (w) {
                if (!w.document) {
                    throw new Error("jQuery requires a window with a document");
                }
                return factory(w);
            };
    } else {
        factory(global);
    }
})(typeof window !== "undefined" ? window : this, function (window, noGlobal) {
    var jQuery = function (selector, context) {
        return new jQuery.fn.init(selector, context);
    };
    return jQuery;
});

这是一种很经典的开场方式,以 ! 定义一个函数并立即执行,并且将对象赋值到全局 window 变量上。当然除了 ! 还有 ~ 等符号都可以定义后面的这个函数,而 ; 应该是为了防止其他的代码对本身造成影响。

实际上( function (window) { "use strict"; } )( window )的写法更被我们理解,如Jquery未压缩的源码。而!定义函数的方法唯一优势就是代码相对较少,所以压缩后的Js代码大多数会以!开头。

动态加载

Lay.prototype.link = function (href, fn, cssname) {
    var that = this,
        link = doc.createElement('link'),
        head = doc.getElementsByTagName('head')[0];
    if (typeof fn === 'string')
        cssname = fn;
    var app = (cssname || href).replace(/\.|\//g, '');
    var id = link.id = 'layuicss-' + app,
        timeout = 0;
    link.rel = 'stylesheet';
    link.href = href + (config.debug ? '?v=' + new Date().getTime() : '');
    link.media = 'all';
    if (!doc.getElementById(id)) {
        head.appendChild(link);
    }
    if (typeof fn != 'function') return that;
    (function poll() {
        if (++timeout > config.timeout * 1000 / 100) {
            return error(href + ' timeout');
        };
        if (parseInt(that.getStyle(doc.getElementById(id), 'width')) === 1989) {
            fn();
        } else {
            setTimeout(poll, 100);
        }
    }());
    return that;
}

先来看看官方文档:

方法:layui.link(href)
href 即为 css 路径。注意:该方法并非是你使用 layui 所必须的,它一般只是用于动态加载你的外部 CSS 文件。

虽然官方只给出了一个参数,但是我们看源码的话可以知道后两个参数是加载完后运行的函数和自定义的Id。
有趣的是,临时创建的 poll函数 如果parseInt(that.getStyle(doc.getElementById(id), 'width')) === 1989判断为 false ,也就是样式没有被引入的时候会重新调用 poll函数 最后要么加载成功循环结束,要么加载超时调用 Layui hint 打印出超时信息。
因为同样的手段在加载 module 时也同样使用到,所以如果你使用过 Layui 那么[module] is not a valid module这样的警告或多或少能遇到几次。

模块引入

用过 Layui 的兄dei应该对 layui.use 不陌生,先来看官方文档:

方法:layui.use([mods], callback)
layui 的内置模块并非默认就加载的,他必须在你执行该方法后才会加载。

对于用了 Layui 有段时间的我来说,也只是按照官方的例子使用,并不知道实现的原理。
接下来就是见证遗迹的时候,看看 layui.use 做了什么:

Lay.fn.use = function (apps, callback, exports) {
    function onScriptLoad(e, url) {
        var readyRegExp = navigator.platform === 'PLaySTATION 3' ? /^complete$/ : /^(complete|loaded)$/;
        if (e.type === 'load' || (readyRegExp.test((e.currentTarget || e.srcElement).readyState))) {
            config.modules[item] = url;
            head.removeChild(node);
            (function poll() {
                if (++timeout > config.timeout * 1000 / 4) {
                    return error(item + ' is not a valid module');
                };
                config.status[item] ? onCallback() : setTimeout(poll, 4);
            }());
        }
    }
    function onCallback() {
        exports.push(layui[item]);
        apps.length > 1 ? that.use(apps.slice(1), callback, exports) : (typeof callback === 'function' && callback.apply(layui, exports));
    }
    var that = this,
        dir = config.dir = config.dir ? config.dir : getPath;
    var head = doc.getElementsByTagName('head')[0];
    apps = typeof apps === 'string' ? [apps] : apps;
    if (window.jQuery && jQuery.fn.on) {
        that.each(apps, function (index, item) {
            if (item === 'jquery') {
                apps.splice(index, 1);
            }
        });
        layui.jquery = layui.$ = jQuery;
    }
    var item = apps[0],
        timeout = 0;
    exports = exports || [];
    config.host = config.host || (dir.match(/\/\/([\s\S]+?)\//) || ['//' + location.host + '/'])[0];
    if (apps.length === 0 || (layui['layui.all'] && modules[item]) || (!layui['layui.all'] && layui['layui.mobile'] && modules[item])) {
        return onCallback(), that;
    }
    if (config.modules[item]) {
        (function poll() {
            if (++timeout > config.timeout * 1000 / 4) {
                return error(item + ' is not a valid module');
            };
            if (typeof config.modules[item] === 'string' && config.status[item]) {
                onCallback();
            } else {
                setTimeout(poll, 4);
            }
        }());
    } else {
        var node = doc.createElement('script'),
            url = (modules[item] ? dir + 'lay/' : /^\{\/\}/.test(that.modules[item]) ? '' : config.base || '') + (that.modules[item] || item) + '.js';
        node.async = true;
        node.charset = 'utf-8';
        node.src = url + function () {
            var version = config.version === true ? config.v || (new Date()).getTime() : config.version || '';
            return version ? '?v=' + version : '';
        }();
        head.appendChild(node);
        if (!node.attachEvent || (node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code]') < 0) || isOpera) {
            node.addEventListener('load', function () {
                onScriptLoad(e, url);
            }, false);
        } else {
            node.addEventListener('onreadystatechange', function (e) {
                onScriptLoad(e, url);
            });
        }
        config.modules[item] = url;
    }
    return that;
};

首先跳过前两个创建的函数,经过一堆巴拉巴拉的赋值后来到第2个if中我们直接可以判断语句apps.length === 0,根据文档可知我们第一个参数是一个数组 [mods] ,当然前面的赋值apps = typeof apps === 'string' ? [apps] : apps;可以看出即使你传的是一个字符串也会被封装成数组。

很明显第一次进来apps.length === 0和下面的if ( config.modules[item] ) 也必为 false ,那么我们直接移步到 else 内。

创建一个 script 元素并赋予属性和模块的地址,通过 appendChild 追加到 head 之后留下一个 addEventListener 监听 script 的加载( ps:attachEvent 是给非人类使用的浏览器准备的 )并将开始创建的function onScriptLoad(e, url)函数抛进去,然后整段代码除了return that到这里戛然而止。

再来看看function onScriptLoad(e, url)函数,首先开幕雷击"PLaySTATION 3" === navigator.platform

Layui 的业务已经发展到PS3上了吗?

仅关心PC端浏览器的部分e.type === 'load', 因为监听的是 load 所以这里必为 true 并执行config.modules[item] = url后将追加的 script 元素移除。剩余的代码就是动态加载时使用的技巧,直到config.status[item]true 时循环结束。

定义模块

由于config.status[item]不会自动变成 true,之后的骚操作由 layui.define 接手。

先看官方文档:

方法:layui.define([mods], callback)

通过该方法可定义一个 layui 模块。参数 mods 是可选的,用于声明该模块所依赖的模块。callback 即为模块加载完毕的回调函数,它返回一个 exports 参数,用于输出该模块的接口。

以比较常用的 laypage.js 模块为例,基础源码如下:

// Laypage 模块的部分代码(部分变量名为猜测,但不影响内容本身)
layui.define(function (exports) {
    'use strict';
    var MOD_NAME = 'laypage',
        LayPage = function (options) {
            var that = this;
            that.config = options || {}, that.config.index = ++laypage.index, that.render(true);
        };
    var laypage = {
        render: function (options) {
            var laypage = new LayPage(options);
            return laypage.index
        },
        index: layui.laypage ? layui.laypage.index + 10000 : 0,
        on: function (elem, even, fn) {
            return elem.attachEvent ? elem.attachEvent("on" + even, function (param) {
                param.target = param.srcElement, fn.call(elem, param)
            }) : elem.addEventListener(even, fn, false), this
        }
    };
    exports(MOD_NAME, laypage);
});

因为 Layui 已经注册了全局的变量,所以当模块文件通过元素追加的方式引入时,调用了 layui.define 方法:

Lay.fn.define = function (deps, callback) {
    var that = this,
        type = typeof deps === 'function',
        mods = function () {
            var e = function (app, exports) {
                layui[app] = exports;
                config.status[app] = true;
            }
            typeof callback === 'function' && callback(function (app, exports) {
                e(app, exports);
                config.callback[app] = function () {
                    callback(e);
                }
            });
            return this;
        };
    type && (callback = deps, deps = []);
    if (!layui['layui.all'] && layui['layui.mobile']) {
        return mods.call(that);
    } else {
        that.use(deps, mods);
        return that;
    }
};

因为不管你在定义的模块中有没有引入其他模块,如 laypage 和 laytpl 这些 Layui 本身提供的模块都会因 (callback = deps, deps = []) 回到 [mods], callback 的参数格式。

再经过一系列巴拉巴拉的步骤回到定义的 mods 方法中,由layui[app] = exports, config.status[app] = true给全局 layui 变量添加属性(app)且给属性赋值(exports),并把 status 改为 true 至此模块加载完成。

总结

正如 Layui 官方所说:我们认为,这恰是符合当下国内绝大多数程序员从旧时代过渡到未来新标准的最佳指引

作为一个后端的工作者(以后可能要接触前端框架的人)没有接触过前端框架,只对原生态的 HTML / CSS / JavaScript 有所了解,那么 Layui 无非是较优的选择。

而写这篇文章无非就是为了感谢 Layui 对非前端工作者做出的贡献,也可能是我对使用了两年多 Layui 最后的告别吧,感谢贤心。

相关网站

其他

如果你没有接触过 UglifyJS 或其他 JS 压缩器,而你又恰巧使用 Visual Studio Code 工具开发,那么 Minify 扩展插件就已经足够日常使用了。

事件

今天(2021-10-13),Layui 闭站!!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容