🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
刚才的示例很简单?实际上Sea.js本身小巧而不失灵活,让我们再来深入地了解下如何使用Sea.js! ## 定义模块 Sea.js是[CMD](https://github.com/cmdjs/specification/blob/master/draft/module.md)这个模块系统的一个运行时,Sea.js可以加载的模块,就是CMD规范里所指明的。那我们该如何编写一个CMD模块呢? Sea.js提供了一个全局方法——`define`,用来定义一个CMD模块。 #### `define(factory)` ~~~ define(function(require, exports, module) { // 模块代码 // 使用require获取依赖模块的接口 // 使用exports或者module来暴露该模块的对外接口 }) ~~~ `factory`是这样一个函数`function (require?, exports?, module?) {}`,如果模块本身既不依赖其他模块,也不提供接口,`require`、`exports`和`module`都可以省略。但通常会是以下两种形式: ~~~ 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](http://wiki.commonjs.org/wiki/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从用于生产的角度来说,必须支持具名模块,因为开发时模块拆得太小,生产环境必须把这些模块文件打包为一个文件,如果模块都是匿名的,那就傻逼了。([为什么会傻逼?](https://github.com/seajs/seajs/issues/930)) > 所以Sea.js支持具名模块也是无奈之举。 #### `define(anythingelse)` 除去以上两种形式,在CMD标准中,可以给define传入任意的字符串或者对象,表示接口就是对象或者字符串。不过这只是包含在标准中,在Sea.js并没有相关的实现。 ## 配置Sea.js Sea.js为了能够使用起来更灵活,提供了配置的接口。可配置的内容包括静态服务的位置,简化模块标识或路径。接下来我们来详细地了解下这些内容。 #### seajs.config(config) **config**:Object,配置键值对。 Sea.js通过`.config`API来进行配置。你甚至可以在多个地方调用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/。这种方便性,我觉得过了点。](http://example.com/assets/1.0.0/sea.js%EF%BC%8C%E5%88%99base%E4%BB%8D%E4%B8%BAhttp://example.com/assets/%E3%80%82%E8%BF%99%E7%A7%8D%E6%96%B9%E4%BE%BF%E6%80%A7%EF%BC%8C%E6%88%91%E8%A7%89%E5%BE%97%E8%BF%87%E4%BA%86%E7%82%B9%E3%80%82) 使用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模块的加载,把注意力放在核心功能即可。 还有一些别的配置,比如`vars`、`map`等,可以参考[配置](https://github.com/seajs/seajs/issues/262)。 ## 使用模块 #### `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](https://github.com/spmjs/spm2)的README文件。`spm是一个包管理工具,不是构建工具!`,它与npm非常相似。 #### spm的包规范 一个spm的模块至少包含: ~~~ -- dist -- overlay.js -- overlay.min.js -- package.json ~~~ ##### package.json 在模块中必须提供一个package.json,该文件遵循[Common Module Definition](https://github.com/cmdjs/specification)模块标准。与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`的核心内容,目前有两个插件`init`和`build`。 也可以使用`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 ~~~ #### search `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](https://github.com/spmjs/spm2)。spm的第一个版本可以在[这里](https://github.com/spmjs/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 把所有要用到的插件都打包好了,同时相应的默认配置也为支付宝做了处理,方便内部员工使用,不用再配置了。