语义化版本控制 2.0.0

译者注1:Semantic Versioning 2.0.0的原文地址是http://semver.org

概要

版本号MAJOR.MINOR.PATCH(主版本号.副版本号.补丁版本号)的递增规则如下:

  1. 当以不向后兼容的方式,变更API时,递增主版本号。
  2. 当以向后兼容的方式,增加功能时,递增副版本号。
  3. 当以向后兼容的方式,修正BUG时,递增补丁版本号。

预发布信息和构建元数据可以作为附加标签,扩展MAJOR.MINOR.PATCH(主版本号.副版本号.补丁版本号)的格式。

引言

在软件开发管理的世界里,有一个可怕的地方叫做“依赖关系地狱”。当你的系统越来越大,集成了更多的包,你就会深刻的认识到自己的卑微和渺小。直到有一天,你在这个坑里彻底绝望。

系统依赖的模块发布新版本,就成了噩梦。如果依赖关系的规定过于严格,会遇到一个版本依赖关系的死结(升级一个依赖包之前,必须升级完所有的依赖包)。如果依赖关系规定的太松散,版本依赖关系的混乱就会成为你的切肤之痛(假定你的版本兼容性很重要)。当版本依赖关系的死结、混乱,让你不能轻松、安全的向前迁移项目的时,你就坠入了“依赖关系地狱”。

为了解决这个问题,我提出了一套简单的规则和要求,决定版本号如何分配和递增。这些规则都是基于(但不限于)业界早已存在的流行做法。使用此制度的软件必须声明一个公共API,可以在代码内部声明,也可以严格地写入文档中,但必须是清晰而准确的。一旦确定了公共API,都必须通过明确的版本号递增,来表明系统所做的变更。认真考虑一下X.Y.Z(主版本号.副版本号.补丁版本号)的版本格式。不影响API的BUG修正,递增补丁版本号。向后兼容API的功能新增或变更,递增副版本号。不向后兼容的API变更,递增主版本号。

我把这个制度称为“语义化版本控制”。在这个方案中,规范了从一个版本到下一个版本的版本号,规范了更改版本号传达源代码意图的方法,以及源代码做了什么样的修改。

语义化版本控制规范(SemVer)

在本文档中,对关键词(”MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL”)的使用,遵循RFC2119标准的规范。

