- Introduction
- Compile Less
- Application Required Files
- Compile Index Page For Development
- Development Build
- Distribution Build
Introduction
We focus on orchestrating the build process on Gulp. I prefer gulp because it provides full flexibility on the tasks content.
The build process is based on a file structure similar to the angular guidelines
We will cover all the required tasks and required libraries that an Angular 1.x project needs in order to perform the following gulp tasks:
* Build a development environment.
– Auto refresh on js change.
– Auto css injection on less change.
* Distribution environment.
Take a look on the entire gulp file
Compile Less
The app.less file includes all the necessary less files that are required in order to build the stylesheet of the project. Our next steps are easy.
- Keep track of source maps
- Compile the less files
- Concatenate them on a single file
gulp.task('compile-less', function () {
return gulp.src('app/less/app.less')
.pipe(sourcemaps.init())
.pipe(less({
paths: ['app/bower_components/', 'app/less', 'app']
}))
.pipe(concat('wiregoose.css'))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest('app/'));
});
Application Required Files
Application Javascript
The requirement is to load firstly the pure js files and then the angular components. This is because a component mainly consists of a angular component componentName-{service/controller/...}
and a module registration componentName
. If we include the angular component firstly, it will try to add the component on the registered module. The registrated module does not exists yet, so it will throw an error. Our Javascript inclusion order should guarantee that it will firstly include all the pure Javascript and module registrations and secondly all the components.
Our Angular components, are postfixed by the Angular component type. This is the only way to separate the files. We should firstly include all the Javascript files that does not postfixed by the component type, and finally the Javascript files that are postfixed by the component type.
globby is all we need for our file inclusion.
var NG_SUFFIXES = [
'service',
'controller',
'filter',
'directive',
'provider'
];
function getAppScripts() {
return globby.sync([
'{frontend/app,frontend/app/!(bower_components)/**}/*.js',
'!{frontend/app,frontend/app/!(bower_components)/**}/*{' + NG_SUFFIXES.join(',') + '}.js',
'!frontend/app/project-name.js'
])
.concat(globby.sync([
'{frontend/app,frontend/app/!(bower_components)/**}/*{' + NG_SUFFIXES.join(',') + '}.js',
'!frontend/app/project-name.js'
]));
}
Third-party Javascript/Css
We usually download all the third-party required packages using a package manager. We use Bower for package management tool. Hence, all the packages are downloaded under bower_components folder.
Each third-party library consists of an arbitrary structure. For instance, Angular
contains the angular.js
file in the root folder, bootstrap
contains the javascript on /dist/js/bootstrap.js
and the css on /dist/css/bootstrap.css
.
In order to specify the paths of the third party libraries, we use two configuration files. One for Javascript and one for css.
// frontend/lib-info/scripts.json
[
{
"name": "angular",
"path": "frontend/app/bower_components/angular/angular.js"
},
{
"name": "bootstrap",
"path": "frontend/app/bower_components/bootstrap/dist/js/bootstrap.js"
}
...
]
All Together
We talked about creating and gathering all the application required files. We can now easily create a function that provides all those paths.
var THIRD_PARTY_SCRIPT_INFO = require('./frontend/lib-info/scripts.json');
var THIRD_PARTY_CSS_INFO = require('./frontend/lib-info/css.json');
function getIndexArgs() {
var indexArgs = {
thirdPartyScripts: THIRD_PARTY_SCRIPT_INFO.map(_.property('path')),
thirdPartyCss: THIRD_PARTY_CSS_INFO.map(_.property('path')),
appCss: 'project-name.css',
appScripts: getAppScripts()
};
return indexArgs;
}
Compile Index Page For Development
The main idea is to automatically assembly the index page by injecting all the required files. In order to add the paths in the index page, we use a template language that injects the dependencies as strings. We choose mustache for templating.
// frontend/app/index.mustache
<html ng-app="wg.app" lang="en">
...
{{#thirdPartyCss}}
<link rel="stylesheet" href="{{#stripBase}}{{{.}}}{{/stripBase}}"/>
{{/thirdPartyCss}}
<link rel="stylesheet" href="{{{appCss}}}"/>
...
<body>
...
{{#thirdPartyScripts}}
<script src="{{#stripBase}}{{{.}}}{{/stripBase}}"></script>
{{/thirdPartyScripts}}
{{#appScripts}}
<script src="{{#stripBase}}{{{.}}}{{/stripBase}}"></script>
{{/appScripts}}
</html>
The next step is trivial. We have to include the index.mustache and run the mustache engine, providing all the required files.
gulp.task('compile-index', function () {
return gulp.src('frontend/app/index.mustache')
.pipe(mustache(getIndexArgs(), {extension: '.html'}))
.pipe(gulp.dest('frontend/app'));
});
Development Build
Our need for an useful and elegant build during the development process is to serve the index page from localhost providing automations:
- Refresh the page when a js or a html file is changed
- Compile the less files when a less file is changed
- Inject the css file on the index page (without reload) when the css files is changed
- Compile the index page and reload the application when the index.mustache page is changed
For our needs, we will use a lightweight server that has the ability to:
* Server the files
* Refresh the current page
* Inject string on the header of the current page
Browersync provides exactly what we need.
gulp.task('serve-dev', ['compile-dev'], function() {
browserSync({
port: 8000,
server: {
baseDir: 'frontend/app'
}
});
gulp.task('reload', function (cb) {
browserSync.reload();
cb();
});
gulp.watch('frontend/app/**/*.less', ['compile-less']);
gulp.watch(['frontend/app/**/*.js', '!frontend/app/project-name.js'], ['compile-js']);
gulp.watch(['frontend/app/project-name.js', 'frontend/app/**/*.html'], ['reload']);
gulp.watch('frontend/app/project-name.css', function () {
gulp.src('frontend/app/project-name.css')
.pipe(browserSync.reload({stream: true}));
});
gulp.watch('frontend/app/index.mustache', ['compile-index']);
});
Distribution Build
We already talked about gathering all the required files. The extra steps that a distribution build requires is to concatenate and minify all the javascript and css files. Additionally, we should create a /dist
folder that is isolated from the other folders and will contain all required files that the application needs to run. We can briefly list all the necessary steps that we need, all the files will be created on the /dist
folder:
- Collect all third party scripts, concatenate, minify (vendors.js)
- Collect all application scripts, ng-inject, concatenate, minify (project-name.js)
- Collect all third party css, concatenate, minify (vendors.css)
- Collect the application css, minify (project-name.css)
- Build index page, providing the above files (vendors.js, project-name.js, vendors.css, project-name.css)
- Copy the html files in the dist folder, keeping the same folder structure
- Copy all the assets (imgs, fonts, etc.)
All the above could potentially create 7 additionally tasks on the gulpfile. We almost never run the tasks of distribution build separately. We usually want to execute one gulp task and create the /dist
folder. Thus, we will use a stream concatenation library.
gulp.task('compile-dist', function (cb) {
var indexArgs = getIndexArgs();
return eventStream.concat(
gulp.src(indexArgs.thirdPartyScripts)
.pipe(uglify())
.pipe(concat('vendors.min.js'))
.pipe(gulp.dest('public/lib')),
gulp.src('frontend/app/project-name.js')
.pipe(uglify())
.pipe(concat('project-name.min.js'))
.pipe(gulp.dest('public')),
gulp.src(indexArgs.thirdPartyCss)
.pipe(csso())
.pipe(concat('vendors.min.css'))
.pipe(gulp.dest('public/lib')),
gulp.src('frontend/app/project-name.css')
.pipe(csso())
.pipe(concat('project-name.min.css'))
.pipe(gulp.dest('public')),
gulp.src('frontend/app/assets/**')
.pipe(gulp.dest('public/assets')),
gulp.src(['frontend/app/**/*.html', '!frontend/app/index.html'])
.pipe(gulp.dest('public')),
gulp.src('frontend/app/index.mustache')
.pipe(mustache(getDistIndexArgs(), {extension: '.html'}))
.pipe(gulp.dest('public'))
);
});
More actions on distribution build
We cover the basic steps that a distribution build requires. We can create a more powerful distribution build that is based on the above. The purpose of the post is to give the basic idea of building an Angular application. So, i will intuitively list some of those steps:
- Minify images
- Convert images to sprites
- Versioning on files
- Minify html, when it is possible
Take a look on the entire gulp file