让Grunt为不同的设置生成index.html

209

我想使用Grunt作为我的webapp的构建工具。

我至少想要有两种设置:

I. 开发环境 - 从单独的文件中加载脚本,而不是将它们合并在一起,

这样我的index.html看起来会像这样:

<!DOCTYPE html>
<html>
    <head>
        <script src="js/module1.js" />
        <script src="js/module2.js" />
        <script src="js/module3.js" />
        ...
    </head>
    <body></body>
</html>

二、生产环境设置 - 将我的脚本压缩和合并成一个文件加载,

使index.html相应更改:

<!DOCTYPE html>
<html>
    <head>
        <script src="js/MyApp-all.min.js" />
    </head>
    <body></body>
</html>
问题是,当我运行grunt devgrunt prod时,我该如何让grunt根据配置生成这些index.html呢?
或者说,也许我方向错了,总是生成MyApp-all.min.js可能更容易,但是在其中放入所有脚本(已连接)或异步从单独的文件加载这些脚本的加载器脚本?
你们怎么做的?

3
试试使用Yeoman工具,其中包括一个名为"usemin"的任务可以实现你想要的功能。此外,Yeoman生成器包含了许多易于学习的“最佳实践”,这些实践在使用新工具时很难学会。 - acanimal
12个回答

161
我最近发现了这些与Grunt v0.4.0兼容的任务:

Grunt 任务,围绕预处理npm模块。

Grunt 任务,自动化为未来任务配置环境。

以下是我的Gruntfile.js中的片段。

环境设置:

env : {
    
    options : {
        
        /* Shared Options Hash */
        //globalOption : 'foo'
        
    },
    
    dev: {
        
        NODE_ENV : 'DEVELOPMENT'
        
    },
    
    prod : {
        
        NODE_ENV : 'PRODUCTION'
        
    }
    
},

预处理:

preprocess : {
    
    dev : {
        
        src : './src/tmpl/index.html',
        dest : './dev/index.html'
        
    },
    
    prod : {
        
        src : './src/tmpl/index.html',
        dest : '../<%= pkg.version %>/<%= now %>/<%= ver %>/index.html',
        options : {
            
            context : {
                name : '<%= pkg.name %>',
                version : '<%= pkg.version %>',
                now : '<%= now %>',
                ver : '<%= ver %>'
            }
            
        }
        
    }
    
}

任务:

grunt.registerTask('default', ['jshint']);

grunt.registerTask('dev', ['jshint', 'env:dev', 'clean:dev', 'preprocess:dev']);

grunt.registerTask('prod', ['jshint', 'env:prod', 'clean:prod', 'uglify:prod', 'cssmin:prod', 'copy:prod', 'preprocess:prod']);

/src/tmpl/index.html模板文件中(例如):

<!-- @if NODE_ENV == 'DEVELOPMENT' -->
    
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.js"></script>
    <script src="../src/js/foo1.js"></script>
    <script src="../src/js/foo2.js"></script>
    <script src="../src/js/jquery.blah.js"></script>
    <script src="../src/js/jquery.billy.js"></script>
    <script src="../src/js/jquery.jenkins.js"></script>
    
<!-- @endif -->

<!-- @if NODE_ENV == 'PRODUCTION' -->
    
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
    
    <script src="http://cdn.foo.com/<!-- @echo name -->/<!-- @echo version -->/<!-- @echo now -->/<!-- @echo ver -->/js/<!-- @echo name -->.min.js"></script>
    
<!-- @endif -->

我相信我的设置与大多数人不同,上述内容的实用性取决于您的情况。对我而言,虽然这是一段很棒的代码,但 Yeoman grunt-usemin 比我个人需要的更加强大。

注意:今天我才刚刚发现上面列出的任务,所以我可能会错过某些功能和/或我的流程可能在后续变化。目前,我喜欢 grunt-preprocessgrunt-env 提供的简单性特性。 :)


我不确定这是否对任何人有帮助,但我已经在GitHub上创建了此演示存储库,展示了一个完整(更复杂的设置)使用我上面概述的技术的示例。


