grunt任务之seajs模块打包
grunt与seajs
grunt是前端流行的自定义任务的脚手架工具,我们可以使用grunt来为我们做一些重复度很高的事情,如压缩,合并,js语法检查等。通过定义grunt的配置文件Gruntfile.js,配置并注册grunt的任务,最终我们可以通过命令行来执行任务。
seajs主要用于模块化,通过define定义一个模块,可以通过require加载模块,exports导出模块。具体的seajs实现可通过本博客的系列博文--Seajs源码解析系列来进一步了解。
在实际生产中,如果紧紧定义一系列seajs模块而并不进行合并压缩的话,加载性能很低,原因大家都懂的,seajs在浏览器端处理依赖模块,并进行异步加载,这个过程中会有多个http请求,大大降低页面的加载速度。所以结合grunt构建工具,我们可以将模块的依赖处理放到服务端进行,并将所有模块合并压缩,完成生产所需的最终文件。
在seajs社区中,已经提供了一款npm模块,即grunt-cmd-transport。我们通过该模块给seajs模块命名,并处理各模块之间的依赖。这项工作听起来很简单,但是在笔者的实践过程中出现的问题却不少,因此本文着重讲解transport任务的相关配置。
grunt的相关文件
grunt相关文件包括了2个,首先是Gruntfile.js,另一个是package.json文件。Gruntfile进行grunt任务的配置及注册,package.json用于向Gruntfile提供参数,并设置依赖的npm模块。
在下面package.json中,定义spm键,设置模块的别名,在Gruntfile中,通过pkg = grunt.file.readJSON()来读取package配置文件,并通过<%= pkg.spm.alias %>获取模块别名。
package.json
{
     "name" : "HelloSeaJS",
     "version" : "1.0.0",
     "author" : "yang li",
     "spm": {
        "alias": {
            "jquery": "jquery"
        }
     },
     "devDependencies" : {
          "grunt" : "0.4.1",
          "grunt-cmd-transport": "~0.2.0",
          "grunt-cmd-concat": "~0.2.0",
          "grunt-contrib-uglify" : "0.2.0",
          "grunt-contrib-clean" : "0.4.0"
     }
}
接下来我们进行设置grunt。Gruntfile.js其实就是一个node模块,依然使用闭包将所有的逻辑进行包裹,并提供了grunt参数,通过grunt.initConfig进行任务的配置。
对于seajs模块而言,首先需要处理各模块之间的依赖,我们通过设置transport任务来完成。seajs遵循的是CMD规范,在定义模块时不需要制定模块名和模块的依赖组,只需设置工厂函数即可。其实在未使用grunt进行合并seajs时(即在浏览器端处理模块依赖),seajs设置模块id和uri相同,为绝对路径。在这个过程中有些小技巧,在Seajs源码解析系列中并未提到,现在在这里着重分析下:
<script src="../sea-debug.js"></script>
<script>
seajs.config({
base: "../gallery/",
alias:{
jquery: 'jquery/jquery-1.11.1'
}
})
seajs.use("../application.js")
</script>
对于上述代码,application.js并没有合并seajs模块,我们通过seajs.use创建了一个匿名use模块,通过
var mod = Module.get(uri, isArray(ids) ? ids : [ids])
来实现,并设置依赖。在此处,依赖为[‘../application.js’];然后设置use模块的callback,并调用load函数加载依赖模块。在load函数中,use模块调用resolve函数解析出依赖的绝对路径,即[‘http://localhost:63342/mywork/js/application.js’],并创建一个新的Module表示该模块,这里用appMod表示,并以uri为key保存到modCache中。调用appMod.fetch加载对应的文件并设置回调函数onRequest,在application.js中定义了一个匿名模块define(function(){return {};}),此时模块的配置信息
meta = {
id: id,uri: Module.resolve(id), // 绝对url
deps: deps,
factory: factory
}
中id=‘undefined’,url=’’,
meta.uri ? Module.save(meta.uri, meta) :
// Save information for "saving" work in the script onload eventanonymousMeta = meta
由于此时meta.uri为空,因此meta信息保存在全局变量anonymousMeta中,用于后续处理。
红色字体强调了在调用fetch时设置的回调onRequest函数,当文件加载完毕,执行onRequest,
function onRequest() {
delete fetchingList[requestUri]fetchedList[requestUri] = true
// Save meta data of anonymous module
if (anonymousMeta) {Module.save(uri, anonymousMeta)
anonymousMeta = null
}
// Call callbacks
var m, mods = callbackList[requestUri]delete callbackList[requestUri]
while ((m = mods.shift())) m.load()
}
此时anonymousMeta已在模块define时设置,因此将该meta配置文件配置到uri对应的Module对象上,
Module.save = function(uri, meta) {
var mod = Module.get(uri)// Do NOT override already saved modules
if (mod.status < STATUS.SAVED) {
mod.id = meta.id || uri
mod.dependencies = meta.deps || []
mod.factory = meta.factory
mod.status = STATUS.SAVED
}
}
由于meta.id=undefined,因此最终mod.id=uri。
对于通过define(id,deps,function(){})设置了id的具名模块,是根据id生成uri。在meta中,通过Module.resolve(id)完成,
Module.resolve = function(id, refUri) {
  if (!id) return ""
  id = parseAlias(id)
  id = parsePaths(id)
  id = parseVars(id)
  id = normalize(id)
  var uri = addBase(id, refUri)
  uri = parseMap(uri)
  return uri
}
通过对id的一系列设置(别名解析,路径修正,变量解析以及添加扩展名,最终添加协议等)生成uri。
transport任务
transport任务是打包seajs模块的难点,上节提到了seajs模块的id和uri之间的关系,它们是由seajs来维护的。但是如果通过grunt对seajs进行打包,则模块之间的关系由transport来维护。通过transport生产的seajs模块,有一个显著的变化,即匿名模块变为了具名模块,并且设置了依赖模块。
下面通过配置项来讲解transport任务:
grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        transport : {
            options : {
                path: ['.'],
                alias: '<%= pkg.spm.alias %>' // 注意在package.json中jquery的alias设置
            },
            utils: { // 存放引用的模块,设定模块名和依赖,模块的idleading要与在application中引用的路径一致
                options: {
                    idleading: '../dist/src/'
                },
                files: [{
                    expand: true,
                    cwd: 'lib/src',
                    src: '*',
                    filter: 'isFile',
                    dest: '.build/lib/src'
                }]
            },
            application : {
                options: {
                    idleading: '../dist/'
                },
                files:  [
                    {
                        expand: true,
                        cwd : 'lib',
                        src : 'application.js',
                        filter : 'isFile',
                        dest : '.build/lib'
                    }
                ]
            }
        }
}
在options中,设定了路径为‘.’,即相对于Gruntfile文件的当前路径,alias为package.json中定义的alias;在utils任务中,设置了idleading选项,最终模块的id = idleading + 文件名。值得注意的是idleading路径的设置,这里需要小心设置,它是根据引用最终打包后文件的html的位置决定的。最后,将lib/src下的所有文件设置完id和依赖后放到.build/lib/src下。application任务和utils任务类似,只是单独设置application.js文件的id和依赖。
着重讲解idleading的设置。我们计划将生成的文件(处理完依赖且合并压缩后的文件)放到dist文件夹下面,最终通过view/hello.html引用,

