实现一个JavaScript模块化加载器
对任何程序,都存在一个规模的问题,起初我们使用函数来组织不同的模块,但是随着应用规模的不断变大,简单的重构函数并不能顺利的解决问题。尤其对JavaScript程序而言,模块化有助于解决我们在前端开发中面临的越来越复杂的需求。
为什么需要模块化
对开发者而言,有很多理由去将程序拆分为小的代码块。这种模块拆分的过程有助于开发者更清晰的阅读和编写代码,并且能够让编程的过程更多的集中在模块的功能实现上,和算法一样,分而治之的思想有助于提高编程生产率。
在本文中,我们将集中讨论JavaScript的模块化开发,并实现一个简单的module loader。对于模块化的基础知识,可以参考阅读这篇文章。
实现模块化
使用函数作为命名空间
在JavaScript中,函数是唯一的可以用来创建新的作用域的途径。考虑到一个最简单的需求,我们通过数字来获得星期值,例如通过数字0得到星期日,通过数字1得到星期一。我们可以编写如下的程序:
var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
function dayName(number) {
return names[number];
}
console.log(dayName(1));
上面的程序,我们创建了一个函数dayName()
来获取星期值。但问题是,names
变量被暴露在全局作用域中。更多的时候,我们希望能够构造私有变量,而暴露公共函数作为接口。
对JavaScript中的函数而言,我们可以通过创建立即调用的函数表达式来达到这个效果,我们可以通过如下的方式重构上面的代码,使得私有作用域成为可能:
var dayName = function() {
var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
return {
name: function(number) {
return names[number];
},
number: function(name) {
return names.indexOf(name);
}
};
}();
console.log(dayName.name(3));
console.log(dayName.number("Sunday"));
上面的程序中,我们通过将变量包括在一个函数中,这个函数会立即执行,并返回一个包含两个属性的对象,返回的对象会被赋值给dayName
变量。在后面,我们可以通过dayName
变量来访问暴露的两个函数接口name
和number
。
对代码进一步改进,我们可以利用一个exports
对象来达到暴露公共接口的目的,这种方法可以通过如下方法实现,代码如下:
var weekDay = {};
(function(exports) {
var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
exports.name = function(number) {
return names[number];
};
exports.number = function(name) {
return names.indexOf(name);
};
})(weekDay); // outside of a function, this refers to the global scope object
console.log(weekDay.name(weekDay.number("Saturday")));
上面的这种模块构造方式在以浏览器为核心的前端编码中非常常见,通过暴露一个全局变量的方式来将代码包裹在私有的函数作用域中。但这种方法依然会存在问题,在复杂应用中,你依然无法避免同名变量。
从全局作用域中分离,实现require
方法
更进一步的,为了实现模块化,我们可以通过构造一个系统,使得一个函数可以require
另一个函数的方式来实现模块化编程。所以我们的目标是,实现一个require
方法,通过传入模块名来取得该模块的调用。这种实现方式要比前面的方法更为优雅的体现模块化的理念。对require
方法而言,我们需要完成两件事。
- 我们需要实现一个
readFile
方法,它能通过给定字符串返回文件的内容。 - 我们需要能够将返回的字符串作为代码进行执行。
我们假设已经存在了readFile
这个方法,我们更加关注的是如何能够将字符串作为可执行的程序代码。通常我们有好几种方法来实现这个需求,最常见的方法是eval
操作符,但我们常常在刚学习JavaScript的时候被告知,使用eval
是一个非常不明智的决策,因为使用它会导致潜在的安全问题,因此我们放弃这个方法。
一个更好的方法是使用Function
构造器,它需要两个参数:使用逗号分隔的参数列表字符串,和函数体字符串。例如:
var plusOne = new Function("n", "return n+1");
console.log(plusOne(5)); // 6
下面我们可以来实现require
方法了:
// module.js
function require(name) {
// 调用一个模块,首先检查这个模块是否已被调用
if(name in require.cache) {
return require.cache[name];
}
var code = new Function("exports, module", readFile(name));
var exports = {},
module = {exports: exports};
code(exports, module);
require.cache[name] = module.exports;
return module.exports;
}
// 缓存对象,为了应对重复调用的问题
require.cache = Object.create(null);
// todo:
function readFile(fileName) { ... }
在页面中使用require
函数:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>demo</title>
<script src="module.js"></script>
</head>
<body>
<script>
var weekDay = require("weekDay");
var today = require("today");
console.log(weekDay.name(today.dayNumber()));
</script>
</body>
</html>
通过这种方式实现的模块化系统通常被称为是CommonJS模块
风格的,Node.js正式使用了这种风格的模块化系统。这里只是提供了一个最简单的实现方法,在真实应用中会有更加精致的实现方法。
慢载入模块和AMD
对浏览器编程而言,通常不会使用CommonJS
风格的模块化系统,因为对于Web而言,加载一个资源远没有在服务端来的快,这收到网络性能的影响,尤其一个模块如果过大的话,可能会中断方法的执行。Browserify是解决这个问题的一个比较出名的模块化方案。
这个过程大概是这样的:首先检查模块中是否存在require
其他模块的语句,如果有,就解析所有相关的模块,然后组装为一个大模块。网站本身为简单的加载这个组装后的大模块。
模块化的另一个解决方案是使用AMD,即异步模块定义,AMD允许通过异步的方式加载模块内容,这种就不会阻塞代码的执行。关于AMD的相关背景知识,可以参考这篇文章。
我们想要实现的功能大概是这个样子的:
// index.html 中的部分代码
define(["weekDay.js", "today.js"], function (weekDay, today) {
console.log(weekDay.name(today.dayNumber()));
document.write(weekDay.name(today.dayNumber()));
});
问题的核心是实现define
方法,它的第一个参数是定义该模块说需要的依赖列表,参数而是该模块的具体工作函数。一旦所依赖的模块都被加载后,define
便会执行参数2所定义的工作函数。weekDay
模块的内容大概是下面的内容:
// weekDay.js
define([], function() {
var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
return {
name: function(number) { return names[number]},
number: function(name) { return names.indexOf(name)}
}
});
下面我们来关注如何实现define()
方法。为了实现这个方法,我们需要定义一个backgroundReadFile()
方法来异步的获取文件内容。此外我们需要能够监视模块的加载状态,当模块加载完后能够告诉函数去执行具体的工作函数(回调)。
// 通过Ajax来异步加载模块
function backgroundReadFile(url, callback) {
var req = new XMLHttpRequest();
req.open("GET", url, true);
req.addEventListener("load", function () {
if (req.status < 400)
callback(req.responseText);
});
req.send(null);
}
通过实现一个getModule
函数,通过给定的模块名进行模块的调度运行工作。同样,我们需要通过缓存的方式避免同一个模块被重复的载入。实现代码如下:
// module.js 的部分内容
var defineCache = Object.create(null);
var currentMod = null;
function getModule(name) {
if (name in defineCache) {
return defineCache[name];
}
var module = {
exports: null,
loaded: false,
onLoad: []
};
defineCache[name] = module;
backgroundReadFile(name, function(code) {
currentMod = module;
new Function("", code)();
});
return module;
}
有了getModule()
了函数之后,define
方法可以借助该方法来为当前模块的依赖列表获取或创建模块对象。define
方法的简单实现如下:
// module.js 的部分内容
function define(depNames, moduleFunction) {
var myMod = currentMod;
var deps = depNames.map(getModule);
deps.forEach(function(mod) {
if(!mod.loaded) {
mod.onLoad.push(whenDepsLoaded);
}
});
// 用于检查是否所有的依赖模块都被成功加载了
function whenDepsLoaded() {
if(!deps.every(function(m) { return m.loaded; })) {
return;
}
var args = deps.map(function(m) { return m.exports; });
var exports = moduleFunction.apply(null, args);
if (myMod) {
myMod.exports = exports;
myMod.loaded = true;
myMod.onLoad.forEach(function(f) { f(); });
}
}
whenDepsLoaded();
}
关于AMD的更加常见的实现是RequireJS,它提供了AMD风格的更加流行的实现方式。
小结
模块通过将代码分离为不同的文件和命名空间,为大型程序提供了清晰的结构。通过构建良好的接口可以使得开发者更加建议的阅读、使用、扩展代码。尤其对JavaScript语言而言,由于天生的缺陷,使得模块化更加有助于程序的组织。在JavaScript的世界,有两种流行的模块化实现方式,一种称为CommonJS,通常是服务端的模块化实现方案,另一种称为AMD,通常针对浏览器环境。其他关于模块化的知识,你可以参考这篇文章。
References
- Eloquent JavaScript, chapter 10, Modules
- Browserify运行原理分析
- Why AMD?
- JavaScript模块化知识点小结
实现一个JavaScript模块化加载器的更多相关文章
- RequireJS 是一个JavaScript模块加载器
RequireJS 是一个JavaScript模块加载器.它非常适合在浏览器中使用, 它非常适合在浏览器中使用,但它也可以用在其他脚本环境, 就像 Rhino and Node. 使用RequireJ ...
- 该如何理解AMD ,CMD,CommonJS规范--javascript模块化加载学习总结
是一篇关于javascript模块化AMD,CMD,CommonJS的学习总结,作为记录也给同样对三种方式有疑问的童鞋们,有不对或者偏差之处,望各位大神指出,不胜感激. 本篇默认读者大概知道requi ...
- js模块化加载器实现
背景 自es6以前,JavaScript是天生模块化缺失的,即缺少类似后端语言的class, 作用域也只以函数作为区分.这与早期js的语言定位有关, 作为一个只需要在网页中嵌入几十上百行代码来实现一些 ...
- 小矮人Javascript模块加载器
https://github.com/miniflycn/webkit-dwarf 短小精悍的webkit浏览器Javascript模块加载器 Why 我们有许多仅基于webkit浏览器开发的应用 无 ...
- 【SpringBoot 基础系列】实现一个自定义配置加载器(应用篇)
[SpringBoot 基础系列]实现一个自定义配置加载器(应用篇) Spring 中提供了@Value注解,用来绑定配置,可以实现从配置文件中,读取对应的配置并赋值给成员变量:某些时候,我们的配置可 ...
- JavaScript 模块化加载
存在AMD(Asynchronous Module Definition异步模块定义)规范和CMD(Common Module Definition通用模块定义)规范.对于依赖的模块,AMD是提前执行 ...
- JavaScript文件加载器LABjs API详解
在<高性能JavaScript>一书中提到了LABjs这个用来加载JavaScript文件的类库,LABjs是Loading And Blocking JavaScript的缩写,顾名思义 ...
- 利用require.js实现javascript模块化加载
这种引入很看到很想死吧! <script src="1.js"></script> <script src="2.js">& ...
- 【模块化编程】理解requireJS-实现一个简单的模块加载器
在前文中我们不止一次强调过模块化编程的重要性,以及其可以解决的问题: ① 解决单文件变量命名冲突问题 ② 解决前端多人协作问题 ③ 解决文件依赖问题 ④ 按需加载(这个说法其实很假了) ⑤ ..... ...
随机推荐
- yum在线升级
RPM优点 由於 RPM 是透过预先编译并打包成为 RPM 文件格式后,再加以安装的一种方式,并且还能够进行数据库的记载. 所以 RPM 有以下的优点: RPM 内含已经编译过的程序与配置档等数据,可 ...
- RxAndroid结合Retrofit,看看谁才是最佳拍档!
这篇博文酝酿好久了,今天终于下定决心开始写!RxAndroid和Retrofit都算是当下非常流行的Android开发框架,这两个框架光是单独使用就已经爽歪歪了,那么将RxAndroid和Retrof ...
- CentOS 6.3 安装ATI显卡驱动
环境: centos 6.3 内核(GNU貌似大家都知道) 显卡:ATI Mobility Radeon HD 3470 安装显卡流程: 1.官网下载最新驱动 http://support.amd. ...
- FWFT FIFO读操作注意
FWFT:First Word Fall Through的缩写,好像是Xilinx的说法,Altera对应的概念是Show-ahead synchronous(SASO).即数据在rdreq有效之前就 ...
- jquery处理textarea中的手动换行
textarea的手动换行会产生换行标志,但这个标志存在却看不到,存入数据库中后读出来显示在页面上却不会换行,如何处理呢? 网上众说纷纭,经过测试用 textarea的内容.replace(/\n/g ...
- json 转对象
架包: import com.alibaba.fastjson.JSON; String arryStr="[{\"Name\": \"A\", \& ...
- 关于sqlserver 2008 远程导入表数据
/*不同服务器数据库之间的数据操作*/ --创建链接服务器 exec sp_addlinkedserver 'ITSV ', ' ', 'SQLOLEDB ', '远程服务器名或ip地址 ' ex ...
- 特殊浮点值 Java
// Finite +-0.0 System.out.println("Finite with +-0.0:"); System.out.println("Finite ...
- 常见错误总结_1_对java类进行修改后,无法按修改的类型加载
1.这是因为没有run的原因,对类进行修改一定要run一遍 2.至于要不要重新tomcat部署,取决于你是修改了变量还是方法,拿不定的时候都重新加载一遍看看.
- Visual C++ 打印编程技术-打印基础知识
打印机介绍 1.打印术语 *: 1 英寸= 2.54 厘米(cm)= 25.4 毫米(mm) cpi (Characters Per Inch): 每英寸内所含的字符数,用来表示字符的大小.间距 cp ...