Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revisiting the visit API (aka FastBoot™) #12394

Merged
merged 21 commits into from
Sep 30, 2015
Merged

Conversation

chancancode
Copy link
Member

↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️

It's time to put on some finishing touches for the visit API! Since this is a somewhat big patch, here is a detailed description.

Summary

The visit API exposes ApplicationInstance as a useful primitive for many scenarios, including FastBoot™, widgets, resource discover, testing, and more.

Background

Ember 1.12 introduced the concept of ApplicationInstances. While they are very well designed and could be very useful in many cases, there are no official APIs to create them. The visit API helps fill this void by exposing a supported way to boot and customize instances.

What's in the box 📦

  • Refactoring
  • New APIs
  • Tests
  • Documentation
  • Bonus DLC Pack
  • Emojis

Refactoring

  1. A Clear Boot Phase

    Previously, we don't have a very clear concept of a boot phase, especially for ApplictionInstance. This creates a lot of implicit timing dependencies around when certain options can be set/changed. For example, the ember-cli-fastboot changes the autoboot flag in an application initializer (i.e. in the middle of the boot phase), which created a strange fork in the boot flow.

    As part of this patch I did some work to solidify the boot phase. Options like autoboot should be set
    before boot() is called at instantiation time. What I can do on Application is rather limited, but I tried to make that clear in the new ApplicationInstance boot phase, which takes a hash of boot-time options in favor of ad-hoc methods like overrideRouterLocation.

  2. Asynchronous APIs

    Booting an Application (and to a lesser extent, an ApplicationInstance) is an inertially asynchronous task. Naturally, we would like to express the asynchrony here with promises/async functions. However, the architecture we have predated Ember's adoption of promises, and a lot of code assumes that it is a "synchronous". Specifically, a lot of tests assumes the last call to app.advanceReadiness() or app.reset() will complete the boot process when the same runloop ends, which makes it impossible to async-ify the process.

    This wasn't clear to me when I started working on the code – I actually spent some time making them fully async, only to find a lot of tests failing. However, we should start embracing the asynchrony here going forward. I converted the primary (internal) APIs async, which we can start using for new code like the visit API. Internally, they wrap their synchronous counterparts which we will use for legacy paths.

    This makes things a bit annoying in the short term, but it should help us iterate towards the right direction.

  3. Globals Mode

    Previously, setting the autoboot flag also skipped some seemingly unrelated code paths in the boot process. It turns out that most of these code paths are there for the legacy "globals mode" (where the application object acts as a "namespace" for attaching classes, i.e. App.MyComponent = ...).

    While it's fair to overload the autoboot flag as an opt-out for these features, they are ultimately unrelated concerns. To make the distinction clear, I added an internal _globalsMode flag to track these code paths. Its value is currently tied to the autoboot flag, but you can manually hardcode the autoboot = true, _globalsMode = false combo and run the tests, which should reveal that spots where we are relying on these behaviors. Since most of these features have already been deprecated, separating it from autoboot as a concept should help to review and remove them independently.

  4. Expansion of the environment concept

    With HTMLBars, most of the initial render path has already been jQuery-free (which is why FastBoot works in node). The last bit I had to plumb was the view.appendTo(rootElement) call, which uses jQuery to resolve the rootElement selector into an Element.

    The current visit implement works around this by exposing the "root view" without attaching its element. This isn't ideal since we already deprecated/removed views and it requires the use of other private APIs like appendTo and renderToElement.

    In practice, if we were given an actual Element object as the rootElement, our renderer already knows how to append it into the DOM without jQuery. All I need to do is to tell plumb that through to the view layer so that it can skip the jQuery lookup if the user has opt-ed to not user jQuery. To do this, I expanded on the environment abstraction and made it instance specific through the -environment:main container key. I only did what I needed to do to make my scenarios work, but that should lay the ground work for the eventual "optional jQuery" world.

See also #11291 and #11289 (comment)

New (Public) APIs

  • Application.autoboot flag
  • Application#boot()
  • Application#visit()
  • ApplicationInstance#getURL()
  • ApplicationInstance#visit()

Tests

I expanded the unit tests coverage, and added some end-to-end integration tests for the a few target scenarios we intend to support.

Documentation

You can find them in the diff.

Bonus DLC Pack

To redeem, enter the following code into the Ember Data store: F83J-7BD2-D16Z-3QA4

Emojis

👍 ⭐ 👉 ❤️ 🎧 🔑 🚀 👺

Differences from current implementation

  • Application.autoboot should only be set before boot time (e.g. at create time)
  • Application#visit() no longer rejects if a redirect occurred during the initial transition
  • Application#visit() no longer attach a view property to the instance it returns
  • Application#visit() no longer prevents the router from inserting the top-level view into the DOM (use rootElement to customize the insertion point as usual)
  • Application#visit() resolves only after rendering is complete and the root view is attached to the DOM
  • Application#visit() now takes an option hash for customizing the instance
  • Uncaught errors in application/instance initializers now causes Application#visit() to reject instead of synchronously throws
  • Removed ApplicationView#overrideRouterLocation (use the location boot option instead)
  • New flags that enables a much wider range of scenarios
  • Better documentation

