首页 前端知识 Layui源码解读之use函数(模块加载)

Layui源码解读之use函数(模块加载)

2024-05-19 09:05:10 前端知识 前端哥 37 406 我要收藏

一、layui.use 用法

layui.use() 函数用于模块加载

layui.use([mods], callback)

  1. mods:如果填写(选填),必须是一个 layui 合法的模块名(不能包含目录)。
    从 layui 2.6 开始,若 mods 不填,只传一个 callback 参数,则表示引用所有内置模块。

  2. callback:即为模块加载完毕的回调函数。
    从 layui 2.6 开始,该回调会在 html 文档加载完毕后再执行,确保你的代码在任何地方都能对元素进行操作。

例子: 加载指定模块

layui.use(['layer', 'laydate'], function(){
  var layer = layui.layer
  ,laydate = layui.laydate;

  //do something
});

例子: 引用所有模块(layui 2.6 开始支持)

layui.use(function(){
  var layer = layui.layer
  ,laydate = layui.laydate
  ,table = layui.table;

  //do something
});

二、layui.use 源码

// 使用特定模块
Layui.prototype.use = function(apps, callback, exports, from) {
    var that = this;
    var dir = config.dir = config.dir ? config.dir : getPath; // 获取路径
    var head = doc.getElementsByTagName('head')[0]; // 获取第一个header

    // 对传入的 apps 参数进行处理
    apps = function(){

        // 传入字符串时, 应转为数组 layui.use('form',...)
        if(typeof apps === 'string'){
            return [apps];
        }

        // 第一个参数为 function 时, 则自动加载所有内置模块,且执行的回调即为该 function 参数
        else if (typeof apps === 'function') {
            callback = apps;
            return ['app'];
        }
        return apps;
    }(); // 立即执行

    // 如果页面已经存在 jQuery 1.7+ 库且所定义的模块依赖 jQuery,则不加载内部 jquery 模块
    if(win.jQuery && jQuery.fn.on) {
        that.each(apps, function(index, item){

            // 找到内部 jquery, 并删除
            if(item === 'jquery'){
                apps.splice(index, 1);
            }
        });
        layui.jquery = layui.$ = jQuery; // layui 为实例对象
    }

    var item = apps[0]; // 获取 apps 数组第一位元素
    var timeout = 0; // 初始化超时时间为 0
    exports = exports || [];

    // 获取静态资源host
    config.host = config.host || (dir.match(/\/\/([\s\S]+?)\//)||['//'+ location.host +'/'])[0]

    // 加载完毕
    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); // 从 head 中移除 node
            (function poll(){

                // 判断 timeout > 2500 ?
                if(++timeout > config.timeout * 1000 / 4) {

                    // 超时报错
                    return error(item + ' is not a valid module', 'error'); // 记得 return, 停止执行
                }

                // 判断当前模块状态是否为 true ,为 true 执行 onCallback, 否则轮询
                config.status[item] ? onCallback() : setTimeout(poll, 4);
            })()
        }
    }

    // 回调函数
    function onCallback(){

        // 向 exports 中推入模块
        exports.push(layui[item]); // layui 为实例对象中除了 v 属性标识版本号, 其余全为模块

        apps.length > 1
        ? that.use(apps.slice(1), callback, exports, from)
        : ( typeof callback === 'function' && function(){

            // 保证文档加载完毕再执行调用
            if(layui.jquery && typeof layui.jquery === 'function' && from !== 'define' ) {
                return layui.jquery(function (){
                    callback.apply(layui, exports);
                });
            }
            callback.apply(layui, exports);
        }());
    }

    // 如果引入了聚合板,内置的模块则不必重复加载
    if(apps.length === 0 || (layui['layui.all'] && modules[item])){
        return onCallback(), that;
    }

    // 获取加载的模块 URL
    // 如果是内置模块, 则按照 dir 参数拼接模块路径
    // 如果是扩展模块, 则判断模块路径值是否以 {/} 开头,
    // 如果路径值是 {/} 开头, 则模块路径即为后面紧跟的字符。
    // 否则, 则按照 base 参数拼接模块路径
    var url = (modules[item] ? (dir + 'modules/')
        : (/^\{\/\}/.test(that.modules[item]) ? '' : (config.base || ''))
    ) + (that.modules[item] || item) + '.js';

    url = url.replace(/^\{\/\}/, '');

    // 如果扩展模块(即:非内置模块)对象已经存在,则不必再加载
    if(!config.modules[item] && layui[item]){
        config.modules[item] = url; // 并记录起该扩展模块的 url
    }

    // 首次加载模块
    if(!config.modules[item]){
        var node = doc.createElement('script'); // 创建script

        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); // 挂载节点

        // 对 IE 添加监听
        if(node.attachEvent && !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) && !isOpera){
            node.attachEvent('onreadystatechange', function(e){
                onScriptLoad(e, url);
            });
        } else {
            node.addEventListener('load', function(e){
                onScriptLoad(e, url);
            }, false)
        }

        config.modules[item] = url;
    } else { // 非首次加载
        (function poll(){
            if(++timeout > config.timeout * 1000 / 4) {
                return error(item + ' is not a valid module', 'error');
            };

            // 已加载到模块中
            (typeof config.modules[item] === 'string' && config.status[item])
            ? onCallback()
            : setTimeout(poll, 4);
        }()); // 轮询 必须是立即执行函数
    }

    return that;
}

三、layui.use 关键代码分析

var dir = config.dir = config.dir ? config.dir : getPath; // 获取路径

获取 layui 所在的目录,用于以后加载相关资源。获取完路径之后存储到 config 全局变量和局部变量 dir 中。

layui 是如何知道它的路径的呐?这里就要说一下 layui 获取路径函数 getPath(立即执行函数)

var getPath = function () {

    // 当前doc中如果有 currentScript 属性, 就返回 src
    // currentScript 更多内容查看 https://developer.mozilla.org/zh-CN/docs/Web/API/Document/currentScript
    var jsPath =  doc.currentScript
    ? doc.currentScript.src
    : function() {
        var js = doc.scripts; // 获取当前页面所有 script 标签
        var last = js.length - 1;
        var src;

        // 倒叙遍历(有利于性能)
        for(var i = last; i > 0 ;i--){

            // 当前 script 处于可交互状态时,赋值 src
            // 更多 readyState 内容查看 https://developer.mozilla.org/zh-CN/docs/Web/API/Document/readyState
            if(js[i].readyState === 'interactive'){
                src = js[i].src;
                break;
            }
        }
        return src || js[last].src; // 默认返回 src, 如果 src 不存在, 则返回数组中最后一个 script 的 src 值
    }();

    // jsPath 是形如'http://127.0.0.1:5500/src/layui.cpy.js'这样的值, 不符合需要,所以进行处理
    return config.dir = GLOBAL.dir || jsPath.substring(0, jsPath.lastIndexOf('/') + 1); 
}(); // 立即执行函数

getPath 是一个立即执行函数。首先会判断当前环境支不支持 doc.currentScript 如果支持 doc.currentScript,直接从 doc.currentScript.src 获取当前脚本运行的地址。如果不支持 doc.currentScript,则遍历文档所有的<script>标签,判断哪个<script>标签的readyState为’interactive’,则说明此标签的src属性为当前脚本运行的地址。

这里需要了解的有

  1. doc.currentScript 属性返回当前正在运行的脚本所属的 <script> 元素

    例如

    html文件

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
    <script src="./getPath.js"></script>
    </body>
    </html>
    

    getPath.js文件

    console.log(document.currentScript);
    

    请添加图片描述

     可以看出返回的是script标签元素,当前环境支持时,可以通过document.currentScript.src获取当前运行脚本所属的src属性值。如下图
    

    请添加图片描述

  2. 遍历 script 标签时,readyState 属性。

    Document.readyState 属性描述了document 的加载状态。

    当该属性值发生变化时,会在 document 对象上触发 readystatechange 事件。

    readyState 属性取一下值

    • loading(document正在加载)
    • interactive(可交互。文档已被解析,"正在加载"状态结束,但是诸如图像,样式表和框架之类的子资源仍在加载。)
    • complete(完成。文档和所有子资源已完成加载。表示 load 状态的事件即将被触发。)

