I’ve been developing web applications for as long as I can remember and there are certain repetitive tasks that I do between the development and deployment of each final product. For example, current web standards demand that web resources like CSS, and JavaScript be minified or images be compressed. We could easily do this by hand or with helper applications, but why would you want to?
Instead, repetitive tasks can, and should, be transformed into an automated workflow, something that is particularly useful when it comes to continuous integration and continuous deployment.
We’re going to see how to create a an automated workflow using the Gulp toolkit to do simple tasks like cleaning, minification, copying, altering, and even deploying projects.
The inspiration behind this article is from my efforts on converting The Polyglot Developer from WordPress to a statically generated website with Hugo. I needed to comply with the web standards outlined in various tests such as Pingdom, GTMetrix, and Google PageSpeed Insights.
I needed to build my project, combine my CSS that were related via @import
statements into a single file, inline all CSS and JavaScript into the HTML files, minify the HTML, CSS, and JavaScript, and then use rsync
to deploy it to my server. These are things I didn’t want to do by hand every time I made a change.
Let’s start by creating a new web project. This project should contain a CSS file, a JavaScript file, and an HTML file.
mkdir my-project
cd my-project
mkdir src
touch index.html
touch styles.css
touch scripts.js
If you don’t have the mkdir
and touch
commands in your command line, feel free to create those files and directories manually.
While our web project won’t be using Node.js, our Gulp workflow automation will. For this reason we’ll need to initialize the project and install the Gulp development dependency. From the command line, execute the following:
npm init -y
npm install gulp --save-dev
For this project, we’re going to take our content in the src directory, do our automation, then placed the transformed content in a new dist directory. This is common when working with JavaScript libraries online, or even the very popular Bootstrap framework.
Open the project’s src/index.html file and include the following very simple HTML markup:
<html>
<head>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<h1>Hello World</h1>
<p>
This is another example from The Polyglot Developer
</p>
<script src="scripts.js"></script>
</body>
</html>
The JavaScript and CSS files are linked to the HTML, so let’s fill them with some style data and code. Open the project’s src/styles.css and include the following:
body {
background-color: #000000;
color: #FFFFFF;
}
Again, this is a very simple example so we won’t have some crazy style data. Likewise, our src/scripts.js file looks like the following:
console.log("A simple print for JavaScript");
We’re going to inline the CSS and JavaScript and minify everything. This will reduce the number of remote requests needed by the web browser and compress the server response making everything faster. Remember, I’m coming at this example from my own experience with web application optimization. Automated workflows can extend way beyond what I’m using it for.
Gulp operates off a gulpfile.js file which should be created at the root of your project. Once created, open it and include the following:
var gulp = require("gulp");
gulp.task("build", () => {
});
If we were to run gulp build
the build
task would execute and anything inside it would be ran. Because we’re using Gulp as a development dependency rather than global, it makes sense to create an NPM script for it.
Open the project’s package.json and manipulate the scripts
section to look like the following:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "./node_modules/gulp/bin/gulp.js build"
},
This says that when we call npm run build
the local Gulp will execute the build
task. So let’s dig deeper into what we hope to accomplish with the Gulp script, because as of now it doesn’t do anything.
Most quality automated workflows start by cleaning the project for any prior builds. The last thing we want is some weird caching issue to mess up our deployment. From the command line, install the following project dependency:
npm install gulp-clean --save
Within our gulpfile.js file we need to import the downloaded dependency and create a task for it. Open the gulpfile.js file and include the following:
var gulp = require("gulp");
var clean = require("gulp-clean");
gulp.task("clean", function () {
return gulp.src("./dist", { read: false })
.pipe(clean());
});
gulp.task("build", ["clean"], () => {
});
In theory, if we wanted to create an NPM script for clean that executed the clean
task we could. However, we’re focused on building. When calling npm run build
all dependent tasks are run and then whatever is in the build
task. This means that clean
is run in the process. The actual clean
task will delete the dist directory if it exists in the project.
We’re not done yet. The next step is to inline our CSS and JavaScript. From the command line, install the following dependency:
npm install gulp-replace --save
The above dependency will allow us to work with regular expressions for replacing things. Our goal is to search for <link>
and <script>
tags and replace them with the file content, thus inlining them.
Within the gulpfile.js file, include the following:
var gulp = require("gulp");
var clean = require("gulp-clean");
var replace = require("gulp-replace");
var fs = require("fs");
gulp.task("clean", function () {
return gulp.src("./dist", { read: false })
.pipe(clean());
});
gulp.task("inline", function() {
return gulp.src("src/**/*.html")
.pipe(replace(/<link rel=\"stylesheet\" href=\"styles\.css\"[^>]*>/, function(s) {
var style = fs.readFileSync("src/styles.css", "utf8");
return "<style>" + style + "</style>";
}))
.pipe(replace(/<script src=\"scripts\.js\"[^>]*><\/script>/, function(s) {
var script = fs.readFileSync("src/scripts.js", "utf8");
return "<script>" + script + "</script>";
}))
.pipe(gulp.dest("./dist/"));
});
gulp.task("build", ["clean", "inline"], () => {
});
Notice the inline
task above. All files with the HTML extension within all directories found in the src directory will be piped into the regular expressions. The replaced versions of these files will be placed in the dist directory. I can’t take complete credit for the code above. The idea came from Stack Overflow.
If you ran what we have so far, you may have run into an issue. When running build
, the clean
and inline
tasks run asynchronously. This means that you can’t control which completes or even starts first. This could lead to terrible problems down the road.
Instead, we need to run each in sequence using another Node.js dependency. From the command line, execute the following:
npm install run-sequence --save
With some minor modifications to our gulpfile.js, we can make it look like the following:
var gulp = require("gulp");
var clean = require("gulp-clean");
var replace = require("gulp-replace");
var fs = require("fs");
var runSequence = require("run-sequence");
gulp.task("clean", function () {
return gulp.src("./dist", { read: false })
.pipe(clean());
});
gulp.task("inline", function() {
return gulp.src("src/**/*.html")
.pipe(replace(/<link rel=\"stylesheet\" href=\"styles\.css\"[^>]*>/, function(s) {
var style = fs.readFileSync("src/styles.css", "utf8");
return "<style>" + style + "</style>";
}))
.pipe(replace(/<script src=\"scripts\.js\"[^>]*><\/script>/, function(s) {
var script = fs.readFileSync("src/scripts.js", "utf8");
return "<script>" + script + "</script>";
}))
.pipe(gulp.dest("./dist/"));
});
gulp.task("build", (callback) => {
runSequence("clean", "inline", callback);
});
Anything added to the runSequence
method will be executed in sequence. If you did have things that should be run asynchronously as well, the runSequence
function supports it. Just read the official documentation for the package.
This brings us to the minification.
From the command line, execute the following to get our necessary Gulp package:
npm install gulp-htmlmin --save
Heading back into the project’s gulpfile.js file, add the following:
// ...
var htmlmin = require("gulp-htmlmin");
// ...
gulp.task("minify", () => {
return gulp.src(["dist/**/*.html"])
.pipe(htmlmin({
collapseWhitespace: true,
minifyCSS: true,
minifyJS: true,
removeComments: true,
useShortDoctype: true,
}))
.pipe(gulp.dest("./dist/"));
});
gulp.task("build", (callback) => {
runSequence("clean", "inline", "minify", callback);
});
We’ve added a minify
task that will take all HTML files, this time found in the dist directory, minify the CSS, minify the JavaScript, and minify the HTML. The resulting files will be dropped back into the dist directory.
Go ahead and try the npm run build
command. You should end up with some very slick HTML files in your project.
You just saw how to create an automation workflow with Gulp that transforms websites so they comply better with the current web standards. It is just one of many possible examples when it comes to automating parts of your project.
A video version of this tutorial can be seen below.