Folder Structure
Recently, before starting a Node.js (using Locomotive.js) and Durandal project, I searched the web for the optimal folder structure. I didn’t found nothing really useful so I decided to go on using the Durandal’s convention, separating views and viewmodels, in the public folder of the Node.js application. I ended up using the following structure:
- App: the main folder of the Durandal application
- views: HTML files of the application (V in MVC)
- viewmodels: the business code of the application (C in MVC)
- dataservices: functions that interact with the REST endpoints on the Node.js server, returning promises for the controllers
- models: client-side models (M in MVC), a sort of replication of the entities used on server side, populated using the data from the dataservices
- datacontext: middle-layer between dataservices and the business code. It’s used to cache the data, for faster access by different parts of the application. Think it as a primitive version of Breeze, for those of you who have already used it. The inspiration came from this blog post of John Papa. In my application this layer is mostly used for accessing the data that has to be shared by different parts of the application. I didn’t use Breeze because it was simply too much for my requirements.
The App folder contains also the main.js
file, the single entry point of the application and used by Require.js, but we will talk about this later.
On the same level of the App
folder, there is my assets folder that contains all the resources used by the Durandal application.
- assets: the assets of the Durandal application
- img: images
- lib: the libraries of the application. Durandal, Require.js, Knockout.js, require-css and many jQuery plugins
- styles: CSS files. I didn’t use preprocessors on this project
- fonts: Font-Awesome and Glyphicons
main.js
Now let’s talk about the main configuration file. As the documentation of Require.js states:
“It is suggested that you keep all inline script out of the HTML, and only reference require.js with a require call like so to load your script”:
<script src="/assets/lib/requirejs/require.js" data-main="App/main" ></script>
“Inside of main.js, you can use require() to load any other scripts you need to run. This ensures a single entry point, since the data-main script you specify is loaded asynchronously.”
My main.js
file looks like the following. Follow the comments for an explanation of each entry.
requirejs.config({
baseUrl: '.', // root of the application -> the App folder, all file and folders are relative to this path
map : { // http://requirejs.org/docs/api.html#config-map
'*': {
'style': 'assets/lib/require-css/css' // map configuration used by require-css plugin (https://github.com/guybedford/require-css)
}
},
paths: { // paths for module names not found under baseUrl (http://requirejs.org/docs/api.html#config-paths)
'async': 'assets/lib/requirejs-plugins/async', // Require.js plugin for asynchronous dependencies i.e. Google Maps (https://github.com/millermedeiros/requirejs-plugins)
'bootstrap': 'assets/lib/bootstrap/js/bootstrap',
'durandal':'assets/lib/durandal', // Durandal library
'jquery': 'assets/lib/jquery/jquery.min',
'jqueryui' : 'assets/lib/jquery-ui/jqueryui',
'knockout': 'assets/lib/knockout.js/knockout',
'mapping': 'assets/lib/knockout.js/knockout.mapping.min',
'moment': 'assets/lib/moment/moment.min', // essential for working with dates in javascript
'plugins' : 'assets/lib/durandal/plugins', // Durandal plugins
'text': 'assets/lib/requirejs-text/text', // Require.js text plugin for text resources, i.e. HTML templates (https://github.com/requirejs/text)
'transitions' : 'assets/lib/durandal/transitions' // Durandal transitions
...
},
shim: { // dependencies for libraries under paths (http://requirejs.org/docs/api.html#config-shim)
'bootstrap': {
deps: ['jquery'],
exports: 'jQuery'
},
'knockout': {
deps: ['jquery'],
exports: 'ko'
},
'select2': {
deps: ['jquery', 'style!assets/lib/select2/select2'], // in this case, the select2.css file is a dependency of the library
exports: 'select2'
}
...
}
});
// entry point
define(['durandal/system', 'durandal/app', 'durandal/viewLocator'], function (system, app, viewLocator) {
//>>excludeStart("build", true);
system.debug(true);
//>>excludeEnd("build");
app.title = 'FullStack Hotel Manager';
app.configurePlugins({
router:true,
dialog: true,
widget: {
kinds: ['grid'] // the wonderful durandal-grid widget (https://github.com/tyrsius/durandal-grid)
}
});
app.start().then(function() {
//Replace 'viewmodels' in the moduleId with 'views' to locate the view.
//Look for partial views in a 'views' folder in the root.
viewLocator.useConvention();
//Show the app by setting the root view model for our application with a transition.
app.setRoot('App/viewmodels/shell', 'entrance', 'wrap');
});
});
That’s it!
Optimization
This part has been the most challenging, due to the specific requirements of my project. First of all, some views of my application are generated at runtime so I couldn’t use the almond implementation of Require.js, which doesn’t allow dynamic loading. Furthermore I used the require-css plugin to inject dinamically CSS stylesheets used by the libraries and I wanted to inline as well that code into the main file.
I tried to use grunt-durandal and Gulp.js, but without success. I kept getting errors on the final file. The only solution that worked for me has been using the standard Require.js optimization tool r.js with its arguments in a configuration file, named buildconfig.js
, in the public folder.
The buildconfig.js
replicates most of the main.js
content. However I had to include all the files of my application in the include section, as well as the Durandal widgets, plugins and transitions. The file looks like the following, remember that the explanation of each entry is available at the following link.
({
baseUrl: '.',
mainConfigFile : 'App/main.js',
map: {
'*': {
'style': 'assets/lib/require-css/css'
}
},
name: 'App/main',
separateCSS: false,
findNestedDependencies: true,
include : [
"App/main",
"App/viewmodels/shell","text!App/views/shell.html",
"App/datacontext/authorization",
"App/dataservice/ ... ",
...
"App/models/ ... ",
...
"App/viewmodels/ ... ","text!App/views/ ... .html",
...
"widgets/grid/viewmodel","text!widgets/grid/view.html",
"plugins/dialog",
"plugins/widget",
"transitions/entrance"
],
insertRequire : ["App/main"],
optimize: 'uglify2',
out: '../build/public/App/main.js',
uglify2: {
compress: {
global_defs: {
DEBUG: false
}
}
}
})
The optimization tool can be launched with the command
public> node r.js -o buildconfig.js
That’s it!