使用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>

 

posted @ 2015-01-16 15:30  relucent  阅读(258)  评论(0)    收藏  举报