layui.config().define().extend().use()源码解析
作为一个后端的工作者(以后可能要接触前端框架的人)没有接触过前端框架,只对原生态的 HTML/CSS/JavaScript 有所了解,那么 Layui 无非是较优的选择。
本文就是我在整合 SpringBoot 和 Layui 时,对 Layui 的源码产生了一些兴趣,所以特意分析一下。
我的Demo
首先是 blog.html
页面的代码:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Layui Demo</title>
</head>
<body>
<div class="slogan">
Hello World!
</div>
<footer>
<script type="text/javascript" src="/static/lib/jquery-3.6.0.min.js" charset="utf-8"></script>
<script type="text/javascript" src="/static/layui/layui.js" charset="utf-8"></script>
<script type="text/javascript">
layui.extend({
lib: '{/}/static/lib/layui-lib'
}).use('lib', function () {
const lib = layui.lib;
console.log("Refresh slogan")
lib.refreshSlogan();
});
</script>
</footer>
</body>
</html>
然后,是 /static/lib/layui-lib.js
脚本文件源码:
layui.define(['jquery'], function(exports) {
const $ = layui.jquery;
let lib = {
refreshSlogan : function() {
$('.slogan').text("艾欧尼亚昂扬不灭!")
}
};
exports('lib', lib);
});
前置知识
JavaScript 立即执行函数 点击了解更多
JS window对象详解 点击了解更多
JS document对象详解 点击了解更多
JavaScript prototype
layui.js源码分析
<script type="text/javascript" src="/static/layui/layui.js" charset="utf-8"></script>
这段代码,加载 layui.js 框架文件。
首先最外层的 ;!function(win){...}(window);
就是个(以运算符!
开头的)立即执行函数。
;
应该是为了防止其他的代码对layui.js
本身造成影响。
;!function(win) {
var doc = win.document, config = {
modules: {} //记录模块物理路径
,status: {} //记录模块加载状态
,timeout: 10 //符合规范的模块请求最长等待秒数
,event: {} //记录模块自定义事件
}
// 相当于Layui对象构造器:使用函数来构造对象
Layui = function(){
this.v = '2.6.8'; // layui 版本号
},
//内置模块
//作为当前function(win){}函数内的局部变量,将被用于初始化Layui.prototype.modules属性
,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' //颜色选择器
,slider: 'slider' //滑块
,carousel: 'carousel' //轮播
,flow: 'flow' //流加载
,util: 'util' //工具块
,code: 'code' //代码修饰器
,jquery: 'jquery' //DOM 库(第三方)
,all: 'all'
,'layui.all': 'layui.all' //聚合标识(功能性的,非真实模块)
};
//全局配置
Layui.prototype.config = function(options){
options = options || {};
for(var key in options){
config[key] = options[key];
}
return this;
};
//记录全部模块
//立即执行函数,初始化所有“内置模块”
//因此,Layui对象的可以通过modules属性可以访问到所有内置模块
Layui.prototype.modules = function(){
var clone = {};
for(var o in modules){
clone[o] = modules[o];
}
return clone;
}();
// window 是全局对象,layui 作为它的属性,也就拥有了全局作用域
//exports layui
win.layui = new Layui();
}(window);
代码从上到下依次执行,其中非立即函数,需要等待其他代码调用时才会执行。
layui.extend源码分析
本文示例中,调用
layui.extend({
lib: '{/}/static/lib/layui-lib'
})
因此,传入的 options 实际为
//拓展模块
Layui.prototype.extend = function(options){
//表示Layui对象
var that = this;
//验证模块是否被占用
options = options || {};
for(var o in options){ // 遍历对象 options,变量 o 为属性名
if(that[o] || that.modules[o]){
error(o+ ' Module already exists', 'error');
} else {
that.modules[o] = options[o];
}
}
return that;
};
Layui的原型属性modules,原本就初始化了所有内置模块,仙子调用 extend,则是注册扩展模块,modules
会因此而增加新的属性 lib:
extend代码量很少,功能也十分清晰
layui.use源码解析
layui.extend执行完成后,就要执行layui.use方法了:
layui.use('lib', function () {
// ES6语法const变量
const lib = layui.lib;
console.log("Refresh slogan")
lib.refreshSlogan();
});
首先看use函数定义:
// apps表示依赖模块(需要预加载的模块)
// callback表示预加载完成后的回调函数,通常也就是我们的业务函数
// exports
// from 表示调用来源,比如'define'表示layui.define函数需要加载依赖模块;再比如本例中就是undefined;
Layui.prototype.use = function(apps, callback, exports, from){}
然后use函数内,局部变量赋值:
var that = this
// 返回变量 getPath 的值,也就是当前layui.js所在的路径
,dir = config.dir = config.dir ? config.dir : getPath
// 从window.document获取head标签元素
,head = doc.getElementsByTagName('head')[0];
比如,我的 getPath="http://localhost:8080/static/layui/"
接着就是对use函数的第一个参数的“修正”:
apps = function(){
// 如果 apps 是字符串类型,则转换成只有一个元素的数组
if(typeof apps === 'string'){
return [apps];
}
//当第一个参数为 function 时,则自动加载所有内置模块,且执行的回调即为该 function 参数;
else if(typeof apps === 'function'){
callback = apps;
return ['all'];
}
// 如果 apps 已经是数组类型了,直接返回
return apps;
}();
接着避免jquery重复加载的代码:
//如果页面已经存在 jQuery 1.7+ 库且所定义的模块依赖 jQuery,则不加载内部 jquery 模块
if(win.jQuery && jQuery.fn.on){
// 这里自定义了
that.each(apps, function(index, item){
// index表示数组下标
// item表示数组元素
if(item === 'jquery'){
// array.splice(index,howmany,item1,.....,itemX)
// index 必需。规定从何处添加/删除元素。
// howmany 可选。规定应该删除多少元素。
// item1, ..., itemX 可选。要添加到数组的新元素
apps.splice(index, 1); // 表示删除数组中当前 'jquery' 这一个元素
}
});
layui.jquery = layui.$ = jQuery;
}
array.splice(index, howmany, item1,.....,itemX) 点击了解更多
插播:layui.each 源码解析BEGIN
layui.use 中用到了 layui.each 这个函数,那也只好提一嘴:
// 第一个参数可以是数组,可以是对象;因此这个遍历可能是遍历数组的元素,也可能是遍历对象的属性
// 第二个参数是回调函数
Layui.prototype.each = function(obj, fn){
var key
,that = this
,callFn = function(key, obj){ //回调
// 当 obj 是数组时,key 表示数组下标,obj[key] 表示指定下标的数组元素
// 当 obj 时对象时,key 表示对象属性,obj[key] 表示对象的属性值
// 需要注意的是fn.call为什么会有三个参数的问题?
// fn.call(obj, args1, args2, ...); //obj是指定函数赖以执行的对象, arg1等是传给函数的参数(假如有的话)
// 因此,第一个obj[key]在回调函数fn中对应this的值!
// call函数第二个参数 key,才是回调函数fn的第一个参数
// call函数第三个参数 obj[key]则表示回调函数fn的第二个参数
return fn.call(obj[key], key, obj[key])
};
if(typeof fn !== 'function') return that;
obj = obj || [];
//优先处理数组结构
if(that._isArray(obj)){
for(key = 0; key < obj.length; key++){
if(callFn(key, obj)) break;
}
} else {
// for(.. in ..) 可以遍历 obj 对象的属性
for(key in obj){ // key 表示属性名
if(callFn(key, obj)) break;
}
}
return that;
};
插播:layui.each 源码解析 END
紧接着,又是一段本地变量初始化的代码:
// 起初,我还担心是不是会“数组越界”,
// 实际上不会报错,只会返回undefined
// 因为是递归调用layui.use来加载所有模块的(稍后介绍),所以每次执行use时,都取数组的中第一个!
var item = apps[0]
,timeout = 0;
exports = exports || [];
//静态资源host
// 本例中dir=getPath="http://localhost:8080/static/layui/"
// [\s\S]+ 表示 匹配 一次或者多次 空白字符或者非空白字符
// [\s\S]+? 其中,? 总是尝试匹配尽可能少的字符
// \/\/([\s\S+?])\/ 就表示匹配 "//字符/" 的格式
config.host = config.host || (dir.match(/\/\/([\s\S]+?)\//)||['//'+ location.host +'/'])[0];
看一下正则表达式中,有和没有?的区别把:
先跳过 onScriptLoad 和 onCallback,因为它们不是立即执行函数,所以不是立即执行的。
//如果引入了聚合板,内置的模块则不必重复加载
if( apps.length === 0 || (layui['layui.all'] && modules[item]) ){
// 先调用onCallback() 函数,再返回 Layui 对象
return onCallback(), that;
}
既然调用了onCallback() 函数,所以就来分析一下源码:
//回调
function onCallback(){
exports.push(layui[item]);
apps.length > 1 ?
// 如果apps有2个及以上,递归调用use加载剩下的依赖模块
that.use(apps.slice(1), callback, exports, from)
// 没有更多模块需要预加载了,可以执行回调了
: ( typeof callback === 'function' && function(){ // 这是一个立即执行函数,封装了callback回调
//保证文档加载完毕再执行回调
if(layui.jquery && typeof layui.jquery === 'function' && from !== 'define'){
return layui.jquery(function(){
callback.apply(layui, exports);
});
}
// fn.apply(thisObj,[arg1,arg2,arg3])
// apply的第一个参数,表示callback函数内部的this。
// apply的第二个参数是数组,将多个参数组合成为一个数组传入
callback.apply(layui, exports);
}() );
}
接下来,才是真正的模块加载逻辑。
layui模块加载路径拼接规则
//获取加载的模块 URL
//如果是内置模块,则按照 dir 参数拼接模块路径
//如果是扩展模块,则判断模块路径值是否为 {/} 开头,
//如果路径值是 {/} 开头,则模块路径即为后面紧跟的字符。
//否则,则按照 base 参数拼接模块路径
var url = ( modules[item] ? (dir + 'modules/')
: (/^\{\/\}/.test(that.modules[item]) ? '' : (config.base || ''))
) + (that.modules[item] || item) + '.js';
url = url.replace(/^\{\/\}/, '');
记录模块的url路径源码:
//如果扩展模块(即:非内置模块)对象已经存在,则不必再加载
// layui[item] 存在的例子比如 layui['jquery'], layui['$']
// config.modules 记录模块物理路径
if(!config.modules[item] && layui[item]){
config.modules[item] = url; //并记录起该扩展模块的 url
}
layui加载模块原理
//首次加载模块
if(!config.modules[item]){
var node = doc.createElement('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); // 在 <head> 中加入一个 <script>,效果见下图
// 兼容浏览器,注册加载完成事件,回调自定义的onScriptLoad函数
if(node.attachEvent && !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) &&
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');
};
// config.status 记录模块加载状态
(typeof config.modules[item] === 'string' && config.status[item])
? onCallback()
: setTimeout(poll, 4);
}());
}
head.appendChild(node);
执行后的效果:
然后就是 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> 中把 <script> 移除
head.removeChild(node);
//
(function poll() {
if(++timeout > config.timeout * 1000 / 4){
return error(item + ' is not a valid module', 'error');
};
config.status[item] ? onCallback() : setTimeout(poll, 4);
}());
}
}
layui.define源码分析
//定义模块
//第一个参数deps,表示定义新扩展模块时,需要用到其他依赖模块
//第二个参数factory,是新定义的扩展模块业务函数
Layui.prototype.define = function(deps, factory){
var that = this
,type = typeof deps === 'function'
// 这个callback要等layui.use加载完某个模块才会回调
,callback = function(){
var setApp = function(app, exports){
layui[app] = exports;
//定义模块状态为已加载
config.status[app] = true;
};
typeof factory === 'function' && factory(function(app, exports){
//参数app表示模块名
//参数exports表示导出对象
setApp(app, exports);
//存储的作用:可以通过调用layui.factory(modName)重新执行模块工厂函数
config.callback[app] = function(){
factory(setApp);
}
});
return this;
};
//type为true,则用户是layui.define(function() {});这样的写法
//实际上没有设置依赖
type && (
factory = deps,
deps = []
);
//预加载扩展模块需要用到的模块
that.use(deps, callback, null, 'define');
return that;
};
layui.config源码解析
//全局配置
Layui.prototype.config = function(options){
options = options || {};
// 遍历options中属性,然后设置给config
for(var key in options){
config[key] = options[key];
}
return this;
};
比如,修改 config.base 可以定义组件模块的目录
layui.config({
base: '/assets/plugin/layui/modules/' //自定义layui组件的目录
})