剛才的示例很簡單?實際上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 把所有要用到的插件都打包好了,同時相應的默認配置也為支付寶做了處理,方便內部員工使用,不用再配置了。