1
@sthomps 很高兴能帮到你!自从我发现了这些任务,我就一直喜欢上了这个工作流程。顺便说一下,我对这个过程进行了一点小改动...不再将多个上下文变量传递给我的HTML模板,而是选择传递一个变量 path : '/<%= pkg.name %>/dist/<%= pkg.version %>/<%= now %>/<%= ver %>',它连接了所有变量(这是我的构建路径)。在我的模板上,我会有:<script src="http://cdn.foo.com<!-- @echo path -->/js/bulldog.min.js"></script>。无论如何,我很高兴能够为你节省一些时间!:D - mhulse
4
您可以仅使用grunt-template完成相同的操作,只需为开发/生产环境传入不同的data对象即可。 - Mathias Bynens
@MathiasBynens 感谢你的提示!我很期待检查我们的代码。在我最初发布时,我认为没有太多grunt0.4.x插件可以做到这种事情。知道还有更多选择很好。感谢分享! - mhulse
@DarrellO'Donnell 谢谢!很高兴能帮到你。顺便说一下,不确定是否有帮助,但我已经更新了我的答案,包括一个演示存储库的链接,其中包含我提到的技术的工作示例。 - mhulse
谢谢。预处理对我来说效果更好。usemin 太复杂了。 - mynameistechno
我知道有人提到维护脚本标签会很麻烦,但在我的情况下,这是一个完美的解决方案。我正在使用requirejs,因此只有两种可能包含的脚本标签:要么是开发环境的data-main="main.js"脚本标签,要么是链接到合并/缩小的bundle.js的脚本标签。重点是,如果您正在使用脚本加载器(出于自己的利益,您应该这样做),那么这是一个完美的解决方案。 - Derek

35

我已经想出了自己的解决方案。虽然还不够完美,但我认为我会朝着这个方向前进。

本质上,我使用了 grunt.template.process() 来从模板生成我的 index.html ,该模板分析当前配置并生成我的原始源文件列表或链接到一个带有缩小代码的单个文件。下面的示例是针对 js 文件,但是相同的方法也可以扩展到 css 和任何其他可能的文本文件。

grunt.js

/*global module:false*/
module.exports = function(grunt) {
    var   // js files
        jsFiles = [
              'src/module1.js',
              'src/module2.js',
              'src/module3.js',
              'src/awesome.js'
            ];

    // Import custom tasks (see index task below)
    grunt.loadTasks( "build/tasks" );

    // Project configuration.
    grunt.initConfig({
      pkg: '<json:package.json>',
      meta: {
        banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - ' +
          '<%= grunt.template.today("yyyy-mm-dd") %> */'
      },

      jsFiles: jsFiles,

      // file name for concatenated js
      concatJsFile: '<%= pkg.name %>-all.js',

      // file name for concatenated & minified js
      concatJsMinFile: '<%= pkg.name %>-all.min.js',

      concat: {
        dist: {
            src: ['<banner:meta.banner>'].concat(jsFiles),
            dest: 'dist/<%= concatJsFile %>'
        }
      },
      min: {
        dist: {
        src: ['<banner:meta.banner>', '<config:concat.dist.dest>'],
        dest: 'dist/<%= concatJsMinFile %>'
        }
      },
      lint: {
        files: ['grunt.js'].concat(jsFiles)
      },
      // options for index.html builder task
      index: {
        src: 'index.tmpl',  // source template file
        dest: 'index.html'  // destination file (usually index.html)
      }
    });


    // Development setup
    grunt.registerTask('dev', 'Development build', function() {
        // set some global flags that all tasks can access
        grunt.config('isDebug', true);
        grunt.config('isConcat', false);
        grunt.config('isMin', false);

        // run tasks
        grunt.task.run('lint index');
    });

    // Production setup
    grunt.registerTask('prod', 'Production build', function() {
        // set some global flags that all tasks can access
        grunt.config('isDebug', false);
        grunt.config('isConcat', true);
        grunt.config('isMin', true);

        // run tasks
        grunt.task.run('lint concat min index');
    });

    // Default task
    grunt.registerTask('default', 'dev');
};

index.js (the index task):

