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];

看一下正則表達式中,有和沒有?的區別把:

先跳過 onScriptLoadonCallback,因為它們不是立即執行函數,所以不是立即執行的。

//如果引入了聚合板,內置的模塊則不必重復加載
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);
  }() );
}

array.slice(start, end) 點擊了解更多

接下來,才是真正的模塊加載邏輯。

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組件的目錄
})

參考文檔

Layui 源碼淺讀(模塊加載原理)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM