好程序员web前端教程分享js中的模块化一
1.script标签引入
最开始的时候,多个script标签引入js文件。但是,这种弊端也很明显,很多个js文件合并起来,也是相当于一个script,造成变量污染。项目大了,不想变量污染也是很难或者不容易做到,开发和维护成本高。 而且对于标签的顺序,也是需要考虑一阵,还有加载的时候同步,更加是一种灾难,幸好后来有了渲染完执行的defer和下载完执行的async,进入新的时代了。
接着,就有各种各样的动态创建script标签的方法,最终发展到了上面的几种方案。
2.AMD与CMD
2.1AMD
异步模块定义,提供定义模块及异步加载该模块依赖的机制。AMD遵循依赖前置,代码在一旦运行到需要依赖的地方,就马上知道依赖是什么。而无需遍历整个函数体找到它的依赖,因此性能有所提升。但是开发者必须先前知道依赖具体有什么,并且显式指明依赖,使得开发工作量变大。而且,不能保证模块加载的时候的顺序。 典型代表requirejs。require.js在声明依赖的模块时会立刻加载并执行模块内的代码。require函数让你能够随时去依赖一个模块,即取得模块的引用,从而即使模块没有作为参数定义,也能够被使用。他的风格是依赖注入,比如:
/api.js
define(‘myMoudle‘,[‘foo‘,‘bar‘],function(foo,bar){
//引入了foo和bar,利用foo、bar来做一些事情 return { baz:function(){return ‘api‘} }
});
require([‘api‘],function(api) {
console.log(api.baz())
})
复制代码
然后你可以在中间随时引用模块,但是模块第一次初始化的时间比较长。这就像开始的时候很拼搏很辛苦,到最后是美滋滋。
2.2CMD
通用模块定义,提供模块定义及按需执行模块。遵循依赖就近,代码在运行时,最开始的时候是不知道依赖的,需要遍历所有的require关键字,找出后面的依赖。一个常见的做法是将function toString后,用正则匹配出require关键字后面的依赖。CMD 里,每个 API 都简单纯粹。可以让浏览器的模块代码像node一样,因为同步所以引入的顺序是能控制的。 对于典型代表seajs,一般是这样子:
define(function(require,exports,module){
//...很多代码略过 var a = require(‘./a‘); //要用到a,于是引入了a //做一些和模块a有关的事情
});
复制代码
对于b.js依赖a.js
//a.js
define(function(require, exports) {
exports.a = function(){//也可以把他暴露出去 // 很多代码 };
});
//b.js
define(function(require,exports){
//前面干了很多事情,突然想要引用a了 var fun = require(‘./a‘);
????console.log(fun.a()); // 就可以调用到及执行a函数了。
})
//或者可以use
seajs.use([‘a.js‘], function(a){
//做一些事情
});
复制代码
AMD和CMD对比: AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。
AMD需要先列出清单,后面使用的时候随便使用(依赖前置),异步,特别适合浏览器环境下使用(底层其实就是动态创建script标签)。而且API 默认是一个当多个用。
CMD不需要知道依赖是什么,到了改需要的时候才引入,而且是同步的,就像临时抱佛脚一样。
对于客户端的浏览器,一说到下载、加载,肯定就是和异步脱不了关系了,注定浏览器一般用AMD更好了。但是,CMD的api都是有区分的,局部的require和全局的require不一样。
3.CommonJS与ES6
3.1 ES6
ES6模块的script标签有点不同,需要加上type=‘module‘
<script src=‘./a.js‘ type=‘module‘>...</script>
复制代码
对于这种标签都是异步加载,而且是相当于带上defer属性的script标签,不会阻塞页面,渲染完执行。但是你也可以手动加上defer或者async,实现期望的效果。 ES6模块的文件后缀是mjs,通过import引入和export导出。我们一般是这样子:
//a.mjs
import b from ‘b.js‘
//b.mjs
export default b
复制代码
ES6毕竟是ES6,模块内自带严格模式,而且只在自身作用域内运行。在ES6模块内引入其他模块就要用import引入,暴露也要用export暴露。另外,一个模块只会被执行一次。 import是ES6新语法,可静态分析,提前编译。他最终会被js引擎编译,也就是可以实现编译后就引入了模块,所以ES6模块加载是静态化的,可以在编译的时候确定模块的依赖关系以及输入输出的变量。ES6可以做到编译前分析,而CMD和AMD都只能在运行时确定具体依赖是什么。
3.2CommonJS
一般服务端的文件都在本地的硬盘上面。对于客户,他们用的浏览器是要从这里下载文件的,在服务端一般读取文件非常快,所以同步是不会有太大的问题。require的时候,马上将require的文件代码运行
代表就是nodejs了。用得最多的,大概就是:
//app.js
var route = require(‘./route.js‘)//读取控制路由的js文件
//route.js
var route = {......}
module.exports = route
复制代码
require 第一次加载脚本就会马上执行脚本,生成一个对象
区别: CommonJS运行时加载,输出的是值的拷贝,是一个对象(都是由module.export暴露出去的),可以直接拿去用了,不用再回头找。所以,当module.export的源文件里面一些原始类型值发生变化,require这边不会随着这个变化而变化的,因为被缓存了。但是有一种常规的操作,写一个返回那个值的函数。就像angular里面$watch数组里面的每一个对象,旧值是直接写死,新值是写一个返回新值的函数,这样子就不会写死。module.export输出一个取值的函数,调用的时候就可以拿到变化的值。
ES6是编译时输出接口,输出的是值的引用,对外的接口只是一种静态的概念,在静态解释后已经形成。当脚本运行时,根据这个引用去原本的模块内取值。所以不存在缓存的情况,import的文件变了,谁发出import的也是拿到这个变的值。模块里面的变量绑定着他所在的模块。另外,通过import引入的这个变量是只读的,试图进行对他赋值将会报错。