Faça com que o Grunt gere index.html para diferentes configurações


208

Estou tentando usar o Grunt como uma ferramenta de construção para meu aplicativo da web.

Quero ter pelo menos duas configurações:

I. Configuração do desenvolvimento - carregue scripts de arquivos separados, sem concatenação,

então meu index.html seria algo como:

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

II Configuração de produção - carregue meus scripts minificados e concatenados em um arquivo,

com index.html de acordo:

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

A questão é: como posso fazer o grunhido tornar esses index.html dependendo da configuração quando executo grunt devou grunt prod?

Ou talvez eu esteja cavando na direção errada e seria mais fácil sempre gerar, MyApp-all.min.jsmas colocar dentro dele todos os meus scripts (concatenados) ou um script do carregador que carrega de forma assíncrona esses scripts a partir de arquivos separados?

Como você faz isso, pessoal?


3
Experimente a ferramenta Yeoman, que inclui uma tarefa 'usemin' que faz o que você faz. Além disso, os geradores da Yeamon incluem muitas "boas práticas" fáceis de aprender e difíceis de aprender ao usar uma nova ferramenta.
precisa saber é o seguinte

Respostas:


161

Descobri recentemente essas v0.4.0tarefas compatíveis com o Grunt :

  • pré-processo

    Tarefa Grunt em torno do módulo npm pré-processo.

  • grunhido-env

    Tarefa Grunt para automatizar a configuração do ambiente para tarefas futuras.

Abaixo estão trechos do meu Gruntfile.js.

Configuração ENV:

env : {

    options : {

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

    },

    dev: {

        NODE_ENV : 'DEVELOPMENT'

    },

    prod : {

        NODE_ENV : 'PRODUCTION'

    }

},

Pré-processo:

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

        }

    }

}

Tarefas:

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

E no /src/tmpl/index.htmlarquivo de modelo (por exemplo):

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

Tenho certeza de que minha configuração é diferente da maioria das pessoas, e a utilidade das opções acima dependerá da sua situação. Para mim, embora seja um código incrível, o grunhido-usemin da Yeoman é mais robusto do que eu pessoalmente preciso.

NOTA: Eu apenas descobriu as tarefas listadas acima de hoje, para que eu possa estar faltando uma característica e / ou o meu processo pode mudar no futuro. Por enquanto, estou adorando a simplicidade e os recursos que o pré-processo e o env-grunt têm a oferecer. :)


Atualização de janeiro de 2014:

Motivado por um voto negativo ...

Quando publiquei esta resposta, não havia muitas opções para o Grunt 0.4.xque ofereciam uma solução que funcionava para minhas necessidades. Agora, meses depois, eu acho que existem mais opções por aí que poderiam ser melhores do que as que eu publiquei aqui. Enquanto eu pessoalmente ainda uso e gosto de usar essa técnica para minhas compilações , peço que os futuros leitores tenham tempo para ler as outras respostas dadas e pesquisar todas as opções. Se você encontrar uma solução melhor, poste sua resposta aqui.

Atualização de fevereiro de 2014:

Não tenho certeza se será de alguma ajuda para alguém, mas criei este repositório de demonstração no GitHub que mostra uma configuração completa (e mais complexa) usando as técnicas que descrevi acima.


Obrigado, vou dar uma olhada!
Dmitry Pashkevich

3
Sua solução me salvou horas batendo minha cabeça contra uma parede. Obrigado.
Sthomps 24/05

1
@sthomps Ainda bem que ajudou! Desde que descobri essas tarefas, amei o fluxo de trabalho. Para sua informação, fiz uma pequena alteração no processo ... Em vez de passar várias variáveis ​​de contexto para meus modelos de html, optei por passar uma var path : '/<%= pkg.name %>/dist/<%= pkg.version %>/<%= now %>/<%= ver %>'que concata todos os vars (esse é o meu caminho de construção). No meu modelo que eu vou ter: <script src="http://cdn.foo.com<!-- @echo path -->/js/bulldog.min.js"></script>. Enfim, estou feliz por ter poupado algum tempo! : D
mhulse

4
Você poderia fazer a mesma coisa usando apenas o modelo grunt , simplesmente passando um dataobjeto diferente para dev / prod.
Mathias Bynens

2
Cara, eu amo essa solução. É limpa, legível e não é projetada demais.
Gaurang Patel

34

Eu vim com minha própria solução. Ainda não polido, mas acho que vou seguir nessa direção.

Em essência, estou usando grunt.template.process () para gerar meu index.htmlde um modelo que analisa a configuração atual e produz uma lista dos meus arquivos de origem originais ou links para um único arquivo com código minificado. O exemplo abaixo é para arquivos js, mas a mesma abordagem pode ser estendida para css e quaisquer outros arquivos de texto possíveis.

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.tmplcom a lógica de geração inserida em:

<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. Descobriu que o Yeoman , que é baseado no grunhido, tem uma tarefa interna integrada que se integra ao sistema de compilação do Yeoman. Ele gera uma versão de produção de index.html a partir de informações na versão de desenvolvimento de index.html, além de outras configurações do ambiente. Um pouco sofisticado, mas interessante de se ver.


5
O grunt-template é um invólucro muito levegrunt.template.process()(que é o que você está usando aqui) que tornaria isso ainda mais fácil. Você pode fazer o mesmo usando o grunt-template simplesmente passando umdataobjetodiferentepara dev / prod.
Mathias Bynens

15

Não gosto das soluções aqui (incluindo a que eu dei anteriormente ) e aqui está o porquê:

  • O problema com a resposta mais votada é que você precisa sincronizar manualmente a lista de tags de script ao adicionar / renomear / excluir um arquivo JS.
  • O problema com a resposta aceita é que sua lista de arquivos JS não pode ter correspondência de padrões. Isso significa que você precisa atualizá-lo manualmente no Gruntfile.

Eu descobri como resolver esses dois problemas. Configurei minha tarefa difícil para que, sempre que um arquivo for adicionado ou excluído, as tags de script sejam geradas automaticamente para refletir isso. Dessa forma, você não precisa modificar seu arquivo html ou grunt quando adicionar / remover / renomear seus arquivos JS.

Para resumir como isso funciona, tenho um modelo html com uma variável para as tags de script. Eu uso https://github.com/alanshaw/grunt-include-replace para preencher essa variável. No modo dev, essa variável vem de um padrão globbing de todos os meus arquivos JS. A tarefa de observação recalcula esse valor quando um arquivo JS é adicionado ou removido.

Agora, para obter resultados diferentes no modo dev ou prod, basta preencher essa variável com um valor diferente. Aqui está um 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

jsSrcFileArrayé o seu padrão típico de arquivo grunhido. jsScriptTagspega jsSrcFileArraye concatena-os juntamente com scriptetiquetas de ambos os lados. destPathé o prefixo que eu quero em cada arquivo.

E aqui está a aparência do HTML:

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

</head>

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

Agora, como você pode ver na configuração, eu gero o valor dessa variável como uma scriptetiqueta codificada quando é executada no prodmodo. No modo dev, essa variável será expandida para um 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>

Deixe-me saber se você tiver alguma dúvida.

PS: Essa é uma quantidade louca de código para algo que eu gostaria de fazer em todos os aplicativos JS do lado do cliente. Espero que alguém possa transformar isso em um plugin reutilizável. Talvez eu vá algum dia.


1
Parece promissor. Alguma chance de você compartilhar alguns trechos?
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 thatComo você fez isso?
CodyBugstein

2
Outra pergunta: Você conhece uma maneira de excluir apenas blocos de <script>tags HTML ?
CodyBugstein

@Irmray não sai do topo da minha cabeça. Você quer dizer sem nenhuma forma de modelagem (por exemplo, incluir-substituir-substituir)? O primeiro pensamento que me vem à cabeça seria xslt. Provavelmente não é uma boa solução, no entanto.
Daniel Kaplan

1
Esta resposta está no local, embora eu pessoalmente removido destPathde jsScriptTagse trocou grunt.file.expandMappingcom grunt.file.expandque os arquivos que eu queria já estavam nos lugares corretos. Isso simplificou bastante as coisas. Obrigado @DanielKaplan, você salvou-me uma enorme quantidade de tempo :)
DanielM

13

Eu tenho me perguntado a mesma pergunta há algum tempo, e acho que esse plug-in do grunhido pode ser configurado para fazer o que você deseja: https://npmjs.org/package/grunt-targethtml . Ele implementa tags html condicionais, que dependem do destino pesado.


2
Eu vi esse plug-in, mas não gosto da ideia de especificar manualmente todos os arquivos (e de fato ter alguma lógica) no meu index.html, porque já tenho uma lista dos arquivos js / css de origem na minha configuração do grunt e não não quero me repetir. Bottom line é - não é em index.html, onde você deve decidir quais arquivos para incluir
Dmitry Pashkevich

+1 para grunt-targethtml. Embora seja um pouco feio adicionar instruções if para "decidir" em seu index.html quais recursos carregar. Ainda assim, faz sentido. Este é o local em que você normalmente procurará incluir recursos em seu projeto. Além disso, acompanhar isso me levou a verificar o grun-contrib. Tem ótimas coisas nele.
carbontax

8

Eu estava procurando uma solução mais simples e direta, então combinei a resposta desta pergunta:

Como colocar se outro bloco no gruntfile.js

e veio com as seguintes etapas simples:

  1. Mantenha duas versões dos seus arquivos de índice conforme listado e nomeie-os como index-development.html e index-prodoction.html.
  2. Use a seguinte lógica no bloco de concat / cópia do Gruntfile.js para o seu arquivo 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. execute 'grunt --Release' para escolher o arquivo index-production.html e deixe o sinalizador para ter a versão de desenvolvimento.

Não há novos plugins para adicionar ou configurar, nem novas tarefas grunhidas.


3
A única desvantagem aqui é que existem dois arquivos index.html para manter.
Adam Marshall

5

Essa tarefa difícil chamada scriptlinker parece uma maneira fácil de adicionar os scripts no modo dev. Você provavelmente pode executar uma tarefa de concat primeiro e depois apontá-la para o arquivo concatenado no modo prod.


+1. A documentação é confusa e algumas coisas (appRoot, relativa) nem sempre funcionam como pretendido, mas ainda assim: ferramenta útil.
hashchange

1
@hashchange Eu não uso essa ferramenta. Acabei usando github.com/alanshaw/grunt-include-replace . Eu tenho uma variável no meu arquivo html que representa as tags de script. Então eu preencho essa variável com uma String do html que eu quero. No modo dev, essa variável é uma lista dos scripts. No modo prod, essa variável é a versão concatenada e minificada.
Daniel Kaplan

Obrigado pelo ponteiro para grunhir-incluir-substituir. (Na verdade, eu precisava de uma ferramenta para adicionar todos os arquivos de especificação de um diretório para um arquivo index.html Mocha Scriptlinker é bom para isso..)
hashchange

@hashchange você está certo sobre a sucção da documentação. Como você diz onde colocar os blocos de script no seu arquivo html?
Daniel Kaplan

1
Você define um comentário HTML. Dê uma olhada neste arquivo . As inserções ocorrem em <!--SINON COMPONENT SCRIPTS-->e<!--SPEC SCRIPTS--> . E aqui está a tarefa do Grunt que faz isso (uma tarefa de trabalho real, em oposição às coisas dos documentos). Espero que ajude;)
hashchange

5

grunt-dom-munger lê e manipula HTML com seletores CSS. Ex. leia as tags do seu html. Remova nós, adicione nós e muito mais.

Você pode usar o grunt-dom-munger para ler todos os seus arquivos JS vinculados pelo index.html, aumentá-los e depois usar o grunt-dom-munger novamente para modificar o index.html para vincular apenas o JS minificado


5

Encontrei um plugin chamado grunt-dev-prod-switch. Tudo o que faz é comentar alguns blocos que ele procura, com base na opção --env que você passa para o grunhido (embora isso limite o desenvolvimento, a produção e o teste).

Depois de configurá-lo como explicado aqui , você pode executar, por exemplo:

grunt serve --env=dev, e tudo o que faz é comentar os blocos envolvidos por

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

e descomentará os blocos envolvidos por

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

Também funciona em javascript, eu o uso para configurar o endereço IP correto para conectar-se à minha API de back-end. Os blocos mudam para

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

No seu caso, seria tão simples quanto isto:

<!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 é um script fantástico que funcionaria muito bem aqui. Eu o uso no meu script de construção automática do JQM.

https://github.com/imaginethepoet/autojqmphonegap

Dê uma olhada no meu arquivo grunt.coffee:

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

Ele analisa todos os arquivos em base.html e os suga para criar o index.html que funciona de maneira fantástica para aplicativos de várias páginas (phonegap). Isso permite um desenvolvimento mais fácil, pois todos os desenvolvedores não estão trabalhando em um aplicativo de página única longa (evitando muitos check-ins de conflito). Em vez disso, você pode dividir as páginas e trabalhar em pequenos pedaços de código e compilar a página inteira usando um comando watch.

O Bake lê o modelo em base.html e injeta as páginas html do componente em exibição.

<!DOCTYPE html>

Demonstrações móveis do jQuery

app.initialize ();

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

Você pode dar um passo adiante e adicionar injeções em suas páginas para "menus", "pop-ups" etc., para que você possa realmente dividir as páginas em componentes gerenciáveis ​​menores.


Talvez você possa melhorar sua resposta com uma demonstração de código que usa grunt-bake?
Dmitry Pashkevich

4

Use uma combinação de wiredep https://github.com/taptapship/wiredep e usemin https://github.com/yeoman/grunt-usemin para que o grunhido cuide dessas tarefas. O Wiredep adicionará suas dependências um arquivo de script por vez e o usemin concatenará todas elas em um único arquivo para produção. Isso pode ser feito com apenas alguns comentários html. Por exemplo, meus pacotes do bower são incluídos automaticamente e adicionados ao html quando executo bower install && grunt bowerInstall:

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

2

Esta resposta não é para noobs!

Usar modelo Jade ... passar variáveis ​​para um modelo Jade é um caso de uso padrão

Estou usando o grunhido (grunt-contrib-jade), mas você não precisa usar o grunhido. Basta usar o módulo jpm npm padrão.

Se você estiver usando o grunhido, seu arquivo de grunt gostaria de 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"
        }
      ]
    }
  },

Agora podemos acessar facilmente os dados passados ​​por grunt no modelo Jade.

Assim como a abordagem usada pelo Modernizr, defino uma classe CSS na tag HTML de acordo com o valor da variável passada e posso usar a lógica JavaScript a partir daí, dependendo da presença ou não da classe CSS.

Isso é ótimo se você usar Angular, pois você pode usar ng-if para incluir elementos na página com base na presença da classe.

Por exemplo, posso incluir um script se a classe estiver presente ...

(Por exemplo, eu posso incluir o script de recarga ao vivo no dev, mas não na produção)

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

2

Considere processhtml . Permite definir vários "destinos" para compilações. Os comentários são usados ​​para incluir ou excluir condicionalmente material do HTML:

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

torna-se

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

Ele até pretende fazer coisas bacanas como esta (veja o 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">
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.