设置transport:util任务的idleading = ‘../dist/src/’,文件经过transport之后,lib/src/name.js文件会被设置并且保存到.build/lib/src中,此时name.js的模块名为’../dist/src/name’,依赖为[]。同理,lib/application.js保存到.build/lib中,并且模块名为’../dist/application’,依赖为[‘./src/util’,’jquery’,’./src/test’,’./src/name’]。然后经过合并压缩之后,生产最终的application.js文件,在view/hello.html中引用(开篇提到)。
在hello.html引用的application文件包含了5个模块,并且每个模块都有id和依赖,因此根据上节具名模块的id与uri的关系,可知道模块id影响到文件的加载。之所以在设置idleading = “../dist/”是根据hello.html的位置决定的。在Module.resolve(id)中,有一步骤为addBase,即有当前相对路径转换为绝对路径,而当前路径是相对于html的位置定义的,具体原因是html引入了seajs,seajs判断当前html的位置,设置为当前路径。这也正是idleading的设置为”../dist/”的原因。
下图可以印证了上文所述:

当然如果html的路径有变化,相应的idleading也要改变:

如果在view/layout/hello.html中引用文件,那么需要改变transport:util的idleading = ‘../../dist/lib/src’,transport:application的idleading = ‘../../dist/lib’,只有保证每个模块的uri正确,才能fetch文件并且执行相应的回调函数。
因此,对于transport任务而言,idleading的设置需要十分注意。
concat、uglify、clean任务
这两个任务很容易定义,而且grunt官网上就是以uglify为例讲解Gruntfile的配置,因此,这两个任务的配置我们有很多资料可以参考。我们使用通配符来匹配文件,使用expand来批量处理,也可以自定义过滤函数。废话不多说,呈上这两个任务的配置:
        concat : {
            options : {
                separator: '/*-------每个文件的分割-------*/',
                banner:  '/*! <%= pkg.name %> - v<%= pkg.version %> - ' +
                    '<%= grunt.template.today("yyyy-mm-dd") %> */',
                footer: '/*-------合并文件的footer-------*/'
            },
            utils: {
                src: ['.build/lib/src/*.js','!.build/lib/src/*-debug.js'],
                dest: '.build/util.js'
            },
            application : {
                src: ['.build/lib/application.js','.build/util.js'],
                dest: 'dist/application.js'
            }
        },
        uglify : {
            main : {
                files : {
                    'dist/application.min.js' : ['dist/application.js'] //对dist/application.js进行压缩,之后存入dist/application.js文件
                }
            }
        },
        clean : {
            build : ['.build'] //清除.build文件
        }
