前言
Javascript模块化规范有CommonJs规范,和主要适用于浏览器环境的AMD规范,以及国内的CMD规范,它是SeaJs遵循的模块化规范。因为以前项目中用SeaJs做过前端的模块管理工具,所以这里总结一下自己的使用心得。
在试用SeaJs和官方推荐的CMD包管理工具——Spm2.x的过程中,遇到了很多高低版本不兼容和配置参数没弄明白的问题,后来在网上各处找资料才大概弄懂。这里我强调一下版本,是因为可能有的同学项目开始较早,用了以前版本的Seajs,再去看Seajs官网的API有些地方会不适用!下面各节对框架的描述中也都会带上版本。
文中可能会有一些理解有误或者没讲清楚的地方,有大神路过恳请指导...
一个简单的DEMO
Seajs可以实现前端Js文件的按需加载和前端模块管理的功能,不熟悉Seajs API (v2.3.0)的同学可以看看这里,非常简单。
首先上一个使用Seajs的demo。目录结构如下:
html:
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<script src="../js/lib/seajs1.2.0.js"></script>
</head>
<body>
<h1>spm前端构建测试</h1>
<script>
seajs.config({
base: '../js'
// paths:{'pathtest':'src'}, //生效版本:seajs2.3.0
});
seajs.use("src/eat.js",function(eat){
if(eat) eat.start();
});
</script>
</body>
</html>
eat.js
"use strict"
define(function(require,exports,module){
var rice=require("./rice"),
water=require("./water"),
i=require("./i");
exports.start=function(){
i.eat(rice.rice);
i.drink(water.water);
}
})
rice.js
"use strict"
define({rice:"i'm rice"})
i.js
"use strict"
define(function () {
return {
name:"tzyy",
eat:function(rice){
alert(rice+",Delicious!");
},
drink: function (water) {
alert(water+",爽!");
}
}
})
water.js
"use strict"
define({water:"i'm water"});
项目结构:
这样就实现的前端代码的模块化,和模块的按需加载、执行。
但是,这样还不够,我们打开Chrome控制台,network标签,刷新网页发现:
代码确实是模块化了,同时代码也分散到了各个文件中。当项目中的文件非常多,由于http请求是无状态的,每次都要建立、断开连接,另外浏览器下载静态文件的并发数量也是有上限的,会导致页面加载的速度比所有代码在同一个文件中要慢。
所以我们要对js文件进行压缩、合并。那么问题来了:
1.define函数是seajs提供的,require函数也是seajs给注入的,代码压缩,函数名字发生变化后seajs还认得吗?
2.现在的代码中require接收的参数都是模块文件的路径,文件要是都合并了,这些代码还上哪找去?
幸好有spm,它提供了构建CMD模块代码的功能,可以对模块文件进行压缩、合并的操作。
用spm 2.2.5进行模块代码的压缩&合并
了解关于CMD模块的压缩和合并之前最好先了解一下模块标识和构建过程相关的知识,以免掉入万人坑。
这两个点官方文档讲得很好,我就不再缀述了。
seajs模块标识
https://github.com/seajs/seajs/issues/258
spm2.x构建过程详解
http://docs.spmjs.org/doc/build-task
package.json配置
使用spm2构建cmd模块需要在模块根目录创建一个package.json文件。 继续以上面的项目为例:
{
"family":"tzyy",
"name":"test",
"version": "1.0.0",
"spm": {
"source": "src",
"idleading":"pack/","output": ["eat.js"]
}
}
spm视图像java项目里的maven那样建立一个依赖管理的机制,family、name、version,这三个属性用来定位一个项目,是必须填写的。但是我们这里只是用spm来做构建工具,所以这几个属性随便填写就可以了。
spm属性的值是一个json,其属性
src——指定源码目录,spm将使用src配置地址中的文件来构建项目
idleading——指定模块id前缀,spm在生成transport文件的过程中将这个属性作为模块id的前缀
output——一个数组,指定应该输出哪些文件的构建结果
压缩&合并
首先安装好nodejs、npm、spm2.2.5,安装的教程网上有很多。
进入/js目录,shift+右键——在此处打开命令窗口,然后执行命令:
这个命令在/js下创建了一个mypack目录来存放压缩后的文件
两个文件的内容为:
eat.js:
"use strict";define("pack/eat",["./rice","./water","./i"],function(a,b){var c=a("./rice"),d=a("./water"),e=a("./i");b.start=function(){e.eat(c.rice),e.drink(d.water)}}),define("pack/rice",[],{rice:"i'm rice"}),define("pack/water",[],{water:"i'm water"}),define("pack/i",[],function(){return{name:"tzyy",eat:function(a){alert(a+",Delicious!")},drink:function(a){alert(a+",爽!")}}});
eat-debug.js
"use strict"; define("pack/eat-debug", [ "./rice-debug", "./water-debug", "./i-debug" ], function(require, exports, module) {
var rice = require("./rice-debug"), water = require("./water-debug"), i = require("./i-debug");
exports.start = function() {
i.eat(rice.rice);
i.drink(water.water);
};
}); /**
* Created by zouchengzhuo on 2015/10/21.
*/
"use strict"; define("pack/rice-debug", [], {
rice: "i'm rice"
}); /**
* Created by zouchengzhuo on 2015/10/21.
*/
"use strict"; define("pack/water-debug", [], {
water: "i'm water"
}); /**
* Created by zouchengzhuo on 2015/10/21.
*/
"use strict"; define("pack/i-debug", [], function() {
return {
name: "tzyy",
eat: function(rice) {
alert(rice + ",Delicious!");
},
drink: function(water) {
alert(water + ",爽!");
}
};
});
可以看到eat.js已经被压缩,并且其依赖模块也被压缩后合并在了同一个文件中。
将html中启动模块的代码修改一下:
seajs.use("mypack/eat.js",function(eat){
if(eat) eat.start();
});
运行正常,看看网络请求:
刚刚那些文件只产生了一个网络请求,3ms远远小于 3ms+4ms+12ms+14ms吧。
seajs config中使用了别名的压缩&合并
但是,当我们的seajs项目中使用了别名的配置时,情况又不同了!修改项目结构如下:
为water.js配置别名如下:
seajs.config({
base: '../js',
alias:{
"water":"src/alias/water.js"
}
});
从src目录启动,可以正常运行。然后构建一下,发现:
得到的eat-debug.js:
/**
* Created by zouchengzhuo on 2015/10/21.
*/
"use strict"; define("pack/eat-debug", [ "./rice-debug", "water-debug", "./i-debug" ], function(require, exports, module) {
var rice = require("./rice-debug"), water = require("water-debug"), i = require("./i-debug");
exports.start = function() {
i.eat(rice.rice);
i.drink(water.water);
};
}); /**
* Created by zouchengzhuo on 2015/10/21.
*/
"use strict"; define("pack/rice-debug", [], {
rice: "i'm rice"
}); /**
* Created by zouchengzhuo on 2015/10/21.
*/
"use strict"; define("pack/i-debug", [], function() {
return {
name: "tzyy",
eat: function(rice) {
alert(rice + ",Delicious!");
},
drink: function(water) {
alert(water + ",爽!");
}
};
});
报错,找不到water模块,最终得到的输出文件中也没有包含water模块。
原因是如果seajs配置了别名,在package.json的spm属性里边应该也配置一下,这样spm会首先将别名替换成全名,然后再构建。
修改package.json,把alias配置贴过去,如下:
{
"family":"tzyy",
"name":"test",
"version": "1.0.0",
"spm": {
"source": "src",
"idleading":"pack/",
"alias":{
"water":"src/alias/water.js"
},
"output": ["eat.js"]
}
}
再次运行,结果依然不对!这种现象官网上并没有解释,经过试验和网上查资料,
我的理解:
这里要注意一下,由于seajs的config中指定了base路径 ../js,别名携程 src/alias/water.js是可以的,但是spm构建的时候,不会这么想,它似乎将src/alias/water.js作为一个*标识来处理,而*标识一般被认为是其他的公用模块,不会被transport,也不会被合并到输出文件中。 这里不对的话请指正:)
在package.json中将alias里边water的别名配置为 ./alias/water.js 这种相对路径,输出文件表现就正常了:
/**
* Created by zouchengzhuo on 2015/10/21.
*/
"use strict"; define("pack/eat-debug", [ "./rice-debug", "./alias/water-debug.js", "./i-debug" ], function(require, exports, module) {
var rice = require("./rice-debug"), water = require("./alias/water-debug.js"), i = require("./i-debug");
exports.start = function() {
i.eat(rice.rice);
i.drink(water.water);
};
}); /**
* Created by zouchengzhuo on 2015/10/21.
*/
"use strict"; define("pack/rice-debug", [], {
rice: "i'm rice"
}); /**
* Created by zouchengzhuo on 2015/10/21.
*/
"use strict"; define("pack/alias/water-debug", [], {
water: "i'm water"
}); /**
* Created by zouchengzhuo on 2015/10/21.
*/
"use strict"; define("pack/i-debug", [], function() {
return {
name: "tzyy",
eat: function(rice) {
alert(rice + ",Delicious!");
},
drink: function(water) {
alert(water + ",爽!");
}
};
});
但是此时cmd中依然会报错
不知道这是spm2.2.5的bug,还是我上面的理解有问题,官方文档和网上也没找到资料,求指导~
启动模块合并
从上一小节看来,压缩之后不是会自动合并模块吗? 为什么还要合并呢?
下面我们来变一下项目内容。增加一个启动模块drink.js:
drink.js内容:
define(function(require,exports,module){
var water=require("water"),
i=require("./i.js");
return function(){
i.drink(water.water);
}
});
在package中增加一个输出模块 drink.js
{
"family":"tzyy",
"name":"test",
"version": "1.0.0",
"spm": {
"source": "src",
"idleading":"pack/",
"alias":{
"water":"./alias/water.js"
},
"output": ["eat.js","drink.js"]
}
}
运行
可以看到生成了两个模块
html中增加一个启动模块
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<script src="../js/lib/seajs1.2.0.js"></script>
</head>
<body>
<h1>spm前端构建测试</h1>
<script>
seajs.config({
base: '../js',
alias:{
"water":"src/alias/water.js"
}
});
seajs.use("mypack/eat.js",function(eat){
if(eat) eat.start();
});
seajs.use("mypack/drink.js",function(drink){
if(drink) drink();
});
</script>
</body>
</html>
在浏览器中运行,正常,打开控制台查看网络请求
可以看到请求了两个模块启动文件,eat.js 和 drink.js ,并没有被合并为一个文件。
而且,打开drink-debug.js,可以看到
define("pack/drink-debug", [ "./alias/water-debug.js", "./i-debug" ], function(require, exports, module) {
var water = require("./alias/water-debug.js"), i = require("./i-debug");
return function() {
i.drink(water.water);
};
}); /**
* Created by zouchengzhuo on 2015/10/21.
*/
"use strict"; define("pack/alias/water-debug", [], {
water: "i'm water"
}); /**
* Created by zouchengzhuo on 2015/10/21.
*/
"use strict"; define("pack/i-debug", [], function() {
return {
name: "tzyy",
eat: function(rice) {
alert(rice + ",Delicious!");
},
drink: function(water) {
alert(water + ",爽!");
}
};
});
而drink.js中,water和i 两个模块在eat.js中也是存在的,这也是对资源的浪费。
此时我们可以再新建一个模块,将上面那两个模块放入该模块中启动:
main.js
define(function (require,exports,module) {
var eat=require("./eat"),
drink=require("./drink");
eat.start();
drink();
})
package.json输出模块改为main.js
{
"family":"tzyy",
"name":"test",
"version": "1.0.0",
"spm": {
"source": "src",
"idleading":"pack/",
"alias":{
"water":"./alias/water.js"
},
"output": ["./main.js"]
}
}
html中启动代码修改为:
seajs.use("mypack/main.js");
再次运行可以看到所有文件都被压缩、合并了
当启动模块越来越多时,我们就可以这样干。
css的压缩、合并
spm2.x对css的压缩合并是通过seajs的seajs.importStyle函数来完成的。
我们在src目录下建一个css文件夹(之前把src目录放到js目录里边了,不想改了,大家记得调换一下顺序)
然后在main.js中require :
define(function (require,exports,module) {
var eat=require("./eat"),
drink=require("./drink");
require("./css/style.css"),
require("./css/style2.css");
eat.start();
drink();
})
运行spm build,最后得到的main-debug.js中可以看到如下代码片段:
这样css文件也被压缩到了main.js文件中。
define("pack/main-debug", [ "./eat-debug", "./rice-debug", "./alias/water-debug.js", "./i-debug", "./drink-debug", "./css/style-debug.css", "./css/style2-debug.css" ], function(require, exports, module) {
var eat = require("./eat-debug"), drink = require("./drink-debug");
require("./css/style-debug.css"), require("./css/style2-debug.css");
eat.start();
drink();
}); 以及最下方的
define("pack/css/style-debug.css", [], function() {
seajs.importStyle("body{color:red}");
}); define("pack/css/style2-debug.css", [], function() {
seajs.importStyle("h1{font-size:100px}");
});
这里有一些关于seajs版本的坑
1.seajs2.2.1中,可以直接require css文件,但是seajs2.3.0不行,需要引入一个seajs-css插件
2.seajs2.3.0中,通过seajs.use启动一个transport文件中的模块时(也就是从合并过的代码中启动模块时),use中写的模块id必须和define函数中写的模块id相同,但是在seajs2.2.1中却是必须和文件路径相同。 seajs2.3.0如此做的原因是,可以在合并过的文件中可以通过模块id准确获取模块exports的内容,好传回use的回调函数中。
这些坑在seajs官网文档里面都不太容易找到。
开发、生产环境自动切换
定义一个变量,来设置模块启动路径,可以很方便的切换开发、生产环境。
将html中seajs配置、启动模块代码做如下修改:
var environment=location.hostname=="localhost"?"src":"mypack";
seajs.config({
base: '../js',
alias:{
"water":"src/alias/water.js"
}
});
seajs.use(environment+"/main.js");
这样通过localhost访问时,自动启用src目录下的代码,生产环境时,自动启用打包目录下的代码。
最后
前端领域的技术正在飞速发展,日新月异,各种技术层出不穷! 这篇文章主要在讲我使用spm2.2.5作为seajs的构建工具的使用经历,spm3.x和spm2.x的差别都非常大。和CommonJs、AMD规范相关的还有很多优秀的模块管理工具,希望能在学习使用这些工具的过程中能不断深入理解模块化的精髓!
这篇文章是学习过程中的产物,可能有很多没讲清楚或者理解有误的地方,如果您路过发现,还望在评论里指出,我会在文中更正!