This article provides a brief introduction to package managers and module loaders within the context of the Aurelia front end build-pipline.
Package managers
One of the benefits of JavaScript is the abundance of great open source and free-to-use libraries out there that accelerate our development process. Because of this, it’s common to pull in many third-party libraries as a part of the standard development process. To make this easier, several package managers have been created. Package managers provide an easy way to retrieve packages from a given package repository. You may have heard of Bower, which historically has been popular for installing front-end type dependencies such as jQuery or Bootstrap. You may also have heard of NPM as a way of installing server-side NodeJS packages. NPM originated from the NodeJS community and has its own package repository to allow community packages to be stored and shared at npm.org. Conversely, Bower has no dedicated package repository, and packages are instead stored on Github. Packages are listed in a registry for easy discoverability. More recently, there has been a shift to also manage client-side dependencies with NPM. One of the reasons that this has come about is a change in the NPM package format that makes it more suitable for use with client-side applications as of NPM v3. This is useful in terms of single-page applications because it allows us to use just one package manager to take care of both the development- and run-time dependencies of a project.
The popularity of NPM along with its ability to manage client-side as well as server-side dependencies has led to it being selected as the recommended package manager for Aurelia projects. Because of this, you’ll more likely find NPM-related assistance in official Aurelia documentation and sample code.
Module loaders
As JavaScript applications have become more complex, we’ve started to run into a lot of the same kinds of issues on the front end that back-end developers have been facing for years. One of these issues is the question of how to organize the now larger JavaScript applications so that they are maintainable and easy to reason about. Modules are a huge part of the answer to this question, as described in the following.
Key benefits
- Encapsulation: Modules allow developers to write small, well-defined chunks of JavaScript functionality that do not blend into the rest of the application code. Consumers and maintainers of the module should be able to more easily understand what a module does. Encapsulation: Modules allow developers to write small, well-defined chunks of JavaScript functionality that do not blend into the rest of the application code. Consumers and maintainers of the module should be able to more easily understand what a module does.
- Maintenance: Changing JavaScript dependencies can be done in one place in the project rather than scattered throughout the website. This means that you there should be fewer regression tests that need to be defined.
- Optimization: You can optimize how dependencies are consumed by the application by loading modules only when we need them, rather than loading them all up front as we may have done in the past using simple JavaScript script includes.
- Namespacing: Using modules gives us a pattern for building JavaScript applications that avoid polluting the Global namespace.
It’s possible to achieve these key benefits just using the module pattern without the use of a module loader. However, alone do not provide us with a mechanism for how to combine them together in various ways and load them into our browser. That’s where module loaders come in. Module loaders provide a way of declaring modules as dependencies of other modules. For example, in the first iteration of the my-books sample application, there are three modules: app, add-book, and list-books. The app module depends on the add-book, and list-books modules. Without a module loader, you’d need to be careful to include references the add-book, and list-books files first, so that these modules were loaded into the web page before the app module. In the case of a module loader, you simply declare the add-book, and list-books modules as dependencies of the app module, and the module loader takes care of loading these files into the browser at the appropriate time. Module loaders typically provide two advantages when it comes to how we load dependencies in the browser:
- Asynchronous module loading: In this case, we can load multiple modules in parallel. For example, we could load the add-book, and list-books modules asynchronously before loading the app module. This module loading is done in a non-blocking fashion using callbacks.
- Lazy module loading: Modules are loaded only when you need them and not before.
Modules provide a way to structure JavaScript applications to make them more maintainable and easier to reason about. But, as the number of modules in our application increases, we can end up with a proliferation of JavaScript files that each need to be loaded into the browser. This is an issue due to the current limitations around the number of parallel connections that can be opened by a browser, which now is limited to six in most modern browsers. What this means is that the browser will load batches of six files from the server, including images, CSS, JavaScript files, and so on. So, you may be able to imagine the effect of attempting to load hundreds of JavaScript modules on page load – slow page load times. But never fear, there is a solution to this problem: bundling.
Bundling
Imagine you’re building an application on the scale of Gmail or Facebook, where each piece of functionality of the application is well-factored and split into its own module. Can you imagine the number of separate JavaScript files you would end up with, each needing to be delivered to the browser when the application loads? Module loaders typically provide a method of combining a group of modules into one or more files called bundles. Generally, these are configured by declaring which JavaScript files should be included in a bundle with a given name. For example, all the app-related logic from our previous example could be combined into a single app bundle file. This is generally achieved by running a command on a bundle name, which concatenates all the dependent files into a single bundle file to be loaded into the browser. Various options are available for creating these bundles. The Aurelia CLI provides an easy way to bundle all our application modules. These modules are then loaded at runtime by requireJS.