Layui 源码浅读(模块加载原理)
经典开场
// Layui
;! function (win) {
var Lay = function () {
this.v = '2.5.5';
};
win.layui = new Lay();
}(window);
// Jquery
(function (global, factory) {
"use strict";
if (typeof module === "object" && typeof module.exports === "object") {
module.exports = global.document ?
factory(global, true) :
function (w) {
if (!w.document) {
throw new Error("jQuery requires a window with a document");
}
return factory(w);
};
} else {
factory(global);
}
})(typeof window !== "undefined" ? window : this, function (window, noGlobal) {
var jQuery = function (selector, context) {
return new jQuery.fn.init(selector, context);
};
return jQuery;
});
这是一种很经典的开场方式,以 ! 定义一个函数并立即执行,并且将对象赋值到全局 window 变量上。当然除了 ! 还有 ~ 等符号都可以定义后面的这个函数,而 ; 应该是为了防止其他的代码对本身造成影响。
实际上( function (window) { "use strict"; } )( window )
的写法更被我们理解,如Jquery未压缩的源码。而!定义函数的方法唯一优势就是代码相对较少,所以压缩后的Js代码大多数会以!开头。
动态加载
Lay.prototype.link = function (href, fn, cssname) {
var that = this,
link = doc.createElement('link'),
head = doc.getElementsByTagName('head')[0];
if (typeof fn === 'string')
cssname = fn;
var app = (cssname || href).replace(/\.|\//g, '');
var id = link.id = 'layuicss-' + app,
timeout = 0;
link.rel = 'stylesheet';
link.href = href + (config.debug ? '?v=' + new Date().getTime() : '');
link.media = 'all';
if (!doc.getElementById(id)) {
head.appendChild(link);
}
if (typeof fn != 'function') return that;
(function poll() {
if (++timeout > config.timeout * 1000 / 100) {
return error(href + ' timeout');
};
if (parseInt(that.getStyle(doc.getElementById(id), 'width')) === 1989) {
fn();
} else {
setTimeout(poll, 100);
}
}());
return that;
}
先来看看官方文档:
方法:layui.link(href)
href 即为 css 路径。注意:该方法并非是你使用 layui 所必须的,它一般只是用于动态加载你的外部 CSS 文件。
虽然官方只给出了一个参数,但是我们看源码的话可以知道后两个参数是加载完后运行的函数和自定义的Id。
有趣的是,临时创建的 poll函数 如果parseInt(that.getStyle(doc.getElementById(id), 'width')) === 1989
判断为 false ,也就是样式没有被引入的时候会重新调用 poll函数 最后要么加载成功循环结束,要么加载超时调用 Layui hint 打印出超时信息。
因为同样的手段在加载 module 时也同样使用到,所以如果你使用过 Layui 那么[module] is not a valid module
这样的警告或多或少能遇到几次。
模块引入
用过 Layui 的兄dei应该对 layui.use 不陌生,先来看官方文档:
方法:layui.use([mods], callback)
layui 的内置模块并非默认就加载的,他必须在你执行该方法后才会加载。
对于用了 Layui 有段时间的我来说,也只是按照官方的例子使用,并不知道实现的原理。
接下来就是见证遗迹的时候,看看 layui.use 做了什么:
Lay.fn.use = function (apps, callback, exports) {
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);
(function poll() {
if (++timeout > config.timeout * 1000 / 4) {
return error(item + ' is not a valid module');
};
config.status[item] ? onCallback() : setTimeout(poll, 4);
}());
}
}
function onCallback() {
exports.push(layui[item]);
apps.length > 1 ? that.use(apps.slice(1), callback, exports) : (typeof callback === 'function' && callback.apply(layui, exports));
}
var that = this,
dir = config.dir = config.dir ? config.dir : getPath;
var head = doc.getElementsByTagName('head')[0];
apps = typeof apps === 'string' ? [apps] : apps;
if (window.jQuery && jQuery.fn.on) {
that.each(apps, function (index, item) {
if (item === 'jquery') {
apps.splice(index, 1);
}
});
layui.jquery = layui.$ = jQuery;
}
var item = apps[0],
timeout = 0;
exports = exports || [];
config.host = config.host || (dir.match(/\/\/([\s\S]+?)\//) || ['//' + location.host + '/'])[0];
if (apps.length === 0 || (layui['layui.all'] && modules[item]) || (!layui['layui.all'] && layui['layui.mobile'] && modules[item])) {
return onCallback(), that;
}
if (config.modules[item]) {
(function poll() {
if (++timeout > config.timeout * 1000 / 4) {
return error(item + ' is not a valid module');
};
if (typeof config.modules[item] === 'string' && config.status[item]) {
onCallback();
} else {
setTimeout(poll, 4);
}
}());
} else {
var node = doc.createElement('script'),
url = (modules[item] ? dir + 'lay/' : /^\{\/\}/.test(that.modules[item]) ? '' : config.base || '') + (that.modules[item] || item) + '.js';
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);
if (!node.attachEvent || (node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code]') < 0) || isOpera) {
node.addEventListener('load', function () {
onScriptLoad(e, url);
}, false);
} else {
node.addEventListener('onreadystatechange', function (e) {
onScriptLoad(e, url);
});
}
config.modules[item] = url;
}
return that;
};
首先跳过前两个创建的函数,经过一堆巴拉巴拉的赋值后来到第2个if中我们直接可以判断语句apps.length === 0
,根据文档可知我们第一个参数是一个数组 [mods] ,当然前面的赋值apps = typeof apps === 'string' ? [apps] : apps;
可以看出即使你传的是一个字符串也会被封装成数组。
很明显第一次进来apps.length === 0
和下面的if ( config.modules[item] )
也必为 false ,那么我们直接移步到 else 内。
创建一个 script 元素并赋予属性和模块的地址,通过 appendChild 追加到 head 之后留下一个 addEventListener 监听 script 的加载( ps:attachEvent 是给非人类使用的浏览器准备的 )并将开始创建的 function onScriptLoad(e, url)
函数抛进去,然后整段代码除了return that
到这里戛然而止。
再来看看function onScriptLoad(e, url)
函数,首先开幕雷击 "PLaySTATION 3" === navigator.platform
?
仅关心PC端浏览器的部分e.type === 'load'
, 因为监听的是 load 所以这里必为 true 并执行config.modules[item] = url
后将追加的 script 元素移除。剩余的代码就是动态加载时使用的技巧,直到 config.status[item]
为 true 时循环结束。
定义模块
由于config.status[item]
不会自动变成 true,之后的骚操作由 layui.define 接手。
先看官方文档:
方法:layui.define([mods], callback)
通过该方法可定义一个 layui 模块。参数 mods 是可选的,用于声明该模块所依赖的模块。callback 即为模块加载完毕的回调函数,它返回一个 exports 参数,用于输出该模块的接口。
以比较常用的 laypage.js 模块为例,基础源码如下:
// Laypage 模块的部分代码(部分变量名为猜测,但不影响内容本身)
layui.define(function (exports) {
'use strict';
var MOD_NAME = 'laypage',
LayPage = function (options) {
var that = this;
that.config = options || {}, that.config.index = ++laypage.index, that.render(true);
};
var laypage = {
render: function (options) {
var laypage = new LayPage(options);
return laypage.index
},
index: layui.laypage ? layui.laypage.index + 10000 : 0,
on: function (elem, even, fn) {
return elem.attachEvent ? elem.attachEvent("on" + even, function (param) {
param.target = param.srcElement, fn.call(elem, param)
}) : elem.addEventListener(even, fn, false), this
}
};
exports(MOD_NAME, laypage);
});
因为 Layui 已经注册了全局的变量,所以当模块文件通过元素追加的方式引入时,调用了 layui.define 方法:
Lay.fn.define = function (deps, callback) {
var that = this,
type = typeof deps === 'function',
mods = function () {
var e = function (app, exports) {
layui[app] = exports;
config.status[app] = true;
}
typeof callback === 'function' && callback(function (app, exports) {
e(app, exports);
config.callback[app] = function () {
callback(e);
}
});
return this;
};
type && (callback = deps, deps = []);
if (!layui['layui.all'] && layui['layui.mobile']) {
return mods.call(that);
} else {
that.use(deps, mods);
return that;
}
};
因为不管你在定义的模块中有没有引入其他模块,如 laypage 和 laytpl 这些 Layui 本身提供的模块都会因 (callback = deps, deps = [])
回到 [mods], callback 的参数格式。
再经过一系列巴拉巴拉的步骤回到定义的 mods 方法中,由layui[app] = exports, config.status[app] = true
给全局 layui 变量添加属性(app)且给属性赋值(exports),并把 status 改为 true 至此模块加载完成。
总结
正如 Layui 官方所说:我们认为,这恰是符合当下国内绝大多数程序员从旧时代过渡到未来新标准的最佳指引。
作为一个后端的工作者(以后可能要接触前端框架的人)没有接触过前端框架,只对原生态的 HTML / CSS / JavaScript 有所了解,那么 Layui 无非是较优的选择。
而写这篇文章无非就是为了感谢 Layui 对非前端工作者做出的贡献,也可能是我对使用了两年多 Layui 最后的告别吧,感谢贤心。
相关网站
其他
如果你没有接触过 UglifyJS 或其他 JS 压缩器,而你又恰巧使用 Visual Studio Code 工具开发,那么 Minify 扩展插件就已经足够日常使用了。
Layui 源码浅读(模块加载原理)的更多相关文章
- 从SpringBoot源码分析 配置文件的加载原理和优先级
本文从SpringBoot源码分析 配置文件的加载原理和配置文件的优先级 跟入源码之前,先提一个问题: SpringBoot 既可以加载指定目录下的配置文件获取配置项,也可以通过启动参数( ...
- 05 flask源码剖析之配置加载
05 Flask源码之:配置加载 目录 05 Flask源码之:配置加载 1.加载配置文件 2.app.config源码分析 3.from_object源码分析 4. 总结 1.加载配置文件 from ...
- Spark 源码浅读-SparkSubmit
Spark 源码浅读-任务提交SparkSubmit main方法 main方法主要用于初始化日志,然后接着调用doSubmit方法. override def main(args: Array[St ...
- Node.js require 模块加载原理 All In One
Node.js require 模块加载原理 All In One require 加载模块,搜索路径 "use strict"; /** * * @author xgqfrms ...
- AMD-require.js模块加载原理
项目中使用大了require.js,功能实现,现重新学习下模块加载原理相关知识,借鉴如下博文:https://blog.csdn.net/ai52011/article/details/7711361 ...
- 第三课:sea.js模块加载原理
模块加载,其实就是把js分成很多个模块,便于开发和维护.因此加载很多js模块的时候,需要动态的加载,以便提高用户体验. 在介绍模块加载库之前,先介绍一个方法. 动态加载js方法: function l ...
- Dubbo源码分析之ExtensionLoader加载过程解析
ExtensionLoader加载机制阅读: Dubbo的类加载机制是模仿jdk的spi加载机制: Jdk的SPI扩展加载机制:约定是当服务的提供者每增加一个接口的实现类时,需要在jar包的META ...
- Flask源码之:配置加载
加载配置文件的思路: 1. 读取配置文件中的所有键值对,并将键值对全都放到Config对象.(Config是一个字典,因为它继承了Dict) 2. 把包含所有配置文件的Config对象,赋值给 app ...
- webpack4.X源码解析之懒加载
本文针对Webpack懒加载构建和加载的原理,对构建后的源码进行分析. 一.准备工作 首先,init之后创建一个简单的webpack基本的配置,在src目录下创建两个js文件(一个主入口文件和一个非主 ...
随机推荐
- vs中python包安装教程
vs安装python很简单,只需要在vs安装包中选择python就可以了,这里使用的python3.7: 如果有了解,都知道安装python包的指令:"pip install xxx&quo ...
- Codeforces Global Round 8 C. Even Picture(构造)
题目链接:https://codeforces.com/contest/1368/problem/C 题意 构造一个只含有灰.白块的网格,要求: 所有灰块为一个连通块 每个灰块与偶数个灰块相邻 恰有 ...
- fzu2200 cleaning
Problem Description N个人围成一圈在讨论大扫除的事情,需要选出K个人.但是每个人与他距离为2的人存在矛盾,所以这K个人中任意两个人的距离不能为2,他们想知道共有多少种方法. In ...
- Codeforces #640 div4 F~G (构造二连弹)
题意:求一个只由\(01\)组成的字符串,使得它所有长度为\(2\)的子串满足:每对子串的数字和为\(0,1,2\)的个数为\(a,b,c\). 题解:我们先考虑子串数字和为\(1\)的情况,构造出一 ...
- L3-007 天梯地图 (30分) 最短路+dp
最短路+dp思路:nuoyanli 520 Let's play computer game 输入样例1: 10 15 0 1 0 1 1 8 0 0 1 1 4 8 1 1 1 5 4 0 2 3 ...
- MySql 执行 DELETE/UPDATE时,报 Error Code: 1175错误
MySql 执行 DELETE FROM Table 时,报 Error Code: 1175. You are using safe update mode and you tried to upd ...
- Vue的七种传值方式
目录 1,父传子 2,子传父 3,兄弟组件传值 4,父组件使用子组件的数据和方法 5,子组件使用父组件的数据和方法 6,Vuex传值 6.1,定义store 6.2,挂载 6.3,使用 7,路由传值 ...
- 基于CentOS-7的redis下载和安装
1.下载和安装 在我安装的虚拟机中,我把所有自己安装的软件都放在了/ph/install 目录下,具体以自己实际情况为准. [root@localhost ~]$ cd /ph/install #进入 ...
- Explain 索引优化分析
Explain 语法 # 语法 explain + DQL语句 mysql> explain select * from city where countrycode ='CHN' or cou ...
- Sublime text 3 中 Package Control安装
安装前 ctrl+shift+p 在命令板中输入PC,如下图表示没安装: 使用ctrl+~调出sublime软件的控制台命令窗口:粘贴运行 import urllib.request,os,hash ...