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

(后续会继续补充……)

时间: 2024-10-09 20:46:42

JS模块化开发:使用SeaJs高效构建页面的相关文章

JS模块化开发(四)——构建工具gulp

gulp.js——基于流的自动化构建工具 步骤: 1. 全局安装 gulp: $ npm install --global gulp 2. 作为项目的开发依赖(devDependencies)安装: $ npm install --save-dev gulp 3. 在项目根目录下创建一个名为 gulpfile.js 的文件: var gulp = require('gulp'); gulp.task('default', function() { // 将你的默认的任务代码放在这 }); 4.

JS模块化开发(三)——seaJs+grunt

1.seaJs直接构建存在的问题 由于模块之间的依赖require引用的是模块名,当多个js模块被合并成一个时,会由于找不到模块名而报错 2.seaJs+grunt开发 用到的插件:grunt-cmd-transport和grunt-cmd-concat       (CMD规范) grunt-cmd-transport:提取本模块的ID和本模块所依赖的ID(数组格式),作为define函数的两个参数:define(ID,[依赖数组],function(require,exports,modul

JS模块化开发(requireJS)

使用模块化开发的好处: 通过 exports 暴露接口.这意味着不需要命名空间了,更不需要全局变量.这是一种彻底的命名冲突解决方案. 通过 require 引入依赖.这可以让依赖内置,开发者只需关心当前模块的依赖,其他事情 Sea.js/ Require.js 都会自动处理好.对模块开发者来说,这是一种很好的 关注度分离,能让程序员更多地享受编码的乐趣 实现JS模块化开发,目前有两个规范,一个是AMD规范,一个是CMD规范. RequireJS遵循的是AMD规范.AMD推崇依赖前置. SeaJS

js模块化开发

为什么要进行模块化开发? 1.当你写了一个这样的comm.js文件时,里面有function tab(){}方法,这时,你给你的同事用,你同事把这个comm.js引入页面后,在页面上又写了一个function tab(){},这时,就会覆盖你在comm.js中的tab方法.因为这个同事不知道你comm.js中有tab这个方法名. 2.如果你同事在页面上引入了你写的comm.js后,又引入了一个第三方插件xxx,而这个xxx插件里面刚好也有tab方法,那这时就会出现问题.你同事就会找你,说你写的c

JS模块化开发----require.js

前言 前端开发中,起初只要在script标签中嵌入几十上百行代码就能实现一些基本的交互效果,后来js得到重视,应用也广泛起来了,jQuery,Ajax,Node.Js,MVC,MVVM等的助力也使得前端开发得到重视,也使得前端项目越来越复杂,然而,JavaScript却没有为组织代码提供任何明显帮助,甚至没有类的概念,更不用说模块(module)了,所以,进行模块化开发的重要性就不言而喻了.那么什么是模块呢? 一个模块就是实现特定功能的文件,有了模块,我们就可以更方便地使用别人的代码,想要什么功

js模块化开发--AMD--CMD

什么是模块化开发? 前端开发中,起初只要在script标签中嵌入几十上百行代码就能实现一些基本的交互效果,后来js得到重视,应用也广泛起来了,jQuery,Ajax,Node.Js,MVC,MVVM等的助力也使得前端开发得到重视,也使得前端项目越来越复杂,然而,JavaScript却没有为组织代码提供任何明显帮助,甚至没有类的概念,更不用说模块(module)了,那么什么是模块呢? 一个模块就是实现特定功能的文件,有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块.模块开发

js模块化开发——前端模块化

在JavaScript发展初期就是为了实现简单的页面交互逻辑,寥寥数语即可:如今CPU.浏览器性能得到了极大的提升,很多页面逻辑迁移到了客 户端(表单验证等),随着web2.0时代的到来,Ajax技术得到广泛应用,jQuery等前端库层出不穷,前端代码日益膨胀 这时候JavaScript作为嵌入式的脚本语言的定位动摇了,JavaScript却没有为组织代码提供任何明显帮助,甚至没有类的概念,更不用说模块(module)了,JavaScript极其简单的代码组织规范不足以驾驭如此庞大规模的代码 模

JS模块化开发(一)

模块化开发要解决的问题: 1.冲突 比如:多人协作开发时,不同js库中的函数重名问题 可以用命名空间解决: var module={} module.a=1; module.b=function(){} ... 命名空间的缺点: 1)写起来较为复杂(特别是涉及到多级命名空间时) 2)只能减少冲突,不能完全消除冲突(两个开发人员恰巧用了同名的命名空间) 2.依赖 引入了多个存在依赖关系js库,后期维护不断增删js库,导致混乱,出现问题

从node.js模块化开发来了解javascript闭包

之前看过很多关于javascript闭包的解释,只有短短几个demo,一大段晦涩难懂的介绍,巴拉巴拉一大段闭包的概念,但是到最后也没看懂闭包到底是什么意思,起什么作用,直到昨晚凌晨,我在学习node.js模块开发的时候,才突然恍然大悟,所以赶紧记下来,免得以后忘记.代码来源:廖雪峰的网站node.js教程. 直接上代码: 我们首先编写了一个hello.js文件,这个hello.js文件就是一个模块,模块的名字就是文件名(去掉.js后缀),所以hello.js文件就是名为hello的模块. 我们把