2013-08-04

Yeoman, Express and AngularJS

I'm a great fan of yeoman: it provides for free a lot of goodies that let you easily bootstrap and managing an application. Tests setup, minification, dependencies manager, script automation, etc: using the yeoman workflow you can save a lot of time, being more productive and following the best devel practices.

I already used yeoman for AngularJS applications: now I want to build a NodeJS/Express application powered by AngularJS with all the goodies provided by yeoman.

And... before searching for and trying an existing express-angularjs yeoman generators I'd like to do it by hand, just for fun and for diving into NodeJS/grunt & co.

AngularJS setup with yo

Start with creating and empty dir as suggested on this StackOverflow question http://stackoverflow.com/questions/14106248/yeoman-inside-expressjs:
$ mkdir express-angular
$ cd express-angular
Then launch the angular generator:
$ yo angular --minsafe
If you want you can init this folder with git.

Express initialization

After that rename the package.json because it will be replaced soon by the express init command:
$ mv package.json package_yo.json
And init this folder with express:
$ express -e -f .
The -e option stands for using the ejs template engine, the -f will force the overwrite of existing files.

Now you can launch a diff on the package.json files and merge the differences. After that you can get rid of the temporary package_yo.json.
This way we won't lost the devDependencies and other setups that let your grunt routines works fine.

Now launch the following commands
$ npm install
$ node app -> run on port 3000
Express server listening on port 3000
If you open a browser on http://localhost:3000 you will see a working Express view.

But... this is not what we want, where are the view provided by the AngularJS generator? Wait a moment.

Express integration with AngularJS

Now let's tell Express to use the AngularJS views created by yo.
Firstly you can get rid of public and views dirs needed by Express because we will use the app folder generated by yo.
$ rm -fR public
$ rm -fR views
You have also to edit the app.js:
...
 // all environments
 app.set('port', process.env.PORT || 3000);
-app.set('views', __dirname + '/views');
 app.set('view engine', 'ejs');
+app.engine('html', require('ejs').renderFile);
 app.use(express.favicon());
 app.use(express.logger('dev'));
 app.use(express.bodyParser());
...

 app.use(express.methodOverride());
 app.use(app.router);
-app.use(express.static(path.join(__dirname, 'public')));

 // development only
 if ('development' == app.get('env')) {
+app.set('views', __dirname + '/app');
 
+  app.use(express.static(path.join(__dirname, 'app')));
   app.use(express.errorHandler());
 }
+else {
+app.set('views', __dirname + '/dist'); 
+  app.use(express.static(path.join(__dirname, 'dist')));
+}
...
Let me explain, we are doing the following things:
  • get rid of the /views, we will use the app/ folder with the yeoman templates
  • get rid of the public dir, we will use the app dir for serving the resources or the dist if you are on a production environment
  • we are telling Express to map the EJS template engine to ".html" files. This way you can avoid to rename the app/index.html file. See http://expressjs.com/api.html#app.engine for more info
Now let's modify the app/index.html putting some EJS flavour.
...
   <head>
     <meta charset="utf-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
-    <title></title>
+    <title><%= title %></title>
     <meta name="description" content="">
...
The title var will use what it's provided by routes/index.js.

Now we have to change the routes/index.js also in order to avoid problems (we are using index.html instead of index.ejs):
 exports.index = function(req, res){
-  res.render('index', { title: 'Express' });
-};
+  res.render('index.html', { title: 'Express AngularJS app' });
+};
And now if you re-run the "node app" you will see your Express-based angularJS application, so it works!

As you will see the work is not finished.

Fix grunt build

Well, but what about grunt tasks provided by yeoman? Let's try:
$ grunt
    Warning: Running "htmlmin:dist" (htmlmin) task
    File dist/404.html created.
    Warning: app/index.html
    Parse Error: <%= title %></title>
... ouch! What happened? The htmlmin task failed trying to parse the Ejs statements.

So, before finishing we have to apply a few changes to our Gruntfile.js:
    concurrent: {
      server: [
        'coffee:dist'
      ],
      test: [
        'coffee'
      ],
      dist: [
        'coffee',
        'imagemin',
-        'svgmin',
+        'svgmin'
+        //'htmlmin'

      ]
    },
...
    // Put files not handled in other tasks here
    copy: {
      dist: {
        files: [{
          expand: true,
          dot: true,
          cwd: '<%= yeoman.app %>',
          dest: '<%= yeoman.dist %>',
          src: [
            '*.{ico,png,txt}',
+            '*.html', 'views/*.html',
            '.htaccess',
            'bower_components/**/*',
            'images/{,*/}*.{gif,webp}',
            'styles/fonts/*'
          ]
In the changes above we just disabled the htmlmin task and add the *.html files stuff to the copy section.

Finished? Not yet!

Grunt server and livereload

Now the grunt command will work fine, even the build task. If you run "node app" all it's fine but running "grunt server" you may notice that the page title is not "Express AngularJS app" as expected but "<%= title %>".

What's the matter? The Gruntfile.js is not aware about Express so it needs some extra love and the grunt-express task can help you! See the grunt-express readme for more info https://github.com/blai/grunt-express.

Basically you'll have to do these things:
  • install grunt-express with a "npm install grunt-express --save-dev"
  • add a module.exports to your app.js. This is required by grunt-express if you want to avoid a "Fatal error: Server should provide a function called "listen" that acts as http.Server.listen"
  • modify your Gruntfile.js replacing the connect/livereload stuff with the grunt-express task
This way the "grunt server" command will run Express with live reload enabled.
Here you can see the most significant part of Gruntfile.js, basically you'll have to comment the connect/livereload stuff and adding the following lines:
    express: {
      options: {
        port: 9000,
        // Change this to '0.0.0.0' to access the server from outside.
        hostname: 'localhost'
      },
      livereload: {
        options: {
          server: path.resolve('./app.js'),
          livereload: true,
          serverreload: false,
          bases: [path.resolve('./.tmp'), path.resolve(__dirname, yeomanConfig.app)]
        }
      },
      test: {
        options: {
          server: path.resolve('./app.js'),
          bases: [path.resolve('./.tmp'), path.resolve(__dirname, 'test')]
        }
      },
      dist: {
        options: {
          server: path.resolve('./app.js'),
          bases: path.resolve(__dirname, yeomanConfig.dist)
        }
      }
    },
For the full list of changes you can see this package as reference: https://github.com/davidemoro/express-angular. Note: I had to set the serverreload option to false because it didn't worked for me.

Done!

Here you can see the result:
Notes: I'm quite new to these arguments because I come from the Python, Zope and Plone world, so let me know if you have some hints!

Links

15 comments:

  1. thx for the guide is great maybe you should color this line green as well "app.set('views', __dirname + '/app');"

    ReplyDelete
  2. (20130824) I've fixed an error in the code related to app.set('view'. Now it's ok.

    ReplyDelete
  3. Do you want to know how to deploy this hello world app on the AppFog PaaS (or a generic NodeJS/Express app)? See this article: http://davidemoro.blogspot.com/2013/08/deploy-your-nodejsexpress-app-to-appfog.html

    ReplyDelete
  4. Dude, what about you access directly to express views? /views/index.html, it's bad, no restrict that file, what is your solution?

    ReplyDelete
    Replies
    1. Hi, surely there are better configurations and probably at this time there are already existing express-based frameworks or yeoman recipes that reach this goal (see http://sailsjs.org with its out of the box automatic asset minification).

      Anyway it was an experiment of mine and this is just how I did it. So if you want feel free to fork this repo and share with others your solution.

      Thank you

      Delete
  5. This comment has been removed by the author.

    ReplyDelete
  6. thx for the guide, really well done, but I stump in some problem with grunt
    node app: works;
    grunt: executes all without errors, but chome starts and closes immediately;
    grunt server: gives me this:
    Running "express:prod" (express) task
    Starting background Express server
    Express server listening on port 9000

    Done, without errors.

    Elapsed time
    express:prod 183ms
    Total 184ms
    Stopping Express server
    ===========================
    can you advice me what is wrong?
    Thanx

    ReplyDelete
    Replies
    1. Hi Giuseppe,

      you could have a look at your grunt file.
      Are you able to find a more detailed error trace?

      Delete
    2. Another question, what happens if you type "grunt server"?

      Delete
    3. Hi, when I type grunt server i get:
      Running "server" task
      Warning: Task "clean:server" not found. Use --force to continue.

      Aborted due to warnings.

      Elapsed time
      server 9ms
      Total 9ms
      if I type grunt server --force
      I get the same but
      Done, but with warnings.
      and the server does not start.
      what should I look on my grunt file?

      Delete
    4. Hi, I'm sorry but I'm not able to reproduce your problem. Let me know if you have more info about this issue!

      Delete
    5. For those like Giuseppe or like me who where stuck at the end with grunt, you should fork this repo and have a look to the Gruntfile.js. It helped me a lot : https://github.com/blai/grunt-express-angular-example

      Delete
  7. Thanks for this article. Bit of a lifesaver

    ReplyDelete