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

文章源自:http://www.diguage.com/archives/82.html

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

       前段时间转载了阮一峰老师的两篇讲解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模块定义规范中有好多定义方法。简单起见,目前只考虑使用如下这一种方式。)如下:
define(function(require, exports, module) {

	// The module code goes here

});

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

  1. 对外提供模块接口。在上一步中,我们在函数内定义了模块,但是这是在函数内定义的,在函数外部不容易访问到。该怎么向外提供模块接口呢?定义方法如下:
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 。

  1. 获取外部依赖模块。模块定义要了,需要使用的时候,就可以使用require函数获取外部依赖。具体代码如下:
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分别加载吗?

刚刚D瓜哥开窍了一下,main.js只是一个例子,可以根据自己的组件名称命名,然后在组件中加载相对应的JavaScript文件即可。另 外,在配置package.json时,突然觉得,在/assets/main/src/下每个目录应该算是一个模块,都有一个打包的配置文件 package.json,用于配置该模块的必要信息。不知这样理解是否正确?这个还有待考证。

相关推荐