-
Notifications
You must be signed in to change notification settings - Fork 49
Conversation
README.md
Outdated
@@ -57,7 +57,8 @@ import { getInlinePrebootCode } from 'preboot'; | |||
const prebootOptions = {}; // see PrebootRecordOptions section below | |||
const inlineCode = getInlinePrebootCode(prebootOptions); | |||
|
|||
// now simply insert the inlineCode into the HEAD section of your server view | |||
// now simply insert the inlineCode into a `<script>` tag just after |
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.
Note to self: maybe remove the word simply
; it may be discouraging for people who get stuck at this point.
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.
Done.
src/lib/api/event.recorder.ts
Outdated
// Remove the current script from the DOM so that child indexes match | ||
// between the client & the server. The script is already running so it | ||
// doesn't affect it. | ||
if (document.currentScript && document.currentScript.parentNode) { |
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 should take document
from win
, shouldn't I?
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.
Yep
src/lib/api/event.recorder.ts
Outdated
eventSelector.events.forEach((eventName: string) => { | ||
// get the appropriate handler and add it as an event listener | ||
const handler = createListenHandler(_document, prebootData, eventSelector, appData); | ||
_document.addEventListener(eventName, handler); |
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.
This is the place that I suspect may come in conflict between various app roots as selectors are global. We should perhaps scope them to the current app root by assigning it a unique attribute and checking if event.target
in the created event handlers is contained in the proper app root.
I hope we don't have to support nested app roots. ;)
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.
Or maybe just attach the listener to serverRoot
instead of _document
.
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.
The second one if it works properly would be the way to go
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.
One other thing - we should probably use the capture phase instead of the bubble one. Otherwise if any of the respective event handlers call stopPropagation()
it will not get to the server node & the event won't be captured.
src/lib/api/inline.preboot.code.ts
Outdated
* @param customOptions PrebootRecordOptions that override the defaults | ||
* @returns Generated inline preboot code is returned | ||
*/ | ||
export function getInlinePrebootCode(customOptions?: PrebootOptions): string { |
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.
The issues here are that if someone uses getInlinePrebootCode
directly:
- They need to move it from
<head>
to just after the opening app root tag which may not be easy in all scenarios. This is a breaking change. - If multiple app roots are used, each of them will need to get this injected separetely which increases the HTML.
Should we still support this function or just go all the way & require people to use getInlinePrebootCodeDefinition
& getInlinePrebootCodeInvocation
separately?
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.
Yeah let's cut it. I'll confirm with @jeffwhelpley offline, but so long as we document this change very well, I don't foresee it being a huge problem.
Is there any way we can streamline the process with a utility for React users?
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 don't know React well enough so I can't speak for it. How is it used there? AFAIK React has DOM rehydration so there should be no separate server & client node, should it?
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.
Sorry I haven't given comments yet on this PR. I have been slammed getting ready for ng-conf.
So, we did have a decent number of React users before they introduced hydration. It probably isn't as popular any more, but just realize that hydration doesn't solve all the problems. For example, if a user clicks on a button before the client is active, nothing is going to happen.
Regardless, the number of people is likely really small. I suggest we just move forward and we can always add some sugar later for non-Angular users if people complain.
src/lib/provider.ts
Outdated
.forEach(appRootElem => { | ||
if (appRootElem) { | ||
const inlineCodeInvocation = getInlinePrebootCodeInvocation(prebootOpts); | ||
const scriptWithInvocation = createScriptFromCode(doc, nonce, inlineCodeInvocation); |
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.
In the current version this could be moved out of the loop, near the place where inlineCodeDefinition
is created. However, since IE doesn't have document.currentScript
and the polyfill for that works only in IE 6-10 we'll need to tag each of those scripts with a unique attribute & inject its value as a variable into the script. Therefore, each of those scripts will be distinct so we'll need to create them in the loop anyway.
The only other solution I see is to trop IE support in Preboot. But I assume that's off the table. ;)
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.
Sounds fine
src/lib/api/event.replayer.ts
Outdated
@@ -190,7 +190,7 @@ export class EventReplayer { | |||
|
|||
// remove the freeze overlay if it exists | |||
const doc = this.getWindow().document; | |||
const prebootOverlay = doc.body.querySelector('#prebootOverlay') as HTMLElement; | |||
const prebootOverlay = doc.querySelector('#prebootOverlay') as HTMLElement; |
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.
Would getElementById
be faster here?
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.
It should be, I'll change it.
src/lib/api/inline.preboot.code.ts
Outdated
@@ -31,6 +29,8 @@ const eventRecorder = { | |||
createBuffer | |||
}; | |||
|
|||
export const initFunctionName = 'prebootInit'; |
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.
Maybe a little more specific, prebootInitFn
?
Another note to self: we still have the logic to handle multiple app roots in the recorder code; it should be changed to only work on one app root. Otherwise we have an |
PR updated; I still need to fix IE & refactor Preboot further a little so that it still works fine for multiple app roots but I addressed most comments otherwise. |
PR rebased & updated; the logic has been rewritten, all it needs is tests & bug reports! |
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.
LGTM. This is fantastic work. Some very minor nits. Plan is to add tests in a follow-up (and maybe some CI in between)
README.md
Outdated
``` | ||
import { getInlinePrebootCode } from 'preboot'; | ||
```ts | ||
import { getInlinePrebootCodeDefinition, getInlinePrebootCodeInvocation } from 'preboot'; |
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 think PrebootCode
makes this too verbose, and getInlineDefinition
/getInlineInvocation
is enough to avoid collisions with other libs.
src/lib/api/event.recorder.ts
Outdated
[].slice.call(_document.getElementsByTagName('script'), -1)[0]; | ||
|
||
if (!currentScript) { | ||
console.error('Preboot starting failed, no currentScript has been detected.'); |
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.
Change starting to initialization
src/lib/api/event.recorder.ts
Outdated
const _document = <Document>(theWindow.document || {}); | ||
let serverNode = currentScript.parentNode; | ||
if (!serverNode) { | ||
console.error('Preboot starting failed, the script is detached'); |
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.
Same note here
@@ -1,6 +1,6 @@ | |||
import { | |||
assign, getEventRecorderCode, getInlinePrebootCode, stringifyWithFunctions, | |||
validateOptions | |||
assign, getEventRecorderCode, getInlinePrebootCodeDefinition, getInlinePrebootCodeInvocation, |
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.
Fix formatting here, one import per line
Currently Preboot suffers from various race conditions: 1. It waits for the `document.body` to be available and then applies its logic. However, the application root may not be available at this point - see the issue angular#72. 2. Even if the application root exists when Preboot starts listening for events the server node may not be available in full yet. Since Preboot searches for nodes matching desired selectors inside of the existing `serverNode`, it may skip some of them if the server node hasn't loaded fully. This is especially common if the internet connection is slow. 3. Preboot waits for `body` in a tight loop, checking every 10 milliseconds. This starves the browser but may also be clamped by it to a larger delay (around 1 second); that's what happens in modern browsers if the tab where the page loads is inactive which is what happens if you open a link in a new tab in Chrome or Firefox. This means Preboot may start listening for events after Angular reports the app is stable and the `PrebootComplete` event fires. This then leaves the server node active, never transferring to the client one which makes for a broken site. To solve it, we're doing the following: 1. Since we want to attach event listeners as soon as possible when the server node starts being available (so that it can be shallow-cloned) we cannot wait for it to be available in full. Therefore, we switched to delegated event handlers on each of the app roots instead of directly on specific nodes. 2. As we support multiple app roots, to not duplicate large inline preboot code, we've split `getInlinePrebootCode` into two parts: function definitions to be put in `<head>` and the invocation separate for each app root put just after the app root opening tag. 3. To maintain children offset numbers (otherwise selectors won't match between the client & the server) we've removed the in-app-root script immediately when it starts executing; this won't stop the execution. `document.currentScript` is a good way to find the currently running script but it doesn't work in IE. A fallback behavior is applied to IE that leverages the fact that start scripts are invoked synchronously so the current script is the last one currently in the DOM. 4. We've removed `waitUntilReady` as there's no reliable way to wait; we'll invoke the init code immediately instead. 5. We've switched the overlay used for freeing the UI to attach to `document.documentElement` instead of `document.body` so that we don't have to wait for `body`. 6. The mentioned overlay is now created for each app root separately to avoid race conditions. Fixes angular#82 Fixes angular#72 BREAKING CHANGES: 1. When used in a non-Angular app, the code needs to be updated to use `getInlineDefinition` and `getInlineInvocation`; the previously defined `getInlinePrebootCode` has been removed. The `getInlineDefinition` output needs to be placed before any content user might interactive with, preferably in <head>. The `getInlineInvocation` output needs to be put just after the opening tag of each app root. Only `getInlineDefinition` needs to have options passed. An example expected (simplified) layout in EJS format: ```html <html> <head> <script><%= getInlineDefinition(options) %></script> </head> <body> <app1-root> <script><%= getInlineInvocation() %></script> <h2>App1 header</h2> <div>content</div> </app1-root> <app2-root> <script><%= getInlineInvocation() %></script> <h2>App2 header</h2> <span>content</span> </app2-root> </body> </html> ``` 2. The refactor breaks working with frameworks that replace elements like AngularJS with UI Router as it no longer uses serverSelector and clientSelector but expects its clientNode to remain in the DOM.
@CaerusKaru Feedback addressed. I also changed the commit message to mention the changed function names (without the |
remove unused proxy imports from some test files
A bug fix via a refactor.
See #82.
There should be no race conditions.
Yes, at least in the inline code handling as it no longer waits for the app root to be available so it cannot be fully put in
<head>
.Currently Preboot suffers from various race conditions:
It waits for the
document.body
to be available and then applies its logic.However, the application root may not be available at this point - see the issue
add condition to check that the application root is available in the DOM before prebooting #72.
Even if the application root exists when Preboot starts listening for events
the server node may not be available in full yet. Since Preboot searches for
nodes matching desired selectors inside of the existing
serverNode
, it mayskip some of them if the server node hasn't loaded fully. This is especially
common if the internet connection is slow.
Preboot waits for
body
in a tight loop, checking every 10 milliseconds.This starves the browser but may also be clamped by it to a larger delay
(around 1 second); that's what happens in modern browsers if the tab where the
page loads is inactive which is what happens if you open a link in a new tab in
Chrome or Firefox. This means Preboot may start listening for events after
Angular reports the app is stable and the
PrebootComplete
event fires. Thisthen leaves the server node active, never transferring to the client one which
makes for a broken site.
To solve it, we're doing the following:
Since we want to attach event listeners as soon as possible when the server
node starts being available (so that it can be shallow-cloned) we cannot wait
for it to be available in full. Therefore, we switched to delegated event
handlers on each of the app roots instead of directly on specific nodes.
As we support multiple app roots, to not duplicate large inline preboot code,
we've split
getInlinePrebootCode
into two parts: function definitions to beput in
<head>
and the invocation separate for each app root put just afterthe app root opening tag.
To maintain children offset numbers (otherwise selectors won't match between
the client & the server) we've removed the in-app-root script immediately when
it starts executing; this won't stop the execution.
document.currentScript
isa good way to find the currently running script but it doesn't work in IE. A
fallback behavior is applied to IE that leverages the fact that start scripts
are invoked synchronously so the current script is the last one currently in
the DOM.
We've removed
waitUntilReady
as there's no reliable way to wait; we'llinvoke the init code immediately instead.
We've switched the overlay used for freeing the UI to attach to
document.documentElement
instead ofdocument.body
so that we don't have towait for
body
.The mentioned overlay is now created for each app root separately to avoid
race conditions.
Fixes #82
Fixes #72
BREAKING CHANGES:
getInlineDefinition
andgetInlineInvocation
; the previously definedgetInlinePrebootCode
has been removed. ThegetInlineDefinition
outputneeds to be placed before any content user might interactive with, preferably
in . The
getInlineInvocation
output needs to be put just after theopening tag of each app root. Only
getInlineDefinition
needs to have optionspassed. An example expected (simplified) layout in EJS format:
like AngularJS with UI Router as it no longer uses serverSelector and
clientSelector but expects its clientNode to remain in the DOM.
TODO:
document.currentScript
.Maybe switch the selectors to be scoped to the concrete app root so that they don't interfere with each other?(we're now attaching delegated handlers to the server node, not to document). This may be an issue as we now have separate code for each app root. We need to switch the logic to not use theappRoots
array as each of the app roots is now handled separately.getInlinePrebootCode
.