Seajs 是时下比较热的一款模块加载框架,除了能实现代码模块按需自动加载、增加代码的可复用性之外,还能够培养我们的模块化低耦合开发思维。爱折(zhuang)腾(bi)的人值得一试。
摆脱 seajs 提供的 spm 构建工具 而改用 Grunt 去构建,这个过程是曲折的,艰辛的,没点折腾的耐心估计不成,在这里要感谢优秀的导师 海龙,被我抓住讲了 1个小时,分享了他在折腾时遇到的问题,让我走少了很多弯路(海龙哥自己摸这个 Grunt 配 Seajs,摸了 1个星期,我听完后折腾一天就搞明白了,在站巨人的肩膀 果然就是不一样)。
如果已经对 Seajs 十分理解的童鞋 可以直接 滚到最后看第二部分内容,而对 Seajs 一知半懂的 最好继续慢慢往下看,基础没打好,爬得再快一样会掉下来,还不懂自己错在哪。
What is Seajs
看了不少站点使用seajs,但仅仅仅限于异步加载js(没错,就仅仅是异步加载一个 js文件,里面还是完完全全是普通的js代码),而非 Seajs 所推崇的 匿名模块代码,这样也叫熟练使用 Seajs (择选自不少应聘者简历上的技能说明) 也是醉了。
回顾下我爱抚 Seajs 的经过,在这里,让我来说说 我所理解的 Seajs。
这里是 Seajs 的站点 http://seajs.org/
在粗略看过下大致的使用方法之后,我就随手搞了个demo试试。
demo 目录结构如下:
demo_00 | + css | + hellosea.css | + js | + lib | | + jquery-1.11.0.min.js | | + sea.js | | | + hellosea | + page.js | + util.js | + html + hellosea.html
html部分:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Grunt seajs demo</title> <link rel="stylesheet" href="../css/hellosea.css"> </head> <body> <div id="box" class="box"></div> </body> <script src="http://www.jackness.org/wp-content/themes/JStyle/images/default/blank.png" _src="../js/lib/sea.js"></script> <script> // seajs 的简单配置 seajs.config({ base: "../js/lib/", alias: { "jquery": "jquery-1.11.0.min.js" } }); seajs.use("../js/hellosea/page.js"); </script> </html>
page.js 部分
define(function(require, exports, module) {
var util = require('./util'),
$ = require('jquery');
setInterval(function() {
$('#box').css('background-color',util.randomColor());
}, 1500);
});
demo地址: http://www.jackness.org/lab/2015/seajs/demo_00/html/hellosea.html
打开 demo,很好果然 万事起头难,报错了
调试了一下,原来是 这句问题
$ = require('jquery');
require 返回 null,恩,赶紧去看看官网怎么说。 看来是因为 Seajs 本身的约定: ID 和路径匹配原则
ID 和路径匹配原则
所谓 ID 和路径匹配原则 是指,使用 seajs.use 或 require 进行引用的文件,如果是具名模块(即定义了 ID 的模块),会把 ID 和 seajs.use 的路径名进行匹配,如果一致,则正确执行模块返回结果。反之,则返回 null。
举个例子来消化下,就拿 require(‘jquery’) 做例子吧
要让 require(‘jquery’) 正确返回 有 2种形式:
1 种是 通过 在 jquery里面定义匿名模块,即
define(function(require,exports,module){
//.. do something
module.exports = jQuery;
}
另一种就是在js文件里面定义具名模块。在例子里面,我们在页面上是这样定义的:
<!-- html code --> <script> // seajs 的简单配置 seajs.config({ base: "../js/lib/", alias: { "jquery": "jquery-1.11.0.min.js" } }); </script>
根据 路径即 id原则,我们需要在 jquery 文件里面这样定义:
define('jquery-1.11.0.min', [], function(require,exports,module){
return $;
});
然后我们看看例子里的 jquery 是怎么写的, 在底部找到相关代码
"function"==typeof define&&define("jquery/jquery/1.10.1/jquery",[],function(){return x}))
这里定义了 jquery文件的 id 为 “jquery/jquery/1.10.1/jquery”, 但是我们页面请求的 id 为 “jquery-1.11.0.min”, id 对不上,所以模块匹配不到,返回 null。
基于路径即id原则,有 2种修改方式:
- 1. 修改 jquery 源码,把 define(“jquery/jquery/1.10.1/jquery”,[],function(){return x})) 改为 define(“jquery-1.11.0.min”,[],function(){return x}))
- 2. 调整 jquery 目录路径,使 jquery/jquery/1.10.1/jquery 具名模块生效
我选择 第二种方式来修复,调整 jqeruy目录结构, 顺便 把 seajs 也跟着改
调整后的目录结构为
demo_00 | + css | + hellosea.css | + js | + lib | | + jquery | | | + jquery | | | + 1.10.1 | | | + jquery.js | | | | | + sea | | + sea | | + 2.2.0 | | + sea.js | | | + hellosea | + page.js | + util.js | + html + hellosea.html
demo地址: http://www.jackness.org/lab/2015/seajs/demo_01/html/hellosea.html
至于为什么要使用 jquery/jquery/1.10.1/jquery.js 这种结构,多了 1层的 jquery,而不直接使用 jquery/1.10.1/jquery.js,其实是因为考虑到扩展。拿jquery来举例,如果有个 基于jquery的日历控件 calendar.js 根据 推荐的结构 就可以这样放置:
js + lib | + jquery | | + jquery | | | + 1.10.1 | | | + jquery.js | | | | | + calendar | | + 1.0.0 | | + calendar.js | + ... | + ...
这样一看就知道 日历控件是基于 jquery 开发出来的了。
回到我们的例子上, 我们看到函数能成功运行固然开心,但是一打开 页面请求信息栏一看,简直吓尿了,所有的js 都进行了请求。为了模块化,代价是请求数的暴增,这样seajs的价值何在,还能好好玩耍吗,还不如不用?
恩,我们 继续看看 官网,在一篇 为什么要有约定和构建工具 文章 里 找到了答案。
不想请求那么多个文件, 答案就是把文件进行合并。 那么 问题来了。
如何将模块合并在 1个 js 里面
继续拿 上面的 demo 来做例子,jquery 属于 js库,而seajs属于页面加载的必备文件,当然就不必合并了,那么剩下的我们就有必要进行合并了,即 page.js、util.js
调整下目录结构,我们在 js/hellosea/ 下面 创建一个文件夹 【dist】 是用来放合并之后的文件,即产出目录;创建一个文件夹 【src】 用来放置我们的开发源文件:
js + lib | + jquery | | + jquery | | + 1.10.1 | | + jquery.js | | | + sea | + sea | + 2.2.0 | + sea.js | + hellosea + dist | + page.min.js | + src + page.js + util.js
我们首先将 html 上面的 seajs.use 所用的文件 指向 产出目录,即:
// seajs 的简单配置 seajs.config({ base: "../js/lib/", alias: { "jquery": "jquery/jquery/1.10.1/jquery.js" } }); seajs.use("../js/hellosea/dist/page.min.js");
我们再看看 page.js、util.js 这2个文件
page.js
define(function(require, exports, module) {
var util = require('./util'),
$ = require('jquery');
setInterval(function() {
$('#box').css('background-color',util.randomColor());
}, 1500);
});
util.js
define(function(require,exports,module){
var util = {};
var colorRange = ['0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'];
util.randomColor = function(){
return '#' +
colorRange[Math.floor(Math.random() * 16)] +
colorRange[Math.floor(Math.random() * 16)] +
colorRange[Math.floor(Math.random() * 16)] +
colorRange[Math.floor(Math.random() * 16)] +
colorRange[Math.floor(Math.random() * 16)] +
colorRange[Math.floor(Math.random() * 16)];
};
module.exports = util;
});
这两个文件里面都是通过匿名函数的方式创建的,根据文章的意思,我们如果想将他们合并在一个文件里面,就要在合并文件里面将这 2个 文件中的匿名函数 变为 具名函数, 并且遵守路径即id原则
我们把 hellosea/js/src/page.js 中的代码修改为具名函数:
define('../js/hellosea/src/page', ['./util', 'jquery'], function(require, exports, module) {
var util = require('./util'),
$ = require('jquery');
setInterval(function() {
$('#box').css('background-color',util.randomColor());
}, 1500);
});
第二个参数是 我们把函数中有依赖其他文件的 文件 id(路径)抽出来放到这里面。Seajs 这样设定的原因是:这样 Sea.js 就不需要通过 factory.toString() 和正则匹配的方式来获取依赖,直接从第二个参数中就可以拿到依赖数组
同理 我们把 hellosea/js/src/util.js 也修改为具名函数:
define('../js/hellosea/src/util', [], function(require, exports, module) {
var util = {};
var colorRange = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
util.randomColor = function() {
return '#' +
colorRange[Math.floor(Math.random() * 16)] +
colorRange[Math.floor(Math.random() * 16)] +
colorRange[Math.floor(Math.random() * 16)] +
colorRange[Math.floor(Math.random() * 16)] +
colorRange[Math.floor(Math.random() * 16)] +
colorRange[Math.floor(Math.random() * 16)];
};
module.exports = util;
});
然后 我们把 hellosea/js/src/page.js 这入口文件里的代码拷贝到 hellosea/js/dist/page.min.js。 根据路径即 id原则,我们把 id 改一下:
define('../js/hellosea/dist/page.min', ['./util', 'jquery'], function(require, exports, module) {
var util = require('./util'),
$ = require('jquery');
setInterval(function() {
$('#box').css('background-color',util.randomColor());
}, 1500);
});
然后 再把 hellosea/js/src/util.js 的代码考到 hellosea/js/dist/page.min.js 的下面。
运行一下代码,发现有个地方错了:
原因是 找不到 ../js/hellosea/dist/util.js 这个文件。
在 ‘../js/hellosea/dist/page.min’ 模块里面 请求的 ‘./util’ 是不存在的, 因为根据文件自身路径 这请求最终是指向 ‘../js/hellosea/dist/util.js’, 而我们需要他指向的是 ‘../js/hellosea/src/util.js’, 也就是 下面 util 模块的 id。
所以我们要把路径调整到正确的位置:
define('../js/hellosea/dist/page.min', ['../src/util', 'jquery'], function(require, exports, module) {
var util = require('../src/util'),
$ = require('jquery');
setInterval(function() {
$('#box').css('background-color',util.randomColor());
}, 1500);
});
这就是 seajs 的工作、运行原理了。了解清楚原理,我们就可以更好地理解构建工具的工作方式。接下来我们说说 seajs的 构建工具。
使用 Grunt 作为 seajs的 构建工具
如真像上面说的,我们的每次修改代码、编写代码、调试代码 都需要我们手工一个个文件地进行合并、调整、压缩,如此繁琐的操作估计各个程序大大就算耐性再高都会被玩坏,更别说效率了。所以,代我们完成这样些操作的 构建工具 应运而生。
seajs 官网 提供了 seajs特供的构建工具 spm
操作虽然简单,但是需要遵守的规则也更多了,其中包括 文件路径。
这文件路径 如果是在全新的一个项目里面搞还好,但是如果是针对旧项目的seajs改造,目录架构改动之大以至于估计没开始你就已经选择放弃了。
所以这里介绍的是更具自由的 Grunt 来作为 seajs的 构建工具
根据我上面说的 seajs 工作原理,我们其实想让构建工具完成的是以下的几个事情
- 将入口文件拷贝到 产出目录
- 创建一个临时目录
- 将需要合并的js文件转为具名函数,并保持独立地保存在这个临时目录
- 将临时目录下独立的具名函数文件 合并为 1个 js 文件
- 将这个合并的 js 文件 拷贝到 我们的输出目录
- 压缩 这个 合并后的 文件
- 将这个临时目录删除
seajs 提供以下 grunt 的插件来 构建 项目
- grunt-cmd-transport(用于将匿名函数转为 具名函数)
- grunt-cmd-concat(根据文件中的 id 自动 拷贝对应目录文件的内容到 同一文件下)
所以 我的 package.js 这样配置
{ "name": "helloSeajs", "version": "1.0.0", "author": "jackness Lau", "devDependencies": { "grunt": "0.4.1", "grunt-cmd-transport": "0.1.1", "grunt-cmd-concat": "0.1.0", "grunt-contrib-uglify": "0.2.0", "grunt-contrib-clean": "0.4.0" } }
下面说说作为核心的 grunt-cmd-transport、grunt-cmd-concat 这 2个插件的使用配置
grunt-cmd-transport 介绍
这插件 用于将文件中定义的匿名函数转为 具名函数。配置如下:
grunt.initConfig({ transport: { hellosea: { options: { // 是否采用相对地址 relative: true, // 生成具名函数的id的格式 默认值为 {{family}}/{{name}}/{{version}}/{{filename}} format: '../js/hellosea/{{filename}}' }, files: [{ // 相对路径地址 'cwd':'js/hellosea/', // 需要生成具名函数的文件集合 'src':['dist/hellosea.js','src/util.js'], // 生成存放的文件目录。里面的目录结构与 src 里各个文件名带有的目录结构保持一致 'dest':'.build' }] } }, });
grunt-cmd-concat 介绍
这插件 用于根据文件中的 id 自动 拷贝对应目录文件的内容到 同一文件下。配置如下:
grunt.initConfig({ concat:{ hellosea: { options: { // 相对路径地址 relative: true }, files: { // 合并后的文件地址 'js/hellosea/dist/hellosea.js': ['.build/dist/hellosea.js'] } } } });
我们在上面demo 的基础上进行 构建工具改造
demo压缩包地址 http://www.jackness.org/lab/2015/seajs/demo_03.zip
解压压缩包之后 我们 先 npm install 一下
npm install
以下是 Gruntfile.js 内容:
module.exports = function(grunt) {
grunt.initConfig({
/**
* step 1:
* 将入口文件拷贝到 产出目录
*/
copy: {
hellosea:{
files:{
"js/hellosea/dist/hellosea.js":["js/hellosea/src/hellosea.js"]
}
}
},
/**
* step 2:
* 创建一个临时目录
* 将需要合并的js文件转为具名函数,并保持独立地保存在这个临时目录
*/
transport: {
hellosea: {
options: {
// 是否采用相对地址
relative: true,
// 生成具名函数的id的格式 默认值为 {{family}}/{{name}}/{{version}}/{{filename}}
format: '../js/hellosea/{{filename}}'
},
files: [{
// 相对路径地址
'cwd':'js/hellosea/',
// 需要生成具名函数的文件集合
'src':['dist/hellosea.js','src/util.js'],
// 生成存放的文件目录。里面的目录结构与 src 里各个文件名带有的目录结构保持一致
'dest':'.build'
}]
}
},
/**
* step 3:
* 将临时目录下独立的具名函数文件 合并为 1个 js 文件
* 将这个合并的 js 文件 拷贝到 我们的输出目录
*/
concat: {
hellosea: {
options: {
// 是否采用相对地址
relative: true
},
files: {
// 合并后的文件地址
'js/hellosea/dist/hellosea.js': ['.build/dist/hellosea.js']
}
}
},
/**
* step 4:
* 压缩 这个 合并后的 文件
*/
uglify: {
hellosea: {
files: {
'js/hellosea/dist/hellosea.js': ['js/hellosea/dist/hellosea.js'] //对dist/application.js进行压缩,之后存入dist/application.js文件
}
}
},
/**
* step 5:
* 将这个临时目录删除
*/
clean: {
build: ['.build']
}
});
grunt.loadNpmTasks('grunt-cmd-transport');
grunt.loadNpmTasks('grunt-cmd-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.registerTask('default', ['copy','transport', 'concat', 'uglify', 'clean']);
grunt.registerTask('build', function(name,step){
switch(step){
case "1":
grunt.task.run('copy:' + name);
break;
case "2":
grunt.task.run('transport:' + name);
break;
case "3":
grunt.task.run('concat:' + name);
break;
case "4":
grunt.task.run('uglify:' + name);
break;
case "5":
grunt.task.run('clean:' + name);
break;
default:
grunt.task.run(['copy:' + name, 'transport:' + name, 'concat:' + name, 'uglify:' + name, 'clean'])
break;
}
});
};
我们执行下任务 看看是否能执行成功
grunt
出错, 提示在执行 transport 时报错, 找不到 js/hellosea/dist/util.js 文件。额 但是我们想要的 应该是 js/hellosea/src/util.js 才对, Why?
打开 js/hellosea/src/hellosea.js 文件
define(function(require, exports, module) {
var util = require('./util'),
$ = require('jquery');
setInterval(function() {
$('#box').css('background-color',util.randomColor());
}, 1500);
});
恩, 问题应该出在这里了
var util = require('./util'),
想想我们定义构建工具做了什么:
- 1. 复制 hellosea/src/hellosea.js 文件到 hellosea/dist/hellosea.js
- 2. 将 hellosea/dist/hellosea.js 转为具名函数
所以这 hellosea/src/hellosea.js 同样用于 hellosea/dist/hellosea.js
所以 require(‘./util’) 变成了相对于 hellosea/dist/hellosea.js 这个文件目录来执行的了,当然找不到了。
因此,我们要对 js/hellosea/src/hellosea.js 这样修改:
js/hellosea/src/hellosea.js 文件
define(function(require, exports, module) {
var util = require('../src/util'),
$ = require('jquery');
setInterval(function() {
$('#box').css('background-color',util.randomColor());
}, 1500);
});
修改后的 压缩包: http://www.jackness.org/lab/2015/seajs/demo_04.zip
好是好,但是,问题又来了,线上有问题的话 怎么调试
根据地址进行 发行版 与开发版 的切换
下面是 Seajs 的建议方案:
// seajs 的简单配置
seajs.config({
base: "../js/lib/",
alias: {
"jquery": "jquery/jquery/1.10.1/jquery.js"
}
});
if(location.href.indexOf("dev=1") != -1){
seajs.use("../js/hellosea/src/hellosea");
} else {
seajs.use("../js/hellosea/dist/hellosea");
}
在确定没问题的情况下,上线时候可以将开发版部分代码去掉
// seajs 的简单配置 seajs.config({ base: "../js/lib/", alias: { "jquery": "jquery/jquery/1.10.1/jquery.js" } }); seajs.use("../js/hellosea/dist/hellosea");
恩,好像已经讲完可以投入使用的感觉,我又有个问题了,如果我想将非本项目的一个 通用 js 也合并在一起呢,情况会不会一样?
对非本项目的 js 进行 seajs 构建
同样,在上面 demo的 基础上 增加一个 通用头部的调用。 目录结构如下
demo + css | + html | + js + lib | + jquery | | + jquery | | + 1.10.1 | | + jquery.js | | | + sea | + sea | + 2.2.0 | + sea.js | + hellosea | + dist | | + hellosea.js | | | + src | + hellosea.js | + util.js | + common + head.js
hellosea.js 增加一个 头部js的引用:
define(function(require, exports, module) {
require('../../common/head');
var util = require('../src/util'),
$ = require('jquery');
setInterval(function() {
$('#box').css('background-color',util.randomColor());
}, 1500);
});
Gruntfile.js 代码中也增加对 ‘../../common/head’ 的处理:
module.exports = function(grunt) {
grunt.initConfig({
...
/**
* step 2:
* 创建一个临时目录
* 将需要合并的js文件转为具名函数,并保持独立地保存在这个临时目录
*/
transport: {
hellosea: {
options: {
// 是否采用相对地址
relative: true,
// 生成具名函数的id的格式 默认值为 {{family}}/{{name}}/{{version}}/{{filename}}
format: '../js/hellosea/{{filename}}'
},
files: [{
// 相对路径地址
'cwd':'js/hellosea/',
// 需要生成具名函数的文件集合
'src':['dist/hellosea.js','src/util.js', '../../common/head.js'],
// 生成存放的文件目录。里面的目录结构与 src 里各个文件名带有的目录结构保持一致
'dest':'.build'
}]
}
},
...
});
...
};
demo 压缩包: http://www.jackness.org/lab/2015/seajs/demo_05.zip
执行 构建 顺利通过
打开网页, 确定下有没合并成功, 艾玛 head.js 单独请求了,并没有合并到 /dist/hellosea.js 里面
http://www.jackness.org/lab/2015/seajs/demo_05/html/hellosea.html
错在哪里呢? 好吧,我们将构建过程 分开一步步来执行,看看 哪里出错了
跑到 步骤 3, 发现有异常了, hellosea/dist/hellosea.js 里面并没有 common/head.js 部分的代码
hellosea/dist/hellosea.js
define("../js/hellosea/dist/hellosea", [ "../../common/head", "jquery", "../src/util" ], function(require, exports, module) {
require("../../common/head");
var util = require("../src/util"), $ = require("jquery");
setInterval(function() {
$("#box").css("background-color", util.randomColor());
}, 1500);
});
define("../js/hellosea/src/util", [], function(require, exports, module) {
var util = {};
var colorRange = [ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F" ];
util.randomColor = function() {
return "#" + colorRange[Math.floor(Math.random() * 16)] + colorRange[Math.floor(Math.random() * 16)] + colorRange[Math.floor(Math.random() * 16)] + colorRange[Math.floor(Math.random() * 16)] + colorRange[Math.floor(Math.random() * 16)] + colorRange[Math.floor(Math.random() * 16)];
};
module.exports = util;
});
回到 步骤 2, 当将 ../js/hellosea/dist/hellosea 匿名函数转为 具名函数 并保存 到 .build/dist/hellosea.js 里面的时候,文件代码如下
define("../js/hellosea/dist/hellosea", [ "../../common/head", "jquery", "../src/util" ], function(require, exports, module) {
require("../../common/head");
var util = require("../src/util"), $ = require("jquery");
setInterval(function() {
$("#box").css("background-color", util.randomColor());
}, 1500);
});
而步骤 3 是将 此 入口函数里的 引用 所有文件 合并到 一个文件里面,所以,问题就来了。
基于 “.build/dist/hellosea.js” 这个目录 “../../common/head” 这文件不存在, 所以就导致 执行步骤 3之后 head.js 没有合并到 入口函数里面。
这样,我们就出大招吧,将整个 js 目录搬到 .build 目录下!这样就能找到对应的文件了
因此,我在 构建过程中增加了 2个步骤:
module.exports = function(grunt) {
grunt.initConfig({
...
/**
* step 2:
* 创建 .build/js/common 临时目录
* 将公用 common 目录下的 文件 转为 具名函数,并保存在 .build/js/common 目录下
* 创建 .build/js/hellosea 临时目录
* 将需要合并的js文件转为具名函数,并保持独立地保存在 .build/js/hellosea 临时目录下
*/
transport: {
common:{
options: {
relative: true,
format: '../js/common/{{filename}}' //生成的id的格式
},
files: [{
'cwd':'js/common/',
'src':['*.js'],
'dest':'.build/js/common/'
}]
},
hellosea: {
options: {
relative: true,
format: '../js/hellosea/{{filename}}' //生成的id的格式
},
files: [{
'cwd':'js/hellosea/',
'src':['dist/hellosea.js', 'src/util.js', '../../common/head.js'],
'dest':'.build/js/hellosea/'
}]
}
},
/**
* step 3:
* 将.build 目录下的具名函数 入口文件,根据id查找对应的文件,并且 合并为 1个 js 文件
* 将这个合并的 js 文件 拷贝到 我们的输出目录
*/
concat: {
hellosea: {
options: {
// relative: true
},
files: {
'js/hellosea/dist/hellosea.js': ['.build/js/hellosea/dist/hellosea.js']
}
}
},
...
});
...
};
demo 压缩包: http://www.jackness.org/lab/2015/seajs/demo_06.zip
执行下,打开网页。 ok head.js 归位了。
http://www.jackness.org/lab/2015/seajs/demo_06/html/hellosea.html
转自:http://www.tuicool.com/articles/zaUfI3