У Grunt генерировать index.html для разных настроек

Я пытаюсь использовать 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>

II. Производственная установка - загружает мои скрипты, миниатюрные и конкатенированные в одном файле,

с index.html соответственно:

<!DOCTYPE html>
<html>
    <head>
        <script src="js/MyApp-all.min.js" />
    </head>
    <body></body>
</html>

Вопрос в том, как я могу заставить grunt сделать этот index.html в зависимости от конфигурации, когда я запускаю grunt dev или grunt prod?

Или, может быть, я копаю в неправильном направлении, и было бы легче всегда генерировать MyApp-all.min.js, но вставлять в него либо все мои скрипты (конкатенированные), либо загрузчик script, который асинхронно загружает эти сценарии из отдельных файлов?

Как вы это делаете, ребята?

Ответ 1

Я придумал свое решение. Пока еще не отполирован, но я думаю, что собираюсь двигаться в этом направлении.

В сущности, я использую 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):

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>

UPD. Узнал, что Yeoman, основанный на хрюке, имеет встроенный usemin, которая интегрируется с системой сборки Yeoman. Он генерирует производственную версию index.html из информации в версии разработки index.html, а также других параметров среды. Немного сложный, но интересный, чтобы посмотреть.

Ответ 2

Недавно я обнаружил эти задачи Grunt v0.4.0:

  • grunt-preprocess

    Задача Grunt вокруг модуля npm preprocess.

  • grunt-env

    Задача Grunt для автоматизации конфигурации среды для будущих задач.

Ниже приведены фрагменты из моего Gruntfile.js.

Настройка ENV:

env : {

    options : {

        /* Shared Options Hash */
        //globalOption : 'foo'

    },

    dev: {

        NODE_ENV : 'DEVELOPMENT'

    },

    prod : {

        NODE_ENV : 'PRODUCTION'

    }

},

Preprocess:

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-preprocess и grunt-env может предложить.:)


Обновление за январь 2014 года:

Мотивировано пониженным голосом...

Когда я опубликовал этот ответ, было не так много вариантов для Grunt 0.4.x, который предлагал решение, которое работало для моих нужд. Теперь, спустя несколько месяцев, я бы предположил, что есть больше вариантов, которые могут быть лучше, чем то, что я разместил здесь. В то время как я все еще лично использую и использую этот метод для своих построек, я прошу, чтобы будущие читатели нашли время, чтобы прочитать другие ответы и изучить все варианты. Если вы найдете лучшее решение, отправьте свой ответ здесь.

Обновление за февраль 2014:

Я не уверен, поможет ли это кому-либо, но я создал этот демонстрационный репозиторий на GitHub, который показывает полная (и более сложная настройка) с использованием техники (ов), описанной выше.

Ответ 3

Мне не нравятся решения здесь (в том числе тот, который я ранее дал), и вот почему:

  • Проблема с самым высоким голосовым ответом заключается в том, что вам нужно вручную синхронизировать список тегов script при добавлении/переименовании/удалении JS файла.
  • Проблема с принятым ответом заключается в том, что ваш список JS файлов не может иметь соответствие шаблонов. Это означает, что вам нужно обновить его вручную в файле Grunt.

Я выяснил, как решить обе эти проблемы. Я настроил свою задачу, чтобы каждый раз, когда файл был добавлен или удален, теги script автоматически генерируются, чтобы отразить это. Таким образом, вам не нужно изменять свой html файл или файл grunt при добавлении/удалении/переименовании ваших JS файлов.

Чтобы обобщить, как это работает, у меня есть html-шаблон с переменной для тегов script. Я использую https://github.com/alanshaw/grunt-include-replace для заполнения этой переменной. В режиме dev эта переменная исходит из шаблона глобуса всех моих JS файлов. Задача часов пересчитывает это значение при добавлении или удалении JS файла.

Теперь, чтобы получить разные результаты в режиме dev или prod, вы просто заполняете эту переменную с другим значением. Вот код:

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 - ваш типичный шаблон для файлового глобуса. jsScriptTags принимает jsSrcFileArray и объединяет их вместе с тегами script с обеих сторон. destPath - это префикс, который требуется для каждого файла.

И вот как выглядит HTML:

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

</head>

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

Теперь, как вы можете видеть в конфиге, я генерирую значение этой переменной в качестве жестко закодированного тега script, когда он запускается в режиме prod. В режиме dev эта переменная будет расширяться до значения, подобного этому:

<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>

Сообщите мне, если у вас есть вопросы.

PS: Это сумасшедший код для чего-то, что я хотел бы сделать в каждом клиентском JS-приложении. Надеюсь, кто-то может превратить это в плагин многократного использования. Может быть, я когда-нибудь.

Ответ 4

Я задавал себе тот же вопрос некоторое время, и я думаю, что этот плагин grunt может быть настроен на то, что вы хотите: https://npmjs.org/package/grunt-targethtml. Он реализует условные html-теги, которые зависят от целевой задачи.

Ответ 5

Я искал более простое, прямолинейное решение, поэтому я объединил ответ с этим вопросом:

Как разместить if else в файле gruntfile.js

