Venus是一個javascript類庫,是一個canvas的wrapper,為了學習spm,我們使用cmd的模式來重構這個類庫。
spm提供了初始cmd模塊的腳手架,我們可以使用下面的命令來安裝這個腳手架:
$ spm plugin install init
運行:
$ spm init
就可以初始化一個cmd模塊的項目,回答一些spm的問題,就能在當前目錄生成必要的文件和文件夾:
|~examples/
| `-index.md
|~src/
| `-venus.js
|~tests/
| `-venus-spec.js
|-LICENSE
|-Makefile
|-package.json
`-README.md
我們在src
中添加venus
的代碼。
或者將現(xiàn)有的模塊轉(zhuǎn)化為cmd模塊。
本例中的Venus本來就已經(jīng)存在,那我們?nèi)绾螌⑵滢D(zhuǎn)成cmd模塊呢?
在Venus的源碼中我驚喜地發(fā)現(xiàn)這段代碼:
// File: vango.js
/*
* wrapper for browser,nodejs or AMD loader evn
*/
(function(root, factory) {
if (typeof exports === "object") {
// Node
module.exports = factory();
// AMD loader
} else if (typeof define === "function" && define.amd) {
define(factory);
} else {
// Browser
root.Vango = factory();
}
})(this, function() {
// Factory for build Vango
})
這段代碼可以令vango.js
支持瀏覽器(通過script直接引入)、node環(huán)境以及AMD加載器。
于是事情就簡單了,因為我們可以很簡單地將一個Node模塊轉(zhuǎn)成CMD模塊,添加如下的wrapper即可:
// File: vango.js
define(function (require, exports, module) {
(function(root, factory) {
if (typeof exports === "object") {
// Node
module.exports = factory();
// AMD loader
} else if (typeof define === "function" && define.amd) {
define(factory);
} else {
// Browser
root.Vango = factory();
}
})(this, function() {
// Factory for build Vango
// return Vango
})
})
上面那段有點黑魔法的代碼還有一個更復雜的形式,即Universal Module Definition:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define('backbone', ['jquery', 'underscore'], function (jQuery, _) {
return factory(jQuery, _);
});
} else if (typeof exports === 'object') {
// Node.js
module.exports = factory(require('jquery'), require('underscore'));
} else {
// Browser globals
root.Backbone = factory(root.jQuery, root._);
}
}(this, function (jQuery, _) {
var Backbone = {};
// Backbone code that depends on jQuery and _
return Backbone;
}));
當然并不是所有的CMD模塊都得這么寫,你可以按照自己的方式,使用require
、exports
和module
這三個關鍵字,遵循CMD的規(guī)范即可。
最后src
有兩個文件,venus.js
就很簡單了:
define(function (require, exports, module) {
var Vango = require('./vango');
exports.Vango = Vango;
})
我們的venus的cmd版本搞定了,vango.js作為vango具體實現(xiàn),而venus.js只是這些將這些畫家暴露出來。
作為標準的cmd模塊,我們可以使用spm-build
來構建,別忘了之前提到的,你可以使用spm plugin install build
來安裝。
在項目的根目錄下運行spm build
:
$ spm build
Task: "clean:build" (clean) task
Task: "spm-install" task
Task: "transport:src" (transport) task
transport: 2 files
Task: "concat:css" (concat) task
concated: 0 files
Task: "transport:css" (transport) task
transport: 0 files
Task: "concat:js" (concat) task
concated: 2 files
Task: "copy:build" (copy) task
Task: "cssmin:css" (cssmin) task
Task: "uglify:js" (uglify) task
file: ".build/dist/venus.js" created.
Task: "clean:dist" (clean) task
Task: "copy:dist" (copy) task
copied: 2 files
Task: "clean:build" (clean) task
cleaning: ".build"...
Task: "spm-newline" task
create: dist/venus-debug.js
create: dist/venus.js
Done: without errors.
從構建的log中可以看出,spm
完全就是使用grunt來構建的,涉及到多個grunt task。你完全可以自己編寫Gruntfile.js來實現(xiàn)自定義的構建過程。
venus就被構建好了,spm
在目錄中生成了一個dist
文件夾:
|~dist/
|-venus-debug.js
`-venus.js
venus-debug.js
中的內(nèi)容為:
define("island205/venus/1.0.0/venus-debug", [ "./vango-debug" ], function(require, exports, module) {
var Vango = require("./vango-debug");
exports.Vango = Vango;
});
define("island205/venus/1.0.0/vango-debug", [], function(require, exports, module) {
// Vango's code
})
venus.js
的內(nèi)容與之一樣,只是經(jīng)過了壓縮,去掉了模塊名最后的-debug
。
spm
將src
中的vango.js和venus.js根據(jù)依賴打到了一起。作為包的主模塊,venus.js被放到了最前面。
這是Sea.js的默認約定,打包后的模塊文件其中的一個define即為該包的主模塊(ID 和路徑相匹配的那一個),也就是說,你通過
require('island205/venus/1.0.0/venus')
,雖然Sea.js加載的是整個打包的模塊,但是會把的一個factory的exports作為venus暴露的接口。
如果你用過npm,那你對spm的發(fā)布功能應該不會陌生了。spm也像npm一樣,有一個公共倉庫,我們可以通過spm plublish
將venus發(fā)布到倉庫中,與大家共享。
$ spm publish
publish: island205/venus@1.0.0
found: readme in markdown.
tarfile: venus-1.0.0.tar.gz
execute: git rev-parse HEAD
yuan: Authorization required.
yuan: `spm login` first
如果你碰到上面這種情況,你需要登錄下。
$ spm publish
publish: island205/venus@1.0.0
found: readme in markdown.
tarfile: venus-1.0.0.tar.gz
execute: git rev-parse HEAD
published: island205/venus@1.0.0
接下來我們使用venus編寫一個名為pixelegos的網(wǎng)頁程序,你可以使用這個程序來生成一些頭像的位圖。例如,spmjs的頭像(這是github為spmjs生成的隨機頭像):
pixelegos完成后的樣子:
創(chuàng)建一個名為pixelegos
的文件夾,初始化一個npm項目:
$ mkdir pixelegos && cd pixelegos && npm init
在目錄中多了一個packege.json文件,在這個文件中包含了一些pixelegos的信息,之后還會保存一些node module和spm的配置。
本項目中需要依賴的cmd模塊包括backbone
、seajs
、venus
、zepto
。我們運行下面的命令安裝這些依賴:
$ spm install seajs/seajs gallery/backbone zepto/zepto island205/venus
在pixelegos
目錄下增加了一個sea-modules
目錄,上面的cmd依賴都安裝在這個目錄中,由于backone依賴于underscore,spm自動安裝了依賴。
├── gallery
│ ├── backbone
│ │ └── 1.0.0
│ │ ├── backbone-debug.js
│ │ ├── backbone.js
│ │ └── package.json
│ └── underscore
│ └── 1.4.4
│ ├── package.json
│ ├── underscore-debug.js
│ └── underscore.js
├── island205
│ └── venus
│ └── 1.0.0
│ ├── package.json
│ ├── venus-debug.js
│ └── venus.js
├── seajs
│ └── seajs
│ └── 2.1.1
│ ├── package.json
│ ├── sea-debug.js
│ ├── sea.js
│ └── sea.js.map
└── zepto
└── zepto
└── 1.0.0
├── package.json
├── zepto-debug.js
└── zepto.js
新建一些html、css、js文件,結(jié)構如下:
├── index.css
├── index.html
├── js
│ ├── canvas.js
│ ├── config.js
│ ├── menu.js
│ ├── pixelegos.js
│ └── tool.js
├── package.json
└── sea-modules
├── gallery
├── island205
├── seajs
└── zepto
給index.html添加如下內(nèi)容:
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<meta content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0' name='viewport' />
<meta name="viewport" content="width=device-width" />
<link rel="stylesheet" href="/index.css" />
<script type="text/javascript" src="https://atts.w3cschool.cn/attachments/image/cimg/sea-debug.js"></script>
<script type="text/javascript" src="https://atts.w3cschool.cn/attachments/image/cimg/config.js"></script>
<script type="text/javascript">
seajs.use('/js/pixelegos')
</script>
</head>
<body>
</body>
</html>
其中,config.js在開發(fā)時用來配置alias,pixelegos作為整個程序的啟動模塊。
// config.js
seajs.config({
alias: {
'$': 'zepto/zepto/1.0.0/zepto',
"backbone": "gallery/backbone/1.0.0/backbone",
"venus": "island205/venus/1.0.0/venus"
}
})
// pixelegos.js
define(function (require, exports, module) {
var Menu = require('./menu')
var Tool = require('./tool')
var Canvas = require('./canvas')
var $ = require('$')
$(function() {
var menu = new Menu()
var tool = new Tool()
var canvas = new Canvas()
tool.on('select', function(color) {
canvas.color = color
})
tool.on('erase', function() {
canvas.color = 'white'
})
})
})
在其他js文件中分別基于backbone實現(xiàn)一些pixelegos的組件。例如:
// menu.js
define(function (require, exports, module) {
var Backbone = require('backbone')
var $ = require('$')
var Menu = Backbone.View.extend({
el: $('header'),
show: false,
events: {
'click .menu-trigger': 'toogle'
},
initialize: function() {
this.menu = this.$el.next()
this.render()
},
toogle: function(e) {
e.preventDefault()
this.show = ! this.show
this.render()
},
render: function() {
if (this.show) {
this.menu.css('height', 172)
} else {
this.menu.css('height', 0)
}
}
})
module.exports = Menu
})
menu.js依賴于backbone和(在config.js將zeptoalias為了),實現(xiàn)了頂部的菜單。
當當當當,巴拉巴拉,我們敲敲打打完成了pixelegos得功能,我們已經(jīng)可以畫出那只Octocat了!
終于來到了我們的重點,關于cmd模塊的構建。
有童靴覺得spm提供出來的構建工具很難用,搞不懂。我用下來確實些奇怪的地方,等我慢慢到來吧。
spm為自定義構建提供了兩個工具:
define(function (require, exports, module) {})
轉(zhuǎn)換為define(id, deps, function(require, exports, module) {})
,可基于package.json中的spm配置來替換被alias掉的路徑等等。本身還可以將css或者html文件轉(zhuǎn)換為cmd包。接下來就是用這些工具將我們零散的js打包成一個名為pixelegos.js的文件。
grunt是目前JavaScript最炙手可熱的構建工具,我們先來安裝下:
" 在全部安裝grunt的命令行接口
$ npm install grunt-cli -g
" 安裝需要用的grunt task
$ npm install grunt grunt-cmd-concat grunt-cmd-transport grunt-contrib-concat grunt-contrib-jshint grunt-contrib-uglify --dev-save
整個打包的流程為:
第一步先把js文件夾中的業(yè)務js轉(zhuǎn)換成具名模塊:
transport : {
options: {
idleading: '/dist/',
alias: '<%= pkg.spm.alias %>',
debug: false
},
app:{
files:[{
cwd: 'js/',
src: '**/*',
dest: '.build'
}]
}
}
這是一些transport的配置,即將js/中的js transport到.build中間文件夾中。
接下來,將.build中的文件合并到一起(包含sea-modules中的依賴項。):
concat : {
options : {
include : 'all'
},
app: {
files: [
{
expand: true,
cwd: '.build/',
src: ['pixelegos.js'],
dest: 'dist/',
ext: '.js'
}
]
}
}
這里我們只對pixelegos.js進行concat,因為它是app的入口文件,將include
配置成all
,只需要concat這個文件,就能將所有的依賴項打包到一起。include
還可以配置成其他值:
既然我們已經(jīng)transport和concat好了文件,那我們直接使用整個文件就行了,于是我們的發(fā)布頁面可寫成:
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<meta content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0' name='viewport' />
<meta name="viewport" content="width=device-width" />
<link rel="stylesheet" href="/index.css" />
<script type="text/javascript" src="https://atts.w3cschool.cn/attachments/image/cimg/sea.js"></script>
<script type="text/javascript">
seajs.use('/dist/pixelegos')
</script>
</head>
<body>
...
</body>
</html>
當我運行index-product.html時我遇到了坑。在backbone包中并沒有指明依賴的具體包,導致打包后的js無法找到.js文件。原本以為backbone中的$會被業(yè)務級的配置所替換,但是事實并非如此。如何解決?
我們必須使用seajs.config接口提供一個dom的engine,在js/中創(chuàng)建engine.js文件:
// engine.js
seajs.config({
alias: {
'$': 'zepto/zepto/1.0.0/zepto'
}
})
接下來把這個文件和pixelegos.js concat在一起:
normalconcat: {
app: {
src: ['js/engine.js', 'dist/pixelegos.js'],
dest: 'dist/pixelegos.js'
}
}
由于grunt-contrib-concat和grunt-cmd-concat產(chǎn)生了task name的沖突,可以通過grunt.renameTask來修改task名。
下一步,uglify!
uglify : {
app : {
files: [
{
expand: true,
cwd: 'dist/',
src: ['**/*.js', '!**/*-debug.js'],
dest: 'dist/',
ext: '.js'
}
]
}
}
大功告成,完整的Gruntfile.js如下:
module.exports = function (grunt) {
grunt.initConfig({
pkg : grunt.file.readJSON("package.json"),
transport : {
options: {
idleading: '/dist/',
alias: '<%= pkg.spm.alias %>',
debug: false
},
app:{
files:[{
cwd: 'js/',
src: '**/*',
dest: '.build'
}]
}
},
concat : {
options : {
include : 'all'
},
app: {
files: [
{
expand: true,
cwd: '.build/',
src: ['pixelegos.js'],
dest: 'dist/',
ext: '.js'
}
]
}
},
normalconcat: {
app: {
src: ['js/engine.js', 'dist/pixelegos.js'],
dest: 'dist/pixelegos.js'
}
},
uglify : {
app : {
files: [
{
expand: true,
cwd: 'dist/',
src: ['**/*.js', '!**/*-debug.js'],
dest: 'dist/',
ext: '.js'
}
]
}
},
clean:{
app:['.build', 'dist']
}
})
grunt.loadNpmTasks('grunt-cmd-transport')
grunt.loadNpmTasks('grunt-contrib-concat')
grunt.renameTask('concat', 'normalconcat')
grunt.loadNpmTasks('grunt-cmd-concat')
grunt.loadNpmTasks('grunt-contrib-uglify')
grunt.loadNpmTasks('grunt-contrib-clean')
grunt.registerTask('build', ['clean', 'transport:app', 'concat:app', 'normalconcat:app', 'uglify:app'])
grunt.registerTask('default', ['build'])
}
我們使用spm將一個非cmd模塊venus轉(zhuǎn)成了標準的cmd模塊venus-in-cmd,然后我們用它結(jié)合多個cmd模塊構建了一個簡單的網(wǎng)頁程序。很有成就,有沒有!接下來我們要進入hard模式了,我們來看看,Sea.js是如何實現(xiàn)的?只有了解了它的內(nèi)部是如何運作的,在使用它的過程才能游刃有余!
更多建議: