一、layui.use 用法
layui.use() 函数用于模块加载
layui.use([mods], callback)
mods:如果填写(选填),必须是一个 layui 合法的模块名(不能包含目录)。
从 layui 2.6 开始,若 mods 不填,只传一个 callback 参数,则表示引用所有内置模块。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属性为当前脚本运行的地址。
这里需要了解的有
-
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属性值。如下图 -
遍历 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中,如果不在说明首次加载,否则进入非首次加载逻辑。
进入首次加载逻辑后,紧接着
-
创建 script 标签,并且设置 async 属性。
async 属性具有一下两个主要性质
- async 执行与文档顺序无关,先加载哪个就先执行哪个
- async 脚本加载完成后立即执行,可以在DOM尚未完全下载完成就加载和执行
-
设置 src 属性值,需要注意的是 src 中会添加版本号
-
挂载
最后监听 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); })() } }
复制
- 根据平台类型选择正则表达式
- 判断当前文件是否加载完毕
- 加载完毕后,会记录到 modules属性中,并把 script 标签从 head 中移出
- 最后轮询判断。 判断当前模块是否加载,如果没有加载继续轮询,直到超过指定超时时间限制,如果标识为已经加载则执行 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); }()); }
复制
- 首先会向 exports 数组中推入模块,里面的内容可以用于实例化
例如 lay 模块
这里的 lay 函数就是要向 exports 数组中推入的内容
复制var lay = function(selector){ return new LAY(selector); } if(window.layui && layui.define){ layui.define(function(exports){ //layui 加载 exports(MOD_NAME, lay); // 导出模块 }); } - 判断是否还有依赖模块,有的话循环调用相同逻辑,没有的话就使用 apply 函数给 callback 动态绑定 this 并且传入 exports 参数
非首次加载
非首次加载和首次加载中poll函数类似,这里不做赘述
如有问题,欢迎指出