var head = doc.getElementsByTagName(‘head’)[0];

获取 html 中 head dom 元素,用于后面向 head 中挂载元素使用。

例如

head.appendChild(node); // 挂载节点

apps 立即执行函数

// 对传入的 apps 参数进行处理
apps = function(){

    // 传入字符串时, 应转为数组 layui.use('form',...)
    if(typeof apps === 'string'){
        return [apps];
    }

    // 第一个参数为 function 时, 则自动加载所有内置模块,且执行的回调即为该 function 参数
    else if (typeof apps === 'function') {
        callback = apps;
        return ['app'];
    }
    return apps;
}(); // 立即执行

立即执行函数中做的事情是,把 string 和 function 类型的参数转为字符串数组形式并返回给 apps ,数组中是layui要加载的模块。需要注意的是当 apps 为函数类型时,layui 会引用所有内置模块并且 apps 函数直接赋值给 callback 用于以后逻辑处理。

jquery 处理逻辑

if(win.jQuery && jQuery.fn.on) {
    that.each(apps, function(index, item){

        // 找到内部 jquery, 并删除
        if(item === 'jquery'){
            apps.splice(index, 1);
        }
    });
    layui.jquery = layui.$ = jQuery; // layui 为实例对象
}

会先判断当前环境中是否已经引入 jquery,如果引入就遍历要加载的模块(apps),找到要加载的模块名为 jquery 的,然后删除。如果已经删除或者没找到 jquery (说明没有引入 jquery 模块)就直接把当前环境中存在的 jquery 赋值给 layui

config.host = config.host || (dir.match(///([\s\S]+?)//)||[‘//’+ location.host +‘/’])[0]

获取 host,这里的 host 和 url 中通过 host取得的值不一样。这里前面加了’//‘后面加了’/'。
所以最终的形式如下

// //127.0.0.1:5500/ 或者 //域名:4097/

聚合板 这个功能不是很常用到,这里直接上代码,不多解释(代码也比较简单)

var modules = config.builtin = {
    lay: 'lay', // 基础 DOM 操作
    layer: 'layer', // 弹层
    laydate: 'laydate', // 日期
    laypage: 'laypage', // 分页
    laytpl: 'laytpl', // 模板引擎
    layedit: 'layedit', // 富文本编辑器
    form: 'form', // 表单集
    upload: 'upload', // 上传
    dropdown: 'dropdown', // 下拉菜单
    transfer: 'transfer', // 穿梭框
    tree: 'tree', // 树结构
    table: 'table', // 表格
    element: 'element', // 常用元素操作
    rate: 'rate', // 评分组件
    colorpicker: 'colorpicker', // 颜色选择器
    silder: 'slider', // 滑块
    carousel: 'carousel', // 轮播
    flow: 'flow', // 流加载
    util: 'util', // 工具块
    code: 'code', // 代码修饰器
    jquery: 'jquery', // DOM 库
    all: 'all',
    'layui.all': 'layui.all' // 聚合标识
}

var item = apps[0]; // 获取 apps 数组第一位元素

if(apps.length === 0 || (layui['layui.all'] && modules[item])){
    return onCallback(), that; // onCallback 函数后续会详细说明
}

拼接 url

var url = (modules[item] ? (dir + 'modules/')
        : (/^\{\/\}/.test(that.modules[item]) ? '' : (config.base || ''))
    ) + (that.modules[item] || item) + '.js';

    url = url.replace(/^\{\/\}/, '');

获取加载的模块 URL。如果是内置模块, 则按照 dir 参数拼接模块路径, 否则如果是扩展模块, 则判断模块路径值是否以 {/} 开头, 如果路径值是 {/} 开头, 则模块路径即为后面紧跟的字符。否则, 则按照 base 参数拼接模块路径。

这里举例拓展模块的写法,这样就知道为何需要写’/^{/}/.test(that.modules[item]'这样的正则表达式。最后拼接完 url 后,把 {/} 去除,就得到了后续发送请求的 url。

//config的设置是全局的
layui.config({
  base: '/res/js/' //假设这是你存放拓展模块的根目录
}).extend({ //设定模块别名
  mymod: 'mymod' //如果 mymod.js 是在根目录,也可以不用设定别名
  ,mod1: 'admin/mod1' //相对于上述 base 目录的子目录
});

//你也可以忽略 base 设定的根目录,直接在 extend 指定路径(主要:该功能为 layui 2.2.0 新增)
layui.extend({
  mod2: '{/}http://cdn.xxx.com/lib/mod2' // {/}的意思即代表采用自有路径,即不跟随 base 路径
})

config.modules

config.modules 中用于存放模块物理路径

首次加载模块

if(!config.modules[item]){
    var node = doc.createElement('script'); // 创建script

    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); // 挂载节点

    // 对 IE 添加监听
    if(node.attachEvent && !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) && !isOpera){
        node.attachEvent('onreadystatechange', function(e){
            onScriptLoad(e, url);
        });
    } else {
        node.addEventListener('load', function(e){
            onScriptLoad(e, url);
        }, false)
    }

    config.modules[item] = url;
}

会判断当前模块的物理路径是否已经在modules中,如果不在说明首次加载,否则进入非首次加载逻辑。
进入首次加载逻辑后,紧接着

  1. 创建 script 标签,并且设置 async 属性。

    async 属性具有一下两个主要性质

    • async 执行与文档顺序无关,先加载哪个就先执行哪个
    • async 脚本加载完成后立即执行,可以在DOM尚未完全下载完成就加载和执行
  2. 设置 src 属性值,需要注意的是 src 中会添加版本号

  3. 挂载

最后监听 load 或者 onreadystatechange 事件,当加载完毕后执行 onScriptLoad 函数。

onScriptLoad 函数

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); // 从 head 中移除 node
        (function poll(){

            // 判断 timeout > 2500 ?
            if(++timeout > config.timeout * 1000 / 4) {

                // 超时报错
                return error(item + ' is not a valid module', 'error'); // 记得 return, 停止执行
            }

            // 判断当前模块状态是否为 true ,为 true 执行 onCallback, 否则轮询
            config.status[item] ? onCallback() : setTimeout(poll, 4);
        })()
    }
}
  1. 根据平台类型选择正则表达式
  2. 判断当前文件是否加载完毕
  3. 加载完毕后,会记录到 modules属性中,并把 script 标签从 head 中移出
  4. 最后轮询判断。 判断当前模块是否加载,如果没有加载继续轮询,直到超过指定超时时间限制,如果标识为已经加载则执行 onCallback 函数

config.status[item] 什么时候变为 true ?

请求到模块后,在模块内会调用 layui.define 函数,在该函数内的 setApp 函数中改变状态为 true

例如 lay 模块

if(window.layui && layui.define){
    layui.define(function(exports){ //layui 加载
    exports(MOD_NAME, lay); // 导出模块
    });
}

onCallback 函数

function onCallback(){

    // 向 exports 中推入模块
    exports.push(layui[item]); // layui 为实例对象中除了 v 属性标识版本号, 其余全为模块

    apps.length > 1 
    ? that.use(apps.slice(1), callback, exports, from)
    : ( typeof callback === 'function' && function(){

        // 保证文档加载完毕再执行调用
        if(layui.jquery && typeof layui.jquery === 'function' && from !== 'define' ) {
            return layui.jquery(function (){
                callback.apply(layui, exports);
            });
        }
        callback.apply(layui, exports);
    }());
}
  1. 首先会向 exports 数组中推入模块,里面的内容可以用于实例化
    例如 lay 模块
    var lay = function(selector){
      return new LAY(selector);
    }
    
    if(window.layui && layui.define){
      layui.define(function(exports){ //layui 加载
        exports(MOD_NAME, lay); // 导出模块
      });
    }
    
    这里的 lay 函数就是要向 exports 数组中推入的内容
  2. 判断是否还有依赖模块,有的话循环调用相同逻辑,没有的话就使用 apply 函数给 callback 动态绑定 this 并且传入 exports 参数

非首次加载

非首次加载和首次加载中poll函数类似,这里不做赘述

如有问题,欢迎指出

转载请注明出处或者链接地址:https://www.qianduange.cn//article/8889.html
评论
会员中心 联系我 留言建议 回顶部
复制成功!