一、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函数类似,这里不做赘述
如有问题,欢迎指出