Layui 原始碼淺讀(模組載入原理)

五歲能抬頭發表於2021-03-01

經典開場

// 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 擴充套件外掛就已經足夠日常使用了。

相關文章