问题
I'm trying to use Grunt as a build tool for my webapp.
I want to have at least two setups:
I. Development setup - load scripts from separate files, without concatenation,
so my index.html would look something like:
<!DOCTYPE html>
<html>
<head>
<script src="js/module1.js" />
<script src="js/module2.js" />
<script src="js/module3.js" />
...
</head>
<body></body>
</html>
II. Production setup - load my scripts minified & concatenated in one file,
with index.html accordingly:
<!DOCTYPE html>
<html>
<head>
<script src="js/MyApp-all.min.js" />
</head>
<body></body>
</html>
The question is, how can I make grunt make these index.html's depending on the configuration when I run grunt dev
or grunt prod
?
Or maybe I'm digging in the wrong direction and it would be easier to always generate MyApp-all.min.js
but put inside it either all my scripts (concatenated) or a loader script that asynchronously loads those scripts from separate files?
How do you do it, guys?
回答1:
I recently discovered these Grunt v0.4.0
compatible tasks:
grunt-preprocess
Grunt task around preprocess npm module.
grunt-env
Grunt task to automate environment configuration for future tasks.
Below are snippets from my Gruntfile.js
.
ENV setup:
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 %>'
}
}
}
}
Tasks:
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']);
And in the /src/tmpl/index.html
template file (for example):
<!-- @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 -->
I'm sure my setup is different than most people, and the usefulness of the above will depend on your situation. For me, while it's an awesome bit of code, the Yeoman grunt-usemin is a more robust than I personally need.
NOTE: I just discovered the above listed tasks today, so I might be missing a feature and/or my process may change down the road. For now, I'm loving the simplicity and features that grunt-preprocess and grunt-env have to offer. :)
Jan 2014 update:
Motivated by a down vote ...
When I posted this answer there weren't many options for Grunt 0.4.x
that offered a solution that worked for my needs. Now, months later, I would guess that there are more options out there that could be better than what I have posted here. While I still personally use, and enjoy using, this technique for my builds, I ask that future readers take the time to read the other answers given and to research all the options. If you find a better solution, please post your answer here.
Feb 2014 update:
I'm not sure if it will be of any help to anyone, but I've created this demo repository on GitHub that shows a complete (and more complex setup) using the technique(s) I've outlined above.
回答2:
I've come up with my own solution. Not polished yet but I think I'm going to move in that direction.
In essense, I'm using grunt.template.process() to generate my index.html
from a template that analyzes current configuration and produces either a list of my original source files or links to a single file with minified code. The below example is for js files but the same approach can be extended to css and any other possible text files.
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 + '\'');
});
}
Finally, index.tmpl
, with generation logic baked in:
<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. Found out that Yeoman, which is based on grunt, has a built-in usemin task that integrates with Yeoman's build system. It generates a production version of index.html from information in development version of index.html as well as other environment settings. A bit sophisticated but interesting to look at.
回答3:
I dislike the solutions here (including the one I previously gave) and here's why:
- The problem with the highest voted answer is that you have to manually sync the list of script tags when you add/rename/delete a JS file.
- The problem with the accepted answer is that your list of JS files can't have pattern matching. This means you've got to update it by hand in the Gruntfile.
I've figured out how to solve both of these issues. 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. This way, you don't need to modify your html file or your grunt file when you add/remove/rename your JS files.
To summarize how that works, I have a html template with a variable for the script tags. I use https://github.com/alanshaw/grunt-include-replace to populate that variable. In dev mode, that variable comes from a globbing pattern of all my JS files. The watch task recalculates this value when a JS file is added or removed.
Now, to get different results in dev or prod mode, you simply populate that variable with a different value. Here's some code:
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
is your typical grunt file-globbing pattern. jsScriptTags
takes the jsSrcFileArray
and concatenates them together with script
tags on both sides. destPath
is the prefix I want on each file.
And here's what the HTML looks like:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Example</title>
</head>
<body>
@@scriptsTags
</body>
</html>
Now, as you can see in the config, I generate the value of that variable as a hard coded script
tag when it's run in prod
mode. In dev mode, this variable will expand to a value like this:
<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>
Let me know if you have any questions.
PS: This is a crazy amount of code for something I'd want to do in every client-side JS app. I hope someone can turn this into a reusable plugin. Maybe I will some day.
回答4:
I have been asking myself the same question for a while, and I think this grunt plugin could be configured to do what you want: https://npmjs.org/package/grunt-targethtml. It implements conditional html tags, that depend on the grunt target.
回答5:
I was looking for a more simple, straight forward solution so I combined the answer from this question:
How to place if else block in gruntfile.js
and came up with following simple steps:
- Keep two versions of your index files as you listed and name them index-development.html and index-prodoction.html.
Use the following logic in your Gruntfile.js's concat/copy block for your index.html file:
concat: { index: { src : [ (function() { if (grunt.option('Release')) { return 'views/index-production.html'; } else { return 'views/index-development.html'; } }()) ], dest: '<%= distdir %>/index.html', ... }, ... },
run 'grunt --Release' to choose the index-production.html file and leave off the flag to have the development version.
No new plugins to add or configure and no new grunt tasks.
回答6:
This grunt task named scriptlinker looks like an easy way to add the scripts in dev mode. You could probably run a concat task first and then point it to your concatenated file in prod mode.
回答7:
grunt-dom-munger reads and manipulates HTML with CSS selectors. Ex. read tags from your html. Remove nodes, add nodes, and more.
You can use grunt-dom-munger to read all your JS files that are linked by your index.html, uglify them and then use grunt-dom-munger again to modify your index.html to only link the minified JS
回答8:
I found a grunt plugin called grunt-dev-prod-switch. All it does is comment out certain blocks it looks for based on an --env option you pass to grunt (although it limits you to dev, prod, and test).
Once you set it up as it explains here, you can run for example:
grunt serve --env=dev
, and all it does is comment out the blocks which are wrapped by
<!-- env:test/prod -->
your code here
<!-- env:test/prod:end -->
and it will uncomment out blocks which are wrapped by
<!-- env:dev -->
your code here
<!-- env:dev:end -->
It also works on javascript, I use it for setting up the right IP address to connect to for my backend API. The blocks just change to
/* env:dev */
your code here
/* env:dev:end */
In your case, it would be as simple as this:
<!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 is a fantastic grunt script that would work great here. I use it in my JQM auto build script.
https://github.com/imaginethepoet/autojqmphonegap
Take a look at my grunt.coffee file:
bake:
resources:
files: "index.html":"resources/custom/components/base.html"
This looks at all the files in base.html and sucks them in to create index.html works fantastic for multipage apps (phonegap). This allows for easier development as all devs are not working on one long single page app (preventing lots of conflict checkins). Instead you can break up the pages and work on smaller chunks of code and compile to the full page using a watch command.
Bake reads the template from base.html and injects the component html pages on watch.
<!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>
You can take this a step further and add injections in your pages for "menus" "popups" etc so you can really break pages into smaller manageable components.
回答10:
Use a combination of wiredep https://github.com/taptapship/wiredep and usemin https://github.com/yeoman/grunt-usemin in order to have grunt take care of these tasks. Wiredep will add your dependencies one script file at a time, and usemin will concatenate them all into a single file for production. This can then be accomplished with just some html comments. For instance, my bower packages are automatically included and added to the html when I run bower install && grunt bowerInstall
:
<!-- build:js /scripts/vendor.js -->
<!-- bower:js -->
<!-- endbower -->
<!-- endbuild -->
回答11:
This answer is not for noobs!
Use Jade templating ... passing variables to a Jade template is a bog standard use case
I am using grunt (grunt-contrib-jade) but you don't have to use grunt. Just use the standard npm jade module.
If using grunt then your gruntfile would like something like ...
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"
}
]
}
},
We can now easily access the data passed by grunt in the Jade template.
Much like the approach used by Modernizr, I set a CSS class on the HTML tag according to the value of the variable passed and can use JavaScript logic from there based on whether the CSS class is present or not.
This is great if using Angular since you can do ng-if's to include elements in the page based on whether the class is present.
For example, I might include a script if the class is present ...
(For example, I might include the live reload script in dev but not in production)
<script ng-if="controller.isClassPresent()" src="//localhost:35729/livereload.js"></script>
回答12:
Consider processhtml. It allows definition of multiple "targets" for builds. Comments are used to conditionally include or exclude material from the HTML:
<!-- build:js:production js/app.js -->
...
<!-- /build -->
becomes
<script src="js/app.js"></script>
It even purports to do nifty stuff like this (see the 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">
来源:https://stackoverflow.com/questions/12401998/have-grunt-generate-index-html-for-different-setups