首页 > Web开发, 挨踢(IT) > Javascript模块化编程(三):模块化编程实战,试用SeaJS

Javascript模块化编程(三):模块化编程实战,试用SeaJS

2012年12月21日 发表评论 阅读评论 4,613 人阅读    

  前段时间转载了阮一峰老师的两篇讲解Javascript模块化编程的文章:
“JavaScript模块化编程(一):模块原型和理论概念详解”,介绍了Javascript模块原型和理论概念;Javascript模块化编程(二):模块化编程实战,require.js详解,介绍了在实战中,如何利用RequireJS库,进行模块化编程。

  在这两篇文章发布出来之后,在和网友的交流讨论中,了解到了SeaJS,这个由国人玉伯自己创建的模块化编程库。然后,我就想学习学习,再写篇文章给大家介绍一下。

背景介绍

  官网的资料是最靠谱的。在SeaJS的官网上发现,有一个“5分钟上手SeaJS”的例子,然后就从这个例子的开始学习。不过,只看明白了个六六七七。我没看明白和我平时的JavaScript编程有啥区别。另外,我没有动手实践,心里面不踏实。所以,动手写个程序,玩味一下。后来,就想到了“猜手机号游戏”!

  由于官网已经有使用SeaJS的教程,我就不重复这方面的工作了,而且我也觉得我我肯定没有官网写的好。由于我不清楚,使用SeaJS进行“模块化编程”和我平时不进行“模块化编程”的区别。所以,我准备从另外一个角度来介绍SeaJS:将一个没有进行模块化编程的程序,改造成使用SeaJS进行“模块化编程”的程序。由于这个想法的跨度比较大,信息量也比较多。所以我把我的想法组织成了三篇文章:第一篇文章,“给哥三十五次机会,哥就能猜中你的手机号”,通过一个小游戏,来吸引大家的兴趣;第二篇,“‘猜手机号游戏’的源码分析:二分查找+面向对象”,来讲解在没有进行模块化编程时,程序的实现细节;然后,这是第三篇,在没有进行模块化编程的基础上,将原来的程序改造成一个使用SeaJS进行模块化编程的例子。

  在阅读这篇文章之前,请阅读前两篇,尤其是“‘猜手机号游戏’的源码分析:二分查找+面向对象”。同时,还建议阅读一下“JavaScript模块化编程(一):模块原型和理论概念详解”Javascript模块化编程(二):模块化编程实战,require.js详解,规范、系统一下关于Javascript模块化编程的知识。

CMD模块定义规范介绍

  想享受模块化编程带来的良好封装,就必须遵循模块化编程的规范。在 SeaJS 中,所有 JavaScript 模块都遵循 CMD(Common Module Definition) 规范。该规范确定了模块的基本书写格式和基本交互方式。所以,使用SeaJS之前,必须阅读一下SeaJS所要求遵循的规范。

  鉴于规范覆盖的东西比较多,看多了头大。所以,我把这个规范提炼简化一下,只关注我们需要用到的。至于,更详细的CMD模块定义规范,等先把例子跑通,理解了整个流程,然后再回头看规范,梳理、规范这部分知识。

  在介绍简化版规范之前,D瓜哥提两个也许大家都回“纳闷”的问题:

  1. 如何定义模块?
  2. 如何获取外部依赖的模块?

  CMD模块定义规范中的主要内容正是回答这两个问题。下面请看经D瓜哥简化的规范如下:

  1. 定义、封装模块的方法。(CMD模块定义规范中有好多定义方法。简单起见,目前只考虑使用如下这一种方式。)如下:
  2. 
    define(function(require, exports, module) {
    
    	// The module code goes here
    
    });
    
    

      这里需要特别说明一下,向参数传递的三个参数名必须按照代码所示中那样写,不能简写,或者使用其他字符串代替;同时,在函数内,exports不能被改写成其他值;可以把exports看成对象添加属性,如exports.key,然后对其复制,又如exports.key = “dValue”。

  3. 对外提供模块接口。在上一步中,我们在函数内定义了模块,但是这是在函数内定义的,在函数外部不容易访问到。该怎么向外提供模块接口呢?定义方法如下:
  4. 
    define(function(require, exports, module) {
      
      // 实用这种方式向外提供模块接口
      module.exports = {
        foo: 'bar',
        doSomething: function() {};
      };
      
      // 或者。由于,D瓜哥将模块封装成了一个对象,所以,本例中,使用这个方式。
      module.exports = yourFunctionName;
      
    });
    
    

      传给 factory 构造方法(就是define(function(){})方法参数中那个函数,称为factory。函数只是factory的一种形式,其他形式以后再补充。)的 exports 参数是 module.exports 对象的一个引用。只通过 exports 参数来提供接口,有时无法满足开发者的所有需求。 比如当模块的接口是某个类的实例时,需要通过 module.exports 来实现。D瓜哥这里就是一个对象,所以只能使用module.exports 。

  5. 获取外部依赖模块。模块定义要了,需要使用的时候,就可以使用require函数获取外部依赖。具体代码如下:
  6. define(function(require, exports, module) {
    
      // 使用require函数获取外部依赖
      var a = require('./a');
      a.doSomething();
    
    });
    

      require函数的参数是a.js文件的相对路径。后缀名可以省略,在SeaJS加载模块的时候会自动加上的。另外,这里可以执行回调函数。不过,我们的任务是跑起来。因为不需要回调函数,所以这部分先略过了。

  总结一下:define函数,定义模块;module对象,保存模块信息;require函数,获取外部依赖模块。

  看到这里,估计大家还是一头雾水。没关系,慢慢往下看,下面的例子跑起来的时候,你再回头看就会明白的。

模块化改造

  先声明一下,下面的改造过程会参考“5分钟入门”的说明。所以,建议大家先看看。当然,一起看也可以。

  通过看”5分钟入门”的例子可以看出,SeaJS的目录结构还是有点复杂的。所以,最简单的方法就是,把她的例子下载下来,在她的基础之上修改:“5分钟入门”例子下载

目录结构

  下载完成后,解压到任意目录下。请看一下目录,

  1. hello-seajs/下放我们的html文件;
  2. hello-seajs/assets/sea-modules下存放的是我们需要用到的第三方模块块;
  3. hello-seajs/assets/main,这个目录可以说最重要,是存放我们自己编写的JavaScript和CSS文件的地方。下面还有四个子目录及一个文件:
    1. src存放正常的代码;
    2. test存放测试代码;
    3. docs存放文档;
    4. examples存放示例代码;
    5. package.json是打包的配置文件;

“改造”模块代码

  下面,我们开始改造我们的模块。

  首先,把我GuessNumber.js放到hello-seajs/assets/main/src/下。然后,按照“第1条规范”的要求改造这个文件中代码。由于整个文件就是GuessNumber对象的定义。同时,这个JavaScript文件又没有引用其他模块。所以,只需要在文件的第一行增加define,在最后一行增加括号分号就行。具体代码如下:


define(function(require, exports, module){
	/**
	* numberScope 需要猜测的数字范围
	*/
	function GuessNumber(numberScope){

        // 为了突出修改的代码,我把一些相同的代码省略了,
        // 完整代码请看:http://www.diguage.com/archives/80.html
	
    }
	
	GuessNumber.prototype = {
    
		constructor: GuessNumber,
	
		// 完整代码请看:http://www.diguage.com/archives/80.html
	}

});

  其次,目前我们已经定义为一个模块。但是外部如何访问这个GuessNumber?所以,我们要向外部提供一个接口,提供方式参考“第2条规范”。具体代码见第18行:


define(function(require, exports, module){
	/**
	* numberScope 需要猜测的数字范围
	*/
	function GuessNumber(numberScope){
    
		// 完整代码请看:http://www.diguage.com/archives/80.html
	
    }
	
	GuessNumber.prototype = {
    
		constructor: GuessNumber,
	
		// 完整代码请看:http://www.diguage.com/archives/80.html
	}

	module.exports = GuessNumber;
    
});

  这时,一个接口已经全部定义完成。下面,我们书写调用这个模块的例子。

  在“规范”的第三条中,我们说明了加载外部依赖模块的方法,我们只需要按说明照做就行。另外,还需要补充一下模块加载时需要注意的地方。具体请看代码注释:


define(function(require) {
	// 这是引入jQuery类库,我们下面说明为什么这样下。
    var $ = require('jquery');
	
	// 引入GuessNumber模块,也就是GuessNumber.js文件。
	// 参数中传递的是GuessNumber.js文件的相对路径。
	// .js的后缀名可以省略,SeaJS在加载的时候会自动加上。
	var GuessNumber = require("./GuessNumber");
    
    // 完整代码请看:http://www.diguage.com/archives/80.html
	
	//格式化显示结果
	function formatResult(num, type) {
		//……
	}
	
	// ……
	
	$("#initButton").click(function(){
		guess.start(scopeArr[type].min, scopeArr[type].max);
		showResult();
	});
});

  从上面的代码中,可以看出,main.js文件的改造,只是把原来的

$(document).ready(function(){

	// 主要的业务代码

});

改造成了,

define(function(require) {
	// 这是引入jQuery类库,我们下面说明为什么这样下。
    var $ = require('jquery');
	
	// 引入GuessNumber模块,也就是GuessNumber.js文件。
	// 参数中传递的是GuessNumber.js文件的相对路径。
	// .js的后缀名可以省略,SeaJS在加载的时候会自动加上。
	var GuessNumber = require("./GuessNumber");
    
    // 和原文件相同的业务代码

});

另外,加了两行倒入必要关联模块的代码。仅此而已。

  main.js与GuessNumber.js不同的还有一点,main.js不需要向外提供访问接口。这点也要注意一下。

  到这里所有的JavaScript都已经修改完毕了。下面,我们修改一下如何在HTML中的引入方式。

在页面中加载模块

  原来的写法是,按顺序使用<scrip>标签把jQuery、GuessNumber.js以及main.js文件引入到HTML页面中即可。如果使用SeaJS,则需要先加载SeaJS的类库,然后使用JavaScript通过SeaJS的接口来加载所需的模块,也就是模块对应的JavaScript文件。具体代码如下:

<!-- 首先,首先我们需要引入 sea.js -->
<script src="assets/sea-modules/seajs/1.3.1/sea.js"></script>
<script type="text/javascript">
    seajs.config({
        alias: {
            // 指定使用的jQuery版本以及说明jQuery的路径
            // 请注意:这里知名了jQuery的路径,所以,我们
            // 在引入jQuery库时,只需要填写jquery即可。
            'jquery': 'gallery/jquery/1.8.2/jquery' 
        }
    });
   
    // 然后SeaJS通过 use 方法来加载模块,以后打包后也是修改这里
    // 也许你会疑问为什么不加载GuessNumber.js文件,
    // 这个在使用require引入依赖时,SeaJS自动加载需要的外部文件
    // 另外,这里的.js后缀名也可以省略,SeaJS会自动补全。
    seajs.use('./assets/main/src/main');
    
</script>
<!-- 这里只展示了和JavaScript引入相关的代码 -->
<!-- 完整代码请看:http://www.diguage.com/archives/80.html 中的HTML代码 -->

  到此,改造工作就全部完成了。你可以打开一下inde.html文件,看看效果了。

打包部署

  根据“高性能网站的十四条黄金法则”中的实践,我们在实际项目上线时,为了提高页面的加载速度,必定要压缩一下JavaScript文件。这些,SeaJS也考虑到了,甚至做得更好:还做了文件合并。

  这里,需要先介绍一下,SPM,一个基于命令行的前端项目管理工具。 SPM 和 SeaJS 关系密切,你甚至可以认为SPM是为SeaJS专门打造的工具。首先,请“安装教程”安装好这个工具。按照过程可能会有一个问题,请参考下面的“出现的问题”。

  使用SPM打包,需要修改一下打包的配置文件。配置文件是:hello-seajs/assets/main/package.json。打开后内容如下:


{
  "name": "main",
  "version": "1.0.0",
  "dependencies": {
    "jquery": "gallery/jquery/1.8.2/jquery"
  },
  "root": "hello-seajs",
  "output": {
    "main.js": ".",
    "main.css": "."
  },
  "spmConfig": {
    "build": {
      "to": "../sea-modules/{{root}}/{{name}}/{{version}}"
    }
  }
}

  不过,这个需要根据我们的实际情况来修改。root属性,由于我们的模块是“猜数”,所以将其修改为GuessNumber;output属性,我们只需要输出JS,所以删除main.css。另外,需要注意,第十四行,这个是打包后的输出路径。好了,开始打包。打包需要执行如下指令:

$ cd hello-seajs/assets/main
$ spm build 
...
BUILD SUCCESS!
$

  打包结束后,在hello-seajs/assets/中就会发现多了一个GuessNumber文件夹,那个就是打包输出出来。

  这里说明一下:D瓜哥只在Linux下执行了这么命令。不知在Windows是否好使。为了方便大家测试运行,打包结果已提交,下载的代码中包含打包结果。

  观察这个结果,大家会发现只有一个main.js和main-debug.js;顾名思义,main.js是用于生产部署的,经过压缩的文件;main-debug.js是为测试使用的,只是合并了代码并没有压缩,使用的时候直接引用这个两个文件中的一个就行,直接把seajs.use()中的路径改一下就OK。GuessNumber.js哪里去了啊?大家可以打开main-debug.js看看(main.js也行,只是压缩过来,可读性不好),原来,GuessNumber.js已经合并到了main.js中了。SPM把两个文件合并成一个文件了,这样在浏览器访问网页时,就可以减少一个HTTP请求,提高网页的加载速度。

  另外,大家也可能会注意到在原来main.js中定义的define()函数,在新的main.js有了一些变化,多了两个参数:第一个参数模块的ID,主要是为了方便区别一个文件中的各个模块;第二个参数是模块依赖的外部模块的路径,因为依赖的模块可能有多个,所以这个参数是一个数组。第三个参数是原来的function,也就是factory。更详细的解释请看:为什么要用 spm 来压缩 CMD 模块?

  懒人要把懒进行到底!打包后还要修改SeaJS的加载路径,这点其实还可以使用如下代码来避免:

// 这个路径只有在部署到服务器上才行,直接打开文件不好使。
seajs.use(location.host === 'localhost' ? './assets/main/src/main' : 'GuessNumber/main/1.0.0/main');

如果是非静态页面,也可以使用变量来配置。

折腾中出现的问题

  折腾这么个玩意,难免出现一些问题,D瓜哥遇到了三个问题。这些问题主要集中在SPM环境搭建过程中。给大家分享一下。

  第一个问题:按照seajs时,提示info.json不存在的错误。终端显示如下:

d@dPC:~/Dev/hello-seajs/assets$ spm install seajs
Start installing ...
success create global config.json to /home/d/.spm
Downloading: http://modules.spmjs.org/info.json
[ERROR] Caught exception: Error: not found config http://modules.spmjs.org/info.json

  大家可以在浏览器地址中打开http://modules.spmjs.org/info.json,会发现可以打开。这是怎么回事呢?

  我查阅了一下SeaJS论坛,里面有类似的问题。其中的一个回复,我拿过来当作解答吧:这段时间是举国同庆的日子,网络不稳定。至于原因,你懂得。估计等过了这段时间就没事了。所以,既然浏览器可以访问,则内容就可以访问到。遇到这个问题,多试两次就可以了。

  第二个问题:按照jquery库时,提示Error: ALREADY_EXISTS。终端显示如下:

d@dPC:~/Dev/hello-seajs/assets$ spm install gallery.jquery
Start installing ...
Downloading: http://modules.spmjs.org/gallery/info.json
Downloaded: http://modules.spmjs.org/gallery/info.json
Downloading: http://modules.spmjs.org/gallery/jquery/1.8.2/jquery.tgz
Downloaded: http://modules.spmjs.org/gallery/jquery/1.8.2/jquery.tgz
** This module already exists: /home/d/Dev/hello-seajs/assets/sea-modules/gallery/jquery/1.8.2
Turn on --force option if you want to override it.
[ERROR] Caught exception: Error: ALREADY_EXISTS

  其实,问题正如反馈信息所示,jQuery库已经存在,不需要再次下载了。我们在hello-sea这里例子的源代码中构建,这个源代码中已经包含了jQuery了,在这里这步可以忽略。

  第三个问题:修改了package.json后,重新编译报错。终端显示如下:

[WARN] http://modules.spmjs.org/GuessNumber/config.json null  

  这个不影响编译,直接忽略就行了。另外说明一下,在第一次打包时,没见这个错误;第二次会出现。

代码下载

  为了方便大家下载代码,我把代码托管到了Github上,大家可以去Github上下载、提交您的修改。Github页面:GuessNumber;不想去Github上下载的,也可以直接点击下载:点击下载

深入学习

  上面的例子只是简要把一个例子跑起来了,给大家一个比较形象的认知。但是,这个例子实在是太简单了。我还需补充我们刚才为了易于理解而简化的一些知识。为了更深入的了解SeaJS,请继续阅读“SeaJS 使用文档”。另外,这里有几个需要重点阅读,具体如下:

  1. CMD模块定义规范
  2. require 书写约定
  3. 模块标识
  4. API 快速参考
  5. 模块的加载启动,重点看里面的”最佳实践”
  6. 模块系统

  把这个列表中的东西看完,SeaJS的学习应该就可以出师了。有好的资料请给我推荐,我再补充上来。

遗留问题

  经过上面这些折腾,我们已经成功运行起来一个使用SeaJS进行模块化编程的例子。但是,我们还是有很多的疑问。具体疑问如下:

  1. D瓜哥在main.js中,并没有使用$(document).ready();等DOM加载完再运行,并也没有讲JS放到HTML文件的最后,为啥还能顺序执行呢?莫非SeaJS有什么内部机制,保证在DOM加载完成后再执行我们自己编写的JavaScript代码?
  2. 这里例子很小,并没有很多很多的模块。在模块很多的情况下,如果组织模块?这个还需要写更多的例子,实验一下。
  3. 同样,在很多模块的情况下,难道要建很多目录准备很多的main.js,让众多的HTML分别加载吗?
  4.   刚刚D瓜哥开窍了一下,main.js只是一个例子,可以根据自己的组件名称命名,然后在组件中加载相对应的JavaScript文件即可。另外,在配置package.json时,突然觉得,在/assets/main/src/下每个目录应该算是一个模块,都有一个打包的配置文件package.json,用于配置该模块的必要信息。不知这样理解是否正确?这个还有待考证。

未完待续

  这篇文章只是初略地让大家认知一下SeaJS。要想更深入地了解SeaJS的原理,D瓜哥觉得最靠谱的方法就是自己实现一个SeaJS。所以,下一篇,D瓜哥准备自己动手实现一个简化版的SeaJS。当然,为了便于理解SeaJS,D瓜哥的实现会参考“CMD模块定义规范”来编写代码。敬请期待!

PS:

  这篇文章代码比较多,排版整的不好。看着不是很爽,如有好的建议,请留言提出,D瓜哥立马改进。谢谢!

参考资料:

  参考资料在文章都出现了,这里就不再赘述了。



作 者: D瓜哥,https://www.diguage.com/
原文链接:https://wordpress.diguage.com/archives/82.html
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。

分类: Web开发, 挨踢(IT) 标签: ,
  1. 2012年12月22日00:48 | #1

    过来支持下地瓜哥。

    • D瓜哥
      2012年12月22日01:04 | #2

      谢谢付总的大力支持!赶快把网站搞起来,D瓜哥也去支持你!哈哈

  2. 2012年12月22日23:01 | #3

    一直坚持设计+前端的我,最近也开始捣鼓原生js及项目管理,学习中、成长中,受教了。

  3. 2013年12月29日21:31 | #4

    我也写了一篇,模块化的实现,呵呵
    http://www.cnblogs.com/hustskyking/p/how-to-achieve-loading-module.html

  4. 2015年4月26日21:51 | #5

    好棒

  5. 2015年8月21日15:05 | #6

    最励志网:http://www.zuilizhi.net/? 前来拜访,欢迎互访!

  6. 2015年8月22日10:05 | #7

    网站真不错 最励志网http://www.zuilizhi.net/?

  7. 2015年8月23日11:28 | #8

    不错不错,来看看。。

  8. 2015年8月31日00:26 | #9

    网站不错,雁过留痕,欢迎互访!

  9. 2015年10月2日09:44 | #10

    好东西 谢谢分享

  1. 2012年12月24日11:14 | #1
  2. 2015年1月12日00:08 | #2