Hello Sea.js

使用指南

刚才的示例很简单?实际上Sea.js本身小巧而不失灵活,让我们再来深入地了解下如何使用Sea.js!

定义模块

Sea.js是CMD这个模块系统的一个运行时,Sea.js可以加载的模块,就是CMD规范里所指明的。那我们该如何编写一个CMD模块呢?

Sea.js提供了一个全局方法——define,用来定义一个CMD模块。

define(factory)

define(function(require, exports, module) {
    // 模块代码
    // 使用require获取依赖模块的接口
    // 使用exports或者module来暴露该模块的对外接口
})

factory是这样一个函数function (require?, exports?, module?) {},如果模块本身既不依赖其他模块,也不提供接口,requireexportsmodule都可以省略。但通常会是以下两种形式:

define(function(require, exports) {
    var Vango = require('vango')
    exports.drawCircle = function () {
        var vango = new Vango(document.body, 100, 100)
        vango.circle(50, 50, 50, {
            fill: true,
            styles:{
                fillStyle:"red"
            }
        })
    }
})

或者:

define(function(require, exports, module) {
    var Vango = require('vango');
    module.exports = {
        drawCircle: function () {
            var vango = new Vango(document.body, 100, 100);
            vango.circle(50, 50, 50, {
                fill: true,
                styles:{
                    fillStyle:"red"
                }
            });
        }
    };
});

注意:必须保证参数的顺序,即需要用到require, exports不能省略;在模块中exports对象不可覆盖,如果需要覆盖请使用module.exports的形式(这与node的用法一致,在后面的原理介绍会有相关的解释)。你可以使用module.exports来export任意的对象(包括字符串、数字等等)。

define(id?, dependencies?, factory)

id:String 模块标识

dependencies:Array 模块依赖的模块标识

这种写法属于Modules/Transport/D规范。

define('drawCircle', ['vango'], function(require, exports) {
    var Vango = require('vango');
    exports.drawCircle = function () {
        var vango = new Vango(document.body, 100, 100);
        vango.circle(50, 50, 50, {
            fill: true,
            styles:{
                fillStyle:"red"
            }
        });
    };
})

与CMD的define没有本质区别,我更情愿把它称作“具名模块”。Sea.js从用于生产的角度来说,必须支持具名模块,因为开发时模块拆得太小,生产环境必须把这些模块文件打包为一个文件,如果模块都是匿名的,那就傻逼了。(为什么会傻逼?

所以Sea.js支持具名模块也是无奈之举。

define(anythingelse)

除去以上两种形式,在CMD标准中,可以给define传入任意的字符串或者对象,表示接口就是对象或者字符串。不过这只是包含在标准中,在Sea.js并没有相关的实现。

配置Sea.js

Sea.js为了能够使用起来更灵活,提供了配置的接口。可配置的内容包括静态服务的位置,简化模块标识或路径。接下来我们来详细地了解下这些内容。

seajs.config(config)

config:Object,配置键值对。

Sea.js通过.configAPI来进行配置。你甚至可以在多个地方调用seajs.config来配置。Sea.js会mix传入的多个config对象。

seajs.config({
    alias: {
        'jquery': 'path/to/jquery.js',
        'a': 'path/to/a.js'
    },
    preload: ['seajs-text']
})
seajs.config({
    alias: {
        'underscore': 'path/to/underscore.js',
        'a': 'path/to/biz/a.js'
    },
    preload: ['seajs-combo']
})

上面两个配置会合并为:

{
    alias: {
        'jquery': 'path/to/jquery.js',
        'underscore': 'path/to/underscore.js',
        'a': 'path/to/biz/a.js'
    },
    preload: ['seajs-text', 'seajs-combo']

}

config可以配置的键入下:

base

base:String,在解析绝对路径标识的模块时所使用的base路径。

默认地,在不配置base的情况下,base与sea.js的引用路径。如果引用路径为http://example.com/assets/sea.js,则base为http://example.com/assets/

在阅读Sea.js这份文档时看到:
当 sea.js 的访问路径中含有版本号时,base 不会包含 seajs/x.y.z 字串。 当 sea.js 有多个版本时,这样会很方便。
即如果sea.js的引用路径为http://example.com/assets/1.0.0/sea.js,则base仍为http://example.com/assets/。这种方便性,我觉得过了点。

使用base配置,根本上可以分离静态文件的位置,比如使用CDN等等。

seajs.config({
    base: 'http://g.tbcdn.cn/tcc/'
})

如果我们有三个CDN域名,如何将静态资源散列到这三个域名上呢?

paths

paths:Object,如果目录太深,可以使用paths这个配置项来缩写,可以在require时少写些代码。

如果:

seajs.config({
    base: 'http://g.tbcdn.cn/tcc/',
    paths: {
        'index': 's/js/index'
    }
})

则:

define(function(require, exports, module) {
    // http://g.tbcdn.cn/tcc/s/js/index/switch.js
    var Switch = require('index/switch')
});

alias

alias:Object,本质上看不出和paths有什么区别,区别就在使用的概念上。

seajs.config({
    alias: {
        'jquery': 'jquery/jquery/1.10.1/jquery'
    }
})

然后:

define(function(require, exports, module) {
    // jquery/jquery/1.10.1/jquery
    var $ = require('jquery');
});

看出使用概念的区别了么?

preload

preload配置项可以让你在加载普通模块之前提前加载一些模块。既然所有模块都是在use之后才加载的,preload有何意义?然,看下面这段:

seajs.config({
    preload: [
        Function.prototype.bind ? '' : 'es5-safe',
        this.JSON ? '' : 'json'
    ]
});

preload比较适合用来加载一些核心模块,或者是shim模块。这是一个全局的配置,使用者无需关系核心模块或者是shim模块的加载,把注意力放在核心功能即可。

还有一些别的配置,比如varsmap等,可以参考配置

使用模块

seajs.use(id)

Sea.js通过use方法来启动一个模块。

seajs.use('./main')

在这里,./main是main模块的id,Sea.js在main模块LOADED之后,执行这个模块。

Sea.js还有另外一种启动模块的方式:

seajs.use(ids, callbacks)

seajs.use('./main', function(main) {
    main.init()
})

Sea.js执行ids中的所有模块,然后传递给callback使用。

插件

Sea.js官方提供了7个插件,对Sea.js的功能进行了补充。

  • seajs-text:用来加载HTML或者模板文件;
  • seajs-style:提供了importStyle,动态地向页面中插入css;
  • seajs-combo:该插件提供了依赖combo的功能,能把多个依赖的模块uri combo,减少HTTP请求;
  • seajs-flush:该插件是对seajs-combo的补充,或者是大杀器,可以先hold住前面的模块请求,最后将请求的模块combo成一个url,一次加载hold住的模块;
  • seajs-debug:Fiddler用过么?这个插件基本就是提供了这样一种功能,可以通过修改config,将线上文件proxy到本地服务器,便于线上开发调试和排错;
  • seajs-log:提供一个seajs.log API,私觉得比较鸡肋;
  • seajs-health:目标功能是,分析当前网页的模块健康情况。

由此可见,Sea.js的插件主要是解决一些附加问题,或者是给Sea.js添加一些额外的功能。私觉得有些功能并不合适让Sea.js来处理。

插件机制

总结一下,插件机制大概就是两种:

  • 使用Sea.js在加载过程中的事件,注入一些插件代码,修改Sea.js的运行流程,实现插件的功能;
  • 给seajs加入一些方法,提供一些额外的功能。

私还是觉得Sea.js应该保持纯洁;为了实现插件,在Sea.js中加入的代码,感觉有点不值;combo这种事情,更希望采取别的方式来实现。 Sea.js应该做好运行时。

构建与部署

很多时候,某个工具或者类库,玩玩可以,但是一用到生产环境,就感觉力不从心了。就拿Sea.js来说,开发的时候根据业务将逻辑拆分到很多小模块,逻辑清晰,开发方便。但是上线后,模块太多,HTTP请求太多,就会拖慢页面速度。

所以我们必须对模块进行打包压缩。这也是SPM的初衷。

SPM是什么?

使用者认为SPM是Sea.js Package Manager,但是实际上代表的是Static Package Manager,及静态包管理工具。如果大家有用过npm,你可以认为SPM是一个针对前端模块的包管理工具。当然它不仅仅如此。

SPM包括:

  • 源服务:类似于npm源服务器的源服务;
  • 包管理工具:相当于npm的命令行,安装、发布模块,解决模块依赖;
  • 构建工具:模块CMD化、合并模块、压缩等;都是针对我们一开始提到的问题;
  • 配置管理:管理配置;
  • 辅助功能:比较像Yeoman,以插件提供一些便于平时开发的组件。

SPM心很大,SPM囊括yo、bower和grunt这三个工具。

spm

spm is a package manager, it is not build tools.

这句话来自github上spm2的README文件。spm是一个包管理工具,不是构建工具!,它与npm非常相似。

spm的包规范

一个spm的模块至少包含:

-- dist
    -- overlay.js
    -- overlay.min.js
-- package.json
package.json

在模块中必须提供一个package.json,该文件遵循Common Module Definition模块标准。与node的package.json兼容。在此基础上添加了两个key。

  • family,即是包发布者在spmjs.org上的用户名;
  • spm,针对spm的配置。

一个典型的package.json文件:

{
    "family": "arale",
    "name": "base",
    "version": "1.0.0",
    "description": "base is ....",
    "homepage": "http://aralejs.org/base/",
    "repository": {
        "type": "git",
        "url": "https://github.com/aralejs/base.git"
    },
    "keywords": ["class"],

    "spm": {
        "source": "src",
        "output": ["base.js", "i18n/*"],
        "alias": {
            "class": "arale/class/1.0.0/class",
            "events": "arale/events/1.0.0/events"
        }
    }
}
dist

dist目录包含了模块必要的模块代码;可能是使用spm-build打包的,当然只要满足两个条件,就是一个spm的包。

安装

$ npm install spm -g

安装好了spm,那该如何使用spm呢?让我们从help命令开始:

help

我们可以运行spm help查看spm所包含的功能:

$ spm help

  Static Package Manager

  Usage: spm <command> [options]

  Options:

    -h, --help     output usage information
    -V, --version  output the version number

  System Commands:

    plugin         plugin system for spm
    config         configuration for spm
    help           show help information

  Package Commands:

    tree           show dependencies tree
    info           information of a module
    login          login your account
    search         search modules
    install        install a module
    publish        publish a module
    unpublish      unpublish a module

  Plugin Commands:

    init           init a template
    build          Build a standar cmd module.

spm包含三种命令,系统命令,即与spm本身相关(配置、插件和帮助),包命令,与包管理相关,插件命令,插件并不属于spm的核心内容,目前有两个插件initbuild

也可以使用help来查看单个命令的用法:

$ spm help install

  Usage: spm-install [options] family/name[@version]

  Options:

    -h, --help               output usage information
    -s, --source [name]      the source repo name
    -d, --destination [dir]  the destination, default: sea-modules
    -g, --global             install the package to ~/.spm/sea-modules
    -f, --force              force to download a unstable module
    -v, --verbose            show more logs
    -q, --quiet              show less logs
    --parallel [number]      parallel installation
    --no-color               disable colorful print


  Examples:

   $ spm install jquery
   $ spm install jquery/jquery arale/class
   $ spm install jquery/jquery@1.8.2

config

我们可以使用config来配置用户信息、安装方式以及源。

; Config username
$ spm config user.name island205

; Or, config default source 
$ spm config source.default.url http://spmjs.org

spm是一个包管理工具,与npm类似,有自己的源服务器。我们可以使用search命令来查看源提供的包。

由于spm在包规范中加入了family的概念,常常想运行spm install backbone,发现并没有backbone这个包。原因就是backbone是放在gallery这族下的。

$ spm search backbone

  1 result

  gallery/backbone
  keys: model view controller router server client browser
  desc: Give your JS App some Backbone with Models, Views, Collections, and Events.

install

然后我们就可以使用install来安装了,注意我们必须使用包的全名,即族名/包名

$ spm install gallery/backbone

        install: gallery/backbone@stable
          fetch: gallery/backbone@stable
       download: repository/gallery/backbone/1.0.0/backbone-1.0.0.tar.gz
           save: c:\Users\zhi.cun\.spm\cache\gallery\backbone\1.0.0\backbone-1.0.0.tar.gz
        extract: c:\Users\zhi.cun\.spm\cache\gallery\backbone\1.0.0\backbone-1.0.0.tar.gz
          found: dist in the package
      installed: sea-modules\gallery\backbone\1.0.0
        depends: gallery/underscore@1.4.4

        install: gallery/underscore@1.4.4
          fetch: gallery/underscore@1.4.4
       download: repository/gallery/underscore/1.4.4/underscore-1.4.4.tar.gz
           save: c:\Users\zhi.cun\.spm\cache\gallery\underscore\1.4.4\underscore-1.4.4.tar.gz
        extract: c:\Users\zhi.cun\.spm\cache\gallery\underscore\1.4.4\underscore-1.4.4.tar.gz
          found: dist in the package
      installed: sea-modules\gallery\underscore\1.4.4

spm将模块安装在了sea_modules中,并且在~/.spm/cache中做了缓存。

`~sea-modules/
  `~gallery/
    |~backbone/
    | `~1.0.0/
    |   |-backbone-debug.js
    |   |-backbone.js
    |   `-package.json
    `~underscore/
      `~1.4.4/
        |-package.json
        |-underscore-debug.js
        `-underscore.js

spm还加载了backbone的依赖underscore

当然,Sea.js也是一个模块,你可以通过下面的命令来安装:

$ spm install seajs/seajs

seajs的安装路径为sea_modules/seajs/seajs/2.1.1/sea.js,看到这里,结合seajs顶级模块定位的方式,对于seajs在计算base路径的时,去掉了seajs/seajs/2.1.1/的原因。

build

spm并不是以构建工具为目标,它本身是一个包管理器。所以spm将构建的功能以插件的功能提供出来。我们可以通过plugin命令来安装build

$ spm plugin install build

安装好之后,如果你使用的是标准的spm包模式,就可以直接运行spm build来进行标准的打包。

SPM2的功能和命令就介绍到这里,更多的命令在之后的实践中介绍。

spm与spm2

spm与spm2

其实之前介绍的spm是其第二个版本spm2。spm的第一个版本可以在这里找到。

spm与spm2同样都是包管理工具,那它们之间有什么不同呢?

  • 从定位上,spm2更加强调该工具是一个CMD包管理工具;
  • 从提供的用户接口(cmd命令)spm2比起spm更加规范,作为包管理工具,在使用方式和命令都更趋同于npm;
  • 在spm2中,构建命令以插件的方式独立出来,并且分层清晰;Transport和Concat封装成了grunt,便于自定义build方式;基于基础的grunt,构建了一个标准的spm-build工具,用于构建标准的CMD模块;
  • 与此类似,deploy和init的功能都是以插件的形式提供的;
  • 修改了package.json规范。

为什么作者对spm进行了大量的重构?

之所以进行大量的重构,就是为了保持spm作为包管理工具的特征。如npm一般,只指定最少的规范(package.json),提供包管理的命令,但是这个包如何构建,代码如何压缩并不是spm关心的事情。

只有规则简单合理,只定义接口,不关心具体实现,才有更广的实用性。

spm本身是从业务需求成长起来的一个包管理工具,spm1更多的是一些需求功能的堆砌,而spm2就是对这些功能的提炼,形成一套适用于业界的工具。

apm

apm的全称是:

Alipay package manager

即支付宝的包管理工具。

apm是基于spm的一套专门为支付宝开发的工具集。我们可以这么看,spm2和apm是spm升级后的两个产物,spm2更加专注于包管理和普适性,而apm更加专注于支付宝业务。由于业务细节和规模的不同,apm可能并不适合其他公司所用,所以需要spm2,而又因为支付宝业务的特殊性和基因,必须apm。

谢谢 @lepture 的指正:

不一定要用 apm,只是 apm 把所有要用到的插件都打包好了,同时相应的默认配置也为支付宝做了处理,方便内部员工使用,不用再配置了。