最终的Gruntfile文件定义如下:
module.exports = function(grunt){
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        transport : {
            options : {
                alias: '<%= pkg.spm.alias %>' // 注意在package.json中jquery的alias设置
            },
            utils: { // 存放引用的模块,设定模块名和依赖,模块的idleading要与在application中引用的路径一致
                options: {
                    idleading: '../../dist/src/'
                },
                files: [{
                    expand: true,
                    cwd: 'lib/src',
                    src: '*',
                    filter: 'isFile',
                    dest: '.build/lib/src'
                }]
            },
            application : {
                options: {
                    idleading: '../../dist/'
                },
                files:  [
                    {
                        expand: true,
                        cwd : 'lib',
                        src : 'application.js',
                        filter : 'isFile',
                        dest : '.build/lib'
                    }
                ]
            }
        },
        concat : {
            options : {
                separator: '/*-------每个文件的分割-------*/',
                banner:  '/*! <%= pkg.name %> - v<%= pkg.version %> - ' +
                    '<%= grunt.template.today("yyyy-mm-dd") %> */',
                footer: '/*-------合并文件的footer-------*/'
            },
            utils: {
                src: ['.build/lib/src/*.js','!.build/lib/src/*-debug.js'],
                dest: '.build/util.js'
            },
            application : {
                src: ['.build/lib/application.js','.build/util.js'],
                dest: 'dist/application.js'
            }
        },
        uglify : {
            main : {
                files : {
                    'dist/application.min.js' : ['dist/application.js'] //对dist/application.js进行压缩,之后存入dist/application.js文件
                }
            }
        },
        clean : {
            build : ['.build'] //清除.build文件
        }
    });
    grunt.loadNpmTasks('grunt-cmd-transport');
    grunt.loadNpmTasks('grunt-cmd-concat');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-clean');
    // 默认被执行的任务列表。
    grunt.registerTask('build',['transport','concat','uglify'])
};
总结
通过对seajs的源码分析可以了解模块id与uri的关系,进而方便对grunt的transport调试。其实之所以用grunt对seajs模块进行打包会出现各种各样的问题,归根结底是路径错误。路径错误包括很多,比如模块依赖的路径错误,模块名错误,以及package.json的alias设置错误,最后,需要十分注意html文件的位置,因为seajs定义的cmd依赖于html文件当前位置。
grunt任务之seajs模块打包的更多相关文章
- CMD模块打包部署总结
		目前线上系统利用Seajs做模板化,但是没有对js和css进行打包,在这次简历搜索优化项目里我尝试对gulp插件对Seajs模块打包. 安装gulp和相关插件 npm install -g gulp ... 
- seajs模块压缩问题
		在优化整理项目代码时,想使用seajs来把代码模块化.看了下官方5分钟上手教程,觉得很不错,也没多想就一直开发下去了,也没出什么问题.等一同事说把代码打包个放到设备上去测试一下,发现怎么也跑不起来,郁 ... 
