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 源码浅读(模块加载原理)

posted @ 2022-04-19 11:02  极客子羽  阅读(1817)  评论(0编辑  收藏  举报