简体   繁体   中英

pushState preventing Backbone.js routes from working

I'm having an issue with HTML5 pushState in a RequireJS/Backbone.js app, I'm pretty sure I'm doing something wrong but I can't identify the problem, I've tried for hours.

Foreword: All of the RequireJS dependencies are in their correct folders.

Here's my issue: I have a basic little setup - the only Backbone component I'm using is the router. On the default '' route, I call a 'home' method in the router. This method does nothing more than alert "Test", and it works.

However, once I add {pushState: true} as a parameter to Backbone.history.start() in the top-level app.js file, the 'home' method is no longer being called.

These are the blocks of code in which it is happening:

index.html:

<!doctype html>
<html>
  <head>
    <title>Todo</title>
    <script data-main="assets/js/app/app.js" src="assets/js/app/lib/require.js"></script>
  </head>
  <body>
    <div id="main"></div>
  </body>
</html>

app.js:

require.config({
  baseUrl: 'assets/js/app',
  paths: {
    'underscore': 'lib/underscore',
    'jquery': 'lib/jquery',
    'backbone': 'lib/backbone',
    'text': 'lib/text',
    'handlebars': 'lib/handlebars',
    'router': 'router/router'
  },
  shim: {
    'underscore': {
      exports: '_'
    },
    'backbone': {
      deps: ['underscore', 'jquery'],
      exports: 'Backbone'
    }
  }
});
require(['router', 'backbone'], function(Router, Backbone) {
  var router = new Router();
  Backbone.history.start({ pushState: true });
});

router.js:

define(['backbone'], function(Backbone) {
  var Router = Backbone.Router.extend({
    routes: {
      '': 'home'
    },
    home: function() {
      alert('test');
    }
  });
  return Router;
});

What am I doing wrong? Is this an incorrect, convoluted approach?

I have come up with a solution to my own problem, funny that.

The issue with implementing pushState is that for any route to work, even the default home route, a back-end server is needed to initially render the page so that Backbone can then fire the correct JavaScript after checking the route.

This means that developing a local instance and navigating to it using the file:// protocol will not work. (Which is what I made the mistake of doing in the above question).

For this simple use case, I coded up a simple ExpressJS server that serves an index.jade view (I removed the index.html) when it encounters any wildcard route, and that then allows Backbone to correctly render the routes with this tiny snippet of code:

app.get('*', function(req, res) {
  res.render('index');
}

However, it is imperative that further changes are made should you wish to support search engine crawling, and those changes involve having route-specific server-side versions of your views that your server can render in case a route is directly accessed. If you do not wish to support SEO crawlability, for example in the case of a web application that requires users to be logged in, then re-routing all routes through a single rendered file is fine. Backbone is smart enough to detect the remaining route paths to render the appropriate view. This is stated in the Backbone documentation

Note that using real URLs requires your web server to be able to correctly render those pages, so back-end changes are required as well. For example, if you have a route of /documents/100, your web server must be able to serve that page, if the browser visits that URL directly. For full search-engine crawlability, it's best to have the server generate the complete HTML for the page ... but if it's a web application, just rendering the same content you would have for the root URL, and filling in the rest with Backbone Views and JavaScript works fine.

NOTE: Using pushState can have an implication on how anchor tags ( <a href='/route'> ) work, as, by default, they will still try to "refresh" the page to fetch the matching route. Backbone's Router offers a navigate method that, when combined with a click event handler, allows you to bypass this default behaviour. shioyama has posted an example of such an event handler as an answer to this question.

The full code changes:

/app.js

var
express = require('express'),
app = express();

app.configure(function() {
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.use(express.static(__dirname + '/assets'));
  app.use(app.router);
  app.locals.pretty = true;
});

app.get('*', function(req, res) {
  res.render('index');
});

app.listen(3030, function() {
  console.log("Listening on 3030");
});

/assets/js/app/app.js

require.config({
  baseUrl: '/js/app',
  paths: {
    'underscore': 'lib/underscore',
    'jquery': 'lib/jquery',
    'backbone': 'lib/backbone',
    'text': 'lib/text',
    'handlebars': 'lib/handlebars',
    'router': 'router/router'
  },
  shim: {
    'underscore': {
      exports: '_'
    },
    'backbone': {
      deps: ['underscore', 'jquery'],
      exports: 'Backbone'
    }
  }
});
require(['router', 'backbone'], function(Router, Backbone) {
  var router = new Router();
  Backbone.history.start({pushState: true});
});

/assets/js/app/router/router.js

define(['backbone', 'jquery'], function(Backbone, $) {
  var Router = Backbone.Router.extend({
    routes: {
      '': 'home',
      'fred': 'fred'
    },
    home: function() {
      $('#main').append('<p>This is the <strong>HOME</strong> route.');
    },
    fred: function() {
      $('#main').append('<p>This is the <strong>FRED</strong> route.');
    }
  });
  return Router;
});

/views/index.jade

!!!
html
  head
    title Todo
    script(data-main='/js/app/app.js', src='/js/app/lib/require.js')
  body
    #main

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM