webman admin 中的 Layui 使用说明

Layui 背景

Layui 是一个上古框架,jQuery 时代末期 Vue 初期的作品,早期因为 layer.js 弹出层起家,在作者的努力下,成体系的开发了一套常用 web 应用组件
现在这个框架已经不推荐新项目入坑了,除非你负责项目的全栈,需要权衡开发效率(现在依然有很多 MMR 上万刀的独立开发者在使用 jQuery 开发应用)
此记录主要为了配合 webman-admin 使用,提供一个快速查询手册的目录,万一哪天我忘记了回来就直接看这里

生命周期

框架运行非常简单,浏览器载入就执行,没有复杂的 SPA 组件生命周期
web 生命周期:浏览器载入页面 -> html 结构持续加载标签 -> 加载到 script 标签就开始执行(通常会将 script 标签置于 html 文件的结尾)
Layui 在这里被配置为当 html 结构加载完毕后执行,类似于 jQuery 当中的

$(document).ready(function() {
	// your code here
});

就算你将 script 标签置于 html 文档开头,也会等待所有 DOM 加载完毕

底层方法

通用 Common

名称 接口 说明 链接
全局配置 layui.config(options: {base: string, version: bool, dir: string, debug: bool} = {}) 在 Layui 模块使用之前,采用该方法进行一些全局化的基础配置 link
链接解析 var url = layui.url(href: string = location.href); 该方法用于将一段 URL 链接中的 pathname、search、hash 等属性进行对象化处理。
如果只是解析 URL 的话不推荐用这个,拿来开发单页应用可使用,因为新的浏览器已经有原生标准 API 支持这项功能,可以查看 MDN 的链接: URL
本地存储 layui.data(key: string, settings: {[key:string]: any})
layui.sessionData(key: string, settings: {[key:string]: any})
data 表示 localStoragesessionData 等于 sessionStorage,两者用法一致
这个封装就是将对象 settings 和原生简单的 JSON.parse, JSON.stringify 了一下,可用可不用
浏览器信息 var device = layui.device(); 可利用该方法对不同的设备进行差异化处理,判断浏览器携带的设备信息方法有很多,可用可不用

模块化相关 Modular

模块化系统,提高项目维护性

名称 接口 说明 链接
定义模块 layui.define([modules], callback) 一般作为单文件组件定义 link
使用模块 layui.use([modules], callback) 可以这样读这个方法:使用 modules 来完成 callback 中的功能 link
扩展模块 layui.extend(obj) link
弃用模块 layui.disuse([modules]) v2.7+
获取回调 layui.factory(modName) 获取 layui.define([], callback) 中的参数 callback

事件 Event

名称 接口 说明 链接
阻止事件冒泡 layui.stope(e) js 原生 event.stopPropagation()
增加自定义模块事件 layui.onevent(modName, events, callback) 一般在内置组件中使用,类似 jQuery 中的 $.on()
执行自定义模块事件 layui.event(modName, events, params) 搭配 onevent 使用,类似 jQuery 中的 $.trigger()
事件注销 layui.off(events, modName) 注销模块相关事件,类似 jQuery 中的 $.off()
防抖包装 layui.debounce(fn, wait) fn 按指定毫秒 wait 延时执行
节流包装 layui.throttle(fn, wait) 限制 fn 在指定毫秒 wait 内不重复执行

模块系统入门

Layui 模块系统与 vue 模块的区别

vue 当前通常作为单文件组件定义,并且使用的是标准的 ES5 模块系统,具有把 htmljscss 合为一个组件的组件系统。
Layui 更为简单,仅仅只是封装 js 模块,它与 htmlcss 是离散的,内聚性较弱,可在不同的 html 文档当中随意引用 js 模块,在你引用了 Layui 模块的地方再单独加载 css 与编写 html 标签。

// 定义模块(通常单独作为一个 JS 文件)
layui.define([mods], function(exports){
  exports('mod1', api);
});
// 使用模块
layui.use(['mod1'], function(args){
  var mod1 = layui.mod1;
});
// 嵌套式使用模块
layui.use(['mod1'], function(args){
  var mod1 = layui.mod1;
  layui.use(['mod2'], function(args2) {
	  var mod2 = layui.mod2;
  })
});

理解 layui.use

layui.use 的使用方法在文档中有介绍link
简而言之这个的用法就是为了控制模块加载,不需要你每次都手动在 html 文件中引入 script 标签,Layui 处理了你重复引入问题以及异步引入问题
核心在于执行 callback(加载 apps 参数内的模块只是为了服务 callback)
其实你可以不用深究下面的代码解释 layui.use 到底干什么的,你只需要把 callback 的业务流当作以前的 jQuery 使用就行

// 你只需要把 callback 的业务流当作以前的 jQuery 的
$(document).ready(function() {

})

// or

$(function() {

})

另外,不管嵌套了多少层 use,其实都可以视为没有特别的影响,无非是为模块引入了作用域概念

// 嵌套式使用模块
layui.use(['mod1'], function(args){
  // mod1 作用域
  var mod1 = layui.mod1;
  layui.use(['mod2'], function(args2) {
      // mod1, mod2 作用域
	  var mod2 = layui.mod2;
  })
});

代码解释

