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


208

Я пытаюсь использовать Grunt в качестве инструмента для сборки моего веб-приложения.

Я хочу иметь как минимум две настройки:

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но поместить в него либо все мои сценарии (сцепленные), либо сценарий загрузчика, который асинхронно загружает эти сценарии из отдельных файлов?

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


3
Попробуйте инструмент Yeoman, который включает в себя задачу 'usemin', которая делает то, что вам нужно. Кроме того, генераторы Yeamon включают в себя множество «хороших практик», которые легко освоить при использовании нового инструмента.
EricSonaron

Ответы:


161

Я недавно обнаружил эти v0.4.0совместимые с Grunt задачи:

  • Грунт-предобработка

    Задача Grunt вокруг модуля предварительной обработки npm.

  • Грунт-окр

    Задача 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
Твое решение спасло меня от многих ударов головой о стену. Спасибо.
sthomps

1
Рад, что это помогло! С тех пор как я обнаружил эти задачи, я люблю рабочий процесс. К вашему сведению, я внес одно небольшое изменение в процесс ... Вместо того, чтобы передавать несколько переменных контекста в мои 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объект для dev / prod.
Матиас Биненс

2
Человек, я люблю это решение .. Это чистое, читаемое и не слишком спроектированное.
Гауранг Патель

34

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

По сути, я использую grunt.template.process (), чтобы сгенерировать my 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 , основанного на grunt, есть встроенная задача usemin, которая интегрируется с системой сборки Yeoman. Он генерирует производственную версию index.html на основе информации в разрабатываемой версии index.html, а также других параметров среды. Немного сложнее, но интересно смотреть.


5
grunt-template - это очень легкая оболочкаgrunt.template.process()(которую вы здесь используете), которая сделает это еще проще. Вы можете сделать то же самое, используя grunt-template , просто передав другойdataобъект для dev / prod.
Матиас Биненс,

15

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

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

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

Чтобы подвести итог, как это работает, у меня есть HTML-шаблон с переменной для тегов сценария. Я использую https://github.com/alanshaw/grunt-include-replace для заполнения этой переменной. В режиме разработки эта переменная исходит из шаблона глобализации всех моих файлов 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режиме. В режиме разработки эта переменная будет расширена до значения, подобного этому:

<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-приложении. Я надеюсь, что кто-то может превратить это в плагин многократного использования. Может быть, я когда-нибудь.


1
Звучит многообещающе. Есть ли шанс, что вы могли бы поделиться некоторыми фрагментами?
Адам Маршалл

I've set up my grunt task so that every time a file is added or deleted, the script tags automatically get generated to reflect thatКак ты это сделал?
CodyBugstein

2
Другой вопрос: знаете ли вы способ просто удалить блоки HTML- <script>тегов?
CodyBugstein

@ Я не из головы. Вы имеете в виду без какой-либо формы шаблонов (например, grunt-include-replace)? Первая мысль, которая возникнет у меня в голове, будет xslt. Вероятно, не очень хорошее решение, хотя.
Даниэль Каплан

1
Этот ответ пятно на, хотя я лично удален destPathиз jsScriptTagsи поменялся grunt.file.expandMappingс grunt.file.expandкак файлами , которые я хотел уже были в правильных местах. Это многое упростило. Спасибо @DanielKaplan, вы сэкономили мне огромное количество времени :)
DanielM

13

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


2
Я видел этот плагин, но мне не нравится идея вручную указывать все файлы (и фактически иметь какую-либо логику) в моем index.html, потому что у меня уже есть список исходных файлов js / css в моей конфигурации grunt и я не Я не хочу повторяться. Итог - это не в index.html, где вы должны решить, какие файлы включать
Дмитрий Пашкевич

+1 за grunt-targetthtml. Хотя немного некрасиво добавлять операторы «решить» в index.html, какие ресурсы загружать. Тем не менее, это имеет смысл. Это место, куда вы обычно обращаете внимание, чтобы включить ресурсы в ваш проект. Кроме того, продолжение этого заставило меня проверить grunt-contrib. В нем есть что-то отличное.
carbontax

8

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

Как разместить если еще блок в gruntfile.js

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

  1. Сохраните две версии ваших индексных файлов, как вы перечислили, и назовите их index-development.html и index-prodoction.html.
  2. Используйте следующую логику в блоке 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',
           ...
        },
        ...
    },
  3. запустите 'grunt --Release', чтобы выбрать файл index-production.html и снимите флажок, чтобы получить версию для разработки.

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


3
Единственным недостатком здесь является поддержка двух файлов index.html.
Адам Маршалл,

5

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


+1. Документация сбивает с толку, и некоторые вещи (appRoot, родственник) не всегда работают как задумано, но все же: полезный инструмент.
hashchange

1
@hashchange Я не использую этот инструмент. Вместо этого я использовал github.com/alanshaw/grunt-include-replace . У меня есть переменная в моем HTML-файле представляют теги сценария. Затем я заполняю эту переменную строкой HTML, который я хочу. В режиме разработки эта переменная является списком сценариев. В режиме prod эта переменная является объединенной минимизированной версией.
Даниэль Каплан

Спасибо за указатель на grunt-include-replace. (Я действительно нуждался в инструменте для добавления всех файлов спецификаций в каталоге в файл Mocha index.html. Scriptlinker хорош для этого.)
hashchange

@hashchange, вы правы насчет документации. Как вы скажете, где поместить плитки скриптов в ваш HTML-файл?
Даниэль Каплан

1
Вы определяете комментарий HTML. Посмотрите на этот файл . Вставки случаются при <!--SINON COMPONENT SCRIPTS-->и <!--SPEC SCRIPTS-->. И вот задача Grunt, которая делает это (фактическая рабочая, в отличие от материала в документах). Надеюсь, это поможет;)
hashchange

5

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

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


5

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

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). Это упрощает разработку, поскольку все разработчики не работают над одним длинным одностраничным приложением (что предотвращает множество проверок на конфликты). Вместо этого вы можете разбивать страницы и работать с более мелкими кусками кода и компилировать до полной страницы с помощью команды watch.

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

<!DOCTYPE html>

Демоверсии jQuery для мобильных устройств

app.initialize ();

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

Вы можете сделать еще один шаг вперед и добавить на свои страницы инъекции для «меню», «всплывающих окон» и т. Д., Чтобы вы могли по-настоящему разбить страницы на более мелкие управляемые компоненты.


Может быть, вы можете улучшить свой ответ с помощью демонстрационного кода, который использует grunt-bake?
Дмитрий Пашкевич

4

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

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

2

Этот ответ не для нубов!

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

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

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

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 для включения элементов на странице в зависимости от того, присутствует ли класс.

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

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

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

2

Рассмотрим 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">
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.