1 | # preboot
|
2 |
|
3 | The purpose of this library is to help manage the transition of state (i.e. events, focus, data) from a server-generated web view to a client-generated web view. This library works with any front end JavaScript framework (i.e. React, Vue, Ember, etc.), but does have a few extra convenience modules for Angular apps.
|
4 |
|
5 | The key features of preboot include:
|
6 |
|
7 | 1. Record and play back events
|
8 | 1. Respond immediately to certain events in the server view
|
9 | 1. Maintain focus even page is re-rendered
|
10 | 1. Buffer client-side re-rendering for smoother transition
|
11 | 1. Freeze page until bootstrap complete for certain events (ex. form submission)
|
12 |
|
13 | In essence, this library is all about managing the user experience from the time from when
|
14 | a server view is visible until the client view takes over control of the page.
|
15 |
|
16 | ## Installation
|
17 |
|
18 | cd into your app root and run the following command:
|
19 |
|
20 | ```
|
21 | npm i preboot --save
|
22 | ```
|
23 |
|
24 | The following sections covers the three different configurations of preboot:
|
25 |
|
26 | - Angular Configuration
|
27 | - Non-Angular Server Configuration
|
28 | - Non-Angular Browser Configuration
|
29 |
|
30 | #### Angular Configuration
|
31 |
|
32 | ```ts
|
33 | import { NgModule } from '@angular/core';
|
34 | import { BrowserModule } from '@angular/platform-browser';
|
35 | import { PrebootModule } from 'preboot';
|
36 | import { AppComponent } from './app.component';
|
37 |
|
38 | @NgModule({
|
39 | declarations: [AppComponent],
|
40 | imports: [
|
41 | BrowserModule.withServerTransition({ appId: 'foo' }),
|
42 | PrebootModule.withConfig({ appRoot: 'app-root' })
|
43 | ],
|
44 | bootstrap: [AppComponent]
|
45 | })
|
46 | export class AppModule { }
|
47 | ```
|
48 |
|
49 | The key part here for preboot is to include `PrebootModule.withConfig({ appRoot: 'app-root' })` where the `appRoot` is the selector(s) to find the root of your application. The options you can pass into `withConfig()` are in the [PrebootOptions](#PrebootOptions) section below. In most cases, however, you will only need to specify the `appRoot`.
|
50 |
|
51 | #### Non-Angular Server Configuration
|
52 |
|
53 | ```ts
|
54 | import { getInlineDefinition, getInlineInvocation } from 'preboot';
|
55 |
|
56 | const prebootOptions = {}; // see PrebootRecordOptions section below
|
57 | const inlineCodeDefinition = getInlineDefinition(prebootOptions);
|
58 | const inlineCodeInvocation = getInlineInvocation();
|
59 |
|
60 | // now insert `inlineCodeDefinition` into a `<script>` tag in `<head>` and
|
61 | // an `inlineCodeInvocation` copy just after the opening tag of each app root
|
62 |
|
63 | ```
|
64 |
|
65 | ```html
|
66 | <html>
|
67 | <head>
|
68 | <script><%= inlineCodeDefinition %></script>
|
69 | </head>
|
70 | <body>
|
71 | <app1-root>
|
72 | <script><%= inlineCodeInvocation %></script>
|
73 | <h2>App1 header</h2>
|
74 | <div>content</div>
|
75 | </app1-root>
|
76 | <app2-root>
|
77 | <script><%= inlineCodeInvocation %></script>
|
78 | <h2>App2 header</h2>
|
79 | <span>content</span>
|
80 | </app2-root>
|
81 | </body>
|
82 | </html>
|
83 | ```
|
84 |
|
85 | #### Non-Angular Browser Configuration
|
86 |
|
87 | ```ts
|
88 | import { EventReplayer } from 'preboot';
|
89 |
|
90 | const replayer = new EventReplayer();
|
91 |
|
92 | // once you are ready to replay events (usually after client app fully loaded)
|
93 | replayer.replayAll();
|
94 | ```
|
95 |
|
96 | #### PrebootOptions
|
97 |
|
98 | * `appRoot` (**required**) - One or more selectors for apps in the page (i.e. so one string or an array of strings).
|
99 | * `buffer` (default `true`) - If true, preboot will attempt to buffer client rendering to an extra hidden div. In most
|
100 | cases you will want to leave the default (i.e. true) but may turn off if you are debugging an issue.
|
101 | * `minify` (deprecated) - minification has been removed in v6. Minification should be handled by the end-user
|
102 | * `disableOverlay` (default `true`) - If true, freeze overlay would not get injected in the DOM.
|
103 | * `eventSelectors` (defaults below) - This is an array of objects which specify what events preboot should be listening for
|
104 | on the server view and how preboot should replay those events to the client view.
|
105 | See Event Selector section below for more details but note that in most cases, you can just rely on the defaults
|
106 | and you don't need to explicitly set anything here.
|
107 | * `replay` (default `true`) - The only reason why you would want to set this to `false` is if you want to
|
108 | manually trigger the replay yourself. This contrasts with the event selector `replay`, because this option is global
|
109 |
|
110 | This comes in handy for situations where you want to hold off
|
111 | on the replay and buffer switch until AFTER some async events occur (i.e. route loading, http calls, etc.). By
|
112 | default, replay occurs right after bootstrap is complete. In some apps, there are more events after bootstrap
|
113 | however where the page continues to change in significant ways. Basically if you are making major changes to
|
114 | the page after bootstrap then you will see some jank unless you set `replay` to `false` and then trigger replay
|
115 | yourself once you know that all async events are complete.
|
116 |
|
117 | To manually trigger replay, inject the EventReplayer like this:
|
118 |
|
119 | ```ts
|
120 | import { Injectable } from '@angular/core';
|
121 | import { EventReplayer } from 'preboot';
|
122 |
|
123 | @Injectable()
|
124 | class Foo {
|
125 | constructor(private replayer: EventReplayer) {}
|
126 |
|
127 | // you decide when to call this based on what your app is doing
|
128 | manualReplay() {
|
129 | this.replayer.replayAll();
|
130 | }
|
131 | }
|
132 | ```
|
133 |
|
134 | **Event Selectors**
|
135 |
|
136 | This part of the options drives a lot of the core behavior of preboot.
|
137 | Each event selector has the following properties:
|
138 |
|
139 | * `selector` - The selector to find nodes under the server root (ex. `input, .blah, #foo`)
|
140 | * `events` - An array of event names to listen for (ex. `['focusin', 'keyup', 'click']`)
|
141 | * `keyCodes` - Only do something IF event includes a key pressed that matches the given key codes.
|
142 | Useful for doing something when user hits return in a input box or something similar.
|
143 | * `preventDefault` - If `true`, `event.preventDefault()` will be called to prevent any further event propagation.
|
144 | * `freeze` - If `true`, the UI will freeze which means displaying a translucent overlay which prevents
|
145 | any further user action until preboot is complete.
|
146 | * `action` - This is a function callback for any custom code you want to run when this event occurs
|
147 | in the server view.
|
148 | * `replay` - If `false`, the event won't be recorded or replayed. Useful when you utilize one of the other options above.
|
149 |
|
150 | Here are some examples of event selectors from the defaults:
|
151 |
|
152 | ```es6
|
153 | var eventSelectors = [
|
154 |
|
155 | // for recording changes in form elements
|
156 | { selector: 'input,textarea', events: ['keypress', 'keyup', 'keydown', 'input', 'change'] },
|
157 | { selector: 'select,option', events: ['change'] },
|
158 |
|
159 | // when user hits return button in an input box
|
160 | { selector: 'input', events: ['keyup'], preventDefault: true, keyCodes: [13], freeze: true },
|
161 |
|
162 | // when user submit form (press enter, click on button/input[type="submit"])
|
163 | { selector: 'form', events: ['submit'], preventDefault: true, freeze: true },
|
164 |
|
165 | // for tracking focus (no need to replay)
|
166 | { selector: 'input,textarea', events: ['focusin', 'focusout', 'mousedown', 'mouseup'], replay: false },
|
167 |
|
168 | // user clicks on a button
|
169 | { selector: 'button', events: ['click'], preventDefault: true, freeze: true }
|
170 | ];
|
171 | ```
|
172 |
|
173 | #### Using with manual bootstrap (e.g. with ngUpgrade)
|
174 |
|
175 | Preboot registers its reply code at the `APP_BOOTSTRAP_LISTENER` token which is called by Angular for every component that is bootstrapped. If you don't have the `bootstrap` property defined in your `AppModule`'s `NgModule` but you instead use the `ngDoBootrap` method (which is done e.g. when using ngUpgrade) this code will not run at all.
|
176 |
|
177 | To make Preboot work correctly in such a case you need to specify `replay: false` in the Preboot options and replay the events yourself. That is, import `PrebootModule` like this:
|
178 |
|
179 | ```ts
|
180 | PrebootModule.withConfig({
|
181 | appRoot: 'app-root',
|
182 | replay: false,
|
183 | })
|
184 | ```
|
185 |
|
186 | and replay events in `AppComponent` like this:
|
187 |
|
188 | ```ts
|
189 | import { isPlatformBrowser } from '@angular/common';
|
190 | import { Component, OnInit, PLATFORM_ID, Inject, ApplicationRef } from '@angular/core';
|
191 | import { Router } from '@angular/router';
|
192 | import { filter, take } from 'rxjs/operators';
|
193 | import { EventReplayer } from 'preboot';
|
194 |
|
195 | @Component({
|
196 | selector: 'app-root',
|
197 | templateUrl: './app.component.html',
|
198 | styleUrls: ['./app.component.scss'],
|
199 | })
|
200 | export class AppComponent implements OnInit {
|
201 | constructor(
|
202 | private router: Router,
|
203 | @Inject(PLATFORM_ID) private platformId: string,
|
204 | private appRef: ApplicationRef,
|
205 | private replayer: EventReplayer,
|
206 | ) {}
|
207 |
|
208 | ngOnInit() {
|
209 | this.router.initialNavigation();
|
210 | if (isPlatformBrowser(this.platformId)) {
|
211 | this.appRef.isStable
|
212 | .pipe(
|
213 | filter(stable => stable),
|
214 | take(1),
|
215 | ).subscribe(() => {
|
216 | this.replayer.replayAll();
|
217 | });
|
218 | }
|
219 | }
|
220 | }
|
221 | ```
|
222 |
|
223 | #### PrebootComplete
|
224 |
|
225 | When you are manually replaying events, you often will want to know when Preboot
|
226 | is done replaying events and switching the buffers. To do this, use the following
|
227 | code in your app:
|
228 |
|
229 | ```es6
|
230 | window.document.addEventListener('PrebootComplete', () => {
|
231 | // put your code here that you want to run once preboot is complete
|
232 | });
|
233 | ```
|
234 |
|
235 | #### Adding a nonce
|
236 |
|
237 | If you're using a CSP, you'll need to add a `nonce` property to preboot's inline script.
|
238 | Preboot allows you to configure this by exporting an optional `PREBOOT_NONCE` token.
|
239 | Example usage is as follows (for an Express server):
|
240 |
|
241 | ```ts
|
242 | import {PREBOOT_NONCE} from 'preboot';
|
243 | import * as express from 'express';
|
244 | import {v4} from 'uuid';
|
245 | import * as csp from 'helmet-csp';
|
246 |
|
247 | const app = express();
|
248 |
|
249 | app.use((req, res, next) => {
|
250 | res.locals.nonce = v4();
|
251 | next();
|
252 | });
|
253 |
|
254 | app.use(csp({
|
255 | directives: {
|
256 | scriptSrc: [`'self'`, (req, res) => `'nonce-${ res.locals.nonce }'`],
|
257 | ...
|
258 | }
|
259 | });
|
260 |
|
261 | ... express boilerplate ...
|
262 |
|
263 | /* when it comes time to render the request, we can inject our new token */
|
264 |
|
265 | app.get('*', (req, res) => {
|
266 | res.render('index', {
|
267 | req,
|
268 | res,
|
269 | providers: [
|
270 | {
|
271 | provide: PREBOOT_NONCE,
|
272 | useValue: res.locals.nonce
|
273 | }
|
274 | ]
|
275 | });
|
276 | });
|
277 |
|
278 | ... other express route handling (see Universal guide for details) ...
|
279 | ```
|
280 |
|
281 | Please note that only the nonce tag will appear on the script,
|
282 | **the value is not rendered in modern browsers**. If you want to make
|
283 | sure your nonce is generating correctly, you can add a callback
|
284 | onto your render method to examine the resultant HTML as follows:
|
285 |
|
286 | ```ts
|
287 | res.render('index', (req, res) => {
|
288 | ...
|
289 | }, function(error, html) {
|
290 | console.log(html.substring(0, 50)); // we only care about the top part
|
291 | res.send(html);
|
292 | });
|
293 | ```
|
294 |
|
295 | #### Browser support
|
296 |
|
297 | If you wish to support Internet Explorer 9-11, you will need to include a [Polyfill](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill) for `CustomEvent`.
|