linqinghao/blog

谈 Javascript 模块化

Opened this issue · 0 comments

前面的话

为什么要说说前端模块化?我觉得理解前端模块化可以帮助我们更好的理清一个项目里的前端架构以及更好的提高自己的开发效率。

以我的见解,前端发展到现在已经可以算是比较成熟的局面了,从ajax到nodejs的出现,各种框架也层出不穷,已经出现了非常好的技术栈结构,比如 npm+webpack+es6+react/angular/vuejs,vuejs在今年也是火热,webpack这模块构建工具也是一匹黑马,感觉稳压grunt和gulp了。

当我还是个小前端的时候,写的js代码真的是惨不忍睹,最简单的js模块化仅仅只是多了个命名空间,防止全局变量命名冲突。写这篇文章,也是帮助自己更好的理解前端模块化。

我认为,前端在发展的路上有三个里程碑:一个是jquery,一个是前端模块化,另一个是组件化。

为什么需要模块化?

  • Ajax的兴起,前端处理的逻辑日益复杂
  • 文件的耦合度越来越高
  • 项目架构组织的需要

最简单的模块化

以往写代码都是这样的:

function foo(){
    //...
}
function bar(){
    //...
}

这样写的坏处就是污染了全局对象,很容易出现命名冲突。

再加上命名空间吧,这样就能防止命名冲突了。但是js是没有命名空间的,用对象来模拟。

var myModule = {  // 命名空间
	foo: function() {},
	bar: function() {}
}; 
myModule.foo();

这样子就减少了全局对象的变量,也能很好的解决命名冲突的问题,但是这样不安全,我们在外部可以随意修改内部成员。比如:

var app = {
	title : "app",
	show: function(){}
}
app.title = "hello";

如何解决这个问题?可以通过js的立即执行函数来隐藏内部成员。于是:

var myModule = (function(){
    var _private = "i am private";
    function foo(){
    	console.log(_private);
    }
  
    return {
        foo: foo,
    };
})();
myModule.foo();
myModule._private;  // undefined

我们只将模块的公用方法暴露出来,这样在模块外部无法修改我们没有暴露出来的变量、函数。

那如何引入依赖?立即执行函数(IIFE)可以这样做:

var Module = (function($){
    var _$body = $("body");     // we can use jQuery now!
    var foo = function(){
        console.log(_$body);    // 特权方法
    }

    // Revelation Pattern
    return {
        foo: foo
    }
})(jQuery);

Module.foo();

这就是实现js模块化的基础。

文件依赖

有了封装,还是不够的。比如我们的项目结构有可能是这样:

<script src="jquery.js"><script>
<script src="dialog.js"><script>
<script src="tooltip.js"><script>
<script src="toast.js"><script>
<script src="handlebar.js"><script>
...

这就造成了一些问题,依赖关系模糊,我们需要手动解析模块和库的依赖项,在特别大的项目里,会越来越难以管理。并且,这样也会造成请求过多。因此,我们需要依赖管理和按需加载。

CommonJS

CommonJS是一种规范,它是由NodeJS发扬光大的,第一个流行的模块化规范是由服务器端的JavaScript应用带来的。

模块的定义和引用

// module1.js
// 模块定义
function log() {
	console.log("Hello Module");
}

module.exports = log;


// index.js
// 加载模块
var module1 = require('./module1');

module1.log();

这就是模块的定义以及引用的简单例子,我们可以发现require是同步的,也就是阻塞的。同步加载对于服务器不是问题,但是浏览器由于网速个方面原因的限制,它是异步加载的。因此CommonJS不适用于浏览器端。后来就出现了AMD以及CMD规范。

AMD和CMD

AMD

AMD 即Asynchronous Module Definition,中文名是异步模块定义的意思。它是一个在浏览器端模块化开发的规范,实际上,AMD 是 RequireJS 在推广过程中对模块定义的规范化的产出。

一个简单的应用Requirejs的例子:

// 定义模块 myModule.js
define(
    ["jquery"],  //依赖
    function($){  //这个回调会在所有依赖都被加载后才执行
		function log() {
			console.log($('body'));
		}
		return {
			log: log;
		}
});

// 加载模块 main.js
require(['myModule'], function(myModule) {
	myModule.log();
});

require()函数在加载依赖的函数的时候是异步加载的,这样浏览器不会失去响应,它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。

CMD

CMD 即Common Module Definition通用模块定义,CMD规范是国内发展出来的,CMD是 SeaJS 对模块定义的规范化产出,SeaJS要解决的问题和requireJS一样,只不过在模块定义方式和模块加载(可以说运行、解析)时机上有所不同。

一个简单的应用Seajs的例子:

// 定义模块  myModule.js
define(function(require, exports, module) {
  var $ = require('jquery.js')
  $('div').addClass('active');
});

// 加载模块
seajs.use(['myModule.js'], function(my){

});

AMD和CMD的区别

两者最明显的区别就是在模块定义对依赖的处理不同

  • AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块
  • CMD推崇就近依赖,只有在用到某个模块的时候再去require

两者都是异步加载模块,只是对依赖模块的执行时机处理不同。

在最后

其实,这不是最后,Javascript模块化还没完。BrowserifyWebpack让Commonjs规范在浏览器实现,ES6的到来也有自己实现的Module,由于本人也在学习的过程,这里就先不表述。

最后,因为本人能力有限,有什么表述错误都欢迎指正!thanx:)

参考资料

  1. JavaScript:彻底理解同步、异步和事件循环
  2. 前端模块化
  3. JavaScript 模块化七日谈