About a month ago, I started to make experiments with the webpack plugin for
Aurelia in order to split my
applications into multiple bundles. The application in question is a strategy
game called Arena of Titans. You can see it there (click play to create a game, or use this
link).
It was working well but the initial load of any page was slow. It was easy to
find out why: the bundle loaded by webpack was over 1.3 megabytes big. Why is it
that big? Well, it contained the game board which is about 1.1 megabytes
minified. Hence my will to split it into several bundles in order to speed up
the loading of the site. We still have to load the big board on the game page
but at least the site would work at an acceptable speed.
I didn't manage to do it with webpack and after some time trying I thought what
about the new aurelia-cli? I was able to do
it just fine. Here's how I did it. You can skip to the part about bundles if
you're not interested in the rest.
aurelia-cli is a project aimed to provide a
command line interface to create and run projects written with the Aurelia
JavaScript framework. You can:
- Create a new project with au new <project-name>. You will then be guided
by an assistant to configure the project: standard ES6 or typescript, pure CSS
or SCSS/Less, …
- Run the project with au run or au run --watch to rebuild the app when
you save a modification.
- Test the project with au test or au test --watch to retest the app
when you save a modification.
It relies on gulp to launch its tasks and on
Browsersync to provide a small HTTP server
that will reload you page when you do a modification.
To avoid messing with the current code, I created a new aurelia-cli project in a
separate folder and I copied the proper files in my project:
- The folder aurelia_project which contains the gulp tasks required by
aurelia-cli to run and the configuration of the project.
- The scripts folder which contains requirejs the
module loader used by project running with aurelia-cli and its text plugin to
load the HTML and CSS.
- test/aurelia-karma.js a small file that allows you to use karma with
aurelia-cli projects. See the section about tests to learn more on that.
- karma.conf.js so my karma configuration file doesn't rely on webpack any
more and meets the expectations of aurelia-cli.
I then updated my package.json file. To do that, I just compared my current
package.json with the one from the aurelia-cli project: I added the proper
dependencies (like aurelia-cli) and removed the old ones (like webpack).
To avoid style warning with on the environement.js file, I added it to the
.eslintignore. This files contains utilities values to configure aurelia
(are we testing? are we in debug mode?):
export default {
debug: true,
testing: false
};
I then removed all of webpack configuration files and run rm -rf node_modules
&& npm install to update the dependencies.
Then, I adapted aurelia.json to my needs:
- The sources of my project are in a folder named app and not in src as
it is by default. So I changed all paths (a replace src by app did
it). I also had to correct aurelia-karma.js. See the section about
tests to learn more about that.
- I configured the router to use push states. Which means my URLs look like
this: /game/create and not like this #/game/create. The problem is
that, by default, requirejs will load its bundle under
/game/scripts/bundle.js instead of /scripts/bundle.js. So I did my
first patch in aurelia-cli to add an option to use absolute path in requirejs. This way if you use
"useAbsolutePath": true in the build.targets section of your
aurelia.json file, your bundles will always be loaded in
/scripts/bundle.js.
I also updated my global.scss file to load fonts with an absolute URL and
aurelia_project/tasks/run.js to change the port of Browsersync and disable
the ghost mode
which mimics on all browsers the action you do in one: I want to be able to test
the game in different browsers running different players. I don't want it to
reproduce what I do in every browsers I have opened.
I reverted Aurelia's configuration function to the classical export function
configure(aurelia) and I added the two lines below to enable development
logging only on debug mode and to enable the testing plugin when testing:
if (environment.debug) {
aurelia.use.developmentLogging();
}
if (environment.testing) {
aurelia.use.plugin('aurelia-testing');
}
I also updated my configuration of bluebird.js a promise library for javascript in
main.js from:
let Promise = require('bluebird').config({
longStackTraces: false,
warnings: false,
});
to:
Promise.config({
longStackTraces: false,
warnings: false,
});
With webpack, you include the SCSS files like javascript but with the extension,
like this: import ./style/global.scss. Webpack will then compile the file
and include it in the bundle thanks the loader you've configured. With
aurelia-cli, things are a little different. You include the file in your HTML
files like you require custom elements:
<require from="./style/global.css"></require>
Note that the extension of the file must be .css whether you're using true
CSS or another language compiled to CSS like SCSS. The reason is that it will be
loaded as CSS. You just have to configure the cssPreprosessor in your
aurelia.json for the code to be correctly processed:
"cssProcessor": {
"id": "sass",
"displayName": "Sass",
"fileExtension": ".scss",
"source": "app/**/*.scss"
}
The ability to split my app into multiple bundles is the main reason I switched
to aurelia-cli. To create a bundle, all you have to do is to add an object with
a name and a list of dependencies to the build.bundles array of your
aurelia.json file.
It works great but the first time I tried, my bundles were not filled
correctly. I tried to include files like this: "[site/**/*.js]" but it
didn't work. I did some search and I found this comment by
TylerJPresley on a issue about creating
multiple bundles with aurelia-cli
which gave me the solution: you have to put stars in front of the file
name. Like this: "[**/site/**/*.js]":
{
"name": "site-bundle.js",
"source": [
"[**/site/**/*.js]",
"**/site/**/*.{css,html}"
]
}
With that, my bundles were correctly created and filled but they were not
loaded. However, it turns out that I was missing some files in them: I did put
the site and game files but not app.js, environment.js and
main.js. As you might expect, without these files Aurelia could not work
correctly. So I create a common-bundle.js for them and other files needed in
the whole application:
{
"name": "common-bundle.js",
"source": [
"[**/locale/**/*.js]",
"[**/app.js]",
"[**/environment.js]",
"[**/main.js]",
"[**/services/options.js]",
"[**/services/storage.js]",
"[**/services/browser-sniffer.js]",
"[**/widgets/aot-options/*.js]",
"**/app.html",
"**/widgets/aot-options/*.html",
"**/widgets/aot-options/*.css",
"**/style/*.css"
]
}
In addition to bundling your app files, you can also include non Aurelia
dependencies from outside your project if they rely on AMD. For instance, here's how I define my
game-create-bundle.js with clipboardjs:
{
"name": "game-create-bundle.js",
"dependencies": [
{
"name": "clipboard",
"path": "../node_modules/clipboard/dist",
"main": "clipboard"
}
],
"source": [
"[**/game/create/**/*.js]",
"**/game/create/**/*.{css,html}"
]
}
You can also prepend javascript files to a bundle. That's how requirejs and
bluebird are loaded. Here's the relevant excerpt from the vendor-bundle.js
(defined here):
"prepend": [
"node_modules/bluebird/js/browser/bluebird.js",
"scripts/require.js"
]
At last but not least, some Aurelia modules contains multiple files that must be
resolved. To bundle those correctly, you need to use an object instead of the
name of the module in you dependencies array. Like this:
"dependencies": [
"aurelia-binding",
{
"name": "i18next",
"path": "../node_modules/i18next/dist/commonjs",
"main": "index"
}
]
You can view the full definition of my vendor-bundle.js where this example
is taken here
and the definition of all my bundles here.
All in all, I think that bundling with aurelia-cli is very powerful, works well
and, once you know for the double stars, is quite easy to setup with the help of
a new project to give you the base configuration.
In the end, I have 6 bundles for the application:
- vendor-bundle.js with requirejs, bluebird.js a promise library and all Aurelia
related files.
- common-bundle.js that contains the translations, app.js, main.js,
environement.js and various services and widgets common to the site and
the game.
- site-bundle.js that contains all pages and widgets for the sites.
- game-common-bundle.js that contains files required to create and play the
game.
- game-create-bundle.js that contains all the widgets to create the game.
- game-play-bundle.js that contains all you need to actually play the game.
aurelia-cli loads its bundles with requirejs which
I find great. Until during a test session a friend reported that he was not
redirected to the game page when he first tried to create a game. The second
time he tried everything worked fine.
I dug into the problem and found out why it was failing. The bundle containing
the game is still big (about 1.3 megabytes). On some slow connection, it took
more than 7 seconds to load it. Which means that we encountered the default
timeout for requirejs and the app would consider the bundle could not be loaded
and thus didn't redirect the player to the page even after the script was
loaded. requirejs nicely logged an error with this link in the console.
The solution is to use the waitSeconds option of requirejs to increase the
value of the timeout (you can also disable it as the documentation says). I did some
tests directly in the generated bundle and it work.
The problem is that at the time of this writing, aurelia-cli doesn't allow you
to give custom options to the loader. So, I made a pull request to allow the
user to do just that in order to
correctly solve my problem while still using the cli. I hope it will be merged
soon.
In order to further improve load time on these slow connection, I preload the
big game-play-bundle.js with the board as soon as a user reaches the create
game page. To do that, I added the line below in the constructor of the create
game page:
require(['game/play/widgets/board/board'], () => {});
You must use this syntax to load the script asynchronously. If you use:
require('game/play/widgets/board/board')
the script will be loaded synchronously which means the user can't prepare the
game until it is completely loaded. That is of course not my goal.
First, since the code is not in the src folder but in app, I had to
correct aurelia-karma.js (see Preparation for where it comes from) the
file that boostrap karma for usage with aurelia-cli. I had to correct this line
(52
at the time of this writing):
originalDefine('/base/src/' + name, [name], function (result) { return result; });
into:
originalDefine('/base/app/' + name, [name], function (result) { return result; });
I think it would be better for aurelia-karma to use paths.root for
aurelia.json (more general). I gave it a try but didn't succeed. But there
may be other ways to do this. See my issue on the subject for more details
paths.root from aurelia.json.
In my test files, I had to change some import paths, remove my import
../setup since it is automatically loaded by aurelia-karma. The
test/unit/setup.js has the same content as before
to import ployfills and initialize Aurelia:
import 'aurelia-polyfills';
import { initialize } from 'aurelia-pal-browser';
initialize();
I also had to move my test-utils.js which contains various stubs I use in my
unit tests from the test/unit folder into the app folder so it is
correctly transpiled by babel. I also created a dedicated bundle
named test-utils-bundle.js to avoid loading it in the true application.
This is it! Converting the project wasn't very hard but some problems (absolute
loading of bundles, some problems with testing and the inability to configure
requirejs) required some time to make it work correctly. Now I think that the
project is ready for the future of Aurelia tooling and I don't think I'll need
to switch tools again.
If you want all the gory details, take a look at this merge commit
and the commits before it in the aurelia-cli branch.
Otherwise, you also take a look at my aurelia.json file here,
browse the complete code of the app there and see it in action
on the website (click play to create a game,
or use this link).
If you have a remark or question about this article or the game, leave a comment
below or contact me on twitter.
Next step on my agenda, setup code coverage of the original sources of the app
(and not the bundle, that would be too easy). I hope I'll be able to make it
work and write about it soon. Stay tuned!