Haga que Grunt genere index.html para diferentes configuraciones

208

Estoy tratando de usar Grunt como herramienta de compilación para mi aplicación web.

Quiero tener al menos dos configuraciones:

I. Configuración de desarrollo : cargar scripts de archivos separados, sin concatenación,

entonces mi index.html se vería así:

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

II Configuración de producción : cargar mis scripts minificados y concatenados en un archivo,

con index.html en consecuencia:

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

La pregunta es, ¿cómo puedo hacer que Grunt haga estos index.html dependiendo de la configuración cuando ejecuto grunt devo grunt prod?

¿O tal vez estoy cavando en la dirección incorrecta y sería más fácil generar siempre, MyApp-all.min.jspero poner dentro de mí todos mis scripts (concatenados) o un script de cargador que carga de forma asíncrona esos scripts de archivos separados?

¿Cómo lo hacen, chicos?

Dmitry Pashkevich
fuente
3
Pruebe la herramienta Yeoman, que incluye una tarea 'usemin' que hace lo que uno quiere. Además, los generadores Yeamon incluyen muchas "buenas prácticas" fáciles de aprender que son difíciles de aprender al usar una nueva herramienta.
EricSonaron

Respuestas:

161

Recientemente descubrí estas v0.4.0tareas compatibles con Grunt :

  • gruñir-preproceso

    Tarea dura alrededor del módulo npm de preproceso.

  • gruñido env

    Grunt tarea para automatizar la configuración del entorno para futuras tareas.

Debajo hay fragmentos de mi Gruntfile.js.

Configuración de ENV:

env : {

    options : {

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

    },

    dev: {

        NODE_ENV : 'DEVELOPMENT'

    },

    prod : {

        NODE_ENV : 'PRODUCTION'

    }

},

Preproceso:

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 %>'
            }

        }

    }

}

Tareas:

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']);

Y en el /src/tmpl/index.htmlarchivo de plantilla (por ejemplo):

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

Estoy seguro de que mi configuración es diferente a la de la mayoría de las personas, y la utilidad de lo anterior dependerá de su situación. Para mí, si bien es un código increíble, Yeoman Grunt-usemin es más robusto de lo que personalmente necesito.

NOTA: Me acaba de descubrir las tareas mencionadas anteriormente hoy, así que puede ser que falte una característica y / o de mi proceso puede cambiar en el futuro. Por ahora, me encanta la simplicidad y las características que Grunt-preprocess y Grunt-env tienen para ofrecer. :)


Actualización de enero de 2014:

Motivado por un voto negativo ...

Cuando publiqué esta respuesta, no había muchas opciones para Grunt 0.4.xque ofrecieran una solución que funcionara para mis necesidades. Ahora, meses después, supongo que hay más opciones que podrían ser mejores que las que he publicado aquí. Si bien todavía uso y disfruto personalmente el uso de esta técnica para mis compilaciones , les pido a los futuros lectores que se tomen el tiempo para leer las otras respuestas dadas y para investigar todas las opciones. Si encuentra una solución mejor, publique su respuesta aquí.

Actualización de febrero de 2014:

No estoy seguro de si será de alguna ayuda para alguien, pero he creado este repositorio de demostración en GitHub que muestra una configuración completa (y más compleja) utilizando las técnicas que he descrito anteriormente.

mhulse
fuente
¡Gracias, lo comprobaré!
Dmitry Pashkevich
3
Su solución me ahorró horas de golpear mi cabeza contra la pared. Gracias.
sthomps
1
@sthomps ¡Me alegra que haya ayudado! Desde que descubrí esas tareas, he estado amando el flujo de trabajo. Para su información, he realizado un ligero cambio en el proceso ... En lugar de pasar varias variables de contexto a mis plantillas html, opté por pasar una var path : '/<%= pkg.name %>/dist/<%= pkg.version %>/<%= now %>/<%= ver %>'que concatena todos los vars (esa es mi ruta de compilación). En mi plantilla Tendré: <script src="http://cdn.foo.com<!-- @echo path -->/js/bulldog.min.js"></script>. De todos modos, ¡estoy feliz de poder ahorrarte algo de tiempo! : D
mhulse
44
Puede hacer lo mismo usando solo grunt-template , simplemente pasando un dataobjeto diferente para dev / prod.
Mathias Bynens
2
Hombre, me encanta esta solución. Es limpia, legible y no está diseñada en exceso.
Gaurang Patel
34

