Router provides URL-based same-page routing using HTML5 history (pushState
) or the location hash, depending on what the user's browser supports.
This makes it easy to wire up route handlers for different application states while providing full back/forward navigation support and bookmarkable, shareable URLs, all handled entirely on the client.
If you've used a server-side routing framework like Express or Sinatra, Router will look very familiar to you. This is no accident!
Getting Started
To include the source files for Router and its dependencies, first load the YUI seed file if you haven't already loaded it.
<script src="http://yui.yahooapis.com/3.18.1/build/yui/yui-min.js"></script>
Next, create a new YUI instance for your application and populate it with the
modules you need by specifying them as arguments to the YUI().use()
method.
YUI will automatically load any dependencies required by the modules you
specify.
<script> // Create a new YUI instance and populate it with the required modules. YUI().use('router', function (Y) { // Router is available and ready for use. Add implementation // code here. }); </script>
For more information on creating YUI instances and on the
use()
method, see the
documentation for the YUI Global Object.
Upgrading from YUI 3.4.x
A beta version of Router was first introduced in YUI 3.4.0, but was named Controller. If you're using Controller in YUI 3.4.0 or 3.4.1, you'll need to make the following changes to your code when upgrading:
-
The name of the class has changed from
Controller
toRouter
. Change all references to theController
class and thecontroller
module in your code to refer to theRouter
class androuter
module, respectively. To ease migration,Controller
is now an alias forRouter
and will still work, but this alias will be removed in a future release of YUI. -
The
html5
,root
, androute
properties are now attributes. If you were accessing them as properties, update your code to access them as attributes instead. For example,var root = myController.root;
becomesvar root = myRouter.get('root');
, andmyController.root = '/foo';
becomesmyRouter.set('root', '/foo');
. -
The function signature for route handlers has changed. Previously, the second argument passed to all route handlers was the
next()
function. As of YUI 3.5.0, the second argument is a response object, and the third argument is thenext()
function. To preserve backwards compatibility, the response object is also a function that will callnext()
, but you should still update your code to take the new argument order into account.Note: As of YUI 3.13.0, the response object is no longer callable, use the
next()
function passed as the thrid argument to route handlers.
URL-based Routing on the Client?
You bet! URLs are an excellent way to maintain state in a web app, since they're easy to read, easy to change, and can be bookmarked and shared.
Server-side web frameworks use URLs to maintain state by routing them to different pages and by storing information in query strings. These same techniques can now be used by client-side web apps to achieve better parity with server-side logic and to provide a more seamless user experience.
Router allows you to define routes that map to callback functions. Whenever the user navigates to a URL that matches a route you've defined, that route's callback function is executed and can update the UI, make Ajax requests, or perform any other necessary actions. See Routing for more details on how this works.
Often you'll want to change the URL in order to trigger a route handler, perhaps because the user has taken an action that should change the state of your application. Router provides a save()
method that sets a new URL and saves it to the user's browser history. There's also a replace()
method to replace the current URL in the user's browser history without creating a new entry. The Updating the URL section describes these methods in detail.
Sample URLs
In browsers that support the HTML5 history API, Router generates real URLs that can gracefully degrade to allow server-side handling of initial pageviews or pageviews without JS enabled. Most modern browsers (including recent versions of Firefox, Chrome, Safari, and Mobile Safari) support HTML5 history.
In browsers that don't support the HTML5 history API, Router falls back on using the location hash to store URL information and trigger history changes. This mostly applies only to older browsers and Internet Explorer. Unfortunately, even Internet Explorer 9 doesn't support the HTML5 history API.
The table below contains examples of the kinds of URLs Router might generate when the save()
method is called on a Router instance, starting from an initial URL of http://example.com/
.
Code | HTML5 URL | Legacy URL |
---|---|---|
router.save('/') |
http://example.com/ |
http://example.com/#/ |
router.save('/pie/yum') |
http://example.com/pie/yum |
http://example.com/#/pie/yum |
router.save('/pie?type=pecan&icecream=true') |
http://example.com/pie?type=pecan&icecream=true |
http://example.com/#/pie?type=pecan&icecream=true |
Using Router
Instantiating Router
To begin adding route handlers, the first thing you'll need to do is create a new Router instance.
var router = new Y.Router();
This is the simplest way of working with Router, but you may also extend Y.Router
to create a custom Router class that suits your needs. The Extending Y.Router
section explains how to do this.
Config Attributes
When instantiating Router, you may optionally pass in a config object containing values for any of the following attributes. For more details on these attributes, see Router's API docs.
Attribute | Default | Description |
---|---|---|
html5 |
auto |
Whether or not to use HTML5 history ( Feature detection is used to determine the correct default setting for the current browser, but you may override this to force all browsers to use or not use HTML5 history. Before changing this value, please read HTML5 URLs vs. Hash URLs and be sure you fully understand the consequences. It's almost always a better idea to leave it alone. |
params |
{} |
Map of params handlers in the form:
This attribute is intended to be used to set params at init time, or to completely reset all params after init. To add params after init without resetting all existing params, use the See Router Params for details. |
root |
'' |
Root path from which all routes should be evaluated. See Setting the Root Path for details. |
routes |
[] |
Array of route objects. This can be used to specify routes at instantiation time, or when extending Each item in the array must be an object with the following properties in order to be processed by the router:
If a route object contains a
Any additional data contained on these route objects will be retained. This is useful to store extra metadata about a route; e.g., a |
Here's an example that sets all the configurable attributes at instantiation time:
var router = new Y.Router({ html5: false, root : '/mysite', routes: [ {path: '/', callbacks: function () { alert('Hello!'); }}, {path: '/pie', callbacks: function () { alert('Mmm. Pie.'); }} ] });
Setting the Root Path
Let's say the URL for your website is http://example.com/mysite/
. Since Router matches routes based on the URL path, it would look for routes beginning with /mysite/
.
You could deal with this by ensuring that all your routes start with /mysite/
, but that's tedious, and it won't work well if you're writing a component that might be used on various sites where you can't anticipate the root path.
This is where the root
config attribute comes in. If you set root
to '/mysite'
, then all routes will be evaluated relative to that root path, as illustrated below.
Note: The root
must be an absolute path.
URL | Route (No root) | Route (Root: /mysite) |
---|---|---|
http://example.com/mysite/ |
/mysite/ |
/ |
http://example.com/mysite/pie/ |
/mysite/pie/ |
/pie/ |
http://example.com/mysite/ice-cream/yum.html |
/mysite/ice-cream/yum.html |
/ice-cream/yum.html |
The root
path also acts a mount point for the router and will only consider paths that fall under the root
(when set). The following example demonstrates these semantics:
router.set('root', '/mysite/'); router.route('/', function () { alert('Hello!'); }); router.route('/pie', function () { alert('Mmm. Pie.'); }); Y.log(router.hasRoute('/')); // => false Y.log(router.hasRoute('/pie')); // => false Y.log(router.hasRoute('/mysite/')); // => true Y.log(router.hasRoute('/mysite/pie')); // => true
Extending Y.Router
While Y.Router
may be instantiated and used directly, you might find it more convenient to extend the Y.Router
class to create a subclass customized for your particular needs.
Use the Y.Base.create()
method to extend Y.Router
and add or override prototype and static members and attributes. You may also optionally specify one or more Base extensions to mix into your new class.
// Create a Y.CustomRouter class that extends Y.Router. Y.CustomRouter = Y.Base.create('customRouter', Y.Router, [], { // Add or override prototype properties and methods here. }, { // Add static properties and methods here. ATTRS: { // Override default attributes here. } });
One benefit of extending Y.Router
is that you can easily add default routes and route handlers to your custom Router class, and they'll be shared by all instances of that class unless overridden at the instance level:
Y.CustomRouter = Y.Base.create('customRouter', Y.Router, [], { // Default route handlers inherited by all CustomRouter instances. index: function (req) { // ... handle the / route ... }, pie: function (req) { // ... handle the /pie route ... } }, { ATTRS: { // Evaluate all routes relative to this root path. root: { value: '/mysite' }, // Share these default routes with all CustomRouter instances. routes: { value: [ {path: '/', callbacks: 'index'}, {path: '/pie', callbacks: 'pie'} ] }, } }); // Create a CustomRouter instance that inherits the defaults and adds to // them. var router = new Y.CustomRouter(); router.route('/cheesecake', function (req) { // ... handle the /cheesecake route });
Now all instances of Y.CustomRouter
will inherit all the custom defaults and can add to or override them. The router
instance created here will handle the /
and /pie
routes in addition to its own /cheesecake
route, and will evaluate all routes from the /mysite
root path.
Routing
Use the route()
method to add a new route to a Router instance. The first parameter is a path specification or regular expression that the URL path must match, or alternatively a route object. The remaining parameters are callback functions, function names, or arrays of either to execute (middleware) when the route is matched.
var router = new Y.Router(); // Add a route using a string as the path specification. router.route('/pie', function () { Y.log('You visited http://example.com/pie'); }); // Add a route using a regular expression as the path specification. router.route(/^\/cake$/, function () { Y.log('You visited http://example.com/cake'); });
Routes are always evaluated in the order they're added. The first route that matches a given URL is executed, and any subsequent routes that also match the URL will not be executed unless the first route passes control to the next callback (middleware) or route. See Chaining Routes and Middleware for more info on executing more than one route callback for a given URL.
When a route callback is specified as a string instead of a function, it's assumed to represent the name of a function on the Router instance.
var router = new Y.Router(); router.pie = function () { Y.log('You visited http://example.com/pie'); }; // Add a route using router.pie as the route callback. router.route('/pie', 'pie');
As an alternative to using the route()
method, routes may be added at instantiation time using the routes
config attribute:
var router = new Y.Router({ routes: [ {path: '/pie', callbacks: function () { Y.log('You visited http://example.com/pie'); }}, {path: /^\/cake$/, callbacks: function () { Y.log('You visited http://example.com/cake'); }} ] });
This is functionally equivalent to adding the routes via the route()
method, except that it will replace any default routes, and the routes are added during the Router's initialization stage rather than after.
Paths, Placeholders, and RegExps
A route path may be specified as either a string or a regular expression. When a string is provided, it's compiled to a regular expression internally. If the regex matches the path portion (not including protocol, domain, query string, etc.) of a URL being dispatched, the route callback will be executed.
Path strings may contain placeholders. When a route is matched, the values of these placeholders will be made available to the route callback on the req.params
object.
A placeholder prefixed by a :
, like :pie
, will match any character except for a path separator (/
).
router.route('/pie/:type/:slices', function (req) { Y.log("You ordered " + req.params.slices + " slices of " + req.params.type + " pie."); }); router.save('/pie/apple/2'); // => "You ordered 2 slices of apple pie." router.save('/pie/lemon+meringue/42'); // => "You ordered 42 slices of lemon meringue pie."
A placeholder prefixed by a *
, like *path
, will match as many characters as it can until the next character after the placeholder, including path separators.
router.route('/files/*path', function (req) { Y.log("You requested the file " + req.params.path); }); router.save('/files/recipes/pie/pecan.html'); // => "You requested the file recipes/pie/pecan.html"
Use *
all by itself as a wildcard to match any path at all:
router.route('*', function () { Y.log("Wildcard route! I match everything! I'm craaaaaaazy!"); }); router.save('/purple/monkey/dishwasher'); // => "Wildcard route! I match everything! I'm craaaaaaazy!" router.save('/blue/jeans/pizza'); // => "Wildcard route! I match everything! I'm craaaaaaazy!"
Wildcards can also be used within a path:
router.route('/purple/*', function () { Y.log("I only like purple stuffz!"); }); router.save('/purple/monkey/dishwasher'); // => "I only like purple stuffz!" router.save('/purple/pants'); // => "I only like purple stuffz!"
Placeholder names may contain any character in the range [A-Za-z0-9_-]
(so :foo-bar
is a valid placeholder but :foo bar
is not).
When a regular expression is used as a path specification, req.params
will be an array. The first item in the array is the entire matched string, and subsequent items are captured subpattern matches (if any).
router.route(/^\/pie\/([^\/]*)\/([^\/]*)$/, function (req) { Y.log("You ordered " + req.params[1] + " slices of " + req.params[2] + " pie."); }); router.save('/pie/maple+custard/infinity'); // => "You ordered infinity slices of maple custard pie."
Route Objects
Alternatively, instead of specifying only a route's path, entire route objects can be specified when calling Router's route()
method. These route objects are the same as those specified as Config Attributes and what Router uses for its internal storage of its routes.
Using route objects to specify routes allows for greater interoperability between routing system and allows for a place to attach metadata for a route. Two examples are adding a name
property to routes to give them a logical name, or getting fully-processed route data from the server or another Router instance.
The following example shows how to add extra metadata to routes by using route objects:
var router = new Y.Router({ routes: [ {path : '/', name : 'home', callbacks: 'logRoute'} ] }); router.route({ path: '/users/*', name: 'users' }, 'logRoute'); router.logRoute = function (req, res, next) { Y.log('Route: ' + req.route.name); }; router.save('/'); // => "Route: home" router.save('/users/'); // => "Route: users"
Route Callbacks
When a route is matched, the callback functions associated with that route will be executed, and will receive three parameters:
req
(Object)-
An object that contains information about the request that triggered the route. It contains the following properties:
params
(Object or Array)-
Parameters matched by the route path specification.
If a string path was used and contained named parameters, then
params
will be a hash with parameter names as keys and the matched substrings as values. If a regex path was used,params
will be an array of matches starting at index0
for the full string matched, then1
for the first subpattern match, and so on. path
(String)-
The current URL path matched by the route.
pendingCallbacks
(Number)-
Number of remaining callbacks the route handler has after this one in the dispatch chain.
pendingRoutes
(Number)-
Number of matching routes after the current route in the dispatch chain.
query
(Object)-
Hash of query string parameters and values specified in the URL, if any.
route
(Object)-
Reference to the current route object whose callbacks are being dispatched.
router
(Object)-
Reference to this router instnace.
src
(String)-
What initiated the dispatch.
In an HTML5 browser, when the back/forward buttons are used, this property will have a value of "popstate". When the
dispath()
method is called, thesrc
will be"dispatch"
. url
(String)-
The full URL.
res
(Object)-
An object that contains methods and information that relate to responding to a request.
The response object contains the following properties (it will eventually contain more, and may be augmented by subclasses or used to pass information from route to route):
req
(Object)-
Reference to the request object.
next
(Function)-
A function to pass control to the next callback or the next matching route if no more callbacks (middleware) exist for the current route handler. If you don't call this function, then no further callbacks or route handlers will be executed, even if there are more that match. If you do call this function, then the next callback (if any) or matching route handler (if any) will be called. All of these functions will receive the same
req
andres
objects that were passed to this route (so you can use these objects to pass data along to subsequent callbacks and routes).err
(String)-
Optional error which will stop the dispatch chaining for this
req
, unless the value is"route"
, which is special cased to jump skip past any callbacks for the current route and pass control the next route handler.
Inside a route callback, the this
keyword will always refer to the Router instance that executed that route.
Router Params
Usually it's desirable to define routes using string paths because doing so results in easily readable routes and named request parameters. When route params require specific validation or formatting there's a tendency to rewrite a string-based route path as a regular expression. Instead of switching to more complex regex-based routes, route param handlers can be registered to validate more complex routes.
A common example of route param validation and formatting is resource id
s which should be a number and formatted as such (instead of a string). The following registers a param handler for id
and it will always make sure it's a number:
router.param('id', function (value) { return parseInt(value, 10); });
Now any routes registered that use the :id
parameter will have that value validated and formatted using the above function.
router.route('/posts/:id', function (req) { Y.log('Post: ' + req.params.id); }); router.save('/posts/10'); // => "Post: 10"
The above first validates and formats the original value of the :id
placeholder from the URL (in this case the string "10"
) by passing it to the registered id
param handler function. That function returns the number 10
which is then assigned to req.params.id
before the route handler is called.
Route param handlers can also be defined as regular expressions. Regex param handlers will have their exec()
called with the param value parsed from the URL, and the resulting matches object/array (or null
) will become the new param value. The following defines regex for username
which will match alphanumeric and underscore characters:
router.param('username', /^\w+$/);
If a param handler regex or function returns a value of false
, null
, undefined
, or NaN
, the current route will not match and be skipped. All other return values will be used in place of the original param value parsed from the URL.
The following defines two additional routes, one for "/users/:username"
, and a catch-all route which will be called when none of the other routes match:
router.route('/users/:username', function (req) { // `req.params.username` is an array because the result of calling `exec()` // on the regex is assigned as the param's value. Y.log('User: ' + req.params.username[0]); }); router.route('*', function () { Y.log('Catch-all no routes matched!'); }); // URLs which match routes: router.save('/users/ericf'); // => "User: ericf" // URLs which do not match routes because params fail validation: router.save('/posts/a'); // => "Catch-all no routes matched!" router.save('/users/ericf,rgrove'); // => "Catch-all no routes matched!"
The last two router.save()
calls above skip the main posts and users route handlers because the param values parsed from the URL fail validation of the id
and username
param handlers. When a param value fails validation, that route is skipped, and in this case the catch-all "*"
route handler is called.
A router's param handlers are also accessible through its params
attribute and can be set, in bulk, at init time or to reset all existing param handlers. The above param handlers could have been registered via the params
attribute:
var router = new Y.Router({ params: { id: function (value) { return parseInt(value, 10); }, username: /^\w+$/ } });
This is functionally equivalent to adding the param handlers via the param()
method, except that it will replace any default params, and the params are added during the Router's initialization stage rather than after.
Chaining Routes and Middleware
By default, only the first route that matches a URL will be executed, even if there are several routes that match. Similarly, if a route has multiple callbacks (i.e. middleware), only the first callback will be executed by default. However, when a route is executed, it will receive a next()
function as its third parameter. Calling this function will execute either the next callback (if any) for that route or the next matching route (if any).
router.route('/pie', function (req, res, next) { Y.log('Callback #1 executed!'); next(); }); router.route('/pie', function (req) { Y.log('Callback #2 executed!'); }); router.route('/pie', function (req) { Y.log('Callback #3 executed!'); }); router.save('/pie'); // => "Callback #1 executed!" // => "Callback #2 executed!"
If you want the first route callback to pass some data along to subsequent callbacks, you can attach that data to either the req
object or the res
object, which are shared between all callbacks and routes that are executed during a single request.
In the following example, both middleware and multiple routes are used. The handleFlavor
middleware function is executed first, and the flavor
data it adds to the req
is passed along to every route callback.
// Used as route middleware to put the `flavor` on the `req`. router.handleFlavor = function (req, res, next) { req.flavor = 'cookie dough'; next(); }; router.route('/ice-cream', 'handleFlavor', function (req, res, next) { Y.log("I sure do like " + req.flavor + " ice cream."); next(); }); router.route('/ice-cream', function (req) { Y.log("Everyone likes ice cream, especially " + req.flavor + "."); }); router.save('/ice-cream'); // => "I sure do like cookie dough ice cream." // => "Everyone likes ice cream, especially cookie dough."
Whether to store data on req
or res
is up to you, but by convention, data pertaining to the request should be stored on req
, and data pertaining to a response or to some other result of that request should be stored on res
.
Route-based middleware provides the ability to specify an arbitrary number of callbacks per route. This enables you to break down the logic of handling a request into distinct parts that can be reused with multiple routes. Middleware can be specified in a very flexible manner, and it can be grouped in arrays of arbitrary depth. Consider the following example:
var users = { ericf : {name: 'Eric Ferraiuolo', isAdmin: true}, rgrove: {name: 'Ryan Grove', isAdmin: false} }; router.findUser = function (req, res, next) { req.user = users[req.param.user]; next(); }; router.logUserName = function (req, res, next) { Y.log('Current user: ' + req.user.name); next(); }; router.logUserAdmin = function (req, res, next) { Y.log('Current user is admin: ' + req.user.isAdmin); next(); }; // Define collections of middleware. Notice how `logAdmin` middleware nests the // `logUser` middleware array. var logUser = ['findUser', 'logUserName'], logAdmin = [logUser, 'logUserAdmin']; router.route('/users/:user', logUser); router.route('/admins/:user', logAdmin); router.save('/users/rgrove'); // => "Current user: Ryan Grove" router.save('/admins/ericf'); // => "Current user: Eric Ferraiuolo" // => "Current user is admin: true"
For a less contrived usage, checkout the GitHub Contributors example app which uses middleware for its advanced routing.
Updating the URL
Call the save()
or replace()
methods to update the URL and store an entry in the browser's history.
The only difference between the two is that save()
saves a new history entry, while replace()
replaces the current history entry.
// Save a new history entry. router.save('/cake'); // Replace the current history entry with a new one. router.replace('/pie');
Changing the URL in this way will trigger a dispatch, and will execute the first route that matches the new URL (if any). A dispatch will also be triggered automatically whenever the user uses the browser's back or forward buttons.
When you need to provide state information to your route handlers, it's best to provide that information as query parameters in the URL. This way, your application's state is always reflected in the current URL, and can be properly restored when a URL is bookmarked or copied.
router.route('/ice-cream', function (req) { var flavor = req.query.flavor; if (flavor === 'vanilla') { Y.log('Vanilla? How boring.'); } else if (flavor === 'americone dream') { Y.log('Mmmm, Colbert-flavored ice cream.'); } else { Y.log('Error! Error! Does not compute!'); } }); router.save('/ice-cream?flavor=vanilla'); // => "Vanilla? How boring." router.save('/ice-cream?flavor=americone+dream'); // => "Mmm, Colbert-flavored ice cream." router.save('/ice-cream?flavor=kitten'); // => "Error! Error! Does not compute!"
Capturing Link Clicks
An excellent way to provide progressive enhancement on a page with URLs that can be handled by both the server and the client is to use normal links, and then add a delegated event handler to capture clicks on those links and use client-side routing when JavaScript is available. You can also use this technique to selectively use client-side routing or server-side routing depending on which link is clicked.
// This sample requires the `node-event-delegate` module. YUI().use('router', 'node-event-delegate', function (Y) { // ... create a router instance as described in the sections above ... // Attach a delegated click handler to listen for clicks on all links on the // page. Y.one('body').delegate('click', function (e) { // Allow the native behavior on middle/right-click, or when Ctrl or Command // are pressed. if (e.button !== 1 || e.ctrlKey || e.metaKey) { return; } // Remove the non-path portion of the URL, and any portion of the path that // isn't relative to this router's root path. var path = router.removeRoot(e.currentTarget.get('href')); // If the router has a route that matches this path, then use the // router to update the URL and handle the route. Otherwise, let the // browser handle the click normally. if (router.hasRoute(path)) { e.preventDefault(); router.save(path); } }, 'a'); });
Now a click on any link, such as <a href="http://example.com/foo">Click me!</a>
, will automatically be handled by the router if there's a matching route. If there's no matching route, or if the user doesn't have JavaScript enabled, the link click will be handled normally by the browser.
To Dispatch or Not to Dispatch?
Dispatching is what it's called when Router looks for routes that match the current URL and then executes the callback of the first matching route (if any).
A dispatch can be triggered in the following ways:
- Automatically, whenever the URL changes after the initial pageview.
- Manually, by calling the
dispatch()
method. - Manually (but only in HTML5 browsers), by calling the
upgrade()
method when the URL contains a hash-based route that was copied from a legacy browser.
It's important to remember that if a user bookmarks or copies an HTML5 URL generated by Router and visits that URL later, the full URL will be sent to the server. On the other hand, if a user bookmarks or copies a hash-based URL in a legacy browser and then visits it later, the hash portion of the URL won't be sent to the server.
For instance, let's say a user visits your website at http://example.com/
. On that page, you have the following code:
<button id="pie">Click me if you like pie!</button> <script> YUI().use('router', 'node', function (Y) { var router = new Y.Router(); router.route('/pie', function () { // Show the user a photo of a delicious pie. }); Y.one('#pie').on('click', function () { router.save('/pie'); }); }); </script>
In an HTML5 browser, when the user clicks the button, Router will change the URL to http://example.com/pie
and then execute the /pie
route, but the browser won't contact the server. If the user bookmarks this URL and then loads it again later, the web server will handle the request. If it doesn't recognize the path /pie
, it may return a 404 error, which would be no good.
In a legacy browser, clicking the button will cause the URL to change to http://example.com/#/pie
, and the /pie
route will be executed, also without contacting the server. The difference is that if the user bookmarks this URL and loads it later, the hash portion won't be sent to the server. The server will only see http://example.com/
, so it will be necessary to handle the request on the client as well.
There are two ways to deal with this. One way would be to implement server-side logic to handle requests for /pie
and render the appropriate page. To provide fallback support for hash-based URLs, we can modify the client-side code to call the dispatch()
method in legacy browsers, by adding the following:
// Always dispatch to an initial route on legacy browsers. Only dispatch to an // initial route on HTML5 browsers if it's necessary in order to upgrade a // legacy hash URL to an HTML5 URL. if (router.get('html5')) { router.upgrade(); } else { router.dispatch(); }
The benefit of this is that HTML5 URLs will be rendered on the server and won't present any difficulties for search crawlers or users with JS disabled. Meanwhile, hash-based URLs will be handled on the client as long as JS is enabled. The drawback with this method is that it may require maintaining duplicate logic on both the server and the client.
The other way to handle this would be to configure the server to render the same initial page for all URLs (it would essentially route all paths to the same page), and always dispatch on the client, regardless of the browser's capabilities:
// Dispatch to an initial route on all browsers. router.dispatch();
On the one hand, this solution avoids most of the server-side complexity and keeps all the router logic on the client. On the other, it requires the client to support JavaScript, so it won't play well with search crawlers that can't run JavaScript or users who disable JavaScript.
Best Practices
HTML5 URLs vs. Hash URLs
As discussed in To Dispatch or Not to Dispatch?, there are benefits and drawbacks to both HTML5 URLs—that is, URLs generated via the HTML5 history API—and hash-based URLs. This adds difficulty to any client-side URL-based routing solution, and unfortunately it means you may need to make some tough choices about the architecture of your application.
The primary benefit of HTML5 URLs is that they can potentially be handled on the server as well as on the client. This has potential benefits for application architecture (rendering an initial pageview on the server is often faster than rendering it on the client) and for preventing link rot (if you rewrite or replace your application at some point, it's relatively easy to set up server-side redirects to point the old URLs to a new location). That said, HTML5 URLs may require more server-side logic in order to work correctly.
Hash-based URLs, on the other hand, can't be handled on the server because browsers don't send the hash portion of a URL to a server. This means they must always be rendered on the client using JavaScript. Even worse, if you eventually rewrite or replace your web app, it's extremely unlikely that you'll be able to keep that old JavaScript around just for the sake of redirecting old URLs, which means any old hash-based URLs in the wild are likely to stop working.
In general, the benefits of HTML5 URLs tend to outweigh the drawbacks when compared to hash-based URLs, which is why Router automatically defaults to using HTML5 URLs in browsers that support it. The hash-based URLs generated by Router should be used only as a fallback for legacy browsers.
Cross-browser URL Compatibility
It's very important that any URL generated by Router be usable in any browser. If a user of a legacy browser copies a URL and shares it with a friend who uses an HTML5 browser, that URL should still work. And if someone copies an HTML5 URL and shares it with a user of a legacy browser, that needs to work too.
When a hash-based URL is loaded in an HTML5 browser and dispatch()
or upgrade()
are called, Router automatically upgrades the URL to an HTML5 URL. So a URL like http://example.com/#/pie
will become http://example.com/pie
, and the HTML5 browser will execute the appropriate route handler.
The difference between the dispatch()
and upgrade()
methods is that dispatch()
always dispatches to the first route that matches the current URL, whereas upgrade()
will only dispatch if the browser is an HTML5 browser and the URL is a legacy hash-based URL that must be handled on the client in order to upgrade it to an HTML5 URL.
When an HTML5 URL like http://example.com/pie
is loaded in a legacy browser, what happens depends on how your server is configured:
If the server is capable of handling the URL, then it should render the page in the appropriate state, and Router won't need to do anything.
If the server is not capable of handling the URL, then it should render the initial page state and you should call Router's
dispatch()
method. Router will parse the HTML5 URL and execute the appropriate route, even in a legacy browser.
For more on how dispatching works, see To Dispatch or Not to Dispatch?.
Progressive Enhancement and SEO
In general, HTML5 URLs that can be rendered by the server when necessary provide the best support for both progressive enhancement (by rendering initial pageviews more quickly, even in browsers with JavaScript disabled) and for search engine optimization (by allowing you to use real, non hash-based URLs that can be crawled by search bots, which may not support JavaScript).
Being able to render the same application states both on the server and the client may require you to write duplicate code. However, if you use JavaScript on the server, and in particular if you use Node.js, you may be able to share code.
Router's routing style and route specification format are intentionally very similar to those of the Express.js server-side framework. With a little care, you may be able to use the same route handler code on both the server and the client.
Supporting Google's Ajax Crawling Scheme
One of the problems with the hash-based URLs Router uses to support legacy browsers is that most search engines don't distinguish between a URL with a hash fragment and one without. This means that, to a search bot, the URLs http://example.com/
and http://example.com/#/pie
might look the same, even though they might represent completely different content in your application.
Google's Ajax Crawling Scheme specifies a way to make hash-based URLs crawlable by the GoogleBot if you're willing to implement some extra server-side logic.
To indicate to the GoogleBot that your hash URLs are crawlable, the hash must be prefixed by #!
instead of the default #
. You can make Router do this automatically by setting the value of the static Y.HistoryHash.hashPrefix
property before initializing any Router instances:
Y.HistoryHash.hashPrefix = '!';
Next, read Google's getting started guide for a description of how the Ajax crawling scheme works and the additional changes you'll need to make to your application. Most of the changes you'll need to make will have to happen in your server-side logic.
Don't skip the server-side changes! Without them, using the #!
prefix won't do you any good, and may even hurt the search ranking of your pages.
Known Limitations
HTML5 history is not supported by Internet Explorer 9 or lower. IE10 is the first version of Internet Explorer that supports HTML5 history. Earlier versions will fall back to hash-based history by default.
Android 2.x is forced to use hash-based history due to a bug in Android's HTML5 history implementation. This bug does not affect Android 3.0 and higher.
Hash-based URLs are case-insensitive in Internet Explorer 8 and 9. Most browsers follow the HTML5 spec and treat URLs—including hash-based URLs—as case-sensitive. IE8 and IE9 ignore case in hash-based URLs, so changing a hash-based URL from
/foo
to/Foo
won't trigger a dispatch.Internet Explorer 6 and 7 only retain the most recent hash-based URL from a previous pageview after navigating to another page and returning. However, history entries created within a single pageview will persist for the duration of that pageview, and bookmarked URLs will still work in all cases.
In Internet Explorer 6 and 7, the page titles displayed for history entries in the browser's history dropdown menu are not correct. Instead of showing the title of each page, it shows part of the URL of each page.
Internet Explorer (all versions) replaces the current history entry when the hash portion of the URL is manually edited in the URL bar instead of adding a new history entry as other browsers do. There's unfortunately nothing YUI can do to detect or work around this.