JS模块化开发:使用SeaJs高效构建页面

一、扯淡部分

很久很久以前,也就是刚开始接触前端的那会儿,脑袋里压根没有什么架构、重构、性能这些概念,天真地以为前端===好看的页面,甚至把js都划分到除了用来写一些美美的特效别无它用的阴暗角落里,就更别说会知道js还有面向对象,设计模式,MVC,MVVM,模块化,构建工具等等这些高大上的概念了。现在想想还真是Too young too naive。前两天某大神在群里分享他招聘前端的心得的时候就说,就是那些以为能写两个页面就可以自称前端的人拉低了行业水平。这样看来前两年我还真的扯了不少后腿呢……

后来干这行干得稍久一些,发现水简直深深深深千尺,而且周围遍布沼泽。即便爬到岸上,迎接你的又是大大小小各种坑。坑爹的IE6,坑爹的兼容,坑爹的浏览器特性……总之,任何一个前端都有被这些大大小小的坑虐到体无完肤的惨痛经历。但(我觉得这个但字是点睛之笔),生活在继续,时代在发展,竞争依然残酷,你不往前走就只能在这片沼泽里不断下沉,最后挣扎的结果也不过是冒出水面两个泡泡然后……爆掉。

在经历了会写页面,会用js写效果的阶段后,大多数人都已经慢慢地能够满足产品提出的各种奇葩的功能需求,但仅仅是满足了需求,而没有考虑性能、团队协作、开发消耗的各种成本等等这些问题。有时候甚至写好的js再回头去看时也会让自己一头雾水:各种方法,各种逻辑杂乱无章地纠缠在一起,根本理不清谁调用了谁,谁为谁定义,谁又是谁的谁!更可怕的是当项目被其他小伙伴接管,每修改一处上线前都担惊受怕:修改这里到底TM对不对啊?

还好前端领域开路者们用他们的智慧朝我们艰难跋涉的水坑里扔了几块石头:尝试让你的代码模块化吧~

 

二、js模块化

为毛要尝试模块化开发?

如今的网页越来越像桌面程序,网页上加载的javascript也越来越复杂,coder们不得不开始用软件工程的思维去管理自己的代码。Javascript模块化编程,已经成为一个非常迫切的需求。理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。但是,Javascript不是一种模块化编程语言,它不支持"类"(class),更遑论"模块"(module)了。(正在制定中的ECMAScript标准第六版将正式支持"类"和"模块",但还需要很长时间才能投入实用。)

——来自阮一峰的博文:《Javascript模块化编程(一):模块的写法

上面其实已经把模块化的意义和目的已经讲述的很清楚了,所以就拿来主义,节省脑细胞留给下面的内容

模块化的概念出来以后,新的问题又来了:需不需要一个统一的模块化标准?我们来试想一下如果没有标准的情况:A以自己的标准写了模块Module1,然后B又以自己的标准写了Module2,恩,在他们看来,这的确是模块,但当Module1想调用模块Module2的时候该怎么调用呢?它们之间火星人与地球人交流,没有同声传译看起来依旧是毫无头绪。于是模块化规范便又成了一个问题。

2009年美国的一位大神发明了node.js (具体内容自行脑补,本文不作讨论),用来开发服务器端的js。我们都知道,传统的服务器端开发语言如PHP、JAVA等都必须进行模块化开发,JS想占据人家的地盘也不例外,模块化是必须的,于是commomJS模块化开发规范诞生了,但这货只是服务器端JS模块化开发的标准,客户端又没用。

—有童鞋:bla了那么多,这跟我在客户端进行js模块化开发有毛关系啊?

—PO主:表着急,了解了这玩意儿的前世今生,用起来才能得心应手~

服务器端JS模块化规范有了,JSer们自然想到了能把commonJS规范拿到客户端就好啦,而且最好两者能够兼容,一个模块不用修改,在服务器和浏览器都可以运行。爽爆~但(这个但字又是一个点睛之笔),由于一个重大的局限,使得CommonJS规范不适用于浏览器环境。服务器端获取资源的方式是本地读取,而客户端拿资源的方式是通过Http来获取,这是一个大问题,因为模块都放在服务器端,浏览器等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous),于是诞生了AMD和CMD。

—有童鞋:核心内容终于TMD来了,就是AMD和CMD这二货