Se me ocurrió mi propia solución. Todavía no está pulido, pero creo que voy a avanzar en esa dirección.

En esencia, estoy usando grunt.template.process () para generar mi index.htmldesde una plantilla que analiza la configuración actual y produce una lista de mis archivos fuente originales o enlaces a un solo archivo con código minificado. El siguiente ejemplo es para archivos js, pero el mismo enfoque se puede extender a css y cualquier otro archivo de texto posible.

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 + '\'');
    });
}

Finalmente, index.tmplcon la lógica de generación integrada:

<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 Descubrí que Yeoman , que se basa en el gruñido, tiene una tarea usemin incorporada que se integra con el sistema de construcción de Yeoman. Genera una versión de producción de index.html a partir de la información en la versión de desarrollo de index.html, así como otras configuraciones del entorno. Un poco sofisticado pero interesante de ver.

Dmitry Pashkevich
fuente
55
Grunt-template es un envoltorio muy livianogrunt.template.process()(que es lo que estás usando aquí) que lo haría aún más fácil. Podrías hacer lo mismo usando grunt-template simplemente pasando undataobjetodiferentepara dev / prod.
Mathias Bynens
15

No me gustan las soluciones aquí (incluida la que di anteriormente ) y he aquí por qué:

  • El problema con la respuesta más votada es que debe sincronizar manualmente la lista de etiquetas de script cuando agrega / renombra / elimina un archivo JS.
  • El problema con la respuesta aceptada es que su lista de archivos JS no puede tener coincidencia de patrones. Esto significa que debe actualizarlo a mano en el Gruntfile.

He descubierto cómo resolver estos dos problemas. He configurado mi tarea gruñona para que cada vez que se agregue o elimine un archivo, las etiquetas de script se generen automáticamente para reflejar eso. De esta manera, no necesita modificar su archivo html o su archivo de gruñido cuando agrega / elimina / renombra sus archivos JS.

Para resumir cómo funciona, tengo una plantilla html con una variable para las etiquetas de script. Yo uso https://github.com/alanshaw/grunt-include-replace para llenar esa variable. En el modo de desarrollo, esa variable proviene de un patrón global de todos mis archivos JS. La tarea de observación recalcula este valor cuando se agrega o elimina un archivo JS.

Ahora, para obtener resultados diferentes en modo dev o prod, simplemente llene esa variable con un valor diferente. Aquí hay un código:

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

jsSrcFileArrayes tu típico patrón gruñido de gruñido jsScriptTagstoma el jsSrcFileArrayy los concatena junto con scriptetiquetas en ambos lados. destPathes el prefijo que quiero en cada archivo.

Y así es como se ve el HTML:

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

</head>

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

Ahora, como puede ver en la configuración, genero el valor de esa variable como una scriptetiqueta codificada cuando se ejecuta en prodmodo. En modo dev, esta variable se expandirá a un valor como este:

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

Hazme saber si tienes alguna pregunta.

PD: Esta es una cantidad loca de código para algo que me gustaría hacer en cada aplicación JS del lado del cliente. Espero que alguien pueda convertir esto en un complemento reutilizable. Quizás lo haga algún día.

Daniel Kaplan
fuente
1
Suena prometedor. ¿Alguna posibilidad de que puedas compartir algunos fragmentos?
Adam Marshall
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¿Cómo hiciste eso?
CodyBugstein
2
Otra pregunta: ¿conoce alguna forma de eliminar bloques de <script>etiquetas HTML ?
CodyBugstein
@ Imray no fuera de mi cabeza. ¿Quieres decir sin ningún tipo de plantilla (por ejemplo, gruñir-incluir-reemplazar)? El primer pensamiento que aparece en mi cabeza sería xslt. Sin embargo, probablemente no sea una buena solución.
Daniel Kaplan
1
Esta respuesta está en el clavo, aunque personalmente quitado destPathde jsScriptTagse intercambiado grunt.file.expandMappingcon grunt.file.expandque los archivos que quería ya estaban en los lugares correctos. Esto simplificó mucho las cosas. Gracias @DanielKaplan, me has ahorrado una gran cantidad de tiempo :)
DanielM
13

Me he estado haciendo la misma pregunta por un tiempo, y creo que este plugin Grunt podría configurarse para hacer lo que quieras: https://npmjs.org/package/grunt-targethtml . Implementa etiquetas html condicionales, que dependen del objetivo gruñido.

Por Quested Aronsson
fuente
2
He visto este complemento, pero no me gusta la idea de especificar manualmente todos los archivos (y de hecho tener lógica) en mi index.html porque ya tengo una lista de archivos js / css de origen en mi configuración de gruñido y no No quiero repetirme. La conclusión es: no está en index.html, donde debe decidir qué archivos incluir
Dmitry Pashkevich,
+1 para grunt-targethtml. Aunque es un poco feo agregar sentencias if para "decidir" en su index.html qué activos cargar. Aún así, tiene sentido. Este es el lugar donde normalmente buscará incluir recursos en su proyecto. Además, el seguimiento de esto me llevó a revisar grunt-contrib. Tiene algunas cosas geniales.
Carbontax
8

Estaba buscando una solución más simple y directa, así que combiné la respuesta de esta pregunta:

Cómo colocar si otro bloque en gruntfile.js

y se me ocurrieron los siguientes pasos simples:

  1. Mantenga dos versiones de sus archivos de índice como los enumeró y asígneles el nombre index-development.html e index-prodoction.html.
  2. Use la siguiente lógica en el bloque concat / copy de su Gruntfile.js para su archivo 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. ejecute 'grunt --Release' para elegir el archivo index-production.html y deje el indicador para tener la versión de desarrollo.

Sin nuevos complementos para agregar o configurar y sin nuevas tareas de gruñido.

Edward Tan
fuente
3
El único inconveniente aquí es que hay dos archivos index.html para mantener.
Adam Marshall
5

Esta tarea gruñona llamada scriptlinker parece una manera fácil de agregar los scripts en modo dev. Probablemente podría ejecutar primero una tarea concat y luego apuntarla a su archivo concatenado en modo prod.

Daniel Kaplan
fuente
+1. La documentación es confusa y algunas cosas (appRoot, relativo) no siempre funcionan según lo previsto, pero aún así: herramienta útil.
hashchange
1
@hashchange No uso esta herramienta. Terminé usando github.com/alanshaw/grunt-include-replace en su lugar. Tengo una variable en mi archivo html que representa las etiquetas del script. Luego relleno esa variable con una Cadena del html que quiero. En modo dev, esta variable es una lista de los scripts. En modo prod, esta variable es la versión concatenada y minificada.
Daniel Kaplan
Gracias por el puntero a gruñir-incluir-reemplazar. (En realidad necesitaba una herramienta para añadir todos los archivos de especificaciones en un directorio en un archivo index.html Mocha Scriptlinker está muy bien para eso..)
hashchange
@hashchange tienes razón sobre la succión de documentación. ¿Cómo le dices dónde colocar los mosaicos de script en tu archivo html?
Daniel Kaplan
1
Usted define un comentario HTML. Echa un vistazo a este archivo . Las inserciones ocurren en <!--SINON COMPONENT SCRIPTS-->y <!--SPEC SCRIPTS-->. Y aquí está la tarea de Grunt que lo hace (una tarea real, a diferencia de las cosas en los documentos). Espero que ayude;)
hashchange
5

grunt-dom-munger lee y manipula HTML con selectores CSS. Ex. lee las etiquetas de tu html. Eliminar nodos, agregar nodos y más.

Puede usar grunt-dom-munger para leer todos sus archivos JS vinculados por su index.html, uglificarlos y luego usar grunt-dom-munger nuevamente para modificar su index.html para vincular solo el JS minificado

brillante
fuente
5

Encontré un plugin grunt llamado grunt-dev-prod-switch. Todo lo que hace es comentar ciertos bloques que busca en función de una opción --env que pasa a gruñir (aunque lo limita a dev, prod y test).

Una vez que lo configure como se explica aquí , puede ejecutar, por ejemplo:

grunt serve --env=dev, y todo lo que hace es comentar los bloques que están envueltos por

    <!-- env:test/prod -->
    your code here
    <!-- env:test/prod:end -->

y eliminará los bloques que están envueltos por

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

También funciona en javascript, lo uso para configurar la dirección IP correcta para conectarme a mi API de back-end. Los bloques simplemente cambian a

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

En su caso, sería tan simple como esto:

<!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>
anónimo
fuente
4

Grunt-Bake es un fantástico script de gruñido que funcionaría muy bien aquí. Lo uso en mi script de creación automática de JQM.

https://github.com/imaginethepoet/autojqmphonegap

Echa un vistazo a mi archivo grunt.coffee:

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

Esto examina todos los archivos en base.html y los absorbe para crear index.html funciona de maravilla para aplicaciones multipágina (phonegap). Esto permite un desarrollo más fácil ya que todos los desarrolladores no están trabajando en una sola aplicación de página larga (evitando muchos registros de conflictos) En cambio, puede dividir las páginas y trabajar en fragmentos de código más pequeños y compilarlos en la página completa utilizando un comando watch.

Bake lee la plantilla de base.html e inyecta las páginas html del componente de servicio.

<!DOCTYPE html>

Demostraciones móviles de jQuery

app.initialize ();

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

Puede llevar esto un paso más allá y agregar inyecciones en sus páginas para "menús", "ventanas emergentes", etc., de modo que realmente pueda dividir las páginas en componentes manejables más pequeños.

imagina a la poeta
fuente
¿Quizás pueda mejorar su respuesta con una demostración de código que utiliza grunt-bake?
Dmitry Pashkevich
4

Use una combinación de wiredep https://github.com/taptapship/wiredep y usemin https://github.com/yeoman/grunt-usemin para que Grunt se encargue de estas tareas. Wiredep agregará sus dependencias un archivo de script a la vez, y usemin las concatenará todas en un solo archivo para producción. Esto se puede lograr con solo algunos comentarios html. Por ejemplo, mis paquetes Bower se incluyen automáticamente y se agregan al html cuando ejecuto bower install && grunt bowerInstall:

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

¡Esta respuesta no es para novatos!

Use plantillas de Jade ... pasar variables a una plantilla de Jade es un caso de uso estándar pantanoso

Estoy usando grunt (grunt-contrib-jade) pero no tienes que usar grunt. Simplemente use el módulo estándar npm jade.

Si usa grunt, entonces su archivo de gruñido le gustaría algo como ...

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"
        }
      ]
    }
  },

Ahora podemos acceder fácilmente a los datos pasados ​​por gruñido en la plantilla Jade.

Al igual que el enfoque utilizado por Modernizr, configuro una clase CSS en la etiqueta HTML de acuerdo con el valor de la variable pasada y puedo usar la lógica de JavaScript a partir de si la clase CSS está presente o no.

Esto es genial si usa Angular ya que puede hacer ng-if para incluir elementos en la página en función de si la clase está presente.

Por ejemplo, podría incluir un script si la clase está presente ...

(Por ejemplo, podría incluir el script de recarga en vivo en dev pero no en producción)

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

Considere el procesohtml . Permite la definición de múltiples "objetivos" para compilaciones. Los comentarios se usan para incluir o excluir condicionalmente material del HTML:

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

se convierte

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

Incluso pretende hacer cosas ingeniosas como esta (ver el archivo 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">
dat
fuente