- js模块化/js模块加载器/js模块打包器
		之前对这几个概念一直记得很模糊,也无法用自己的语言表达出来,今天看了大神的文章,尝试根据自己的理解总结一下,算是一篇读后感. 大神的文章:http://www.css88.com/archives/7 ... 
- 关于用gulp合并压缩seaJs模块
		现在很多人都在用seaJs来开发项目,seaJs上手容易,操作简单.但在后期做合并压缩的时候却中了个巨大无比的坑,但坑也总得有人来填.于是花了将近一个星期的时间来填了这坑,现将填坑的一些心得与大家分享 ... 
- jquery插件封装成seajs模块
		jquery直接在html中引入. jquery插件修改为: define(function (require, exports, moudles) { return function (jquery ... 
- 多模块打包后,扫描不到@controller和@service,实现 ADD DIRECTORY ENTRIES
		多模块打包后,扫描不到@controller和@service等Bean. 原因:打包时没有生成目录信息 解决办法: 1.在eclipse或者myeclipse 打包时 勾选 ADD DIRECTOR ... 
- Webpack - CommonJs & AMD 模块打包器
		Webpack 是一个 CommonJs & AMD 模块打包器.可以把你的 JavaScript 代码分离为多个包,在需要的时候进行加载,支持预处理文件,例如 json, jade, cof ... 
- Maven之多模块打包成一个jar包及assembly
		一.多模块打包 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="htt ... 
- 一份关于webpack2和模块打包的新手指南
		webpack已成为现代Web开发中最重要的工具之一.它是一个用于JavaScript的模块打包工具,但是它也可以转换所有的前端资源,例如HTML和CSS,甚至是图片.它可以让你更好地控制应用程序所产 ... 
随机推荐
- thinkphp 缓存数据
			thinkphp 中内置了缓存操作 3.1版本的数据缓存方法是cache 基本用法: S(array('type'=>'xcache','expire'=>60)); 缓存初始化 缓存初始 ... 
- [LintCode] Trailing Zeroes 末尾零的个数
			Write an algorithm which computes the number of trailing zeros in n factorial. Have you met this que ... 
- PHP 通过百度API 实现通过城市名称获取经度
			$city = $_GET['city'];print_r(getjw($city));/*** $city 需要查询的地址* $key 百度开发者账号*/function getjw($city){ ... 
- bzoj2599: [IOI2011]Race(点分治)
			写了四五道点分治的题目了,算是比较理解点分治是什么东西了吧= = 点分治主要用来解决点对之间的问题的,比如距离为不大于K的点有多少对. 这道题要求距离等于K的点对中连接两点的最小边数. 那么其实道理是 ... 
- JAVA笔试题集(一)--JAVASE部分
			红色答案为参考答案 1.从下列选项中选择正确的Java表达式(多选) A. int k=new String("aa"); B. String str=String ... 
- Weblogic反序列化漏洞补丁更新解决方案
			Weblogic反序列化漏洞的解决方案基于网上给的方案有两种: 第一种方案如下 使用SerialKiller替换进行序列化操作的ObjectInputStream类; 在不影响业务的情况下,临时删除掉 ... 
- 关于Map集合
			Map接口实现Collection接口,是集合三大接口之一. Map接口在声明:public interface Map<K,V>;将键映射到值的对象,一个映射不能包含重复的键,每个键最多 ... 
- 发现meta有个刷新页面的办法。
			meta是html中不可缺少的一个标签,它的应用以方便浏览器搜索并分类当前网页的内容. meta总是放在head标签的第一个位置.今天我在复习前端知识的时候,在网上发现了用meta刷新网页的好办法. ... 
- ExtJS扩展:扩展grid
			ExtJs的grid功能很强大,但是有时候觉得总是少那么一点点功能,我们就来扩展它,让它用起来更方便. 今天我们要扩展的是:根据记录的选择数量来禁用或启用grid toolbar上的某些按钮. 本文所 ... 
- ASP.NET Core 优雅的在开发环境保存机密(User Secrets)
			前言 在应用程序开发的过程中,有的时候需要在代码中保存一些机密的信息,比如加密密钥,字符串,或者是用户名密码等.通常的做法是保存到一个配置文件中,在以前我们会把他保存到web.config中,但是在A ... 
