generator-angular icon indicating copy to clipboard operation
generator-angular copied to clipboard

Feature: generator prompt for html5mode support

Open jjt opened this issue 11 years ago • 68 comments

Anyone who wants to use html5mode with generator-angular has three problems to solve after yo angular is used to create a project:

  1. Refreshing any page other than / results in 404 (index.html is only served from /)
  2. Relative script tags cause 404s upon a refresh of pages other than /
  3. Build blocks in index.html use relative scripts/ path, and the built index.html has relative script tags, as above

I think that having a prompt for html5mode and having the generator configure the project to support this out of the gate would be beneficial. I'm using Angular 1.2.0-8336b3a and generator-angular 0.4.0.

Solution to 1: Rewrite rule to index.html

This is solved by configuring a server with rewrite rules. This can be nginx/apache in front of grunt server, but that would mean setting up and/or configuring a server outside of ones project.

I prefer adding connect middleware as in #132, or this gist:

NOTE: This code is for v0.4.0. For v0.5.0+, try ~~this instead~~ this, as of 0.6.0-rc1

livereload: {
    options: {
      port: 9000,
      // Change this to '0.0.0.0' to access the server from outside.
      hostname: '0.0.0.0',
      middleware: function (connect) {
        return [
          modRewrite([
            '!(\\..+)$ / [L]'
          ]),
          lrSnippet,
          mountFolder(connect, '.tmp'),
          mountFolder(connect, yeomanConfig.app)
        ];
      }
    },
}

From the discussion surrounding #132, it sounds like there were some cases to consider and some errors that weren't resolved. That was 3 months ago though, so things might have changed.

This is working for me, deep link refreshing and all.

Solutions to 2 and 3

These are related. Three solutions that work:

  1. Reference script and css tags with an absolute url for both the script tags in the markup: <script src="/scripts/controller/myController.js"></script>, and the build comments: <!-- build:js /scripts/... -->. I've been doing this manually every time I generate a new component, and it works.
  2. Add <base href="/"> to the head, making all relative urls into absolute urls. This works, but there are some issues with the tag. Plus, it changes the behaviour of all relative urls and anchor links, which seems overbearing.
  3. Change the build scripts to reference /scripts/ as in 1. which takes care of production builds. For development and testing, add another rewrite rule like ^(.+)scripts/(.*) scripts/$1. This would also work, but I think solution 1. is the cleanest.

Thoughts?

jjt avatar Nov 06 '13 00:11 jjt

after thinking about it more, using absolute (relative to domain root) paths does seem the best solution for problems 2 and 3 which would mean https://github.com/yeoman/generator-angular/issues/432.

Problem 1 is an ongoing issue that probably does need to be addressed at some point.

eddiemonge avatar Nov 06 '13 17:11 eddiemonge

Hmm... I can see where using absolute paths relative to the domain root wouldn't be the best default, such as someone serving an ng app from http://site.com/path/ng-app/. But I still think that an absolute url is the way to go here. Maybe what's needed is an app base url config option that people like me can set to / and the people of Site.com can set to /path/ng-app/. I don't count the <base> tag as a good solution, as it causes problems with anchor links. That would break any page that has a ToC, like Wiki articles.

Either we use absolute urls, or we force people to do more production server configuration (rewrite anything with ^(.+)scripts/(.*) to scripts_path/$1, etc). Can you see a way around that?

jjt avatar Nov 06 '13 21:11 jjt

:+1:

It's really annoying that I have to modify my Gruntfile.js so much to have HTML5 mode support. It also makes keeping it up to date with the latest changes in Gruntfile.js created by this generator really complicated.

BTW. I use this workaround that I found somewhere on Stack Overflow for the issue with <base href="/"> tag and anchor links:

angular.module('directives')
.directive 'scrollTo', ($location, $anchorScroll) ->
  restrict: 'A'
  link: (scope, element, attrs) ->
    element.on 'click', (event) ->
      event.stopPropagation()

      location = attrs.scrollTo
      previousHash = $location.hash()
      $location.hash(location)
      $anchorScroll()

      # Reset to the previous hash to keep any additional routing logic from kicking in
      $location.hash(previousHash)

In HTML use the directive instead:

<a scroll-to='anchor'>Anchor link</a>

szimek avatar Nov 08 '13 11:11 szimek

My solution for this is as follows:

  1. in app.js (routing) add the following: $locationProvider.html5Mode(true);
  2. in index.html add the following:

<base href="/"> in <head> tag 3. in server.js add the following general catch-all routing (to prevent that refresh problem)

//general routing
app.use(function (req, res) {
    res.sendfile(__dirname + '/app/index.html');
});

