JavaScript模块化发展
在最开始学习前端的时候只需要一个js文件就能玩转一个小的练手应用,但是随着自己不断的学习,ajax、jQuery等广泛应用,使得我们的代码量变得巨大,代码变得格外的混乱。现在迫切的需要我们将大段的代码分离开来。
前端最开始并没有像java中package概念以及import那样的引包工具。JavaScript源生代码是在ES6的时候才正式的引入import这个API,来调用其他文件。在这之前也同样出现了很多社区来实现模块化开发。
注意下面会讲历史上面出现的一些类库,有一些现在已经没有人用了,所以建议知道有过就行。
function fn1() {} function fn2() {}
将函数挂载到全局上,通过函数名就可以直接调用。但是这种方式污染全局,容易发生命名冲突。
var math = { _addFir: 2, add: function(addSec) { return _addFir + addSec; }, };
对象写法相对来说减少了全局变量,但是一点也不安全。 例如:
math._addFir = 10; console.log(math.add(2)); // 12
对象内部的变量可以被外面修改。
var math = (function() { var _addFir = 2; function add(addSec) { return _addFir + addSec; } return { add: add, }; })();
这样就无法修改函数内部的_addFir参数了
_addFir = 10; console.log(math.add(2)); // 4
var get = (function($) { var $p = $('input'); function getVal() { return $p.val(); } return { getVal: getVal, }; })(jQuery);
使用立即执行的方式写模块,当模块非常大的时候,我们就需要将这个模块分成几部分来进行编写。下面将会展示廖雪峰老师所说的放大模式和宽放大模式(我暂时想不出其他的名字,就使用了现有的)。
var module = (function(mod) { mod.add = function(a, b) { return a + b; }; return mod; })(module);
上面的方式实现了将已存在的对象添加方法,使之放大,但是如果module起初还没有被加载到文件中怎么办,下面就用到了宽放大模式。
var module = (function(mod) { mod.add = function(a, b) { return a + b; }; return mod; })(module || {});
这样就解决了module模块没有加载出来,报错的问题。
我们最初使用html中的<script>
标签来引入js文件。当项目不断变大以后,我们的项目的依赖也开始变多,就像下面。
<body> ... <script src="jQuery.js"></script> <script src="zepto.js"></script> <script src="iScroll.js"></script> <script src="math.js"></script> <script src="dom.js"></script> ... </body>
大量的script标签排列在我们的html文件中。
缺点
LABjs它是一个文件加载器,使用script和wait实现文件异步和同步加载,解决文件之间的相互依赖,使的文件加载的性能大大提高。有了它我们的html中引脚本文件可以成下面这样。
<script src="LAB.js"></script> <script> $LAB .script('jQuery.js').wait() // .wait是等待此文件的加载完成,当所有的文件都需要依赖jQuery中的api,必须等jQuery文件加载好以后才能调用jQuery .script('a.js') .script('b.js') .script('c.js') .script('math.js') // .script(['a.js', 'b.js', 'c.js', 'math.js']) // 同时加载所有的js文件 .wait(function() { // 等所有的js文件加载完成以后,执行这里的代码块 math.add(2, 2); }) </script>
同时LABjs也可以解决所有文件之间都相互依赖的问题
<script src="LAB.js"></script> <script> $LAB .setOptions({AlwaysPreserveOrder:true}) // 下面需要加载的这些文件之间都相互依赖 .script(['a.js', 'b.js', 'c.js', 'math.js']) .wait(function() { math.add(2, 2); })
YUI用来基于模块的依赖管理。
YUI.add('module1', function(Y) {...}, '1.0.0', requires: []);
其中YUI是全局变量,就像是jQuery;第一个参数是此模块的名字;第二个参数中函数的内容就是此模块的内容;第三个参数是此模块的版本号;第四个参数是此模块需要依赖的模块有哪些。 下面将展示如何使用YUI添加和使用一个模块
// hello.js YUI.add('hello', function(Y) { Y.sayHello = function() { Y.DOM.set(el, 'innerHTML', 'hello!'); } }, '1.0.0', requires: ['dom']);
// index.html <div id="entry"></div> <script> YUI().use('hello', function(Y) { Y.sayHello('entry'); // <div id="entry">hello!</div> }) </script>
我不想花太多时间在这个上面,所以后面只会写生成模块和使用模块。如果对这个有兴趣可以到:YUI3
the spec does not define a standard library that is useful for building a broader range of applications. 该规范没有定义一个标准库,可用于构建更广泛的应用程序。
上面这段话来自CommonJS官网中的自我定位,它本质上面是一个规范,需要其他的JavaScript类库、框架等自行实现它定义的API。
CommonJS使得JavaScript不仅仅只适用于浏览器,他让js可以编写更多应用程序,如:
commonjs中的模块加载时同步加载,在服务器端,模块存在服务器本地的,加载速度很快。但是,当程序运行在浏览器端的时候要从服务器端去加载模块会导致性能、可用性、调试、跨域等问题,所以commonjs不适用与浏览器端。
node应用程序就是根据CommonJS规范实现的,下面我将直接使用node来讲解CommonJS中module和require两个API。
在node中每一个文件就是一个模块,每个模块中变量、函数、对象、类都是私有的,除非将这些放入global中去。
// math.js var count1 = 2; global.count2 = 5; // use.js console.log(count1); // count1 is not defined console.log(count2); // 5
node中有一个Module构造函数(node中的lib/module.js),在node应用程序中每个模块都含有一个Module实例,用来存放此模块的信息。
// Module 构造函数 function Module(id, parent) { this.id; // String,模块标识,为该模块文件在系统中的绝对路径 this.exports; // Object,模块导出的对象 this.parent; // Object,调用此模块的模块信息 this.filename; // String,模块文件的绝对路径 this.loaded; // Boolean,表示模块是否加载完成 this.children; // Array,此模块调用了的模块 this.path; // Array,此模块加载的路径 }
module.exports中的属性就是模块对外输出的接口。
// math.js var count = 5; function add(val) { return count + val; } module.exports = { count, add };
// use.js var math = require('math.js'); console.log(math.count); // 5 math.add(5); // 10
注意module.exports只会输出对象的自身属性,prototype上面的方法是私有方法
// math.js function math() {}; // 函数即对象 math.count1 = 5; math.prototype.count2 = 10; module.exports = math;
// use.js var math = require('math.js'); console.log(math); // { [Function: math] count1: 5 } console.log(math.count2); // undefined
exports是node提供的一个变量,用来指向module.exports的引用,相当于每个node文件前面有一段这样的代码exports = module.exports = something
。(something是一个对象)
// math.js var count = 5; function add(val) { return count + val; } exports.count = count; exports.add = add;
// use.js var math = require('math.js'); console.log(math.count); // 5 math.add(5); // 10
注意模块最终输出的是module.exports,而不是exports。
// math.js var count = 5; function add(val) { return count + val; } module.exports = { count, add }; // 此时module的exports指向另一个对象 exports.count = 10; // exports依旧指向的是最开始module.exports指向的对象something
// use.js var math = require('math.js'); console.log(math.count); // 注意,这里打印的还是5 math.add(5); // 10
由上面代码可以看出,当module.exports发生改变的时候,exports失效,这就很正常了。如果想让后面的exports的操作能改变输出的话,使exports的指向module.exports新的引用就行了。
// math.js var count = 5; function add(val) { return count + val; } exports = module.exports = { count, add }; exports.count = 10;
// use.js var math = require('math.js'); console.log(math.count); // 10 math.add(5); // 10
在讲exports的最后,提醒大家,想要使用exports对外输出的时候不是对exports赋值。如果大家看了多次还是不懂exports的用法,那就去看下这篇module.exports与exports??关于exports的总结。
下面是来自廖雪峰的require() 源码解读翻译翻译自《Node使用手册》。
当Node 遇到 require(X) 时,按下面的顺序处理。 (1)如果 X 是内置模块(比如 require('http')) a. 返回该模块。 b. 不再继续执行。 (2)如果 X 以 "./" 或者 "/" 或者 "../" 开头 a. 根据 X 所在的父模块,确定 X 的绝对路径。 b. 将 X 当成文件,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行。 X X.js X.json X.node c. 将 X 当成目录,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行。 X/package.json(main字段) X/index.js X/index.json X/index.node (3)如果 X 不带路径 a. 根据 X 所在的父模块,确定 X 可能的安装目录。 b. 依次在每个目录中,将 X 当成文件名或目录名加载。 (4) 抛出 "not found"
当文件/home/test/use.js
中使用require('math')
,这种情况属于上面的(3)。
首先会确定文件的绝对路径,并依此去寻找每个目录
/home/test/node_modules/math /home/node_modules/math /node_modules/math
在寻找每个目录中的文件的时候,node会现将math当成一个文件。当依此寻找到一个以后就会立马返回。
math math.js math.json math.node
把math当成文件并没有找到的时候,就会将math当成文件夹,并去依此寻找他下面的这些文件。
package.json(main字段) index.js index.json index.node
require会按照上面的顺序依次去查询是否含有这个文件,如果找到了就会立马加载此文件,并停止去遍历那些路径。 如果将确定好的绝对路径目录都寻找了一遍没有找到目标文件时,就会抛出一个错误。
node中模块不会被重复加载,node会将加载过的文件名缓存下来,以后再次访问时就不会重复加载模块了。
注意这里缓存的文件名并不是require中的参数,require('math')和require('./node_modules/math')只会去解析一次此模块。
node中的每个模块实例都有一个require方法。
Module.prototype.require = function(path) { return Module._load(path, this); }
从上面的代码可以看出,require并不是全局变量,而是模块内部的一个方法。
下面是Module._load的源码
Module._load = function(request, parent, isMain) { // 计算绝对路径 var filename = Module._resolveFilename(request, parent); // 第一步:如果有缓存,取出缓存 var cachedModule = Module._cache[filename]; if (cachedModule) { return cachedModule.exports; // 第二步:是否为内置模块 if (NativeModule.exists(filename)) { return NativeModule.require(filename); } // 第三步:生成模块实例,存入缓存 var module = new Module(filename, parent); Module._cache[filename] = module; // 第四步:加载模块 try { module.load(filename); hadException = false; } finally { if (hadException) { delete Module._cache[filename]; } } // 第五步:输出模块的exports属性 return module.exports; };