UNPKG

@behance/router

Version:

A lightweight JavaScript library is built on top of route-recognizer and rsvp.js to provide an API for handling routes

184 lines (126 loc) 7.09 kB
# [router.js](https://github.com/tildeio/router.js) Architecture This is a guide to `router.js`'s internals. `router.js` is a stand-alone microlibrary for client-side routing in JavaScript applications. It's notably used by the [Ember.js Router][Ember Router]. ## Scope of `router.js` and its Dependencies Ember.js's router consumes `router.js`, which in turn consumes [route-recognizer](https://github.com/tildeio/route-recognizer). The division of responsibilities of these three libs is as follows: ### `route-recognizer` `route-recognizer` is an engine for both parsing/generating URLs into/from parameters. It can take a URL like `articles/123/comments` and parse out the parameter `{ article_id: "123" }`. It can take `{ article_id: "123" }` and a route descriptor like `articles/:article_id/comments` and generate `articles/123/comments`. ### `router.js` `router.js` adds the concept of transitions to `route-recognizer`'s URL parsing engine. Transitions can be URL-initiated (via browser navigation) or can be directly initiated via route name (e.g. `transitionTo('articles', articleObject)`). `router.js` resolves all of the model objects that needed to be loaded in order to enter a route. e.g. to navigate to `articles/123/comments/2`, a promise for both the `article` and `comments` routes need to be fulfilled. ### Ember Router The [Ember Router][] adds a DSL for declaring your app's routes on top of `router.js`. It defines the API for the `Ember.Route` class that handles intelligent defaults, rendering templates, and loading data into controllers. ## History `router.js` has gone through a few iterations between 2013 and 2014: * July of 2013 – `router.js` adds promise-awareness. * Jan 2014 – refactored `router.js`'s primitives to handle corner cases. ### Corner Cases 1. Avoid running `model` hooks (responsible for fetching data needed to enter a route) for shared parent routes. 2. Avoid running model hooks when redirecting in the middle of another transition. e.g. during a transition to `articles/123/comments/2` you redirect to `articles/123/comments/3` after resolving Article 123 and you want to avoid re-running the hooks to load Article 123 again. 3. Handle two different approaches to transitions: * URL based (where a URL is parsed into route parameters that are used to load all the data needed to enter a route (e.g. `{ article_id: 123 }`). * direct named transition-based, where a route name and any context objects are provided (e.g. `transitionTo('article', articleObject)`), and the provided context object(s) might be promises that can't be serialized into URL params until they've fulfilled. ## Classes ### `HandlerInfo` A `HandlerInfo` is an object that describes the state of a route handler. For example, the `foo/bar` URL likely breaks down into a hierachy of two handlers: the `foo` handler and the `bar` handler. A "handler" is just an object that defines hooks that `router.js` will call in the course of a transition (e.g. `model`, `beforeModel`, `setup`, etc.). In Ember.js, handlers are instances of `Ember.Route`. A `HandlerInfo` instance contains that handler's model (e.g. `articleObject`), or the URL parameters associated with the current state of that handler (e.g. `{ article_id: '123' }`). Because `router.js` allows you to reuse handlers between different routes and route hierarchies, we need `HandlerInfo`s to describe the state of each route hierarchy. `HandlerInfo` is a top-level class with 3 subclasses: #### `UnresolvedHandlerInfoByParam` `UnresolvedHandlerInfoByParam` has the URL params stored on it which it can use to resolve itself (by calling the handler's `beforeModel`/`model`/`afterModel` hooks). #### `UnresolvedHandlerInfoByObject` `UnresolvedHandlerInfoByObject` has a context object, but no URL params. It can use the context to resolve itself and serialize into URL params once the context object is fulfilled. #### `ResolvedHandlerInfo` `ResolvedHandlerInfo` has calculated its URL params and resolved context/model object. #### Public API `HandlerInfo` has just a `resolve` method which fires all `model` hooks and ultimately resolves to a `ResolvedHandlerInfo` object. The `ResolvedHandlerInfo`'s `resolve` method just returns a promise that fulfills with itself. ### `TransitionState` The `TransitionState` object consists of an array of `HandlerInfo`s (though more might be added to it; not sure yet). #### Public API It too has a public API consisting only of a `resolve` method that will loop through all of its `HandlerInfo`s, swapping unresolved `HandlerInfo`s with `ResolvedHandlerInfo`s as it goes. Instances of `Router` and `Transition` contain `TransitionState` properties, which is useful since, depending on whether or not there is a currently active transition, the "starting point" of a transition might be the router's current hierarchy of `ResolvedHandlerInfo`s, or it might be a transition's hierachy of `ResolvedHandlerInfo`s mixed with unresolved HandlerInfos. ### `TransitionIntent` A `TransitionIntent` describes an attempt to transition. via URL or by named transition (via its subclasses `URLTransitionIntent` and `NamedTransitionIntent`). #### `URLTransitionIntent` A `URLTransitionIntent` has a `url` property. #### `NamedTransitionIntent` A `NamedTransitionIntent` has a target route `name` and `contexts` array property. This class defines only one method `applyToState` which takes an instance of `TransitionState` and plays this `TransitionIntent` on top of it to generate and return a new instance of `TransitionState` that contains a combination of resolved and unresolved `HandlerInfo`s. `TransitionIntent`s don't care whether the provided state comes from a router or a currently active transition; whatever you provide it, both subclasses of `TransitionIntent`s are smart enough to spit out a `TransitionState` containing `HandlerInfo`s that still need to be resolved in order to complete a transition. Much of the messy logic that used to live in `paramsForHandler`/`getMatchPoint` now live way less messily in the `applyToState` methods. This makes it easy to detect corner cases like no-op transitions – if the returned `TransitionState` consists entirely of `ResolvedHandlerInfo`s, there's no need to fire off a transition. It simplifies things like redirecting into a child route without winding up in some infinite loop on the parent route hook that's doing the redirecting. This simplifies `Transition#retry`; to retry a transition, provide its `intent` property to the transitioning function used by `transitionTo`, `handleURL`. `handle` function will make the right choice as to the correct `TransitionState` to pass to the intent's `applyToState` method. This approach is used to implement `Router#isActive`. You can determine if a destination route is active by constructing a `TransitionIntent`, applying it to the router's current state, and returning `true` if all of the `HandlerInfo`s are already resolved. [Ember Router]: http://emberjs.com/guides/routing/