环境特定的构建工具:Grunt、Gulp 和 Broccoli

阅读数:1306 2014 年 3 月 13 日

话题:语言 & 开发架构

项目的开发、分期和产品版本之间可能会有巨大的改变,这是我们需要基于环境以及特定目标信息改变资源(脚本、样式、模板)、生成标记或者其他内容路径的原因之一。很幸运的是,在 Grunt、Gulp 和 Broccolli 生态系统中已经有很多构建工具能够帮助我们完成这些工作。不久之前来自于 Google Chrome 开发者关系团队的工程师 Addy Osmani 对解决该问题的三种方式(字符串替代、条件注释和模板变量)做了对比

最简单的选项—字符串 / 正则表达式替换

对于环境特定的输出,最简单的选项是使用字符串替换,通过一个简单的字符串或者正则表达式找出并移除 / 替代 HTML 文件中的块。构建文件中可能会有很多目标设置,其中的每一个都能够用任意内容替换字符串。

例如,在 Grunt 中使用grunt-string-replace的一个非常基础的例子可能是在 prod 构建的时候将字符串 source.js 替换为 build.js,如下所示:

module.exports = function(grunt) {
  grunt.initConfig({
    'string-replace': {
      prod: {
        src: './app/**/*.html',
        dest: './dist/',
        options: {
          replacements: [{
            pattern: 'source.js',
            replacement: 'build.js'
          }]
        }
      }
    }
  });
  grunt.loadNpmTasks('grunt-string-replace');
  grunt.registerTask('default', ['string-replace']);
};

grunt-replace 也能很好的满足这种场景)

一般情况下,文本替换的方式需要少量配置,像 Stephen Sawchuk(Yeoman 和 Bower 的一位贡献者,他认为对于自己的设置而言其他的替代方案过于复杂)这样的开发人员会使用这种方式。从这个gist中你可以找到他在项目中是如何使用字符串替换的。

如果你并没有使用 Grunt,那么等价方案还有gulp-replacebroccoli-replace

对于简单的场景而言,字符串替换能够很好地完成工作,例如需要更新或者删除多个文件中的一组字符串。但是如果你有大量需要替换的内容,同时需要在源文件和构建文件之间来回跳转以便于算出到底需要替换哪些内容时,这种方式就会变得难以维护。

对于这种场景更好的解决方案是在源文件中就地定义替代品。也就是下面的选项。

条件注释

环境条件注释的基本思想是:在构建文件中通过最少的配置、使用 HTML 注释语法定义将一个字符串替代为目标内容所需要的逻辑和信息(例如路径)。这种方案的可读性比较好,同时也能够很容易地在每一个文件中实现。

下面是一些可选方案:

Grunt:

Gulp:

该列表前三个选项的注释语法在可读性和冗余度方面有所差异,但是它们都能有效地实现同样的结果。所有的选项都能够毫不费力地输出目标 / 环境特定的块,首先让我们来比较一下 targethtml 和 preprocess:

一个grunt-targethtml 示例 (Dev vs. Production)

Dev:
<!--(if target dev)><!-->
 <link rel="stylesheet" href="dev.css" />
<!--<!(endif)-->
<!--(if target dev)><!-->
  <script src="dev.js"></script>
  <script>
    var less = { env:'development' };
  </script>
<!--<!(endif)-->
Production:
<!--(if target prod)><!-->
  <link rel="stylesheet" href="release.css">
<!--<!(endif)-->
<!--(if target prod)><!-->
   <script src="release.js"></script>
<!--<!(endif)-->

同样的grunt-processhtml

Dev:
<!-- @if NODE_ENV='production' -->
 <link rel="stylesheet" href="dev.css">
<!-- @endif -->
<!-- @if NODE_ENV='production' -->
<script src="dev.js"></script>
<script>
  var less = { env:'development' };
</script>
<!-- @endif -->

Production:
<!-- @if NODE_ENV='production' -->
<link rel="stylesheet" href="release.css">
<!-- @endif -->
<!-- @if NODE_ENV='production' -->
<script src="release.js"></script>
<!-- @endif -->

正如我们所看到的,按照注释的形式指定逻辑相当直接,这种方式已经被大量地使用,并没有太多的抱怨。

使用条件注释的一个主要缺点是:标记文件中可能会包含大量与构建相关的逻辑。虽然有一些开发人员不喜欢这样,但是这些任务提供了可读性流。这也意味着我们没必要跳回 Gruntfile 中查看它到底正在干什么。

之前列出了 3 个 Grunt 任务,那么 grunt-processhtml 怎么样呢?它有一点专业,你可以使用模板变量以及其他的一些不错的技巧包含完全不同的文件。

例如,在下面的例子中只有当“dist”目标运行的时候才会将 class 修改为“production”:

<!-- build:[class]:dist production -->
<html class="debug_mode">
<!-- /build -->

或者使用任意一个文件替换目标中的一个块:

<!-- build:include:dev dev/content.html -->
This will be replaced by the content of dev/content.html
<!-- /build -->

或者根据目标删除一个块:

<!-- build:remove -->
<p>This will be removed when any processhtml target is done</p>
<!-- /build -->
<!-- build:remove:dist -->
<p>But this one only when doing processhtml:dist target</p>
<!-- /build -->

如果确定需要配置代码注释块格式的能力,那么可以查看 grunt-devcode ,它支持这些。下面是一些示例配置:

Gruntfile:
Devcode:
    {
      options :
      {
        html: true,        // html files parsing?
        js: true,          // javascript files parsing?
        css: true,         // css files parsing?
        clean: true,
        block: {
          open: 'condition', // open code block
          close: 'conditionend' // close code block
        },
        dest: 'dist'
      },
Markup:
<!-- condition: !production -->
  <li class="right">
    <a href="#settings" data-toggle="tab">
      Settings
    </a>
  </li>
<!-- conditionend -->

如果你想利用占位符的方式实现,那么可以借助于模板变量。

HTML 中的模板变量

构建时模板允许我们将源文件中定义的占位符字符串(例如 <%- title %>)替换为构建文件(例如 Gruntfile)中定义的与特定目标相关的数据(字符串、对象等任何你需要的内容)。与条件注释相比,使用这种方式时你注入的数据并不是在目标文件本身中指定的。这种类型的选项有:

Grunt:

Gulp:

grunt-template 基本上是对 grunt.template.process() 的包装。同时它主要针对的是基于目标的目标字符串操作,你可以通过 data 对象根据 dev/prod 目标改变路径或者内容。下面就是一个这样的例子:

index.html.tpl

Gruntfile.js
module.exports = function(grunt) {
    grunt.initConfig({
        'template': {
            'dev': {
                'options': {
                    'data': {
                        'title': 'I <3 JS in dev'
                    }
                },
                'files': {
                    'dev/index.html': ['src/index.html.tpl']
                }
            },
            'prod': {
                'options': {
                    'data': {
                        'title': 'I <3 JS in prod'
                    }
                },
                'files': {
                    'dist/index.html': ['src/index.html.tpl']
                }
            }
        }
    });
    grunt.loadNpmTasks('grunt-template');
    grunt.registerTask('default', [
        'template:prod'
    ]);
    grunt.registerTask('dev', [
        'template:dev'
    ]);
};

grunt-include-replace 做的事情比较相似,只是模板字符串的格式不同 (<title>@@title</title>)。