-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Conversation
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
- 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
- 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( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^
Also, don't add the function's internal states into the constructor for no reason.
Also, we don't use _environment on the instance itself, so don't need to save it off for no reason.
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).
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 |
Revisiting the visit API (aka FastBoot™)
↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️ ↪️
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 exposesApplicationInstance
as a useful primitive for many scenarios, including FastBoot™, widgets, resource discover, testing, and more.Background
Ember 1.12 introduced the concept of
ApplicationInstance
s. While they are very well designed and could be very useful in many cases, there are no official APIs to create them. Thevisit
API helps fill this void by exposing a supported way to boot and customize instances.What's in the box 📦
Refactoring
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, theember-cli-fastboot
changes theautoboot
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 setbeforeat instantiation time. What I can do onboot()
is calledApplication
is rather limited, but I tried to make that clear in the newApplicationInstance
boot phase, which takes a hash of boot-time options in favor of ad-hoc methods likeoverrideRouterLocation
.Asynchronous APIs
Booting an
Application
(and to a lesser extent, anApplicationInstance
) 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 toapp.advanceReadiness()
orapp.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.
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 theautoboot
flag, but you can manually hardcode theautoboot = 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 fromautoboot
as a concept should help to review and remove them independently.Expansion of the
environment
conceptWith 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 therootElement
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 likeappendTo
andrenderToElement
.In practice, if we were given an actual
Element
object as therootElement
, 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 theenvironment
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
flagApplication#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. atcreate
time)Application#visit()
no longer rejects if a redirect occurred during the initial transitionApplication#visit()
no longer attach aview
property to the instance it returnsApplication#visit()
no longer prevents the router from inserting the top-level view into the DOM (userootElement
to customize the insertion point as usual)Application#visit()
resolves only after rendering is complete and the root view is attached to the DOMApplication#visit()
now takes an option hash for customizing the instanceApplication#visit()
to reject instead of synchronously throwsApplicationView#overrideRouterLocation
(use thelocation
boot option instead)TODOs
ember-cli-fastboot
implementationember-cli-fastboot
-specific hacks into the appropriate package; since the DOMHelper API is not public, we cannot ship until things works out of the box (see also Use domHelper.setMorphHTML rather than calling morph.setHTML directly krisselden/morph-range#7)BootError
and aTransitionError
: for transition errors, it is useful to still return the instance, as there we might have rendered an error page into the DOMOpen questions
ApplicationInstance#visit
implementation correct?autoboot
with Ember CLI'sautorun
flag? (they do related but not identical things)Future extensions
visit()
anddestroy()
it in teardown, instead of callingApp.reset()
. UseApplicationInstance#visit
to implement thevisit
test helper, etcenvironment
through the rest of the view layer to makejQuery
optionalcc @wycats @tomdale @stefanpenner @krisselden @mitchlloyd @chadhietala @joshvfleming 🙏