译者注2:RFC2119标准规范了表示“要求”(Requirement)的动词涵义。关于RFC2119标准可以参考《RFC2119 中文版-RFC文档用于指出要求级别的关键字》。

  1. 使用语义化版本控制的软件必须(MUST)声明一个公共的API。这个API可以在源代码的内部声明,也可以在文档中严格的定义。无论怎么做,都应该是清晰而全面的。
  2. 一个标准的版本号必须(MUST)是X.Y.Z的形式。其中的X、Y和Z都要是正整数,并且禁止(MUST NOT)包含前导零。X是主版本号,Y是副版本号,Z是补丁版本号。每个元素数字的递增,必须(MUST)是1。例如:1.9.0 -> 1.10.0 -> 1.110
  3. 一旦发布一个包的版本,该版本的内容禁止(MUST NOT)再做任何修改。任何的修改都必须(MUST)发布一个新的版本。
  4. 主版本0(0.y.z)用于初始开发。此时的公共API随时都可能会改动,是不稳定的。
  5. 在1.0.0版本中,公共API作出正式的定义。后续的版本号变更,都决定于公共API是否变更,以及如何变更。
  6. 以向后兼容的方式修正BUG时,必须(MUST)递增补丁版本号Z(x.y.Z | x > 0)。BUG修正的定义,是在源代码内部修正错误的行为。
  7. 当以向后兼容的方式变更公共API时,或者公共API中原有的功能标记为“不建议使用(deprecated)”时,必须(MUST)递增副版本号Y(x.Y.z | x > 0 )。源代码内部大量增加、修改功能时,也可以(MAY)递增副版本号。副版本号的递增可以(MAY)包括补丁版本的更新内容。副版本号递增后,补丁版本号必须(MUST)重置为0。
  8. 当以不向后兼容性的方式,变更公共API时,必须(MUST)递增主版本号X(X.y.z | X > 0),可以(MAY)包含副版本和补丁版本级别的变更。主版本递增后,副版本号和补丁版本号必须(MUST)重置为0。
  9. 预发布的版本号,可以(MAY)在补丁版本号后面,添加一个破折号和一系列被点分割的标识符。这些标识符必须(MUST)由ASCII码中的字母和数字以及连字符[0-9A-Za-z-]组成。标识符禁止(MUST NOT)为空。数字禁止(MUST NOT)包含前导0。预发布版本号的优先级比正常的版本号低。预发布版本号表示该版本是不稳定的,可能无法达到其指明的正常版本号的兼容性预期。例如:1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-0.3.7, 1.0.0-x.7.z.92
  10. 构建的元数据,可以(MAY)在补丁版本号或预发布版本号后面,添加一个加号和一系列由连字符分隔的标示符组成。这些标识符必须(MUST)由ASCII码中的字母和数字以及连字符[0-9A-Za-z-]组成。标识符禁止(MUST NOT)为空。在确定版本号的优先级时,构建元数据应该(SHOULD)被忽略。因此,如果两个版本号的差异,仅仅在于构建元数据,就视为具有相同的优先级。例如:1.0.0-alpha+001, 1.0.0+20130313144700, 1.0.0-beta+exp.sha.5114f85
  11. 优先级是指在排序时,版本号之间如何进行比较。优先级必须(MUST)是,根据版本号按照主版本号、副版本号、补丁版本号和预发布标识的顺序进行计算产生的(构建元数据不参与优先级的比较)。优先级总是从左到右的,依次比较主版本号、副版本号、补丁版本号的数字大小。例如:1.0.0 < 2.0.0 < 2.1.0 < 2.1.1。在主版本号、副版本号、补丁版本号都相同时,预发布版本号的优先级低于正常版本号。例如:1.0.0-alpha < 1.0.0。两个预发布版本号的主版本号、副版本号、补丁版本号都相同时,优先级必须(MUST)从左至右的比较由点分隔的标识符,直到发现差异。标识符仅由数字组成的,比较数字的大小。标识符包含字母或连字符的,按照ASCII码的顺序进行比较。数字标识符总是小于非数字标识符。如果前面所有的标识符都是相同的,版本号中预发布字段较长的,优先级较高。例如:1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0

为什么要使用语义化版本控制

语义化版本控制并不新鲜,也不是一个革命性的想法。事实上,你可能做了很多,已经完成了这件事。问题是完成并不等于极致。如果不符合某种正式的规范,版本号在依赖关系管理上基本没有什么用处。给上述想法起个名字,做出清晰的定义,就很容易向软件用户传达你的意图。一旦这些意图是明确的,一个灵活的(但也不能太灵活)依赖关系管理规范,就流畅的运转起来了。

举一个简单的例子,说明语义化版本控制如何把你从“依赖关系地狱”中拯救出来。设想一个叫“消防车”的库,依赖一个使用语义化版本控制的包,名叫“梯子”。“消防车”刚刚生产出来的时候,梯子的版本号是3.1.0。由于“消防车”使用了在3.1.0版本才开始提供的一些功能,你就可以放心地指定依赖的“梯子”的版本大于等于3.1.0,但低于4.0.0。现在,当“梯子”有3.1.1和3.2.0的版本可用时,就可以更新到你的包管理系统,并知道他们将与当前依赖的软件兼容。

作为一个负责任开发者,你当然会验证任何包所标榜的新功能。现实世界可是一个鱼龙混杂的地方,做什么事情都可以,但要警惕。你可以让语义化版本控制,给你提供一个稳健的方式来发布和升级包,不必再被迫为使用依赖包的新版本而反复折腾,为你节省时间,减少麻烦。

如果这一切听起来还不错,你需要做的所有事情,就是开始声明你正在使用语义化版本控制,并遵循其规则。欢迎你在README文档中链接这个网站(译者注3),让更多的人知道这个规则,并从中受益。

译者注3:原文中提到的网站是指http://semver.org

常见问题

在0.y.z的初始开发阶段,我应该怎么处理我的修订?

最简单的做法就是从0.1.0版本开始你的初始开发,在后续的发布中递增副版本号。

我怎么知道什么时候发布1.0.0版本?

