如今公认的JavaScript典型项目须要运行单元测试,合并压缩。有些还会使用代码生成器,代码样式检查或其余构建工具。javascript
Grunt.js是一个开源工具,能够帮助你完成上面的全部步骤。它很是容易扩展,并使用JavaScript书写,因此任何为JavaScript库或项目工做的人均可以按本身的须要扩展它。css
本文解释如何使用Grunt.js构建JavaScript库。Grunt.js依赖Node.js和npm,因此第一节解释其是什么,如何安装和使用。若是你对npm有了解,那你能够跳过这一节。第四和第五节讲述如何配置Grunt和一系列典型Grunt任务。html
本文讨论的代码例子能够在GitHub上访问。前端
开始以前,咱们须要三个工具:html5
Node.js是一个流行的服务器端JavaScript环境。它被用来编写和运行JavaScript服务和JavaScript命令行工具。若是你想进一步连接Node.js,你能够查看Stack Overflow上相关的资料。java
Npm是Node.js的包管理工具。它能从中心仓库下载依赖,解决大部分依赖冲突问题。Npm仓库仅仅存储Node.js服务器段和命令行项目。它不包含用于web和移动app相关的库。咱们用它来下载Grunt.js。node
Grunt.js是一个任务运行工具,咱们用起构建咱们的项目。它在Node.js之上运行而且经过Npm安装。git
你能够直接从下载页面或用其它包管理工具安装node.js。安装成功后在命令行输入 node -v Node.js将输出它的版本号。github
大部分安装器和包管理工具将同时安装Npm。在命令行输入 npm -v 测试是否安装成功。若是成功将输出它的版本号。不一样的系统可能安装方式略有不一样。web
下载和使用安装脚本。
windows安装器包含npm并会添加path变量。仅在你下载Node.exe或从源代码编译Node是才须要独立安装Npm。从这里下载Npm的最新版zip压缩包。解压后赋值到Node.exe的安装目录。若是你愿意,你也能够放到任何位置,将其加入path变量便可。
安装包中内建Npm。
了解Npm基础操做对于使用和安装Grunt.js都有帮助。这节仅包含接触知识。更多细节能够查看npm文档。
本节将解释下面的东西:
Npm是一个包管理工具,能够从中心仓库下载和安装JavaScript依赖。安装包能被用在Node.js项目或命令行工具。
项目一般在package.json文件内部列出其依赖和安装插件。此外npm库也能够从命令行安装。
每一个包能够安装在全局或本地环境。实际的区别是存储位置和访问方式。
全局安装包被直接存储在Node.js安装路径。他们之因此被称为全局,是由于他们能够在任何地方直接访问。
本地安装将下载包安装在当前工做路径。本地安装包只能从其所在目录访问。
本地安装包被存储进node_mudules子目录。不管你使用什么版本控制系统,你能够将其添加仅.ignorefile 文件。
package.json文件包含npm项目描述。它老是位于项目的根目录,而且包含项目名称,版本,协议和其余相似元数据。最重要的是,它包含两个项目依赖列表。
第一个列表包含运行所须要的依赖。任何但愿使用此项目的人必须安装它们。第二个列表包含开发时须要依赖项。包括测试工具,构建工具和代码样式检测工具。
建立package.json最简单的方法是经过 npm install 命令。这条命令会以交互式提问一系列问题,并根据回答在当前工做目录生成基本的package.json文件。只有名字(name)和版本(version)属性是必须的。若是你不打算将你的库发布到Npm,你能忽略其他的部分。
下面的连接包含对package.json的详细描述:
Npm宝能够经过npm install 命令安装。默认安装到本地。全局安装须要指定 -g开关。
不带参数的 npm install 将在当前目录或上层目录查找 package.json 文件。若是发现,将会在当前目录安装全部列出的依赖项。
能够经过 npm install <pkg_name@version> 命令安装具体的npm包。这条命令将从中心仓库找到指定版本的包,并将其安装到当前目录。
版本号是可选的。若是省略将下载最新稳定版。
最后,经过 --sace-dev开关不只能够安装包,还会将其添加到 package.json 的开发依赖中。
咱们将首先将Grunt.js添加进咱们的JavaScript项目。为此咱们须要安装两个Grunt.js模块:
提醒:最新的Grunt.js(4.0)再也不兼容之前的版本。一些老的教程和文档再也不适合新版Grunt.js了。
全部实际的工做是由任务运行器来作。命令行接口仅解析参数和将其传递个任务运行器。若是任务运行器没有安装将不会作任何事情。
命令行接口应该被安装在全局环境,然而任务运行器在本地环境。全局命令行接口保证Grunt命令能够在全部路径访问。任务运行器必须是本地的,由于不一样的项目可能须要不一样的Grunt版本。
安装全局Grunt命令行接口:
npm install -g grunt-cli
切换到项目根目录,经过npm init让Npm帮你生成package.json文件。它会问你些问题而后根据你的回答生成合法的package.json文件。只有名字和版本是必须的;你能够忽略其余选项。
将Grunt.js最新版添加到本地环境,同时添加到package.json文件的开发依赖里。
npm install grunt --save-dev
经过前面的命令建立的package.json文件应该相似相面这样:
{ "name": "gruntdemo", "version": "0.0.0", "description": "Demo project using grunt.js.", "main": "src/gruntdemo.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": "", "author": "Meri", "license": "BSD", "devDependencies": { "grunt": "~0.4.1" } }
Grunt.js运行任务而且经过任务完成工做。然而,Grunt.js安装成功后尚未任务可使用。任务必须从插件载入,而插件一般须要Npm安装。
咱们使用五个插件:
本节将解释如何配置这些插件。咱们从最简单的配置开始,而后一步步解释如何配置任务。余下的子章节将详细讲解如何配置每一个插件。
Grunt将配置信息写到Gruntfile.js或Gruntfile.coffee文件里。因为咱们建立的JavaScript项目,咱们将使用JavaScript版本。最简单的Gruntfile.js看起来像下面这样:
//包装函数 有一个参数 module.exports = function(grunt) { // 默认任务。在本例子中没有任何操做。 grunt.registerTask('default', []); };
配置信息被保存在module.exports函数内部。它包含grunt对象做为其参数,而且经过调用该函数完成配置。
配置函数必须建立至少一个任务别名,而且配置他们。例如,上面的代码片断建立了一个“default”任务别名而且指定为空的任务列表。换句话说,默认的任务别名能够工做,但不会作任何事情。
用 grunt <taskAlias> 命令运行指定的 taskAlias任务。taskAlias的参数是可选的,若是省略,Grunt将使用“default”任务。
保存Gruntfile.js文件,在命令行运行Grunt:
grunt
你应该看到以下输出:
Done, without errors.
若是配置任务返回错误或警告Grunt将发出蜂鸣声(在命令行下输入错误的声音,译者未发现这点)。若是你不想听到蜂鸣声,你可使用 -no-color 参数:
grunt -no-color
从插件添加任务是经过用步骤,对全部插件都是相同的。本节将概要讲述须要的过程,实际的例子将在下面的章节讲解。
首先,咱们须要将插件添加进package.json文件的开发依赖里面,而且使用Npm进行安装:
npm install <plugin name> --save-dev
任务配置必须被存储在一个对象内部,有各自的任务名,而且被传递给 grunt.initConfig方法:
module.exports = function(grunt) { grunt.initConfig({ firstTask : { /* ... 配置第一个任务 ... */ }, secondTask : { /* ... 配置第二个任务 ... */ }, // ... 其余任务 ... lastTask : { /* ... 最后一个任务 ... */ } }); // ... the rest ... };
全面的任务配置信息解释看这里Grunt.js文档。本节仅描述最通用,简单的例子。假设任务接受一个文件列表,并处理他们,而后生出输出文件。
一个简单的任务配置例子:
firstTask: { options: { someOption: value //取决于插件 }, target: { src: ['src/file1.js', 'src/file2.js'], //输入文件 dest: 'dist/output.js' // 输出文件 } }
例子中的任务配置有两个属性。一个是任务选项,名称必须是”options“。Grunt.js不会对options的属性执行任何操做,其行为有插件决定。
其余项能够有任何名字,而且要包含任务目标。最多见的任务是操做和生成文件,因此他们的target有两个属性,”src“ 和 ”dest“。src包含输入的文件列表,dest包含输出的文件名字。
若是你配置多个任务,Grunt将依次执行。下面的任务将运行两次,一次操做src及其子目录的全部js文件,另外一次操做test及其子目录下的全部js文件:
multipleTargetsTask: { target1: { src: ['src/**/*.js'] }, target2: { src: ['test/**/*.js']] } }
最后,将插件载入必须使用 grunt.loadNpmTasks 函数,而且注册任务别名。
上面介绍的结构合起来以下:
module.exports = function(grunt) { grunt.initConfig({ /* ... tasks configuration ... */ }); grunt.loadNpmTasks('grunt-plugin-name'); grunt.registerTask('default', ['firstTask', 'secondTask', ...]); };
JSHint检查JavaScript代码中潜在的问题和错误。他被设计成可配置的,而且有合理的默认值。
咱们将使用 grunt-contrib-jshint 插件,grunt-contrib开头的插件都是有Grunt官方维护的,若是你建立本身的插件千万不要以次开头。
打开命令行,在项目根目录运行 npm install grunt-contrib-jshint --save-dev。将会添加插件到package.json文件的开发依赖,而且安装到本地Npm仓库。
grunt-contrib-jshint插件的参数和JSHint同样。完整的参数列表能够访问JSHint的文档页面。
JSHint 的参数 “eqeqeq” 会将 == 和 != 操做符报告为警告。默认是关闭的,由于这些操做符是合法的。但我建议你开启它,由于严格相等比非严格相等更安全。
同时我建议你开启trailing选项,将会对代码中结尾部的空白元素生成警告。结尾的空白在多行字符串中会引发奇怪的问题。
每个可选项都是布尔值,设置为true将会开启相应检测。下面的例子开启了eqeqeq和trailing选项:
options: { eqeqeq: true, trailing: true }
grunt-contrib-jshint插件的任务名字是“jshint”。咱们将使用上一节中的配置选项,使它检测位于src和test目录下的所有JavaScript文件。
JSHint的配置信息必须写在名为“jshint”的属性内部。能够有两个属性,一个数参数(options)另外一个是目标(target)。
目标能够在任何属性内部,在这里咱们仅使用“target”。其必须包含待验证的JavaScript文件列表。文件列表能够放在目标的src属性中,可使用**和*通配符。
有两个自定义选项,将会验证位于src和test目录及其子目录下的全部js文件。
grunt.initConfig({ jshint: { options: { eqeqeq: true, trailing: true }, target: { src : ['src/**/*.js', 'test/**/*.js'] } } });
最后须要载入和注册 grunt-contrib-jshint 任务:
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.registerTask('default', ['jshint']);
目前为止完整的 Gruntfile.js 文件以下:
module.exports = function(grunt) { grunt.initConfig({ jshint: { options: { trailing: true, eqeqeq: true }, target: { src : ['src/**/*.js', 'test/**/*.js'] } } }); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.registerTask('default', ['jshint']); };
js文件通常是分散的(若是你的项目不是几十行js的话),上线前咱们必须将其打包进一个文件,并添加版本号和项目名字。在文件的开始位置应该包含库名称,版本,协议,构建时间和其余一些信息。
例如,像下面这样:
/*! gruntdemo v1.0.2 - 2013-06-04 * License: BSD */ var gruntdemo = function() { ...
咱们将使用 grunt-contrib-concat 插件完成拼接任务,并生成正确的注释信息。
和前面同样,将插件写进package.json文件的开发依赖里,而后从Npm仓库安装到本地。
打开命令行,运行以下命令:
npm install grunt-contrib-concat --save-dev
首先从package.json文件加载配置信息,并存储在pkg属性。须要使用 grunt.file.readJSON 函数:
pkg: grunt.file.readJSON('package.json'),
如今pkg的值是一个对象,包含所有package.json的信息。项目名被存储在pkg.name属性,版本被存储在pkg.version。版权被存储在pkg.license属性等等。
Grunt提供一套模版系统,咱们可使用它构建页头和文件名称。模版能够在字符串中嵌入JavaScript表达式,经过<%= expression %> 语法。Grunt计算表达式的值并替换模版中的表达式。
例如,模版中的 <%= pkg.name %> 将被替换为 pkg.name 的属性值。若是属性值是字符串,模版的行为相似字符串拼接 ...' + pkg.name + '...
模版中能够引用Grunt中的所有属性。系统提供了一个很是有帮助的日期格式化函数。咱们将使用 grunt.template.today(format) 函数生成当前的时间戳。
让咱们生成一个简单的页头,包含项目名称,版本号,版权和当前的日期。因为咱们须要在 Uglify 任务中使用banner,因此咱们将其存储在变量中:
var bannerContent = '/*! <%= pkg.name %> v<%= pkg.version %> - ' +
'<%= grunt.template.today("yyyy-mm-dd") %> \n' +
' * License: <%= pkg.license %> */\n';
上面的模版生成以下的页头:
/*! gruntdemo v0.0.1 - 2013-06-04 * License: BSD */
项目的名称和版本部分也须要在多处使用。将项目名和版本号拼在一块儿,存储在一个变量中:
var name = '<%= pkg.name %>-v<%= pkg.version%>';
生成的名字以下:
gruntdemo-v0.0.1
target必须包含须要被拼接的文件列表,和合并完成后输出文件的名字。target支持通配符和模版,因此咱们使用前一节生成的模版:
target : { // 拼接src目录下的全部文件 src : ['src/**/*.js'], // place the result into the dist directory, // name variable contains template prepared in // previous section dest : 'distrib/' + name + '.js' }
concat插件也能够经过banner属性添加banner。因为上面咱们已经将banner内容赋给bannerContent变量,因此咱们仅需引入便可:
options: {
banner: bannerContent
}
最后不要忘记从Npm加载 grunt-contrib-concat ,而且将其注册到默认工做流:
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.registerTask('default', ['jshint', 'concat']);
这一节出示包含完整contat配置的Gruntfile.js文件。
注意pkg属性在传递给initConfig方法的参数中定义。咱们不能把他放在其余地方,由于它读取模版信息,而且仅在initConfig方法的参数和grunt对象中有访问模版的权限。
module.exports = function(grunt) { var bannerContent = '... banner template ...'; var name = '<%= pkg.name %>-v<%= pkg.version%>'; grunt.initConfig({ // pkg is used from templates and therefore // MUST be defined inside initConfig object pkg : grunt.file.readJSON('package.json'), // concat configuration concat: { options: { banner: bannerContent }, target : { src : ['src/**/*.js'], dest : 'distrib/' + name + '.js' } }, jshint: { /* ... jshint configuration ... */ } }); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.registerTask('default', ['jshint', 'concat']); };
若是浏览器载入和解析大文件,会使页面载入变得缓慢。可能不是全部项目都会遇到这个问题,但对于移动端的app和使用很是多大型库的大型web应用程序而言,是必需要考虑的。
所以,咱们也将咱们库进行压缩。压缩会将输入的文件变小,经过去除空白元素,注释,替换变量名称等,但不会改变代码逻辑。
压缩功能使用 grunt-contrib-uglify 插件,其经过Grunt集成 UglifyJs。它经过uglify任务拼接和压缩一组文件。
压缩会使生成的文件难于阅读和调试,因此咱们经过生成源程序映射来简化问题。
源程序映射是压缩文件和源文件之间的纽带。若是浏览器支持,浏览器调试工具会显示对人友好的源文件,而不是压缩文件。仅有chrome和nightly版本的 firefox支持源代码映射。你能够在HTML5 rocks 和 Tutsplus 上获取更多信息,我建议你看看阮一峰老师的这篇文章:http://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html
将插件添加到package.json的开发依赖里,而且安装到本地Npm仓库。
使用以下命令:
npm install grunt-contrib-uglify --save-dev
配置uglify任务目标的方式和concat任务相似。必需要包含待压缩的javascript文件列表和输出文件的名字。
支持通配符和模版,因此咱们可使用前面章节中的用到的模版:
target : { // use all files in src directory src : ['src/**/*.js'], // place the result into the dist directory, // name variable contains template prepared in // previous sub-chapter dest : 'distrib/' + name + '.min.js' }
配置banner的方式和concat同样——经过设置“banner”属性而且支持模版。所以,咱们能够重复使用前面章节中准备好的 bannerContent 变量。
经过“sourceMap”属性生成源文件映射。包含生成文件的名字。此外,必须设置“sourceMapUrl”和“sourceMapRoot”属性。前一个包含相对于uglified文件到源文件映射文件的路径,后一个包含是源文件映射到源文件的相对路径。
经过bannerContent变量生成页眉,经过name变量生成源文件映射文件的名字:
options: { banner: bannerContent, sourceMapRoot: '../', sourceMap: 'distrib/'+name+'.min.js.map', sourceMapUrl: name+'.min.js.map' }
最后一步是从Npm加载 grunt-contrib-uglify,而且添加到默认任务列表:
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.registerTask('default', ['jshint', 'concat', 'uglify']);
下面是包含完整uglify配置信息的Gruntfile.js文件:
module.exports = function(grunt) { var bannerContent = '... banner template ...'; var name = '<%= pkg.name %>-v<%= pkg.version%>'; grunt.initConfig({ // pkg must be defined inside initConfig object pkg : grunt.file.readJSON('package.json'), // uglify configuration uglify: { options: { banner: bannerContent, sourceMapRoot: '../', sourceMap: 'distrib/'+name+'.min.js.map', sourceMapUrl: name+'.min.js.map' }, target : { src : ['src/**/*.js'], dest : 'distrib/' + name + '.min.js' } }, concat: { /* ... concat configuration ... */ }, jshint: { /* ... jshint configuration ... */ } }); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.registerTask('default', ['jshint', 'concat', 'uglify']); };
最后要发布的库包括两个文件,而且都在名字中含有版本号。这会给想要自动下载每一个新版本的人形成没必要要的困难。
若是想要查看是否发布了新版本和新版本的名字,必须每次都要获取和解析一个json文件。若是每次都更更名称,则必须更新下载脚本。
所以咱们将使用 grunt-contrib-copy 插件建立无版本号的文件。
将插件添加进package.json的开发依赖,而且从Npm仓库安装到本地。
使用以下命令:
npm install grunt-contrib-copy --save-dev
copy配置信息包括三个目标,分别对应三个发布文件。没有配置选项,基本和前一个插件配置过程同样。
仅有一点不同,就是多任务。每一个任务包含一对 src/dest,待拷贝的名字和待建立的名字。
对前面的任务配置选项稍加修改。将全部文件名字放到变量中,以即可以重复使用:
module.exports = function(grunt) { /* define filenames */ latest = '<%= pkg.name %>'; name = '<%= pkg.name %>-v<%= pkg.version%>'; devRelease = 'distrib/'+name+'.js'; minRelease = 'distrib/'+name+'.min.js'; sourceMapMin = 'distrib/source-map-'+name+'.min.js'; lDevRelease = 'distrib/'+latest+'.js'; lMinRelease = 'distrib/'+latest+'.min.js'; lSourceMapMin = 'distrib/source-map-'+latest+'.min.js'; grunt.initConfig({ copy: { development: { // copy non-minified release file src: devRelease, dest: lDevRelease }, minified: { // copy minified release file src: minRelease, dest: lMinRelease }, smMinified: { // source map of minified release file src: sourceMapMin, dest: lSourceMapMin } }, uglify: { /* ... uglify configuration ... */ }, concat: { /* ... concat configuration ... */ }, jshint: { /* ... jshint configuration ... */ } }); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-copy'); grunt.registerTask('default', ['jshint', 'concat', 'uglify', 'copy']);
最后,配置 grunt.js 运行单元测试,测试最新发布的文件。咱们将使用 grunt-contrib-qunit 插件实现目标。这个插件将在无头的 PhantomJS 实例中运行 QUnit 单元测试。
这个解决方案不能模拟不一样浏览器和查找所有 bug,但对于咱们来讲已经足够了。若是想获得更好的配置,可使用 js-test-driver 或 其余相似工具,然而,关于 js-test-dirver 的配置超出了本文的范围。
Qunit 单元测试常常要运行 src 目录里的 JavaScript 文件,因为测试是开发的一部分。若是你想测试刚刚发布的拼接压缩后的版本工做情况,须要建立一个新的 QUnit HTML 文件,并加载最后发布的文件。
下面是一个例子是 Qunit 的入口文件:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>QUnit Example</title> <link rel="stylesheet" href="../libs/qunit/qunit.css"> </head> <body> <div id="qunit"></div> <div id="qunit-fixture"></div> <script src="../libs/qunit/qunit.js"></script> <!-- Use latest versionless copy of current release --> <script src="../distrib/gruntdemo.min.js"></script> <script src="tests.js"></script> </body> </html>
将插件添加进package.json 的开发者依赖中,而且将其安装到本地 Npm 仓库。
使用以下命令:
npm install grunt-contrib-qunit --save-dev
配置 grunt-contrib-qunit 插件和配置前面的任务一模一样。因为咱们使用默认的 Qunit 配置,因此能够省略选项属性。不能忽略的是必须配置 target,指定所有的 Qunit HTML 文件。
接下来指定位于测试目录下的所有 HTML 文件,及其子目录应该运行 Qunit 测试:
grunt.initConfig({ qunit:{ target: { src: ['test/**/*.html'] } }, // ... all previous tasks ... });
下面是完整的 Gruntfile.js 配置信息:
module.exports = function(grunt) { var name, latest, bannerContent, devRelease, minRelease, sourceMap, sourceMapUrl, lDevRelease, lMinRelease, lSourceMapMin; latest = '<%= pkg.name %>'; name = '<%= pkg.name %>-v<%= pkg.version%>'; bannerContent = '/*! <%= pkg.name %> v<%= pkg.version %> - ' + '<%= grunt.template.today("yyyy-mm-dd") %> \n' + ' * License: <%= pkg.license %> */\n'; devRelease = 'distrib/'+name+'.js'; minRelease = 'distrib/'+name+'.min.js'; sourceMapMin = 'distrib/'+name+'.min.js.map'; sourceMapUrl = name+'.min.js.map'; lDevRelease = 'distrib/'+latest+'.js'; lMinRelease = 'distrib/'+latest+'.min.js'; lSourceMapMin = 'distrib/'+latest+'.min.js.map'; grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), qunit:{ target: { src: ['test/**/*.html'] } }, // configure copy task copy: { development: { src: devRelease, dest: lDevRelease }, minified: { src: minRelease, dest: lMinRelease }, smMinified: { src: sourceMapMin, dest: lSourceMapMin } }, // configure uglify task uglify:{ options: { banner: bannerContent, sourceMapRoot: '../', sourceMap: sourceMapMin, sourceMappingURL: sourceMapUrl }, target: { src: ['src/**/*.js'], dest: minRelease } }, // configure concat task concat: { options: { banner: bannerContent }, target: { src: ['src/**/*.js'], dest: devRelease } }, // configure jshint task jshint: { options: { trailing: true, eqeqeq: true }, target: { src: ['src/**/*.js', 'test/**/*.js'] } } }); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-copy'); grunt.loadNpmTasks('grunt-contrib-qunit'); grunt.registerTask('default', ['jshint', 'concat', 'uglify', 'copy', 'qunit']); };
如今 Grunt.js 配置好了,而且可使用了。咱们的目标是使配置尽量简单,使用成对的 src/dest,通配符和模版。固然,Grunt.js 也提供其余更高级的选项。
若是可以自动下载和管理项目依赖的库,会变得更美好。我发现两个可行的解决方案,Bower 和 Ender。我没有试过他们,但均可以管理前端JavaScript包和其依赖。
文章有些长,拖了好久的文章终于翻译完成了,我最近打算写一本关于 Grunt 指南的书籍,会详细讲解如何构建一套前端自动化工具,若是你支持个人工做,那就给我捐助吧。
原文:http://flippinawesome.org/2013/07/01/building-a-javascript-library-with-grunt-js/