—PO主:……

 

三、AMD和CMD

AMD (Asynchronous Module Definition) :  RequireJS 在推广过程中对模块定义的规范化产出。

AMD用白话文讲就是 异步模块定义,对于 JSer 来说,异步是再也熟悉不过的词了,所有的模块将被异步加载,模块加载不影响后面语句运行。所有依赖某些模块的语句均放置在回调函数中,等到依赖的模块加载完成之后,这个回调函数才会运行。

主要有两个Javascript库实现了AMD规范:require.jscurl.js

(本文主要分享的是SeaJs模块化构建方式,关于requireJs构建方式请移步至:《Javascript模块化编程(一):模块的写法》)

 

CMD (Common Module Definition) : SeaJS 在推广过程中对模块定义的规范化产出。

实现了CMD规范的主要的Javascript库:Sea.js

CMD翻译来就是 通用模块定义,与AMD的相同点:

1. 这些规范的目的都是为了 JavaScript 的模块化开发,特别是在浏览器端的。

2. 目前这些规范的实现都能达成浏览器端模块化开发的目的

当然与AMD也有有两点区别:

1. 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible(PO主:是越懒越好的意思么?)。

2. CMD 推崇依赖就近,AMD 推崇依赖前置

——SeaJs作者玉伯在知乎的回答

看代码理解上面两点的意思:

AMD模块的定义方法

// AMD 默认推荐的是
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好,即依赖前置,执行完引入的模块后才开始执行回调函数
    a.doSomething()
    // 此处略去 100 行
    b.doSomething()
    ...
})

CMD模块的定义方法:

// CMD
define(function(require, exports, module) {
    var a = require('./a')
    a.doSomething()
    // 此处略去 100 行
    var b = require('./b') // 依赖可以就近书写,即依赖就近,什么时候用到什么时候才引入
    b.doSomething()
    // ... 
})

好了,看过两个例子,对于之前没有接触过模块化开发的童鞋来说依旧是一头雾水:那个define是什么东东啊?还有那个require,exports,module,都是干什么的?表捉急,我们一步一步来。

在 CMD 规范中,一个模块就是一个文件。代码的书写格式如下:

define(factory);

来看github上CMD模块定义规范上的解释:

define 是一个全局函数,用来定义模块。

define 接受 factory 参数,factory 可以是一个函数,也可以是一个对象或字符串

factory 为对象、字符串时,表示模块的接口就是该对象、字符串。比如可以如下定义一个 JSON 数据模块:

1 define({ "foo": "bar" });

也可以通过字符串定义模板模块:

1 define('I am a template. My name is {{name}}.');

factory 为函数时,表示是模块的构造方法。执行该构造方法,可以得到模块向外提供的接口。factory 方法在执行时,默认会传入三个参数:require、exports 和 module

1 define(function(require, exports, module) {
2   // 模块代码
3 });
 
四、小例子
 
说了半天概念应该印象还不深刻,我们就来看一个例子用来演示sea.js的基本用法。首先define传入的参数是对象和字符串的情况,我先举一个参数的对象的例子,传字符串大同小异。来看代码:
 
1,我先来定义一个模块m1.js:
define({a:"这里是属性a的值"});

define传入的是一个对象字面量。现在这个东东就可以叫做一个模块了~我想在页面一加载的时候就把a的值alert出来,怎么做呢?继续往下看。

2,在页面上引入这个模块:

1 seajs.use('./m1.js',function(ex){
2      alert(ex.a);
3  }); //弹出“这里是属性a的值”

翻译得直白一点,大意就是:

seajs : Hi~m1.js,我现在要用(use)你了,然后把你的公开接口(exports)存到我回调函数的参数(ex)里,你把想给我调用的东东放到这个参数里吧~么么哒

m1.js : 好的,我定义的对象字面量放到接口里给你了,拿去尽管刷~

然后……a的值就弹出来了。很愉快的一次交易。PS:页面所调用的模块就为整个web应用的js入口。本例中js的入口就是m1.js。接下来再来看看如果define的参数是个函数的情况。
 
1,先定义一个模块m2.js:
1 define(function(require,exports,module){
2     var var1 = "这是要alert出来的值";//私有变量,没有通过接口返出去的其他模块不能访问
3     function alerts(){
4         alert(var1);
5     }
6     exports.alerts = alerts;//将需要公开的方法存入exports接口中
7 });

2,在页面上引入这个模块并执行模块m2.js公开的方法:

1 seajs.use('./m2.js',function(ex){
2      ex.alerts();//ex中存的有m2.js中的公开对象
3 }); //弹出“这是要alert出来的值”

到这里可以简单地说一下factory方法的三个形参的意义了(个人理解):

require : 提供了引入机制,提供了一种方式来建立依赖,和C中的include和java中的import类似;

exports : 提供了导出机制,提供了私有和共有分离,未使用exports语句导出的变量或者函数,其他模块即使引用此模块也不能使用;

module : 提供了模块信息描述。

是不是思路贱贱清晰了呢?刚才我们的例子中只是从页面调用模块的用法,模块之间互相调用还没有体现,SO,接下来就以m1.js和m2.js两个模块作为例子来尝试一下 模块之间互相调用

1,首先m1.js模块不变:

1 define({a:"这里是属性a的值"});

2,m2.js模块要依赖(require)m1.js:

1 define(function(require,exports,module){
2     var var1 = "这是要alert出来的值";//私有变量,没有通过接口返出去的其他模块不能访问
3     var var2 = require('./m1.js').a;//这里就是m2.js模块调用m1.js的方式:var2的值等于当前模块所依赖的m1.js对外接口中属性a的值
4     function alerts(){
5         alert(var2);
6     }
7     exports.alerts = alerts;//将需要公开的方法存入exports接口中
8 });

3,页面上引入m2.js模块(同上一个例子),结果就会把a的属性值给alert出来~

 

五、实例:模块化的拖拽个窗口缩放

 

当然,上面几个例子是简单到不能再简单的例子,估计亲们也已经看出来一些道道,但个人感觉还是没能体现出模块化开发的优势。那下面就来看一个实例:模块化的拖拽个窗口缩放。先看一下效果图:

PS:效果图中的红色区域要先定缩放的范围,即宽高0px-宽高500px。要写这样一个需求的例子,按照之前的编程习惯你会怎么写?反正在之前,我是会把所有的功能写到一个js文件里,效果出来就行,随你们怎么胡搅蛮缠。而自从认识了模块化开发,内心不止一次告诉自己,拿到需求bigger一定要高,一定要高(虽然require.js和sea.js这两个东东在圈内多多少少还是有些争议)……

废话少说,首先来分析一下需要划分多少个模块吧:

1,一开始就要有个入口模块的吧?恩,必须的!入口模块Get√~

2,既然是拖拽,要有个拖拽模块吧?恩,必须的!拖拽模块Get√~

3,既然要缩放,要有个缩放模块吧?恩,必须的!缩放模块Get√~

4,既然限定缩放范围<=500px,那还要有个限定缩放范围的模块吧?恩,这个可以有,但为了以后调整范围数值方便,还是单列个模块吧。限定缩放范围模块Get√~

到这里我们就把本需求划分成了四个模块:

·  入口模块:main.js

·  拖拽模块:drag.js

·  缩放模块:scale.js

·  限定缩放范围模块:range.js

首先,是页面引入入口模块(我尽量把注释都写在代码中,以便对照代码,这样也就不用写大片大片的文字了~):
1  <script>
2     seajs.use('./js/main.js');//没有callback函数表明引入后直接执行入口模块
3 </script>

接下来看看入口模块(main.js)里都应该有些神马东东吧:

 1 //入口模块
 2 define(function(require,exports,module){
 3     var $id = function(_id){return document.getElementById(_id);}
 4     var oInput = $id("button1");
 5     var div1 = $id("div1");
 6     var div2 = $id("div2");
 7     var div3 = $id("div3");//以上是获取页面元素的几只变量
 8     require('./drag.js').drag(div3);//引入拖拽模块,执行拖拽模块接口中的drag方法并传参
 9     exports.oInput = oInput;
10     oInput.onclick = function(){
11         div1.style.display = "block";
12         require('./scale.js').scale(div1,div2);//引入缩放模块,执行缩放模块接口中的scale方法并传参
13     }
14 });

