您的位置:首页 > Web前端 > JavaScript

Javascript模块化编程学习小结

2016-04-04 10:43 519 查看
拥抱模块化的javascript

随着越来越多的后端逻辑转移到前端,网页中的js代码量也是与日俱增,只是靠函数式的方式来组织代码已经渐渐变得力不从心,这时类的方式出现了,Prototype、YUI、Ext等框架渐渐流行开来,以满足大量javascript代码的开发。

而紧接着nodejs横空出世,这个服务端的javascript采用模块化的写法很快就在前端领域产生了巨大影响,大牛们纷纷效仿,各种模块化的规范也是浮出水面。CommonJS想统一前后端的模块写法,但AMD却被认为是最适合浏览器的,无论如何,模块化编程已经在javascript中流行了起来。

但是javascript本身并不是一种模块化编程语言,它不支持类(class),更别说模块(module)了,虽然ECMAScript第六版已经开始支持类和模块,但目前来说,了解javascript模块化编程依然是很有必要的。Javascript社区做了很多努力,在现有运行环境中,实现了模块的效果。

一、模块基本写法-信息隐藏

模块应设计的使其所包含的信息(过程和数据)对于那些不需要用到它的其他模块不可见。每个模块只完成一个独立工程,然后提供该功能的接口。模块间通过接口访问。Javascript中可以用函数去隐藏,封装,然后返回接口对象。比如:

var math = (function(){
var count = 0;
var add = function(){
//...
};
var sub = function(){
//...
};
return {
add: add;
sub: sub
};
})();
这是javascript模块的基本写法,此时外部无法访问也无法修改count变量。

二、模块的升级版写法

如果一个模块很大,必须分成很多部分,每个部分可能新增某些功能,或者模块之间需要有继承关系,这时就需要用这种“放大模式”的写法:

var module = (function(mod){
mod.m1 = function(){
//...
};
return mod;

})(module);
在浏览器环境中,模块的各个部分通常都是网上获取的,有时无法知道哪个部分先加载。如果采用上面的方法可能会导致加载一个不存在的空对象,这时就需要采用下面这种“宽放大模式”。

var module = (function(mod){
//...

return mod;
})(window.module || {});
与“放大模式”相比,“宽放大模式”就是立即执行函数的参数可以是空对象。

三、输入全局变量

独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。

为了在模块内部调用全局变量,必须显式地将其他变量输入模块。

var module = (function($,YAHOO){

//...

})(jQuery,YAHOO);


四、模块规范之CommonJS
2009年,美国程序员Ryan Dahlchu创造了node.js项目,将javascript语言用于服务器端编程。



node.js的模块系统,就是参照CommonJS规范实现的。在CommonJS中,有一个全局性方法require(),用于加载模块。假设有一个模块math.js,就可以像下面这样加载并调用其方法。

var math = require("math");
math.add(2,3); //5
这个主要针对服务端编程,客户端怎么办呢,最好的当然是两者都能兼容,一个模块不用修改,在客户端和服务端都能运行。

但有一个重大局限,采用这种方式加载模块是同步完成的,也就是必须等某一个模块加载完成后才能运行下面的语句,如果加载时间很长,整个应用都会卡在那里等。

这对服务端不是问题,因为所有模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但这显然并不适合浏览器,因此,浏览器端的模块,不能采用“同步加载”,只能采用“异步加载”,于是AMD诞生了。

五、模块规范之AMD

AMD是“Asynchronous Module Definition”的缩写,意思是“异步模块定义”。所有以来这个模块的语句,都定义在一个回调函数中。

AMD也采用require()语句加载模块,但它要求两个参数:

若将前面的代码改成AMD形式,就是下面这样:

require(['math'], function(math){
math.add(2,3);
});


六、require.js
为什么要用require.js?

下面这段代码,相信很多人都见过。

<script src="1.js"></script>
<script src="2.js"></script>
<script src="3.js"></script>
<script src="4.js"></script>
<script src="5.js"></script>
<script src="6.js"></script>
<script src="7.js"></script>
这段代码依次加载多个js文件,这样的写法有很大缺点。首先,加载的时候没浏览器会停止网页渲染,加载文件越多,网页失去响应的时间越长;其次,由于js文件之间往往存在依赖关系,因此必须严格保证加载顺序,当一来关系很复杂时候,代码的编写和维护都会变得困难。

于是,require.js诞生了。



(1)实现js文件的异步加载,避免网页失去响应;

(2)管理模块之间的依赖性,便于代码的编写和维护。

require.js的加载

先去官网下载最新版本,下载后,根据放置目录就可以加载了。

<script src="js/require-2.2.0.js" data-main="main"></script>
你可能会觉得加载这个文件,也可能造成网页失去响应。解决办法有两个,一是将它放在网页底部加载,另一个是这样:

<script src="js/require-2.2.0.js" defer async="true"></script>
async表明异步加载,而IE不支持这个属性,只支持defer,所以defer最好也加上。

现在可以进一步加载自己的代码了,假设我们的代码文件是main,js,也在js目录下:

<script src="js/require-2.2.0.js" data-main="js/main"></script>
data-main属性的作用是指定网页程序的主模块。该文件会被require.js第一个加载。由于require.js默认的文件后缀名是js,所以可以把main.js简写成main。



七、主模块的写法