如果你的软件在产品环境中使用,它可能已经应该是1.0.0。如果的软件用户,已经开始依赖一个稳定的API,应该是1.0.0。如果你担心很多向后兼容的问题,应该已经是1.0.0。

这难道不会妨碍快速开发和快速迭代吗?

所有在快速开发和快速迭代的时候,主版本号都是0。如果你的API每天都在改变,仍然应该会在版本0.y.z阶段。或者,在一个单独的开发分支中,开展下一个主版本的开发工作。

公共API如果连最微小的向后不兼容的改变,都需要一个主版本号的冲击,会不会太迅速的就到了42.0.0版本?

这是一个负责任的开发问题,也是一个有远见的问题。在有大量依赖关系管理的软件中,不应该轻率的推出不兼容的改变。升级所必须付出的成本是有意义的。发布冲击主版本号的不兼容改变,意味着你深思熟虑过将要发生的震荡,有磕碰主要版本发布不兼容的改变意味着你想通过你的变化的影响,并评估过所涉及的投入产出比。

记录整个公共API是​太辛苦了!

为预计会给其他人使用的软件,提供适当的文档,是一个专业开发者的责任。控制软件的复杂性,是保持项目高效的一个非常重要的组成部分。如果没有谁知道如何使用你的软件,或者用什么方法可以安全地调用,保持项目高效是很难做到的。从长远来看,采用语义化版本控制,坚定的看守一个定义良好的公共API,可以让每一个人都能流畅的做每一件事。

如果不小心,使用副版本号发布了一个不能向后兼容的变更,该怎么办啊?

一旦意识到已经破坏了语义化版本规范,要发布修正了问题,并且恢复向后兼容的新的主版本号。即便是这样,这也是一次不能接受的版本发布。如果方便,一定要记录有问题的版本,并通知你的用户,让他们意识到问题的存在。

如果更新了自己的依赖 但不改变公共API,版本号应该怎么规划?

这是向后兼容的,因为不影响公共API。如果软件与你的包有相同的依赖,有可能会有自己的依赖关系规范,而且软件的作者也会关切任何有可能的冲突。确定这个依赖变更是补丁级别的,还是副版本级别的,取决于你更新你的依赖是要修正一个BUG,还要要引入一组新的功能。我们通常期待是后者,哪怕增加大量的代码。因为在这种情况下,就显然是一个副版本级别的变更了。

如果在不经意间,以不符合版本规范的方式,改了变公共API怎么办(也就是,在一个补丁版本的发布中,源代码错误的引入了一个主版本级别的重大变更)?

那你就发挥出色的判断能力吧。如果拥有大量的忠实拥趸,愿意委曲求全的回到公共API的预期轨道上来,那你最好发布一个主版本。哪怕没什么修改,严格的说也只是补丁级别的。请记住,语义化版本控制是通过版本号的变更来传达意图的。如果变更对你的用户很重要,就使用版本号通知他们。

我应该如何处理以后要废弃的功能?

废弃某些功能,在软件开发中很常见的,通常也会推动软件向前发展。

在废弃公共API的一部分时,你要做两件事:

  1. 更新你的文档,让用户知道这些变更。
  2. 发布包含废弃功能的副版本。

你在主版本中彻底删除这些功能之前,至少要在发布的一个副版本中包含这些功能,好让用户平滑的迁移到新的API上来。

在语义化版本控制中,版本号的长度有限制吗?

没有什么限制,但是你最好保持理智。比如,一个版本号有255个字符,就有点出格了。另外,在开发团队中具体的制度可能会有一些自定义的长度限制。

关于作者

语义化版本控制规范是由Tom Preston-Werner编写的。他是Gravatars 的创始人,也是GitHub的共同创办人。

如果你有一些反馈,可以在GitHub上提交一个问题

许可证

知识共享 - CC 3.0

参考资料

使用JSDoc3生成javascript项目的API文档

JSDoc样式的注释

选用JSDoc3作为注释规范,文档生成工具使用grunt-jsdoc,本文不再介绍选型过程和原因。对JSDoc3的深入学习使用,可以参考入门教程http://usejsdoc.org/index.html

JSDoc注释的样式如下例,与单行注释 // 和多行注释 /**/ 不同,而是类似于JAVA的JDoc和PHP的PHPDoc。