That way all my routing is handled in AngularJs RouteProvider... and Express only gets Ajax request (or if your have specific routing that you catch)

pgilad avatar Nov 12 '13 07:11 pgilad

@jjt Your snippet for Solution 1 is out-of-date, I believe. lrSnippet, and mountFolder variables can't be found. Using generator-angular-0.6.0-rc1

saamalik avatar Nov 12 '13 09:11 saamalik

@saamalik Something like this should work with 0.6.0-rc1:

module.exports = function (grunt) {
  require('load-grunt-tasks')(grunt);
  require('time-grunt')(grunt);

  var modRewrite = require('connect-modrewrite')([
    '!\\.ttf|\\.woff|\\.ttf|\\.eot|\\.html|\\.js|\\.coffee|\\.css|\\.png|\\.jpg|\\.gif|\\.svg$ /index.html [L]'
  ]);
  var yeoman = {
    app: require('./bower.json').appPath || 'app',
    dist: 'dist'
  };

  grunt.initConfig({
    yeoman: yeoman,
    connect: {
      options: {
        port: 9000,
        // Change this to '0.0.0.0' to access the server from outside.
        hostname: 'localhost',
        livereload: 35729
      },
      livereload: {
        options: {
          open: true,
          middleware: function (connect, options) {
            return [
              modRewrite,
              require('connect-livereload')(),
              connect.static('.tmp'),
              connect.static(yeoman.app)
            ];
          }
        }
      }
   }
}

szimek avatar Nov 12 '13 09:11 szimek

@szimek Thanks! I removed the require('connect-livereload')() line and everything worked.

saamalik avatar Nov 12 '13 19:11 saamalik

Yeah, my snippet targeted v0.4.0. Thanks for the updated code!

jjt avatar Nov 12 '13 19:11 jjt

I've found that this solution has the least impact on the Gruntfile. It changes nothing and adds code in one place and one place only, connect.livereload.options:

grunt.initConfig({
  // ...
  connect: {
    // ...
    livereload: {
      options: {
        open: true,
        base: [
          '.tmp',
          '<%= yeoman.app %>'
        ],
        // Add this middleware function
        middleware: function (connect, options) {
          return [
            require('connect-modrewrite')(['!(\\..+)$ / [L]']),
            // One can also use something like this:
            // '!\\.ttf|\\.woff|\\.ttf|\\.eot|\\.html|\\.js|\\.coffee|\\.css|\\.png|\\.jpg|\\.gif|\\.svg$ /index.html [L]'
            connect.static('.tmp'),
            connect.static(grunt.config.data.yeoman.app)
          ];
        }
      }
    }
  }
  // ...
}); // End grunt.initConfig()

jjt avatar Nov 15 '13 02:11 jjt

@jjt You don't really need base section if you provide your own middleware stack. Of course if you want to make minimum amount of changes you can just leave it there, but it's quite confusing to specify paths in 2 places. You probably could do something like this (untested):

livereload: {
  options: {
    base: [
      '.tmp',
      '<%= yeoman.app %>'
    ],
    middleware: function (connect, options) {
      return [
        [require('connect-modrewrite')(['!(\\..+)$ / [L]'])],
        options.base.map(function (path) { return connect.static(path); })
      ].reduce(function (a, b) { return a.concat(b); }); // flatten array
    }
  }
}

This should allow to set paths using base options. I created an issue for grunt-contrib-connect (https://github.com/gruntjs/grunt-contrib-connect/issues/56) to be able to add middlewares to an existing stack (this way we wouldn't have to define connect.static at all), but it wasn't accepted. If the code above works, it's probably even shorter than adding your middleware to an existing stack.

Don't you need this custom middleware stack with modrewrite also for test and dist servers? If it's always the same maybe one could just put it in a function at the top:

var html5stack =  function (connect, options) {
  return [
    [require('connect-modrewrite')(['!(\\..+)$ / [L]'])],
    options.base.map(function (path) { return connect.static(path); })
  ].reduce(function (a, b) { return a.concat(b); }); // flatten array
};

and then just:

livereload: {
  options: {
    base: [...],
    middleware: html5stack
  }
}

BTW. Thanks for the tip with shorter rewrite rule.

szimek avatar Nov 15 '13 07:11 szimek

My only change to the generated Gruntfile is to add that 9-line middleware function to connect.options.livereload. I use the same variable as in base, which is yeoman.app so I don't think it violates DRY.

But, the other server targets need the middleware as well. I'd just been testing it on the dev server up until this point. I like the direction you're going though at the bottom of your post, I'll try something along those lines.

Np! I yanked it from this gist.

jjt avatar Nov 15 '13 08:11 jjt

My only concern with calling connect.static for every path manually is that if generator adds a new path to base option, you'll have to update middleware function as well. Using options.base inside middleware function should handle it automatically.

szimek avatar Nov 15 '13 08:11 szimek

Yeah, as long as when the middleware function gets called the value of options.base is set to the correct target then everything should be fine.

jjt avatar Nov 15 '13 08:11 jjt

Okay, this is what I ended up with in the connect.options task config. Seems to work.

connect: {
  options: {
    // ...
    // Modrewrite rule, connect.static(path) for each path in target's base
    middleware: function (connect, options) {
      var optBase = (typeof options.base === 'string') ? [options.base] : options.base;
      return [require('connect-modrewrite')(['!(\\..+)$ / [L]'])].concat(
        optBase.map(function(path){ return connect.static(path); }));
    }
  }
}

jjt avatar Nov 15 '13 08:11 jjt

It will be hard to make it any shorter than this :)

szimek avatar Nov 15 '13 09:11 szimek

I don't think I want it any shorter! As is, this almost feels too "clever".

jjt avatar Nov 15 '13 09:11 jjt

started thinking about how to implement this. added a note in https://github.com/eddiemonge/generator-angular-api

eddiemonge avatar Nov 19 '13 18:11 eddiemonge

Yup, gotta have enough interest in order to justify adding it.

I've been thinking about options for configurable asset path prefixes in both development and production, and I've come to think that a configurable asset prefix would be beneficial for production, in the case of an Angular app and its assets being stored somewhere other than the root (ex. http://mysite.com/our/ng/app/). The dev server doesn't have that problem, as it's served out of the root of a domain by default (http://127.0.0.1:9000/) and so / should work as a prefix for most people.

So with that in mind, here's how I picture it working:

$ Use html5Mode (/foo/bar) instead of hashMode (/#/foo/bar)? (y/N)
   # if y...
$ Asset base path for production (default: /)

We'll add a config var, assetSrcPrefix (str, default "") and have two config prompts assetBasePathProd (str, default:"/"), and html5mode (bool, default:false)

  1. If html5Mode, assetSrcPrefix = "/"
  2. Prepend assetSrcPrefix to all asset paths (js, css) in various files (wherever src="script/..." or href="styles/..." would be output)
  3. Prepend assetBasePathProd to all build targets (ex. build:js <%= assetBasePathProd %>scripts...)
  4. If html5Mode, add connect-rewrite in package.json
  5. If html5Mode, add rewrite rule to Gruntfile

I've done some testing on a freshly generated Angular project with the above changes (to the created index.html/Gruntfile, not the gen itself) and I've got a dev server that serves html5Mode properly, and a build that can reference any root-relative path.

One feature that I'd like to see is an absolute asset url base in the build targets. This naive attempt <!-- build:js http://my.static.server/scripts/plugins.js --> results in the correct script tag being output, but the file ends up at ng-gen-project/dist/http:/my.static.server/scripts/plugins.js.

jjt avatar Nov 19 '13 21:11 jjt

just dropping a +1 here for "have enough interest in order to justify adding it" !

AlJohri avatar Nov 21 '13 02:11 AlJohri

Thanks guys!

chatman-media avatar Nov 22 '13 06:11 chatman-media

+1, this would be fantastic to have

alexpeattie avatar Nov 22 '13 17:11 alexpeattie

+1

shea256 avatar Nov 25 '13 19:11 shea256

+1 - would be fantastic to have it included

karin-n avatar Nov 25 '13 21:11 karin-n

+1 from me as well.

On Mon, Nov 25, 2013 at 4:40 PM, proactivity-nz [email protected]:

+1 - would be fantastic to have it included

— Reply to this email directly or view it on GitHubhttps://github.com/yeoman/generator-angular/issues/433#issuecomment-29245026 .

logstown avatar Nov 25 '13 21:11 logstown

Looks wonderful. Yes please

dansowter avatar Nov 28 '13 06:11 dansowter

Well, looks like a fair bit of interest in this.

Ping @sindresorhus @passy @eddiemonge. I'd be willing to work on a PR for this as described in my comment. If you guys think it's a good approach, let me know and I'll get to it.

jjt avatar Nov 28 '13 10:11 jjt

+1 (v0.6.0)

jeongwooklee avatar Dec 04 '13 08:12 jeongwooklee

+1

zakdances avatar Dec 28 '13 04:12 zakdances

Excellent, not working again.

chatman-media avatar Dec 29 '13 03:12 chatman-media

:+1: really want to see this done.

benjamingr avatar Feb 01 '14 19:02 benjamingr