索引任务的主文件是 index.js
module.exports = function( grunt ) {
    grunt.registerTask( "index", "Generate index.html depending on configuration", function() {
        var conf = grunt.config('index'),
            tmpl = grunt.file.read(conf.src);

        grunt.file.write(conf.dest, grunt.template.process(tmpl));

        grunt.log.writeln('Generated \'' + conf.dest + '\' from \'' + conf.src + '\'');
    });
}

最后,index.tmpl中包含生成逻辑:

<doctype html>
<head>
<%
    var jsFiles = grunt.config('jsFiles'),
        isConcat = grunt.config('isConcat');

    if(isConcat) {
        print('<script type="text/javascript" src="' + grunt.config('concat.dist.dest') + '"></script>\n');
    } else {
        for(var i = 0, len = jsFiles.length; i < len; i++) {
            print('<script type="text/javascript" src="' + jsFiles[i] + '"></script>\n');
        }
    }
%>
</head>
<html>
</html>

更新: 发现基于grunt的Yeoman已经内置了usemin任务,可以与Yeoman的构建系统集成。它会从开发版本的index.html以及其他环境设置中生成生产版本的index.html。虽然有些复杂,但是很有意思。


5
grunt-template 是一个非常轻量级的包装器,围绕着 grunt.template.process()(你正在使用的东西)使其更加容易。通过为 dev/prod 简单地传递不同的 data 对象,你可以使用 grunt-template 来执行相同的操作。 - Mathias Bynens

15

我不喜欢这里的解决方案(包括我之前提供的答案),原因如下:

  • 最高票答案的问题在于,当添加/重命名/删除JS文件时,您必须手动同步脚本标记列表。
  • 被接受的答案的问题是,您的JS文件列表不能有模式匹配。这意味着您必须在Gruntfile中手动更新它。

我已经想出了如何解决这两个问题。 我设置了我的grunt任务,以便每次添加或删除文件时,脚本标记自动生成反映该内容。这样,当您添加/删除/重命名JS文件时,您不需要修改html文件或grunt文件。

总结一下它的工作方式,我有一个带有脚本标记变量的HTML模板。我使用https://github.com/alanshaw/grunt-include-replace来填充该变量。在开发模式下,该变量来自所有JS文件的globbing模式。当添加或删除JS文件时,监视任务会重新计算此值。

现在,要在开发或生产模式下获得不同的结果,您只需用不同的值填充该变量即可。以下是一些代码:

var jsSrcFileArray = [
    'src/main/scripts/app/js/Constants.js',
    'src/main/scripts/app/js/Random.js',
    'src/main/scripts/app/js/Vector.js',
    'src/main/scripts/app/js/scripts.js',
    'src/main/scripts/app/js/StatsData.js',
    'src/main/scripts/app/js/Dialog.js',
    'src/main/scripts/app/**/*.js',
    '!src/main/scripts/app/js/AuditingReport.js'
];

var jsScriptTags = function (srcPattern, destPath) {
    if (srcPattern === undefined) {
        throw new Error("srcPattern undefined");
    }
    if (destPath === undefined) {
        throw new Error("destPath undefined");
    }
    return grunt.util._.reduce(
        grunt.file.expandMapping(srcPattern, destPath, {
            filter: 'isFile',
            flatten: true,
            expand: true,
            cwd: '.'
        }),
        function (sum, file) {
            return sum + '\n<script src="' + file.dest + '" type="text/javascript"></script>';
        },
        ''
    );
};

...