/**
* JSDoc注释。
*/

JSDoc的常用标签

函数注释示例

/**
* 函数注释的示例。
* @param {Integer} augend 被加数。
* @param {Integer} addend 加数。
* @return {Integer} 两数之和。
* @example
* add(1, 2) => 3
*/
function add(augend, addend){
    return augend + addend;
}

参数注释

@param 用于对函数、类的方法的参数进行注释,是JSDoc中最常用的注释标签。

@param 注释必须指定一个参数名,也可以有一个用大括号括起来的参数类型,以及参数的描述信息。参数类型可以是javascript内置的数据类型,如Array、Boolean、Date、Function、Number Object 、String 等,还可以是其他JSDoc所支持的(英文没看太懂,以后用到再认真学习)。

The parameter type can be a built-in JavaScript type, such as string or Object, or a JSDoc namepath to another symbol in your code. If you have written documentation for the symbol at that namepath, JSDoc will automatically link to the documentation for that symbol. You can also use a type expression to indicate, for example, that a parameter is not nullable or can accept any type; see the @type documentation for details.

下面直接使用http://usejsdoc.org/tags-param.html文档中的例子,说明 @param 标签中如何使用参数名、参数类型和参数描述信息。

只有参数名的注释:

/**
 * @param somebody
 */
function sayHello(somebody) {
    alert('Hello ' + somebody);
}

包括参数名和参数类型的注释:

/**
 * @param {string} somebody
 */
function sayHello(somebody) {
    alert('Hello ' + somebody);
}

包括参数名、参数类型和参数描述的注释:

/**
 * @param {string} somebody Somebody's name.
 */
function sayHello(somebody) {
    alert('Hello ' + somebody);
}

返回值注释

@return 说明函数的返回值。例如:

  • @return {Number}
  • @return {Number} Sum of a and b
  • @return {Number|Array} Sum of a and b or an array that contains a, b and the sum of a and b.

参见:http://usejsdoc.org/tags-returns.html

模块注释

@module标签用于标记当前代码文件属于哪个模块。

@link@see 标签中,使用 module:moduleName 可以链接到一个模块。例如,使用 {@link module:foo/bar},可以链接到由 "@module foo/bar 定义的模块。

如果没有提供模块名,将使用模块文件所在路径及文件名作模块名。

参考引用注释

@see 标签可以指向一个相关的引用。示例如下:

/**
 * Both of these will link to the bar function.
 * @see {@link bar}
 * @see bar
 */
function foo() {}

// Use the inline {@link} tag to include a link within a free-form description.
/**
 * @see {@link foo} for further information.
 * @see {@link http://github.com|GitHub}
 */
function bar() {}

类的注释

  • @name :类名称
  • @class :类描述
  • @constructor :表明这是一个构造函数,非常重要。
  • @extends :类继承的父类。
  • @type :数据的类型,主要用来注释属性。
  • @default :默认值,主要用来注释属性。
  • @abstract 标明一个成员是抽象的,需要子类去实现。
  • @public@protected@private:类、方法或属性的访问权限

类的注释示例

要把JSDoc3的注释生成API类库文档,@lends 是个很要紧的标签。

例如,@lends Sample.prototype 表示下面的对象归属于Sample。

较详尽的使用说明参见http://usejsdoc.org/tags-lends.html

有关类的定义和注释示例,如下:

/**
* @name Sample
* @class 示例类
* @public
* @constructor
*/
function Sample(){
    this.something = [];
}

Sample.prototype = 
/** @lends Sample.prototype*/
{
    /**
    * 属性示例。
    * @private
    * @type {Array}
    * @default []
    */
    something : [],

    /**
    * 方法示例。
    * @public
    * @param {String} arg 跟踪方法插件。
    */
    doSomething: function(arg){
    }
};

其他常用注释

  • @example : 示例代码。

  • @enum [<type>] : 一组同样类型的静态属性集合。switch 语句中的分支应该只使用枚举。

  • @overview :对当前代码文件的描述。

  • @copyright :代码的版权信息。

  • @author <name> [<emailAddress>] :代码的作者信息。

  • @version :当前代码的版本。

README.md 是很好的