source code
注意:我去掉了作者的注释,加上了我自己的注释,你可以打开上述链接对照理解

  /**
   * 源码,在 Layui 原型链上直接定义了 use 函数
   * @apps string | array<string> 表示需要加载的模块
   * @callback function 模块加载完毕后执行的函数
   * @exports
   * @from 
   */
  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];

	// 1) apps 值调整
    apps = function(){
      // 如果 apps 是 string 则包装为 [string],这里是为了统一将 apps 参数设置为数组类型
      if(typeof apps === 'string'){
        return [apps];
      }
      // apps 如果是 function,则重载 use 函数参数为 function(callback, exports, from),这是一种在 js 中的函数重载实现方法
      else if(typeof apps === 'function'){
        callback = apps;
        return ['all'];
      }
      /**
       * ! 这种写法不好,不要学,好的办法是接一个 `else { return apps }`,这样更清晰
       * 这里把 apps 理解为是一个数组加载模块形式,比如 layui.use([jquery, mod1, mod2], ...)
       */
      return apps;
    }();

	// 2) 解决 jQuery 加载冲突问题
    if(win.jQuery && jQuery.fn.on){ // 检测在根环境 window 中是否加载了 jQuery 对象
      that.each(apps, function(index, item){ // 如果用户在 apps 中指明加载 jQuery 且 window 已有 jQuery,则剔除掉即将加载的 jQuery 组件,避免重复加载问题
        if(item === 'jquery'){
          apps.splice(index, 1); // 这是 js 中常见的剔除操作,注意 splice 会直接修改 apps 对象。
        }
      });
      layui.jquery = layui.$ = jQuery; // 将 win 环境中的 jQuery 引用,指向到 layui.jquery 中
    }

    var item = apps[0]; // 取出第一个组件
    var timeout = 0;

    exports = exports || []; // 对 exports 的再处理,这种初始化操作最好放在函数的开始位置,这也是 js 的常见惯用法:a || b || c,表示如果 a 和 b 无效值,则返回一个 c。

    // config 这个变量作用域在 use 函数外部,这里需要获取到 host,用 host 拼接出完整的 url 给 <script/> 的 src 属性
    config.host = config.host || (dir.match(/\/\/([\s\S]+?)\//)||['//'+ location.host +'/'])[0];

    /**
     * 当脚本加载时
	 * 会在页面中创建一个 script 标签,并且附加了一个 event 为 load 的事件,当这个 load 执行的时候,会执行一次这个函数
	 * 可以看出整个 `layui.js` 文件当中只有 script 标签引用了一次 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); // 这里的 node 就是下面的 script 标签,你在这里往上查查不到(这就是我说为什么要在离使用它最近的地方声明)
        (function poll() { // 这里用立即执行表达式主要是为了异步执行,避免阻塞
          if(++timeout > config.timeout * 1000 / 4){
            return error(item + ' is not a valid module', 'error');
          }
          config.status[item] ? onCallback() : setTimeout(poll, 4); // 如果执行失败那么就尝试再执行一次
        }());
      }
    }

    /**
     * use 函数的核心功能,这里执行了一个线性迭代过程(一种递归处理方式)
     * 重复的调用 use,直到所有 apps 中的模块被处理完毕(加载 script 标签)
     * 假设 apps 参数的值为 ['a', 'b', 'c']
     * apps.slice(['a', 'b', 'c'].slice(1))
     * apps.slice(['b', 'c'].slice(1))
     * apps.slice(['c'].slice(1))
     */ 
    function onCallback(){
      exports.push(layui[item]);
      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); // this 绑定到 layui 上下文
	          });
	        }
	        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(/^\{\/\}/, '');

    /**
     * config.modules 为模拟物理路径,存储的是 key: url,比如 config.modules['jquery'] = url
     * 这里表示如果 config.modules 当中没有这一项,则记录该扩展模块的 url
     */
    if(!config.modules[item] && layui[item]){
      config.modules[item] = url;
    }

    /**
     * 加载模块的方法
     * 创建一个 script 标签,然后给一个 src,直接插入到 HTML 文档当中,怎么样,简单吧?
     */
    if(!config.modules[item]){
      var node = doc.createElement('script');

      /**
       * 关于 async 的说明
       * https://developer.mozilla.org/en-US/docs/Games/Techniques/Async_scripts
       */
      node.async = true;
      node.charset = 'utf-8';
      node.src = url + function(){
		// config.versoin 表示用于更新模块缓存,默认 false。若设为 true,即让浏览器不缓存。
        var version = config.version === true
        ? (config.v || (new Date()).getTime())
        : (config.version||'');
        return version ? ('?v=' + version) : '';
      }();

	  // 在 html 文件的 head 标签内插入这个 script,因为配置了延迟到 html dom 加载完毕,所以不用担心顺序问题。
      head.appendChild(node);

	  // 这里的判断主要是处理兼容性问题,有的浏览器是 attachEvent(IE)
      if(node.attachEvent && !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) && !isOpera){
        node.attachEvent('onreadystatechange', function(e){
          onScriptLoad(e, url);
        });
      } else {
        // 标准浏览器的 API 为 addEventListener,当脚本装载的时候,执行 onScriptLoad 函数
        node.addEventListener('load', function(e){
          onScriptLoad(e, url);
        }, false);
      }
	  // config.modules 保存一个 hash
      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);
      }());
    }

    // 返回内部 this 的别名 that
    return that;
  };

jQuery 快速入门
Layui

posted @ 2024-06-13 15:52  我听不见  阅读(65)  评论(0编辑  收藏  举报