主模块一般会依赖于其他模块,这时就要使用AMD规范定义的require()函数。

//main.js
require(['moduleA','moduleB','moduleC'], function(moduleA,moduleB,moduleC){

//...

});
第一个参数数组内的模块加载后会以参数的形式传入该函数。

实际例子:

//main.js
require(['jquery','underscore','backbone'], function($, _, Backbone){

//...

});

八、模块的加载

上节的写法是默认了moduleA、moduleB、moduleC与main.js在同一目录下,而实际情况当然要复杂一些,因此我们可以采用require.config()方法,对模块的加载行为进行自定义。

//main.js
require.config({
paths:{
"jquery": "lib/jquery.min",
"underscore": "lib/underscore.min",
"backbone": "lib/backbone.min"

}
})
或者直接更改 基目录(baseUrl)。

//main.js
require.config({
baseUrl: "js/lib",
paths:{
"jquery": "jquery.min",
"underscore": "underscore.min",
"backbone": "backbone.min"
}
})
或者指定远程网址:

require.config({
paths:{
"jquery": "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min"
}
})


九、AMD模块的写法
具体来说,就是模块必须采用特定的额define()函数来定义。如果一个模块不依赖其他模块,则直接定义在define()函数中。

// math.js
define(function (){
var add = function (
b62d
x,y){
return x+y;
};
return {
add: add
};
});
如果这个模块依赖其他模块,那么应该这样写:

// math.js
define(['myLib'], function(myLib){
function foo(){
myLib.doSomething();
}
return {
foo : foo
};
});
当require()函数加载上面这个模块的时候,就会先加载myLib.js文件。

十、requireJS路劲解析问题

baseUrl:基础中的基础

首先需要说明,在requirejs的模块路劲解析里,baseUrl是非常基础的概念,所有的路劲都是requirejs基于baseUrl指定的路劲来寻找。

举个栗子,如下:

<script src="js/require.js" data-main="js/main.js"></script>


在demo.html里加载了require.js,同时在require.js所在的script上申明了data-main属性,那么,requirejs加载下来后,它会做两件事:

1、加载js/main.js

2、将baseUrl设置为data-main指定的文件所在的路劲,这里是js/

那么,下面依赖的lib模块实际路劲为js/lib.js

require(['lib'], function(Lib){
// do sth
});
如果没有通过data-main属性指定baseUrl,也米有通过config显式申明,那么baseUrl默认为加载requirejs的那个页面所在路劲

baseUrl + path:让依赖更简洁、灵活

原始代码:

//main.js
requirejs.config({
baseUrl: 'js'
});

// 加载一堆水果
require(['common/fruits/apple', 'common/fruits/orange', 'common/fruits/grape', 'common/fruits/pears'], function(Apple, Orange, Grape, Pears){
// do sth
});
改进后代码:

//main.js
requirejs.config({
baseUrl: 'js',
paths: {
fruits: 'common/fruits'
}
});

// 加载一堆水果
require(['fruits/apple', 'fruits/orange', 'fruits/grape', 'fruits/pears'], function(Apple, Orange, Grape, Pears){
// do sth
});
对于改进后的好处不需要多说。

paths:简单但需要记住的要点

看看下面三种情况:

//main.js
requirejs.config({
baseUrl: 'js',
paths: {
common: 'common/fruits'
}
});

<strong>// 从左到右,加载的路径依次为 js/apple.js、 js/common/fruits/apple.js、common/apple.js</strong>
require(['apple', 'common/apple', '../common/apple'], function(){
// do something
});

关于define()内部依赖模块路劲解析问题

首先来个例子:

//main.js
requirejs.config({
baseUrl: 'js'
});
// 依赖lib.js,实际加载的路径是 js/common/lib.js,而lib模块又依赖于util模块('./util'),解析后的实际路径为 js/common/util.js
require(['common/lib'], function(Lib){
Lib.say('hello');
});
// lib.js
define(['./util'], function(Util){
//...
});
解析后的实际路径为 js/common/util.js

下面改个写法:

//main.js
requirejs.config({
baseUrl: 'js',
paths: {
lib: 'common/lib'
}
});

// 实际加载的路径是 js/common/lib.js
require(['lib'], function(Lib){
Lib.say('hello');
});
// lib.js
define(['./util'], function(Util){
//...
});
解析后的实际路径为 js/util.js

为什么会出现这个问题?这个对于初识requirejs并且没看过源码的人确实难懂,你只需要记住以下几点:

1、对于baseUrl的解析需要注意,当满足以下条件,将不会相对baseUrl

(1)以“/”开头

(2)以“.js”结尾

(3)包含各种协议

2、最终lib.js中解析的依赖模块路劲跟main.js中require()函数中的依赖模块路劲的parent有关。什么是parent?举个栗子:

// main.js
require(['common/lib'], function(Lib){
//...
});

// lib.js define(['./util'], function(Util){ //... });//解析路劲为common/util
// main.js
require(['lib'], function(Lib){
//...
});

// lib.js define(['./util'], function(Util){ //... });//解析路劲为util
// main.js
require(['a/c/d/lib'], function(Lib){
//...
});

// lib.js define(['./util'], function(Util){ //... });//解析路劲为a/c/d/util


关于其背后的深入原因,有兴趣的同学可以看看叶小钗大神写的这篇博客requireJS路劲加载
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息