This is a cross post of blog post I wrote and published for IsleOfCode on February 11, 2016 here.
I typed up this explanation of some of the magic in ember/ember-cli on the Ember Slack this morning, and felt it would probably be useful to leave it somewhere more permanent.
Sometimes Ember feels more magical than it really is, and the transitions and the resolver are probably the most magical piece of Ember for those that don't know about them.
This is a TL;DR crash course of this process in Ember, it's not very in depth, and it skims past a few things (such as cached resolutions, cached instantiations, and nested routing) which are also useful to understand.
The Magical Transition
Transitions can be triggered either programmatically or by a physical change to the URL.
link-to component, or some method with access to the
router will invoke the
Usually this occurs following a user action, such as a click on a link.
router.transitionTo accepts either a
url, or a
routeName coupled with params or full models for each dynamic route segment you defined for that route in
Via URL change
URL Changes and the back button emit events (basically just like a click), which the router listens to and uses to start a transition if necessary.
The Router assembles the Route
router.js file, you defined a
This map is used to generate url patterns that match a
During a transition, the router will determine the
routeName associated with a given url if it doesn't already have it based on these patterns.
Then the router looks up (or "resolves") the
route associated with that routeName, and instantiates it if necessary.
The router next calls hooks on the route to allow the route to perform setup and give it the chance to abort or redirect the transition.
Hooks are just methods with specific names such as
Only once these hooks have had a chance to do work does the transition complete (or fail).
Some of the hooks are "promise aware", meaning that if the method returns a promise, then the router will wait for that promise to resolve or reject before continuing the transition to the next hook.
This is also how the router knows to transition to a
loading route, because it can start a timer and see how long it's been waiting for the status of a promise to change.
router will lookup the
template for the route (matched because they have the same name as routeName), and it will give the model returned by the route to the controller. The controller then gives the model to the template along with any other properties you defined on it.
If you don't explicitly create a route for a routeName, the router won't find one when it goes to look it up, and when this happens the router will instead utilize a generic route.
If you don't explicitly create a controller for a route, the router won't find one when it goes to look it up, and when this happens the router will instead create a generic controller to serve as a container for the model.
The only file the router MUST be able to find (else it will throw an error) is a
template matching the
routeName, because otherwise it would have nothing to render at all, which is sort of the point of the whole ordeal.
By now you are probably nodding your head and still feel this is magical, and that's probably because telling you that "X" is "looking up" or "resolving" "Y" is great in theory, but how does this happen in practice?
The Magical Resolver
The resolver is what all of these objects are using to "lookup" the module they want. Let's peer in the box to see how the resolver works.
config/environment.js file, set:
ENV.APP.LOG_RESOLVER = true;
Now, while your app is running, you can open the inspector in safari/firefox/chrome etc. and each "lookup" will be logged. This is useful for seeing the patterns the resolver is using to look for modules, as well as for debugging if a module is (for some reason) not being correctly found.
With this flag, and the console open, you will see the various patterns of module names the resolver looks for, and whether that pattern found a module or not.
The "magic" is that at build time each file becomes a module, whose name is basically it's file path.
A module located at
app/components/foo-bar/component.js will have the name
Those modules are loaded via
require (this is likely a different
require than you are used to, it's
loader.js, available here)
In the console, if you type
require.entries, you will see a list of all the modules by name, where their name is a file path that should look familiar to you, as it's the path that file has in your project.
So basically, the resolver process isn't actually that magical, it's just a lot of simple steps combined together:
- convert your js files to modules whose names match their file paths
- concat all these modules into a vendor.js and app.js file
- add a little "app boot" script which looks up the main app module and starts running the app (by finding the router, having it load the first route etc.)
- have the router "resolve" (e.g. find) the current route
- have the router call various methods on the current route to fetch data / perform setup tasks (beforeModel, model, afterModel etc.)
- have the router resolve the necessary controller and template
- give the controller the model returned by the route
- render the template with the content supplied by the controller
The "resolve" at each point in the process is a bunch of sugar for converting module names (e.g.
route:application) to possible file paths for that module and looking in
requirejs.entries for potential matches.
Once a module has been found, that "resolution" is cached so that it can be found faster the next time:
resolver._cache['route:application'] = SomeModuleWeFound;
And there you have it,a crash course in the Ember "magic".