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 表示 localStorage ;sessionData 等于 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 模块系统,具有把 html
,js
,css
合为一个组件的组件系统。
Layui
更为简单,仅仅只是封装 js
模块,它与 html
,css
是离散的,内聚性较弱,可在不同的 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;
};