作為一個后端的工作者(以后可能要接觸前端框架的人)沒有接觸過前端框架,只對原生態的 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組件的目錄
})