и придумал следующие простые шаги:

  • Сохраните две версии ваших индексных файлов по мере их перечисления и назовите их index-development.html и index-prodoction.html.
  • Используйте следующую логику в блоке concat/copy Gruntfile.js для вашего файла index.html:

    concat: {
        index: {
            src : [ (function() {
                if (grunt.option('Release')) {
                  return 'views/index-production.html';
                } else {
                  return 'views/index-development.html';
                }
              }()) ],
           dest: '<%= distdir %>/index.html',
           ...
        },
        ...
    },
    
  • запустите 'grunt --Release', чтобы выбрать файл index-production.html и оставьте флаг для версии разработки.

Нет новых плагинов для добавления или настройки и новых задач для ворчания.

Ответ 6

Эта задача grunt scriptlinker выглядит как простой способ добавить скрипты в режиме dev. Возможно, сначала вы можете запустить задачу concat, а затем указать ее в свой конкатенированный файл в режиме prod.

Ответ 7

grunt-dom-munger читает и обрабатывает HTML с помощью селекторов CSS. Ex. читать теги из вашего html. Удалите узлы, добавьте узлы и т.д.

Вы можете использовать grunt-dom-munger для чтения всех ваших JS файлов, связанных вашим index.html, обмануть их, а затем снова использовать grunt-dom-munger, чтобы изменить ваш index.html, чтобы связать только уменьшенную JS

Ответ 8

Я нашел плагин grunt, называемый grunt-dev-prod-switch. Все, что он делает, это комментировать некоторые блоки, на которые он смотрит, на основе опции --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-адреса для подключения к моему backend 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>

Ответ 9

grunt-bake - фантастический хрюканье script, который отлично работает здесь. Я использую его в своей автоматической сборке JQM script.

https://github.com/imaginethepoet/autojqmphonegap

Взгляните на мой файл grunt.coffee:

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

Это рассматривает все файлы в файле base.html и отсасывает их для создания index.html, который отлично подходит для многостраничных приложений (телефонная задержка). Это позволяет упростить разработку, поскольку все разработчики не работают над одним длинным одностраничным приложением (предотвращая много проверок конфликтов). Вместо этого вы можете разбить страницы и работать с меньшими фрагментами кода и скомпилировать их на полную страницу с помощью команды watch.

Bake считывает шаблон из base.html и вводит страницы компонентов html на часах.

<!DOCTYPE html>

jQuery Mobile Demos

    app.initialize();   

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

Вы можете сделать это еще дальше и добавить инъекции на свои страницы для "всплывающих окон" меню и т.д., чтобы вы могли действительно разбивать страницы на более мелкие управляемые компоненты.

Ответ 10

Используйте комбинацию wiredep https://github.com/taptapship/wiredep и usemin https://github.com/yeoman/grunt-usemin, чтобы справиться с этими задачами. Wiredep будет добавлять ваши зависимости в один файл script за раз, а usemin объединит их всех в один файл для производства. Затем это можно выполнить только с некоторыми комментариями html. Например, мои пакеты bower автоматически включаются и добавляются в html при запуске bower install && grunt bowerInstall:

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

Ответ 11

Рассмотрим processhtml. Он позволяет определять несколько "целей" для сборки. Комментарии используются для условного включения или исключения материала из HTML:

<!-- build:js:production js/app.js -->
...
<!-- /build -->

становится

<script src="js/app.js"></script>

Он даже претендует на такие полезные вещи (см. README):

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

<!-- class is changed to 'production' only when the 'dist' build is executed -->
<html class="production">

Ответ 12

Этот ответ не для noobs!

Использование шаблонов Jade... передача переменных в шаблон Jade является стандартным вариантом использования болота

Я использую grunt (grunt-contrib-jade), но вам не нужно использовать grunt. Просто используйте стандартный модуль npm jade.

Если вы используете grunt, тогда ваш файл grunt хотел бы что-то вроде...

jade: {
    options: {
      // TODO - Define options here
    },
    dev: {
      options: {
        data: {
          pageTitle: '<%= grunt.file.name %>',
          homePage: '/app',
          liveReloadServer: liveReloadServer,
          cssGruntClassesForHtmlHead: 'grunt-' + '<%= grunt.task.current.target %>'
        },
        pretty: true
      },
      files: [
        {
          expand: true,
          cwd: "src/app",
          src: ["index.jade", "404.jade"],
          dest: "lib/app",
          ext: ".html"
        },
        {
          expand: true,
          flatten: true,
          cwd: "src/app",
          src: ["directives/partials/*.jade"],
          dest: "lib/app/directives/partials",
          ext: ".html"
        }
      ]
    }
  },

Теперь мы можем легко получить доступ к данным, переданным grunt в шаблоне Jade.

В отличие от подхода, используемого Modernizr, я устанавливаю класс CSS в теге HTML в соответствии со значением переданной переменной и могу использовать логику JavaScript оттуда на основе наличия или отсутствия класса CSS.

Это отлично, если вы используете Angular, поскольку вы можете сделать ng-if, чтобы включить элементы на странице, основываясь на том, присутствует ли класс.

Например, я могу включить script, если класс присутствует...

(Например, я могу включить live reload script в dev, но не в производстве)

<script ng-if="controller.isClassPresent()" src="//localhost:35729/livereload.js"></script>