grunt.initConfig({

    includereplace: {
        dev: {
            options: {
                globals: {
                    scriptsTags: '<%= jsScriptTags(jsSrcFileArray, "../../main/scripts/app/js")%>'
                }
            },
            src: [
                'src/**/html-template.html'
            ],
            dest: 'src/main/generated/',
            flatten: true,
            cwd: '.',
            expand: true
        },
        prod: {
            options: {
                globals: {
                    scriptsTags: '<script src="app.min.js" type="text/javascript"></script>'
                }
            },
            src: [
                'src/**/html-template.html'
            ],
            dest: 'src/main/generatedprod/',
            flatten: true,
            cwd: '.',
            expand: true
        }

...

    jsScriptTags: jsScriptTags

jsSrcFileArray是典型的grunt文件模式。 jsScriptTagsjsSrcFileArray串联在一起,并在两侧添加script标签。 destPath是我想要添加到每个文件前面的前缀。

这是HTML的样子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>Example</title>

</head>

<body>    
@@scriptsTags
</body>
</html>

现在,正如您在配置文件中所看到的,当以prod模式运行时,我将该变量的值生成为硬编码的script标签。 在开发模式下,这个变量将扩展为像这样的一个值:

<script src="../../main/scripts/app/js/Constants.js" type="text/javascript"></script>
<script src="../../main/scripts/app/js/Random.js" type="text/javascript"></script>
<script src="../../main/scripts/app/js/Vector.js" type="text/javascript"></script>
<script src="../../main/scripts/app/js/StatsData.js" type="text/javascript"></script>
<script src="../../main/scripts/app/js/Dialog.js" type="text/javascript"></script>

如果你有任何问题,请告诉我。

附言:这是一段疯狂的代码,而我希望在每个客户端JS应用程序中都能使用它。我希望有人可以将其转化为可重复使用的插件。也许有一天我会做到。


1
听起来很有前途。你能分享一些片段吗? - Adam Marshall
我已经设置了我的Grunt任务,以便每次添加或删除文件时,脚本标签会自动生成以反映这一变化。你是如何做到的? - CodyBugstein
2
另一个问题:你知道是否有一种方法可以仅删除 HTML 的<script>标记块吗? - CodyBugstein
@Imray 我没有即时想到。你是指没有任何形式的模板化(例如grunt-include-replace)吗?第一个浮现在我脑海中的想法是使用XSLT。然而,这可能不是一个好的解决方案。 - Daniel Kaplan
1
这个答案非常准确,尽管我个人从jsScriptTags中删除了destPath并将grunt.file.expandMappinggrunt.file.expand交换,因为我想要的文件已经在正确的位置。这简化了很多事情。谢谢@DanielKaplan,你节省了我大量的时间 :) - DanielM
@DanielM 很高兴能帮到你。我仍然希望有一天能将其转化为插件。 - Daniel Kaplan

14

2
我看过这个插件,但我不喜欢在我的index.html中手动指定所有文件(并且实际上包含任何逻辑),因为我已经在我的grunt配置中有一个源js/css文件列表,并且不想重复自己。底线是 - 决定包含哪些文件不应该在index.html中进行 - Dmitry Pashkevich
+1 给 grunt-targethtml。虽然在 index.html 中添加 if 语句来“决定”加载哪些资源有点丑陋,但这确实是有意义的。这通常是您寻找包含项目资源的地方。此外,对此的跟进让我查看了 grunt-contrib。它里面有一些很棒的东西。 - carbontax

8
我正在寻找一种更简单、直接的解决方案,因此我结合了这个问题的答案:如何在gruntfile.js中放置if else块,并提出以下简单步骤:
  1. Keep two versions of your index files as you listed and name them index-development.html and index-prodoction.html.
  2. Use the following logic in your Gruntfile.js's concat/copy block for your index.html file:

    concat: {
        index: {
            src : [ (function() {
                if (grunt.option('Release')) {
                  return 'views/index-production.html';
                } else {
                  return 'views/index-development.html';
                }
              }()) ],
           dest: '<%= distdir %>/index.html',
           ...
        },
        ...
    },
    
  3. run 'grunt --Release' to choose the index-production.html file and leave off the flag to have the development version.

无需添加或配置新插件,也没有新的Grunt任务。


3
这里唯一的缺点是需要维护两个index.html文件。 - Adam Marshall

5
我发现了一个名为grunt-dev-prod-switch的grunt插件。它的作用仅是基于通过grunt传递的--env选项寻找特定代码块并将其注释掉(尽管它只局限于dev、prod和test三种环境)。
按照这里的说明设置后,你可以运行以下命令:grunt serve --env=dev,它会将被包含在特定区块中的代码注释掉。
    <!-- env:test/prod -->
    your code here
    <!-- env:test/prod:end -->

它将取消注释由...包装的块。

    <!-- env:dev -->
    your code here
    <!-- env:dev:end -->

它也适用于JavaScript,我用它来设置正确的IP地址以连接到我的后端API。这些块只是简单的更改了一下:

    /* env:dev */
    your code here
    /* env:dev:end */

在您的情况下,只需要简单地执行以下操作:
<!DOCTYPE html>
<html>
    <head>
        <!-- env:dev -->
        <script src="js/module1.js" />
        <script src="js/module2.js" />
        <script src="js/module3.js" />
        ...
        <!-- env:dev:end -->
        <!-- env:prod -->
        <script src="js/MyApp-all.min.js" />
        ...
        <!-- env:prod:end -->
    </head>
    <body></body>
</html>

5

grunt-dom-munger可以使用CSS选择器读取并操作HTML。例如,从您的HTML中读取标记。删除节点、添加节点等等。

您可以使用grunt-dom-munger读取由index.html链接的所有JS文件,对它们进行压缩,然后再次使用grunt-dom-munger修改index.html,以仅链接最小化的JS。


5

这个名为scriptlinker的Grunt任务看起来是在开发模式下添加脚本的简单方法。你可以先运行concat任务,然后在生产模式下指向你的合并文件。


文档有些混乱,而且一些东西(如appRoot、relative)并不总是按预期工作,但仍然是一个有用的工具。 - hashchange
1
@hashchange 我不使用这个工具。我最终使用了 https://github.com/alanshaw/grunt-include-replace。我在我的HTML文件中有一个代表脚本标签的变量。然后,我用我想要的HTML字符串填充该变量。在开发模式下,此变量是脚本列表。在生产模式下,此变量是连接的、缩小的版本。 - Daniel Kaplan
感谢您指向grunt-include-replace。 (实际上,我需要一个工具将目录中的所有规范文件添加到Mocha index.html文件中。Scriptlinker很好用。) - hashchange
@hashchange 你说的文档确实很糟糕。那么你如何告诉它在HTML文件中放置脚本瓷砖的位置呢? - Daniel Kaplan
1
你定义了一个HTML注释。看一下这个文件。插入发生在<!--SINON COMPONENT SCRIPTS--><!--SPEC SCRIPTS-->处。这里是执行此操作的Grunt任务(实际工作中的任务,而不是文档中的内容)。希望能有所帮助 ;) - hashchange

4

使用Wiredep https://github.com/taptapship/wiredep 和 usemin https://github.com/yeoman/grunt-usemin 的组合,让grunt来处理这些任务。Wiredep会逐个脚本文件添加你的依赖项,而usemin会将它们全部合并成一个生产文件。这可以通过一些html注释来完成。例如,当我运行bower install && grunt bowerInstall时,我的Bower包会自动包含并添加到HTML中。

<!-- build:js /scripts/vendor.js -->
<!-- bower:js -->
<!-- endbower -->
<!-- endbuild -->

4

grunt-bake是一个非常好用的grunt脚本,可以在这里很好地工作。我在我的JQM自动构建脚本中使用它。

https://github.com/imaginethepoet/autojqmphonegap

看一下我的grunt.coffee文件:

bake:
    resources: 
      files: "index.html":"resources/custom/components/base.html"

这个功能会查找base.html中的所有文件,并将它们合并起来创建index.html,非常适用于多页面应用(如phonegap)。这样做可以更轻松地进行开发,因为不同的开发人员可以分别开发各自的小块代码,在使用监视命令编译成完整的页面之前,不需要在一个长长的单页应用上频繁检查冲突。Bake会在监视过程中从base.html读取模板,并注入组件html页面。
<!DOCTYPE html>

jQuery Mobile演示

app.initialize();

<body>
    <!--(bake /resources/custom/components/page1.html)-->
    <!--(bake /resources/custom/components/page2.html)-->
    <!--(bake /resources/custom/components/page3.html)-->
</body>

你可以进一步在页面中添加插入代码,例如“菜单”、“弹出窗口”等,以便于你真正将页面拆分为更小的可管理组件。

也许你可以用grunt-bake编写一个代码演示来增强你的回答? - Dmitry Pashkevich

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接