如果你有为项目写说明文档的好习惯,碰巧又使用的是MarkDown格式,碰巧文件名又是README.md,那就很好了。

把README.md文件放在代码清单里边,JSDoc工具是自动为你生成API文档的首页,什么系统概况、设计需求、设计方案及版本更新记录等等的内容都可以放进来。

Grunt自动构建的配置

Grunt的安装使用,请参考教程《Grunt的安装和使用》

提示:JSDoc某些配置有git依赖, 需要在命令行中可以执行git命令。最好先安装一个msysgit(http://msysgit.github.io/),然后在环境变量中增加git的bin目录。

使用JSDoc3插件,在package.json中的NPM依赖配置参考,如下:

"devDependencies": {
    ... ...
    "grunt-jsdoc": "~0.5.4",
    "ink-docstrap": "~0.3.0",
    ... ...
}

使用JSDoc3插件,在Gruntfile中的配置参考,如下:

jsdoc: {
  dist : {
      src: ['README.md', 'src/sample.js'], 
      options: {
        destination: 'api',
        template: "libs/jsdoc3/docstrap/template",
        configure: "libs/jsdoc3/docstrap/template/jsdoc.conf.json"            
      }
  }
}

详细使用方法和参数,参考:https://github.com/krampstudio/grunt-jsdoc-plugin

参考资料

Grunt的安装和使用

安装Grunt前的准备

本教程的内容较少原创,多从其他文档上摘录,详见参考资料部分。

Grunt是一个自动化的项目构建工具。Grunt和Grunt的插件都是通过Node.js的包管理器npm来安装和管理的。

如果还没有安装Node.js,需要先从Node.js网站(http://nodejs.org/)下载安装。

Node.js安装中,以及后继的其他安装中,可能需要系统管理员权限。

安装Grunt之前,可以在命令行中运行node -v查看你的Node.js版本。0.8.0及以上版本的Node.js才能很好的运行Grunt。

安装Grunt的命令行接口

为了方便使用Grunt,你应该在全局范围内安装Grunt的命令行接口(CLI)。

在命令行中,执行如下命令:

npm install -g grunt-cli

这条命令将会把grunt命令植入到你的系统路径中,这样就允许你从任意目录来运行它(定位到任意目录运行grunt命令)。

认识Grunt的两个常用文件

使用Grunt构建的项目中,大多包含有package.json和Gruntfile。

package.json:用于存储已经作为npm模块发布的项目元数据(也就是依赖模块,包括Grunt本身)。

Gruntfile.js:用于配置或者定义Grunt任务和加载Grunt插件。

用一个现有的Grunt项目进行工作

如果已经安装好了Grunt的命令行接口(CLI),并且项目中已经有了package.json和Gruntfile,使用Grunt进行工作是非常容易的。

安装Grunt和相关Grunt插件

  1. 进入项目目录(在命令行窗口定位到项目目录;在windows系统下,也可以进入项目文件夹后,按Shift+鼠标右键,打开右键菜单,选择“在此处打开命令窗口(W)”)。
  2. 在命令行中,运行 npm install,安装项目相关依赖(插件,Grunt内置任务等依赖)。

有些Grunt插件,比如JSDoc3插件,需要在命令行中可以执行git命令。最好先安装一个msysgit(http://msysgit.github.io/),然后在环境变量中增加git的bin目录。

执行Grunt构建

在上述的命令行中执行 grunt,就可以运行Grunt构建了。

至此,通过安装Node.js、在命令行中运行 npm install -g grunt-clinpm installgrunt三个命令,就完成了使用现有的Grunt项目进行工作的所有准备。

准备一个新的Grunt项目

快速工作的项目脚手架

有很多项目的结构和内容都很类似,人们就设计开发了可以快速工作的模板,能够通过模板迅速的自动创建项目,称为脚手架工具(grunt-init)。

在Grunt的GitHub主页(https://github.com/gruntjs)上有很多之用的 grunt-init-* 脚本架模板工具,比如grunt-init-jquery、grunt-init-gruntplugin等等。

安装grunt-init

grunt-init 是一个用于自动创建项目的脚手架工具。

先在命令行中进行全局安装:

npm install -g grunt-init

这样,会把grunt-init命令植入到你的系统路径,从而允许你在任何目录中都可以运行它。

安装和使用grunt-init模板

把需要的模板放在你的 ~/.grunt-init/ 目录中(在Windows平台是%USERPROFILE%/.grunt-init目录)。

%USERPROFILE% 一般是指“C:\Users\你的用户名”这个目录。

建议使用如下的git clone命令把这个模板克隆到该目录:

git clone https://github.com/clientlab/grunt-init-gruntfile.git ~/.grunt-init/gruntfile

在Windows平台上稍微有些不方便,但是可以在文件浏览器中输入%USERPROFILE%,定义到该目录下,然后执行如下命令:

git clone https://github.com/clientlab/grunt-init-gruntfile.git ./.grunt-init/gruntfile

要想使用上述命令,需要先安装一个msysgit(http://msysgit.github.io/),然后在环境变量中增加git的bin目录。

最终的文件目录结构,如下:

.grunt-init/
.grunt-init/gruntfile/
.grunt-init/gruntfile/README.md
.grunt-init/gruntfile/template.js
.grunt-init/gruntfile/root/

然后进入一个准备开发项目的空目录,按Shift+鼠标右键,打开右键菜单,选择“在此处打开命令窗口(W)”,打开命令行,执行如下程序:

grunt-init gruntfile

请注意,此模板将在当前目录中生成文件,如果你不想覆盖现有文件,一定要使用一个新的空目录。

参考资料

使用grunt-run-grunt插件组织团队的开发

项目构建的需求

在组织项目的开发工作时,如果构建工具选型是Grunt,那么常常有以下几个需求:

  1. Gruntfile文件随着项目复杂性的增加,会变的越来越庞大。为了便于开发团队的分工协作,需要在某种程度上分拆Gruntfile,让不同的团队、不同的开发者关注不同的部分。
  2. 把Gruntfile分拆成不同的文件后,既要有统一的入口执行整体的构建,又要能够使分拆后的Gruntfile不依赖于其他部分独立构建,以确保团队分工的互不干扰。
  3. 在整个项目中,package.json和node_modules要能够共用。确保每一部分的开发者,只关注Gruntfile的编写,不需要关注全局共性的package.json和node_modules。

不同的团队、 不同的项目因为细化需求的不同,都会各自选择不同的解决方案。

Gruntfile的分拆方案

Gruntfile文件内的逻辑分组

使用任务的目标名称,在逻辑上分组构建任务。这是大多数项目最初会用到的方法,如grunt中文文档项目(https://github.com/basestyle/grunt-cn)

在传递给grunt.initConfig方法的对象中,使用如下类似的不同任务目标名称区分webapp和pc两类任务:

jshint:{
    webapp: ['app/www/js/main.js'],
    pc: ['js/chart.js','js/common.js','js/demand.js','js/dialog.js','js/formbeautify.js','js/jquery.autopagination.js',
        'js/jquery.cascadeselect.js','js/jquery.datepicker.js','js/jquery.formvalid.js','js/jquery.memberinput.js',
        'js/jquery.multiupload.js','js/jquery.tabs.js','js/pmstation.js','js/project.js','js/setting.js','js/tasktable.js','js/work.js']
},

在自定义的别名任务中,把两类任务分组成webapp和pc两个单任务:

grunt.registerTask('webapp', ['htmlhint:webapp','jshint:webapp','uglify:webapp','cssmin:webapp','copy:webapp','replace:webapp']);
grunt.registerTask('pc', ['jshint:pc','uglify:pc','cssmin:pc','copy:pc','replace:pc']);

如果两类任务调用的任务都相同,也可以使用动态别名任务的方法来处理。

代码示例如下:

grunt.registerTask('build', 'Run all my build tasks.', function(n) {
  if (n == null) {
    grunt.warn('Build num must be specified, like build:001.');
  }
  grunt.task.run('foo:' + n, 'bar:' + n, 'baz:' + n);
});

参见Grunt文档的《常见问题-“动态”别名任务》(中文/英文)。

Grunt配置编程处理在不同的文件中

将配置对象分拆出来,然后在Gruntfile.js中require进来,再将这些不同任务的对象组合成一个对象传递给grunt.initConfig。

上述方案是在我提出问题(有没有分拆Gruntfile.js的方案?(https://github.com/basestyle/grunt-cn/issues/34))后,TooBug[GitHub]和Vincent Hou给出的解决方案。

因为没有进一步展开,所以没有示例代码。

使用自定义任务把构建任务分解在不同的文件中

我在知乎上提出问题《有什么方案可以把较为庞大的gruntfile分拆?(http://www.zhihu.com/question/21766711)》后,墨磊[GitHub]给出的解决方案,基本上就是使用自定义任务把构建任务分解在不同的文件中的。

比如,在gruntjs.com(https://github.com/gruntjs/gruntjs.com)项目中,也是通过把blog、docs等任务从Gruntfile中分拆出来的。

代码示例如下:

// Load local tasks
grunt.loadTasks('tasks'); // getWiki, docs tasks

grunt.registerTask('build', ['clean', 'copy', 'jade', 'docs', 'blog', 'plugins', 'concat']);

上例中的docs、blog任务就是放在tasks目录下,使用grunt.loadTasks方法统一加载的。

选择grunt-run-grunt插件

前文的所述的几个方案,用于分拆Gruntfile虽然很不错,但是并不能解决最初提出的后两个需求:

  • 分拆后的Gruntfile,能够不依赖于其他部分的独立构建。
  • package.json和node_modules要能够共用。

我发现grunt-run-grunt(https://github.com/Bartvds/grunt-run-grunt)插件可以同时满足这三个需求。

很遗憾的是,这个插件关注的人不多,Fork的人也不多。或许其他的开发团队有更高明的方案也说不一定,还需要继续跟进。

项目的组织方案

在选型定案使用grunt-run-grunt插件以后,项目的组织就确定了。

入口Gruntfile的代码示例:

run_grunt:{
  options: {
    'base': require('path').resolve('.')
  },
  main:{
    src: ['src/pro/a.Gruntfile.js', 'src/frameworks/b.lib.Gruntfile.js', 'src/frameworks/c.lib.Gruntfile.js']        
  }
}

需要特别注意的是,options中的base配置是指整个项目所有的Gruntfile都是以这个入口Gruntfile所在的目录为base目录的。

这个扩展机制可以在options中配置最终传递给grunt的命令行参数,属性名是命令行参数名,属性值是命令行参数的值。

上例配置的意义是要求执行类似如下样式的Grunt命令:

grunt --base e:/dev/

各部分的Gruntfile与正常的Gruntfile没有什么区别。唯一的区别就是,这个方案把所有的Gruntfile的base目录设定为入口Gruntfile所在的目录。

比如,src/pro/a.Gruntfile.js中的代码示例如下:

concat: {
  "pro: {
    src: [
      "src/frameworks/b.lib.js",
      "src/frameworks/c.lib.js",
      "src/pro/a.js"
    ],
    dest: "dist/pro/a.js"
  }
}

示例中,不管是src目录,还是dest目录,都是以入口Gruntfile目录为基准的。

当然,base目录不这样安排是最简单的,在入口Gruntfile的run_grunt任务中不配置base就可以了。如此以来,所有的Gruntfile中的目录都是以本Gruntfile所在目录为base目录的。

在javascript项目中使用QUint进行单元测试

QUnit的简介

本文只介绍Qunit的使用,不再介绍单元测试框架的选型过程。

QUnit的官网介绍:

QUnit is a powerful, easy-to-use JavaScript unit testing framework.

QUnit是一个强大的、简单易用的javascript单元测试框架。

下载QUnit

可以到QUnit官网(http://qunitjs.com)上下载qunit.js和qunit.css两个文件。

虽然有jQuery CDN(http://code.jquery.com/)GitHub托管(https://github.com/jquery/qunit)的代码可用,但是为了稳定,还是下载了使用吧。

QUnit上手应用

以下的代码示例直接使用QUnit官网首页的示例。

QUnit单元测试的HTML页面代码示例:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>QUnit Example</title>
  <link rel="stylesheet" href="/resources/qunit.css">
</head>
<body>
  <div id="qunit"></div>
  <div id="qunit-fixture"></div>
  <script src="/resources/qunit.js"></script>
  <script src="/resources/tests.js"></script>
</body>
</html>

tests.js中的示例代码:

test( "hello test", function() {
  ok( 1 == "1", "Passed!" );
});

QUnit的常用API

断言是单元测试中最常用、最核心的概念,就是在测试的时候,对代码的运行结果作出的一些假设。如果断言成立,就说单元测试通过,反之则视为测试失败。

测试方法和模块

最常用的测试代码,示例如下:

test("该测试用例的主题", function(){/*- 具体的测试断言 -*/});

当测试用例较多时,可以用module()方法进行分组。示例如下:

module( "测试分组一" );
test( "测试方法1", function() {});
test( "测试方法2", function() {});
 
module( "测试分组二" );
test( "测试方法3", function() {});
test( "测试方法4", function() {});

在测试结果HTML页面上,会以 测试分组一:测试方法1 的形式呈现。

异步测试

javascript编程中,经常用到回调函数,所以异步测试也是经常用到的。

如下示例:

asyncTest( "asynchronous test: one second later!", function() {
  expect( 1 );
 
  setTimeout(function() {
    ok( true, "Passed and ready to resume!" );
    start();
  }, 1000);
});

注意start()函数的用法。

由于是异步调用,所以使用expect()告诉QUnit期待几个断言是个好习惯。

QUnit支持的断言

一个测试方法只有一个断言是个好习惯。只是好习惯哦!

一目了然,ok和equal是最常用的

  • ok( state, message ) : 真假断言,state为true则通过。
  • equal( actual, expected, message ) : 相等断言,actual和expected相等(==)则通过。
  • notEqual( actual, expected, message ) : 不等断言,actual和expected不相等(!=)则通过。
  • deepEqual( actual, expected, message ) : 递归相等断言,actual和expected全相等(包括其子元素都相等,适用于基本类型,数组和对象)则通过。
  • notDeepEqual( actual, expected, message ) : 递归不相等断言,actual和expected不全相等(包括其子元素都相等,适用于基本类型,数组和对象)则通过。
  • strictEqual( actual, expected, message ) : 全相等断言,actual和expected全相等(===)则通过。
  • notStrictEqual( actual, expected, message ) : 不全相等断言,actual和expected不全相等(===)则通过。
  • raises( block, expected, message ) : 异常断言,block中抛出异常则通过,expected为可选参数,是所期待抛出异常名的正则表达式。

与Grunt构建工具的集成

Grunt构建工具的安装和使用,可以参考教程《Grunt的安装和使用》

QUnit的Grunt任务

QUnit Grunt任务(grunt-contrib-qunit)的使用,请参见GitHub上的说明https://github.com/gruntjs/grunt-contrib-qunit

使用QUint作单元测试,PhantomJS是必须要安装的。PhantomJS(http://phantomjs.org/) 是一个无界面的WebKit浏览器引擎,还有配套的JavaScript API。它原生支持各种web标准技术:DOM处理,CSS选择器,JSON,Canvas,以及SVG。

安装grunt-contrib-qunit任务时,作为npm依赖项,PhantomJS会自己被安装。

也可以在命令行中使用以下的命令进行安装:

npm install phantomjs -g

友情提示:

本来这一切都是顺理成章,非常流畅的。可惜,一道国墙给这个世界的互联互通制造了些许障碍。

如果电脑上从来没有安装过PhantomJS,或者当前的版本不够高,安装程序会自动从bitbucket网站上下载,例如:http://cdn.bitbucket.org/ariya/phantomjs/downloads/phantomjs-1.9.7-windows.zip。但是,非常不幸,这个地址是被墙了的。

你可以从各种不同的途径,获取这个压缩包,然后拷贝到C:\Users\Administrator\AppData\Local\Temp\phantomjs(具体地址根据自己的电脑作调整,或者查看安装报错的信息)。接着,重新启动安装,一切都OK了!

参考资料

佛经

我念一段佛经

我念一段佛经,

祈祷人心的光明。

授你无相的戒律,

约束你的怨嗔。

摩挲你的头顶,

消融你的仇恨。

我念一段佛经,

超度不能转世的灵魂。

阴暗的井底,

冰冷的井水,

一样都是地狱的幽禁。

彻骨的惩罚,

不过是洗手的金盆。

天地轮回中,

从来没有不能原谅的人。

我念一段佛经,

张扬无边的悲悯。