目前,一个典型的前端项目技术框架的选型主要包括如下三个方面:javascript
- JS模块化框架。(Require/Sea/ES6 Module/NEJ)
- 前端模板框架。(React/Vue/Regular)
- 状态管理框架。(Flux/Redux)
系列文章将从上面三个方面来介绍相关原理,而且尝试本身造一个简单的轮子。
本篇介绍的是JS模块化。
JS模块化是随着前端技术的发展,前端代码爆炸式增加后,工程化所采起的必然措施。目前模块化的思想分为CommonJS、AMD和CMD。有关三者的区别,你们基本都多少有所了解,并且资料不少,这里就再也不赘述。html
模块化的核心思想:前端
根据上面的核心思想,能够看出要设计一个模块化工具框架的关键问题有两个:一个是如何将一个模块执行并能够将结果输出注入到另外一个模块中;另外一个是,在大型项目中模块之间的依赖关系很复杂,如何使模块按正确的依赖顺序进行注入,这就是依赖管理。java
下面以具体的例子来实现一个简单的基于浏览器端的AMD模块化框架(相似NEJ),对外暴露一个define函数,在回调函数中注入依赖,并返回模块输出。要实现的以下面代码所示。git
define([
'/lib/util.js', //绝对路径
'./modal/modal.js', //相对路径
'./modal/modal.html',//文本文件
], function(Util, Modal, tpl) {
/* * 模块逻辑 */
return Module;
})复制代码
先不考虑一个模块的依赖如何处理。假设一个模块的依赖已经注入,那么如何加载和执行该模块,并输出呢?
在浏览器端,咱们能够借助浏览器的script标签来实现JS模块文件的引入和执行,对于文本模块文件则能够直接利用ajax请求实现。
具体步骤以下:github
var a = document.createElement('a');
a.id = '_defineAbsoluteUrl_';
a.style.display = 'none';
document.body.appendChild(a);
function getModuleAbsoluteUrl(path) {
a.href = path;
return a.href;
}
function parseAbsoluteUrl(url, parentDir) {
var relativePrefix = '.',
parentPrefix = '..',
result;
if (parentDir && url.indexOf(relativePrefix) === 0) {
// 以'./'开头的相对路径
return getModuleAbsoluteUrl(parentDir.replace(/[^\/]*$/, '') + url);
}
if (parentDir && url.indexOf(parentPrefix) === 0) {
// 以'../'开头的相对路径
return getModuleAbsoluteUrl(parentDir.replace(/[\/]*$/, '').replace(/[\/]$/, '').replace(/[^\/]*$/, '') + url);
}
return getModuleAbsoluteUrl(url);
}复制代码
对于JS文件,利用script标签实现。代码以下:ajax
var head = document.getElementsByTagName('head')[0] || document.body;
function loadJsModule(url) {
var script = document.createElement('script');
script.charset = 'utf-8';
script.type = 'text/javascript';
script.onload = script.onreadystatechange = function() {
if (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete') {
/* * 加载逻辑, callback为define的回调函数, args为全部依赖模块的数组 * callback.apply(window, args); */
script.onload = script.onreadystatechange = null;
}
};
}复制代码
对于文本文件,直接用ajax实现。代码以下:数组
var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'),
textContent = '';
xhr.onreadystatechange = function(){
var DONE = 4, OK = 200;
if(xhr.readyState === DONE){
if(xhr.status === OK){
textContent = xhr.responseText; // 返回的文本文件
} else{
console.log("Error: "+ xhr.status); // 加载失败
}
}
}
xhr.open('GET', url, true);// url为文本文件的绝对路径
xhr.send(null);复制代码
一个模块的加载过程以下图所示。
浏览器
依赖分析
在模块加载后,咱们须要解析出每一个模块的绝对路径(path)、依赖模块(deps)和回调函数(callback),而后也放在模块信息中。模块对象管理逻辑的数据模型以下所示。网络
{
path: 'http://asdas/asda/a.js',
deps: [{}, {}, {}],
callback: function(){ },
status: 'pending'
}复制代码
依赖循环
模块极可能出现循环依赖的状况。也就是a模块和b模块相互依赖。依赖分为强依赖和弱依赖。强依赖是指,在模块回调执行时就会使用到的依赖;反之,就是弱依赖。对于强依赖,会形成死锁,这种状况是没法解决的。但弱依赖能够经过现将一个空的模块引用注入让一个模块先执行,等依赖模块执行完后,再替换掉就能够了。强依赖和弱依赖的例子以下:
//强依赖的例子
//A模块
define(['b.js'], function(B) {
// 回调执行时须要直接用到依赖模块
B.demo = 1;
// 其余逻辑
});
//B模块
define(['a.js'], function(A) {
// 回调执行时须要直接用到依赖模块
A.demo = 1;
// 其余逻辑
});复制代码
// 弱依赖的例子
// A模块
define(['b.js'], function(B) {
// 回调执行时不会直接执行依赖模块
function test() {
B.demo = 1;
}
return {testFunc: test}
});
//B模块
define(['a.js'], function(A) {
// 回调执行时不会直接执行依赖模块
function test() {
A.demo = 1;
}
return {testFunc: test}
});复制代码
对于define函数,须要遍历全部的未处理js脚本(包括内联和外联),而后执行模块的加载。这里对于内联和外联脚本中的define,要作分别处理。主要缘由有两点:
var handledScriptList = [];
window.define = function(deps, callback) {
var scripts = document.getElementsByTagName('script'),
defineReg = /s*define\s*\(\[.*\]\s*\,\s*function\s*\(.*\)\s*\{/,
script;
for (var i = scripts.length - 1; i >= 0; i--) {
script = list[i];
if (handledScriptList.indexOf(script.src) < 0) {
handledScriptList.push(script.src);
if (script.innerHTML.search(defineReg) >= 0) {
// 内敛脚本直接进行模块依赖检查。
} else {
// 外联脚本的首先要监听脚本加载
}
}
}
};复制代码
上面就是对实现一个模块化工具所涉及核心问题的描述。完整的代码点我。