TODOs

Open questions

  • Various TODOs in the code comments that I have no answers to
  • Are we comfortable committing the the three scenarios I listed in the documentation? Do we need to add more?
  • Is the ApplicationInstance#visit implementation correct?
  • Can we consolidate autoboot with Ember CLI's autorun flag? (they do related but not identical things)

Future extensions

  • Formalize the API requirements for the fake DOM, DOMHelper, etc
  • Use these API for tests: boot a new instance in each test case by calling visit() and destroy() it in teardown, instead of calling App.reset(). Use ApplicationInstance#visit to implement the visit test helper, etc
  • Work with @mitchlloyd on Ember Islands type scenarios (see the test)
  • Flesh out the options to enable other scenarios
  • Plumb the environment through the rest of the view layer to make jQuery optional
  • Remove reliance on globals mode in tests
  • Add a mechanism to differentiate between the various types of redirects and common errors, so that a FastBoot server can return the right status code

cc @wycats @tomdale @stefanpenner @krisselden @mitchlloyd @chadhietala @joshvfleming 🙏

Conceptually, these are two orthgonal things – autoboot happens to be
the opt-in for disabling support for globals mode.

After this refactor, autoboot is literally just calling `boot()` at
DOM Ready and *then* creating a instance automatically for you.
Conversely, you mustcall `boot()` yourself if you disable autoboot).

Most of the globals mode featue are already deprecated in 1.x.
Separating the two concepts allow us to review and remove those
feaatures independently.

Locally overriding the flags to use the `(autoboot: true, globalsMode:
false)` combo and running the tests would reveal all the places we are
relying on them internally.
This is a dialed back attempt at making the `ApplicationInstance`
boot process fully async. I wrote all the code to make that happen,
only to find out that a lot of tests implicitly depend on the process
being synchronous. So, I did the following instead:

1. Create an async version of `boot` on both `Application` and on
   `ApplicationInstance`. Going forward, new APIs (`visit`, etc)
   should use the async version.

2. Keep an internal `_bootSync` method on both classes and use them
   in places where we rely on the process being synchronous.

In the short term, having to support both synchronous and
asynchronous might make things look a bit more complicated, but it
will help us iterate towards where we want to be.

Separately, this commit also solidified the concept of a `boot`
phase on `ApplicationInstances`. Instead of overriding things on
the instance in an ad-hoc manner and implicitly rely on the timing
of these calls (e.g. `overrideRouterLocation` must be called before
the router is instantiated/cached), they have now been consolidated
into options that you pass into the `boot()` method.

Having an explicit boot phase makes it clear what can and cannot
happen at runtime.

(TODO: move the "start routing" logic into the boot phase)
Initialize the application. This happens automatically.
By default, this method is called automatically on "DOM ready"; however, if autoboot
is disabled, you would need to call this method yourself. Calling this method before
"DOM ready" is not recommended.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't say it's not recommended but rather there are caveats. The largest caveat being that your application cannot rely on DOM readiness.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually had your use case in mind when I refactored that path, but I think the plot was lost somewhere :P I'll re work this part a bit more to make that more

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is @chadhietala's use case?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They had a streaming (as in HTTP chucked response) setup that keeps the body tag open for sending data (<script> ...json... </script>), which causes the DOM ready to fire very late, which delayed ember boot for no reason.

The actual caveats are rather nuanced:

  1. when you kick off the boot, you must already have everything you need for your app, e.g. if your initializer and a route uses some global Google Analytics or Intercom thing, you must not boot it before they are loaded
  2. When Ember eventually inserts the top level view (didCreateRootView), the rootElement must already be on the page

It is actually not that easy to determine these two things, and for a normal set up the gain is very little, so I think it's too nuanced to describe and recommend publicly in the docs.

However, for @chadhietala's use case, it is not that hard to determine "would have been dom ready if not for the streaming setup" point – you can just insert a synchronous script tag right before you start streaming data, so I was going to make sure that there is a thing you can call to tell Ember this is the "effective" DOM ready. Perhaps I'll just rename _autoBoot back to DOM ready or something

(I don't think we need to document this)

}
});

App.visit('/').then(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^

Conflicts:
  packages/ember-application/lib/system/application.js
  packages/ember-application/lib/system/application-instance.js
  packages/ember-application/tests/system/visit_test.js
  packages/ember-views/lib/mixins/view_support.js
The gets the unflagged branches more closer aligned with master; most
important one being `boot` is back to `@private` again since there are
no real reason you need to call it (`visit` calls it automatically), and
because we cannot feature flag docs (and have one of them public, the
other one private).
@wycats
Copy link
Member

wycats commented Sep 30, 2015

The public API here is not final yet, but it's getting close.

It replaces older unstable code behind a flag, so I'm going to merge it so that people who are experimenting with the visit() API in Canary can continue to provide feedback, and we can hopefully stabilize this for 2.3.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants