使用JS模块化编程AMD时,需要遵循的一些规则约定
模块声明不要写 ID
将模块 ID 交给应用页面决定,便于重构和模块迁移。模块开发者应该适应这点,从模块定义时就决定模块名称的思路中解放出来。这是使用 AMD 的开发者能获得的最大便利。
// good define( function (require) { var sidebar = require('./common/sidebar'); function sidebarHideListener(e) {} return { init: function () { sidebar.on('hide', sidebarHideListener) sidebar.init(); } }; } ); // bad define( 'main', function (require) { var sidebar = require('./common/sidebar'); function sidebarHideListener(e) {} return { init: function () { sidebar.on('hide', sidebarHideListener) sidebar.init(); } }; } );
模块划分应尽可能细粒度
细粒度划分模块,有助于更精细地进行模块变更、依赖、按需加载和引用等方面的管理,有利于让系统结构更清晰,让设计上的问题提早暴露,也能从一定程度上避免一些看起来也合理的循环依赖。
举个例子:在 namespace 模式下我们可能将一些 util function 通过 method 方式暴露,在 AMD 模块划分时,应该拆分成多个模块。
// good: 分成多个模块 define( function () { function comma() {} return comma; } ); define( function () { function pad() {} return pad; } ); // bad define( function () { return { comma: function () {}, pad: function () {} }; } );
在 factory 中使用 require 引用依赖模块,不要写 dependencies 参数
需要啥就在当前位置 require 一个,然后马上使用是最方便的。当模块文件比较大的时候,在头部在 dependencies 中添加一个依赖,然后在 factory 里添加一个参数书写十分不便。
另外,只使用 dependencies 参数声明依赖的方式,解决不了循环依赖的问题。为了项目中模块定义方式的一致性,也应该统一在 factory 中使用 require 引用依赖模块。
// good define( function (require) { var sidebar = require('./common/sidebar'); function sidebarHideListener(e) {} return { init: function () { sidebar.on('hide', sidebarHideListener) sidebar.init(); } }; } ); // bad define( ['./common/sidebar'], function (sidebar) { function sidebarHideListener(e) {} return { init: function () { sidebar.on('hide', sidebarHideListener) sidebar.init(); } }; } );
对于要使用的依赖模块,即用即 require
遵守 即用即 require 的原则有如下原因:
- require 与使用的距离越远,代码的阅读与维护成本越高。
- 避免无意义的
装载时依赖。对于循环依赖,只要依赖环中任何一条边是运行时依赖,这个环理论上就是活的。如果全部边都是装载时依赖,这个环就是死的。遵守即用即 require可以有效避免出现死循环依赖。
// good define( function (require) { return function (callback) { var requester = require('requester'); requester.send(url, method, callback); }; } ); // bad define( function (require) { var requester = require('requester'); return function (callback) { requester.send(url, method, callback); }; } );
对于 package 依赖,require 使用 Top-Level ID;对于相同功能模块群组下的依赖,require 使用 Relative ID
这条的理由与 模块声明不要写 ID 相同,都是为了获得 AMD 提供的模块灵活性。
// good define( function (require) { var _ = require('underscore'); var conf = require('./conf'); return {} } ); // bad define( function (require) { var _ = require('underscore'); var conf = require('conf'); return {} } );
相同功能模块群组 的界定需要开发者自己分辨,这取决于你对未来变更可能性的判断。
下面的目录结构划分中,假设加载器的 baseUrl 指向 src 目录,你可以认为 src 下是一个 相同功能模块群组;你也可以认为 common 是一个 相同功能模块群组,biz1 是一个 相同功能模块群组。如果是后者,biz1 中模块对 common 中模块的 require,可以使用 Relative ID,也可以使用 Top-Level ID。
但是无论如何,common 或 biz1 中模块的相互依赖,应该使用 Relative ID。
project/
|- src/
|- common/
|- conf.js
|- sidebar.js
|- biz1/
|- list.js
|- edit.js
|- add.js
|- main.js
|- dep/
|- underscore/
|- index.html
模块的资源引用,在 factory 头部声明
有时候,一些模块需要依赖一些资源,常见一个业务模块需要依赖相应的模板和 CSS 资源。这些资源需要被加载,但是在模块内部代码中并不一定会使用它们。把这类资源的声明写在模块定义的开始部分,会更清晰。
另外,为了便于重构和模块迁移,对于资源的引用,resource ID 也应该使用 Relative ID 的形式。
define( function (require) { require('css!./list.css'); require('tpl!./list.tpl.html'); var Action = require('er/Action'); var listAction = new Action({}); return listAction; } );
package 内部模块对主模块的依赖,不使用 require(‘.’)
package 开发者会指定一个主模块,通常主模块就叫做 main。package 内其他模块对它的依赖可以使用 require(‘.’) 和 require(‘./main’) 两种方式。
但是,我们无法排除 package 的使用者在配置 package 的时候,认为把另外一个模块作为主模块更方便,从而进行了非主流的配置。
// 非主流 package 配置 require.config({ baseUrl: 'src', packages: [ { name: 'esui', location: '../dep/esui', main: 'notmain' } ] });
使用第三方库,通过 package 引入
通常,在项目里会用到一些第三方库,除非你所有东西都自己实现。就算所有东西都自己实现,基础的业务无关部分,也应该作为独立的 package。
一个建议是,在项目开始就应该规划良好的项目目录结构,在这个时候确定 package 的存放位置。一个项目的源代码应该放在一个独立目录下(比如叫做 src),这里面的所有文件都是和项目业务相关的代码。存放第三方库 package 的目录应该和项目源代码目录分开。
project/
|- src/
|- common/
|- conf.js
|- sidebar.js
|- biz1/
|- list.js
|- edit.js
|- add.js
|- main.js
|- dep/
|- underscore/
|- index.html
如果有可能,定义一种 package 目录组织的规范,自己开发的 package 都按照这个方式组织,用到的第三方库也按照这种方式做一个包装,便于通过工具进行 package 的管理(导入、删除、package间依赖管理等)。
业务重复的功能集合,趁早抽取 package
这和尽早重构是一个道理。那么,什么样的东西需要被抽取成 package 呢?
- 如果项目业务无关的基础库或框架是自己开发的,那一开始就应该作为 package 存在。
- 业务公共代码一般是不需要抽取成 package 的。
- 一些业务公共模块集,如果预期会被其他项目用到,就应该抽取成 package。举个例子,正在开发的项目是面向 PC 的,项目中有个数据访问层,如果之后还要做 Mobile 的版本,这个数据访问层就应该抽象成 package。
CDN
因为性能的考虑,线上环境静态资源通过 CDN 分发是一种常用做法。此时,静态资源和页面处于不同的域名下,线上环境的 Loader 配置需要通过 paths,让 Loader 能够正确加载静态资源。
require.config({
baseUrl: 'src',
paths: {
'biz1': 'http://static-domain/project/biz1',
'biz2': 'http://static-domain/project/biz2'
}
});
如果所有的模块都整体通过 CDN 分发,可以直接指定 baseUrl。
require.config({
baseUrl: 'http://static-domain/project'
});
可以对环境和模块进行区分,不需要太强迫症
有的第三方库,本身更适合作为环境引入,基本上项目所有模块开发时候都会以这些库的存在为前提。这样的东西就更适合作为环境引入,不一定 非要把它当作模块,在每个模块中 require 它。
典型的例子有 es5-shim / jquery 等。
直接作为环境引入的方法是,在页面中,在引入 Loader 的 script 前引入。
<script src="es5-shim.js"></script> <script src="amd-loader.js"></script>

浙公网安备 33010602011771号