恩,还真是全面呢,把拖拽模块和缩放模块都引进来了。看看拖拽模块(drag.js)吧~

 1 //拖拽模块
 2 define(function(require,exports,module){
 3     //这个方法就是实现拖拽的方法,不用详述了吧?
 4     function drag(obj){
 5         var disX = 0;
 6         var disY = 0;
 7         obj.onmousedown = function(e){
 8             var e = e || window.event;
 9             disX = e.clientX - obj.offsetLeft;
10             disY = e.clientY - obj.offsetTop;
11             document.onmousemove = function(e){
12                 var e = e || window.event;
13                 var l = require('./range.js').range(e.clientX - disX, document.documentElement.clientWidth - obj.offsetWidth,0);
14                 var t = require('./range.js').range(e.clientY - disY, document.documentElement.clientHeight - obj.offsetHeight,0);
15                 obj.style.left = l + "px";
16                 obj.style.top = t + "px";
17             }
18             document.onmouseup = function(){
19                 document.onmousemove = null;
20                 document.onmouseup = null;
21             }
22         }
23     }
24     exports.drag = drag;//返回拖拽模块中想要被公开的对象,也就是在本模块中定义的drag方法。注意有参数~
25 });

接下来是缩放模块(scale.js)。缩放模块还需要调用 限定缩放范围模块 (range.js) 的哦~这点不要搞忘了。

 1 //缩放模块
 2 define(function(require,exports,module){
 3     //这个方法就是obj2控制obj1改变大小的方法,也不再详述啦~
 4     function scale(obj1,obj2){
 5         var disX = 0;
 6         var disY = 0;
 7         var disW = 0;
 8         var disH = 0;
 9         obj2.onmousedown = function(e){
10             var e = e || window.event;
11             disX = e.clientX;
12             disY = e.clientY;
13             disW = obj1.offsetWidth;
14             disH = obj1.offsetHeight;
15             document.onmousemove = function(e){
16                 var e = e || window.event;
17                 var w = require('./range.js').range(e.clientX - disX + disW,500,100);//看这里看这里,引入了限定范围的range.js模块~
18                 var h = require('./range.js').range(e.clientY - disY + disH,500,100);
19                 obj1.style.width = w + "px";
20                 obj1.style.height = h + "px";
21             }
22             document.onmouseup = function(){
23                 document.onmousemove = null;
24                 document.onmouseup = null;
25             }
26         }
27     }
28     exports.scale = scale;//将需要公开的对象存入模块接口中,以便其他模块调用~
29 });

最后就是限定范围的模块(range.js)了。

 1 //限定拖拽的范围模块
 2 define(function(require,exports,module){
 3     function range(inum,imax,imin){
 4         if(inum > imax){
 5             return imax;
 6         }else if(inum < imin){
 7             return imin;
 8         }else{
 9             return inum;
10         }
11     }
12     exports.range = range;
13 });

这就是模块化,虽然在这个实例中我们用到了4个js,但在页面上我们只引入了一个入口模块main.js,其他模块都会按需自动引入(如下图所示),而且每个功能模块的区分特别清晰,再也不用担心神马命名冲突啊、依赖混乱啊之类的,而且团队小伙伴每人负责一个模块,只要放出当前模块的公开接口并提供简要的说明文档(因为标准统一),其他小伙伴们写的模块就能非常方便地调用到你写的模块,连修改的时候都不用考虑对其他功能的影响,变得更大胆了呢~

查看完整DEMO请猛戳

 
写在最后
 
其实本文介绍的模块化和seajs的使用依旧比较浅显,但基本的模块化思想已经融入到例子中了。 如果你经历过前文所述的以前写js逻辑的各种纠结各种坑爹,不妨尝试一下将你的代码模块化,那将是一种飞一样的感觉……本文最后会为大家列出一些相关的资料,想深入了解的小伙伴们可以果断收走~
 
SeaJs官网 : http://seajs.org/docs/

CMD 模块定义规范:https://github.com/seajs/seajs/issues/242

玉伯:AMD和CMD的区别:http://www.zhihu.com/question/20351507/answer/14859415 

AMD and CMD are dead之js模块化黑魔法 : http://www.cnblogs.com/iamzhanglei/p/3790346.html

(后续会继续补充……)

 

 

 

posted @ 2014-11-07 17:54  Horve  阅读(3091)  评论(10编辑  收藏  举报